干了这么多年后端,我有个很深的感受:MySQL这玩意儿,你写CRUD写得再溜,一到多表关联、存储过程、触发器这些进阶玩法,就特别容易露怯。尤其是面试的时候,问"存储过程和函数的区别",很多人能背出几条,但问他"你实际项目里敢不敢用触发器",立马就语塞了。这篇文章我不打算给你念手册,就把这些年我在多表查询、存储过程、触发器、自定义函数这几个方向上踩过的坑、总结出来的套路,摊开来讲明白。适合谁看?写过业务代码但没系统梳理过MySQL进阶能力的人,准备面试想突击核心考点的人,以及真正要在项目里设计触发器、存储过程却被现实毒打过的人。
1. 多表查询的核心写法与性能取舍
多表查询这块,表面上是语法问题,实际上全是性能问题。很多新手写的SQL能跑出正确结果,但数据量一上来就卡死,问题往往不在数据库,而在 JOIN 的写法、子查询的使用方式、以及过滤条件放的位置上。
1.1 JOIN 选型:INNER、LEFT、CROSS 的使用边界
先明确一个底层逻辑:JOIN 的本质是笛卡尔积的过滤。两张表不加条件直接JOIN,结果就是行数相乘,这个应该没人不知道。但在实际业务里,大家容易纠结的是"到底用 INNER JOIN 还是 LEFT JOIN"。
我的经验是:先用业务语义定方向,再用数据结果反推验证。比如订单表关联用户表,如果只要"有用户信息的订单",那就是 INNER JOIN;如果要求"所有订单都保留,哪怕用户被删了",就必须 LEFT JOIN。
有个特别容易翻车的细节:LEFT JOIN 右表的过滤条件不能放在 WHERE 里。下面这个SQL,看起来是想查"2024年之后下单的用户名",实际上会把 LEFT JOIN 退化成 INNER JOIN:
sql复制SELECT o.order_id, u.user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.create_time > '2024-01-01';
当 LEFT JOIN 的右表在 WHERE 里出现非空判断(类似 u.create_time IS NOT NULL、u.user_name = 'xxx'),这条SQL的语义就变成了只保留右表匹配上的行,那不就等于 INNER JOIN 吗?最稳妥的写法是把过滤条件挪到 ON 后面:
sql复制SELECT o.order_id, u.user_name
FROM orders o
LEFT JOIN users u ON o.user_id = u.id AND u.create_time > '2024-01-01';
关于 CROSS JOIN,说实话除了生成序列号、做测试数据填充之外,业务里我很少主动用。如果你发现你的查询里有两张毫无关联的表做了 CROSS JOIN,那大概率是模型设计出了问题。
1.2 子查询与JOIN:哪些场景等价互换更划算
子查询和 JOIN 在某些场景下能换着写,但到底谁快,不能拍脑袋。
IN 和 EXISTS 的选择,网上说法很多。核心差异在于:MySQL 5.7以下版本对这两者的执行计划优化很弱,IN 通常是先执行子查询、把结果集缓存下来,再去驱动外表;EXISTS 则是逐行判断,外表小、子查询走索引时会更有优势。MySQL 5.7以上优化器做了大量改写,其实差异已经不明显了。
我的建议是:与其纠结 IN 还是 EXISTS,不如先看执行计划。多说一句,EXPLAIN 的 type 列如果出现 ALL(全表扫描),不管是 IN 还是 EXISTS,都该先解决索引问题。
还有一个等价场景是"子查询当派生表"与 JOIN 的互换:
sql复制-- 写法A:派生表
SELECT d.dept_name, tmp.cnt
FROM dept d
LEFT JOIN (
SELECT dept_id, COUNT(*) AS cnt
FROM employee
GROUP BY dept_id
) tmp ON d.id = tmp.dept_id;
-- 写法B:直接聚合
SELECT d.dept_name, COUNT(e.id)
FROM dept d
LEFT JOIN employee e ON d.id = e.dept_id
GROUP BY d.id, d.dept_name;
这两种写法在结果上基本等价,但写法B有个坑:如果 department 表里的列没有全部出现在 GROUP BY 里,而 sql_mode 又开了 ONLY_FULL_GROUP_BY(MySQL 5.7后的默认配置),就直接报错。所以写法A在应对复杂分组逻辑时反而更可控,把"聚合"和"关联"拆开,思维负担更小。
1.3 聚合函数、GROUP BY 与 HAVING 的边界
聚合查询的核心,是先想清楚"聚合的粒度"。COUNT(*)、SUM()、AVG() 这些函数不会用错,真正用错的是 "GROUP BY 后面到底跟谁"。
一个典型错误:想统计每个用户的订单总额和订单数,结果 GROUP BY 只写了 u.id,SELECT
