复杂 SQL 查询
一、多表连接
1.1 内连接(INNER JOIN)
内连接返回两个表中匹配的行,不匹配的行不返回。
-- 语法
SELECT * FROM tableA A INNER JOIN tableB B ON A.key = B.key;
实战案例:员工与部门查询
sql
-- 建表
CREATE TABLE department (
id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(50) NOT NULL
);
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
dept_id INT,
salary DECIMAL(10,2)
);
INSERT INTO department VALUES (1, '技术部'), (2, '市场部'), (3, '财务部');
INSERT INTO employee VALUES
(1, '张三', 1, 15000),
(2, '李四', 1, 13000),
(3, '王五', 2, 11000),
(4, '赵六', NULL, 9000); -- 赵六没有部门
-- 内连接:只显示有部门的员工
SELECT e.name, e.salary, d.dept_name
FROM employee e
INNER JOIN department d ON e.dept_id = d.id;
-- 结果:张三、李四、王五(赵六被过滤掉)
1.2 左连接(LEFT JOIN)
左连接返回左表所有行,右表不匹配的列填 NULL。
sql
-- 查询所有员工及其部门信息(包括无部门的员工)
SELECT e.name, e.salary, d.dept_name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id;
-- 结果:赵六的 dept_name 为 NULL
实战场景:查缺补漏
sql
-- 查找没有分配部门的员工
SELECT e.name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id
WHERE d.id IS NULL;
-- 结果:赵六
1.3 右连接(RIGHT JOIN)
右连接返回右表所有行,左表不匹配的列填 NULL。与左连接对称,一般习惯用左连接替代。
sql
SELECT e.name, d.dept_name
FROM employee e
RIGHT JOIN department d ON e.dept_id = d.id;
-- 即使某个部门没有员工,也会显示该部门
1.4 全连接(FULL JOIN)
MySQL 不支持直接 FULL JOIN,可用 LEFT JOIN UNION RIGHT JOIN 模拟。
sql
-- 模拟全连接:所有员工和所有部门都显示
SELECT e.name, d.dept_name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.id
UNION
SELECT e.name, d.dept_name
FROM employee e
RIGHT JOIN department d ON e.dept_id = d.id;
二、子查询
2.1 标量子查询
返回单个值(一行一列),可用在 SELECT、WHERE、HAVING 中。
sql
-- 查询工资高于平均工资的员工
SELECT name, salary
FROM employee
WHERE salary > (SELECT AVG(salary) FROM employee);
-- 查询每个员工及其部门平均工资的差额
SELECT name, salary,
salary - (SELECT AVG(salary) FROM employee WHERE dept_id = e.dept_id) AS diff
FROM employee e;
2.2 行子查询
返回一行多列,用 IN 或 = 配合行构造器。
sql
-- 查询与张三同部门且同薪水的员工
SELECT name, dept_id, salary
FROM employee
WHERE (dept_id, salary) = (
SELECT dept_id, salary FROM employee WHERE name = '张三'
) AND name != '张三';
2.3 表子查询
返回多行多列,通常用在 FROM 子句中作为派生表。
sql
-- 查询每个部门工资最高的员工
SELECT d.dept_name, t.name, t.salary
FROM (
SELECT dept_id, name, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
FROM employee
) t
JOIN department d ON t.dept_id = d.id
WHERE t.rk = 1;
2.4 相关子查询
子查询引用外层查询的列,每行执行一次,效率较低。
sql
-- 查询工资高于所在部门平均工资的员工
SELECT e1.name, e1.salary, e1.dept_id
FROM employee e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employee e2
WHERE e2.dept_id = e1.dept_id
);
性能提示:相关子查询对外层每一行都执行一次,数据量大时建议用
JOIN替代。
三、分组统计
3.1 GROUP BY 与聚合函数
sql
-- 统计每个部门的员工数和平均工资
SELECT d.dept_name,
COUNT(e.id) AS emp_count,
ROUND(AVG(e.salary), 2) AS avg_salary,
MAX(e.salary) AS max_salary,
MIN(e.salary) AS min_salary
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
GROUP BY d.id, d.dept_name;
3.2 WHERE vs HAVING
| 特性 | WHERE | HAVING |
|---|---|---|
| 过滤时机 | 分组前 | 分组后 |
| 能否使用聚合函数 | 否 | 是 |
| 能否使用别名 | 部分支持 | 部分支持 |
sql
-- WHERE:分组前过滤掉工资低于10000的员工
-- HAVING:分组后过滤掉平均工资低于12000的部门
SELECT d.dept_name,
COUNT(e.id) AS emp_count,
ROUND(AVG(e.salary), 2) AS avg_salary
FROM department d
JOIN employee e ON d.id = e.dept_id
WHERE e.salary >= 10000 -- 分组前过滤
GROUP BY d.id, d.dept_name
HAVING AVG(e.salary) >= 12000 -- 分组后过滤
ORDER BY avg_salary DESC;
实战案例:销售统计
sql
-- 建表
CREATE TABLE sales (
id INT PRIMARY KEY AUTO_INCREMENT,
product VARCHAR(50),
category VARCHAR(20),
amount DECIMAL(12,2),
sale_date DATE,
region VARCHAR(20)
);
INSERT INTO sales VALUES
(1, '笔记本', '电子产品', 8000, '2024-01-15', '华东'),
(2, '手机', '电子产品', 5000, '2024-01-16', '华东'),
(3, '笔记本', '电子产品', 8000, '2024-02-10', '华南'),
(4, '键盘', '配件', 200, '2024-02-11', '华东'),
(5, '手机', '电子产品', 5500, '2024-03-05', '华北'),
(6, '笔记本', '电子产品', 8200, '2024-03-08', '华东'),
(7, '鼠标', '配件', 50, '2024-03-10', '华南');
-- 需求:查询每季度每个类别销售额 > 10000 的类别,且只统计华东地区的订单
SELECT
QUARTER(sale_date) AS quarter,
category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM sales
WHERE region = '华东' -- 先过滤地区
GROUP BY quarter, category
HAVING total_amount > 10000 -- 再过滤销售额
ORDER BY quarter, total_amount DESC;
四、集合运算
4.1 UNION vs UNION ALL
sql
-- UNION:去重合并(有排序开销)
-- UNION ALL:直接合并(性能更好)
SELECT name, '员工' AS type FROM employee
UNION ALL
SELECT dept_name, '部门' FROM department;
-- 实际场景:合并历史订单和当月订单
SELECT order_id, amount, '历史' AS source FROM orders_2023
UNION ALL
SELECT order_id, amount, '当月' FROM orders_2024;
实战案例:多表合并报表
sql
-- 查询各部门工资排名前2的员工(跨部门统一排名)
SELECT name, dept_id, salary, '高薪' AS level
FROM employee
WHERE salary >= 15000
UNION ALL
SELECT name, dept_id, salary, '中等'
FROM employee
WHERE salary BETWEEN 10000 AND 14999
UNION ALL
SELECT name, dept_id, salary, '初级'
FROM employee
WHERE salary < 10000
ORDER BY salary DESC;
五、综合实战案例
电商订单分析
sql
-- 建表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
register_date DATE
);
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
amount DECIMAL(12,2),
order_date DATETIME,
status VARCHAR(20)
);
CREATE TABLE order_items (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT,
product_name VARCHAR(100),
price DECIMAL(10,2),
quantity INT
);
-- 插入示例数据
INSERT INTO users VALUES
(1, '张三', '2024-01-01'),
(2, '李四', '2024-02-15'),
(3, '王五', '2024-03-20');
INSERT INTO orders VALUES
(1, 1, 5000, '2024-06-01 10:00:00', '已完成'),
(2, 1, 3000, '2024-06-15 14:00:00', '已完成'),
(3, 2, 8000, '2024-07-01 09:00:00', '已完成'),
(4, 3, 2000, '2024-07-10 16:00:00', '已取消');
INSERT INTO order_items VALUES
(1, 1, '手机', 5000, 1),
(2, 2, '键盘', 200, 2),
(3, 2, '鼠标', 50, 4),
(4, 3, '笔记本', 8000, 1),
(5, 4, 'U盘', 100, 20);
-- 综合查询1:用户消费排行(仅统计已完成订单)
SELECT
u.name,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_spent,
ROUND(AVG(o.amount), 2) AS avg_order_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = '已完成'
GROUP BY u.id, u.name
ORDER BY total_spent DESC;
-- 综合查询2:查询客单价高于平均的订单详情
SELECT o.id, u.name, o.amount, o.order_date
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.amount > (
SELECT AVG(amount) FROM orders WHERE status = '已完成'
)
AND o.status = '已完成'
ORDER BY o.amount DESC;
-- 综合查询3:查询每个用户最近的一笔订单
SELECT u.name, t.amount, t.order_date
FROM (
SELECT user_id, amount, order_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
FROM orders
WHERE status = '已完成'
) t
JOIN users u ON t.user_id = u.id
WHERE t.rn = 1;
-- 综合查询4:查询有订单但从未购买过电子产品的用户
SELECT DISTINCT u.name
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id NOT IN (
SELECT DISTINCT o2.user_id
FROM orders o2
JOIN order_items oi ON o2.id = oi.order_id
WHERE oi.product_name IN ('手机', '笔记本', '平板')
);
总结
| 查询类型 | 核心要点 | 性能注意 |
|---|---|---|
| 内连接 | 只返回匹配行 | 关联字段建索引 |
| 左/右连接 | 驱动表全返回 | 小表驱动大表 |
| 子查询 | 标量/行/表/相关 | 相关子查询慎用 |
| 分组统计 | WHERE在前、HAVING在后 | 避免对大表GROUP BY |
| 集合运算 | UNION去重、UNION ALL不去重 | 优先用UNION ALL |