作为一名长期奋战在数据库开发一线的工程师,我深知复合查询在实际业务中的重要性。今天我将结合十多年的实战经验,带大家深入剖析MySQL中的多表查询、自连接和子查询技术,分享那些官方文档不会告诉你的实战技巧和避坑指南。
在进入复合查询之前,我们先回顾几个基础但容易出错的查询场景,并分享一些性能优化的小技巧。
场景1:多条件复合查询
sql复制-- 查询工资高于500或岗位为MANAGER,且姓名以J开头的员工
SELECT * FROM emp
WHERE (sal>500 OR job='MANAGER')
AND ename LIKE 'J%';
实战经验:在MySQL中,LIKE 'J%'这种前缀匹配是可以利用索引的,但如果是LIKE '%J'后缀匹配就无法使用索引。对于这种固定前缀查询,建议确保ename字段上有索引。
场景2:多字段排序
sql复制-- 按部门号升序,工资降序排列
SELECT * FROM emp
ORDER BY deptno ASC, sal DESC;
性能提示:当排序字段较多时,建议为每个排序字段建立复合索引。例如本例中,可以创建(deptno, sal)的复合索引来优化排序性能。
场景3:年薪计算
sql复制-- 计算年薪(工资*12+奖金),NULL值处理
SELECT ename, sal*12+IFNULL(comm,0) AS '年薪'
FROM emp
ORDER BY 年薪 DESC;
避坑指南:处理NULL值时一定要小心。IFNULL(comm,0)确保奖金为NULL时按0计算。我曾经遇到过因为忽略NULL值导致报表数据严重失真的生产事故。
多表查询的核心是理解笛卡尔积和连接条件。先看一个典型例子:
sql复制-- 显示员工名、工资及部门名
SELECT e.ename, e.sal, d.dname
FROM emp e, dept d
WHERE e.deptno = d.deptno;
执行原理:MySQL会先对emp和dept做笛卡尔积(14条员工记录×4个部门=56条临时记录),然后通过WHERE条件过滤出e.deptno=d.deptno的记录。
性能优化建议:
场景1:带过滤条件的连接
sql复制-- 显示10号部门的部门名、员工名和工资
SELECT d.dname, e.ename, e.sal
FROM emp e, dept d
WHERE e.deptno = d.deptno
AND d.deptno = 10;
场景2:非等值连接
sql复制-- 显示员工姓名、工资及工资等级
SELECT e.ename, e.sal, s.grade
FROM emp e, salgrade s
WHERE e.sal BETWEEN s.losal AND s.hisal;
特殊技巧:BETWEEN AND在工资等级这类区间匹配中非常有用。我曾经用这种方法实现了电商系统的会员等级自动评定功能。
自连接是同一个表与自己连接的特殊场景,在处理层级数据时特别有用。
sql复制-- 查询FORD的上级领导(使用自连接)
SELECT m.ename, m.empno
FROM emp e, emp m
WHERE e.ename = 'FORD'
AND e.mgr = m.empno;
对比方案:也可以用子查询实现
sql复制SELECT ename, empno
FROM emp
WHERE empno = (SELECT mgr FROM emp WHERE ename = 'FORD');
性能对比:在MySQL 8.0+版本中,子查询性能已经大幅提升,两种方式差异不大。但在早期版本中,自连接通常性能更好。
我曾经用自连接解决过一个有趣的问题:找出同部门同岗位的员工组合。
sql复制SELECT a.ename, b.ename, a.deptno, a.job
FROM emp a, emp b
WHERE a.deptno = b.deptno
AND a.job = b.job
AND a.empno < b.empno;
关键技巧:
a.empno < b.empno确保每对员工只出现一次,避免(A,B)和(B,A)重复。
返回单一结果的子查询,通常用于比较条件。
sql复制-- 查询与SMITH同部门的员工
SELECT * FROM emp
WHERE deptno = (SELECT deptno FROM emp WHERE ename = 'SMITH')
AND ename != 'SMITH';
常见错误:忽略子查询可能返回NULL或多条记录的情况。稳妥的做法是先用SELECT测试子查询结果。
IN关键字应用:
sql复制-- 查询与10号部门岗位相同的员工(不含10号部门)
SELECT ename, job, sal, deptno
FROM emp
WHERE job IN (SELECT job FROM emp WHERE deptno = 10)
AND deptno != 10;
ALL关键字应用:
sql复制-- 查询工资高于30号部门所有员工的员工
SELECT ename, sal, deptno
FROM emp
WHERE sal > ALL (SELECT sal FROM emp WHERE deptno = 30);
替代方案:这种查询通常也可以用MAX()聚合函数实现,性能视数据量而定。
sql复制-- 查询与SMITH部门和岗位完全相同的员工
SELECT * FROM emp
WHERE (deptno, job) = (SELECT deptno, job FROM emp WHERE ename = 'SMITH')
AND ename != 'SMITH';
语法注意:多列比较时需要用括号将列组合起来,这是很多新手容易忽略的细节。
这是非常强大的功能,可以将子查询结果作为临时表使用。
案例1:查询高于部门平均工资的员工
sql复制SELECT e.ename, e.deptno, e.sal, d.avg_sal
FROM emp e, (SELECT deptno, AVG(sal) avg_sal FROM emp GROUP BY deptno) d
WHERE e.deptno = d.deptno AND e.sal > d.avg_sal;
案例2:查询每个部门工资最高的员工
sql复制SELECT e.ename, e.sal, e.deptno, m.max_sal
FROM emp e, (SELECT deptno, MAX(sal) max_sal FROM emp GROUP BY deptno) m
WHERE e.deptno = m.deptno AND e.sal = m.max_sal;
性能提示:MySQL 8.0+推荐使用窗口函数实现这类需求,性能更好:
sql复制WITH dept_max AS (
SELECT ename, sal, deptno,
MAX(sal) OVER (PARTITION BY deptno) max_sal
FROM emp
)
SELECT ename, sal, deptno, max_sal
FROM dept_max
WHERE sal = max_sal;
sql复制-- UNION去重
SELECT * FROM emp WHERE sal > 2500
UNION
SELECT * FROM emp WHERE job = 'MANAGER';
-- UNION ALL不去重
SELECT * FROM emp WHERE sal > 2500
UNION ALL
SELECT * FROM emp WHERE job = 'MANAGER';
重要区别:UNION会进行去重排序操作,性能开销较大;UNION ALL直接合并结果集。在确定没有重复或不需要去重时,总是优先使用UNION ALL。
分页查询合并:
sql复制(SELECT * FROM emp WHERE deptno = 10 ORDER BY sal DESC LIMIT 2)
UNION ALL
(SELECT * FROM emp WHERE deptno = 20 ORDER BY sal DESC LIMIT 2)
ORDER BY deptno, sal DESC;
注意事项:合并查询中各子查询的列数和数据类型必须一致。我曾经遇到过因为列顺序不一致导致的难以调试的错误。
问题1:子查询返回多行
sql复制-- 错误示例
SELECT * FROM emp WHERE sal = (SELECT sal FROM emp WHERE deptno = 30);
解决方案:改用IN、ANY或ALL操作符,或确保子查询返回单行
问题2:连接条件缺失
sql复制-- 错误示例(产生笛卡尔积)
SELECT * FROM emp, dept;
后果:数据量大的表连接时会导致临时表爆炸,我曾见过因此导致数据库崩溃的案例
问题3:NULL值处理不当
sql复制-- 错误示例(comm为NULL时整个表达式为NULL)
SELECT ename, sal*12+comm FROM emp;
正确做法:使用IFNULL或COALESCE函数处理NULL值
sql复制-- 显示每个部门信息及员工数量
SELECT d.deptno, d.dname, d.loc, COUNT(e.empno) emp_count
FROM dept d LEFT JOIN emp e ON d.deptno = e.deptno
GROUP BY d.deptno, d.dname, d.loc;
关键点:使用LEFT JOIN确保没有员工的部门也会显示,COUNT(empno)不统计NULL值
sql复制-- 显示员工薪资与部门平均薪资的比较
SELECT
e.ename,
e.sal,
e.deptno,
d.avg_sal,
e.sal - d.avg_sal AS diff,
CASE
WHEN e.sal > d.avg_sal THEN '高于平均'
WHEN e.sal < d.avg_sal THEN '低于平均'
ELSE '等于平均'
END AS status
FROM emp e
JOIN (SELECT deptno, AVG(sal) avg_sal FROM emp GROUP BY deptno) d
ON e.deptno = d.deptno;
这个查询在实际HR系统中非常有用,可以直观显示员工薪资在部门中的位置。
对于大数据量查询,EXISTS通常比IN性能更好:
sql复制-- 查询有下属的员工
SELECT * FROM emp e
WHERE EXISTS (
SELECT 1 FROM emp WHERE mgr = e.empno
);
sql复制-- 查询每个部门薪资最高的前三名员工
SELECT e.ename, e.sal, e.deptno
FROM emp e
WHERE (
SELECT COUNT(*)
FROM emp
WHERE deptno = e.deptno AND sal > e.sal
) < 3
ORDER BY deptno, sal DESC;
sql复制-- 使用CTE(Common Table Expression)
WITH dept_stats AS (
SELECT deptno, AVG(sal) avg_sal, MAX(sal) max_sal
FROM emp
GROUP BY deptno
)
SELECT e.ename, e.sal, e.deptno, ds.avg_sal
FROM emp e JOIN dept_stats ds ON e.deptno = ds.deptno
WHERE e.sal > ds.avg_sal;
在实际项目中,复合查询是处理复杂业务逻辑的利器,但也容易成为性能瓶颈。掌握这些技巧后,我解决了许多原本需要应用层处理的复杂查询问题,性能提升往往达到几个数量级。特别是在处理报表类查询时,合理运用多表连接和子查询,可以大幅减少应用层与数据库的交互次数。