☰
SQL聚合思维:MySQL分组查询与联合查询的性能优化实战
2026/10/5 15:44:17 网站建设 项目流程

1. 别再用程序循环统计数据了:真正该学会的SQL聚合思维

先聊个我经常看到的场景。很多刚接触数据库的同学,接到“统计一下每个分类的商品数量”这种需求,第一反应是写一段Python或Java代码,把整张表查出来,然后在内存里for循环加if判断,最后手动组装结果。表里几百条数据时看不出问题,等数据涨到几十万、上百万条,这种写法会把应用服务器的内存直接打爆,响应时间从毫秒级变成秒级甚至超时。

这就是聚合查询、分组查询、联合查询存在的意义。它们不是SQL里的“高级玩法”,而是关系型数据库处理数据的基本功。用一条SQL搞定的事情,根本不需要把数据搬到应用层去算。MySQL作为最常用的开源关系型数据库,这几个查询能力几乎是每个后端开发每天都要用的。

这篇文章我从实际业务出发,把聚合函数怎么用、分组查询的执行逻辑、联合查询的适用场景,以及这三个东西组合起来该注意什么,系统性地过一遍。内容会覆盖不少真实项目中容易踩的坑,比如COUNT(1)和COUNT(*)到底有没有区别、HAVING为什么不能随便替换WHERE、UNION和UNION ALL选错了会出什么事故,这些都是面试高频题,也是生产环境里真正会咬人的地方。

不管你是刚入门SQL的新手,还是写了几年代码但查询优化全靠猜的开发者,这篇文章都值得花十分钟看完。我会用实际的表结构和数据来演示,你可以直接复制到自己的MySQL里跑一遍。

2. 五个聚合函数背后的计算逻辑与使用边界

2.1 COUNT、SUM、AVG、MAX、MIN:从一道统计题说起

先建一张销售订单表,后面的例子基本都围绕它展开:

CREATE TABLE sales_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, customer_id INT NOT NULL, category VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效 0-取消', created_at DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入一批模拟数据,大概几十条就行,覆盖多个分类和日期。然后看第一个需求:统计总订单数、总销售额、平均订单金额、最大单笔金额、最小单笔金额。

SELECT COUNT(*) AS total_orders, SUM(amount) AS total_amount, AVG(amount) AS avg_amount, MAX(amount) AS max_amount, MIN(amount) AS min_amount FROM sales_order WHERE status = 1;

这一条SQL就把五个聚合函数全用上了。执行结果会返回一行数据,这就是聚合查询的核心特征:多行输入,单行输出。每一行原始记录经过聚合函数压缩成一个汇总值,数据量从N行变成1行。

聚合函数的计算逻辑可以理解为“把一列的所有值收集起来,按特定规则算出一个结果”:COUNT数的是行数,SUM做加法,AVG先加再除,MAX和MIN在遍历过程中记录极值。它们都忽略NULL值(除了COUNT(*)),这一点在后面单独说。

2.2 COUNT(*) 与 COUNT(1)、COUNT(列名) 的真相与争议

这是个老生常谈但很多人依然搞混的问题。先说结论:在MySQL的InnoDB引擎下,COUNT()和COUNT(1)性能上没有显著差异,它们都会遍历主键索引或二级索引进行计数,MySQL官方也明确说过COUNT()没有被做特殊优化,但它已经被优化器处理为直接统计行数。

关键区别在于COUNT(列名)。比如COUNT(amount),它只统计amount这一列中非NULL的行数,如果某行的amount是NULL,这一行不会被计入。而COUNT(*)和COUNT(1)统计的是“行数”,不管某一列是不是NULL,只要行存在就算。

看个实际案例:

-- 假设有一行 amount = NULL SELECT COUNT(*) FROM sales_order; -- 返回总行数,包含NULL行 SELECT COUNT(amount) FROM sales_order; -- 返回amount非NULL的行数

真实项目中,我发现很多人用COUNT(字段)去统计行数,如果那个字段恰好允许NULL,计数就会偏小,而且这种错误很难被察觉,因为数据量一大,少几十行根本看不出来。用COUNT(*)或者COUNT(1)最稳妥。

另外补充一点,以前有老说法是COUNT(1)比COUNT(*)快,那是MyISAM时代的经验。InnoDB下两者执行计划基本一样,别被过时经验带偏了。

2.3 SUM与AVG的精度隐患和NULL陷阱

SUM和AVG处理的是数值列,有两个容易出问题的地方。

第一个是精度。amount字段如果定义成DECIMAL(10,2),SUM的结果没问题。但如果列是FLOAT或DOUBLE,累加大量小数后会出现浮点误差,比如0.1 + 0.2算出来是0.30000000000000004这种。金额相关的统计一定要用DECIMAL,而且AVG的返回类型在MySQL 5.7及以后会遵循一个规则:如果参数是DECIMAL,返回DECIMAL;如果是整数类型,返回DECIMAL(20, 4)左右的样子,不会丢精度。这是MySQL做得比较合理的地方。

第二个是NULL。SUM遇到NULL不会报错,而是跳过它继续累加。但如果某一列全是NULL,SUM返回的是NULL而不是0。这个细节很坑,因为Java程序里用BigDecimal接收还好,如果用基本类型double接收,拆箱时直接NullPointerException。建议遇到可能全空的统计,用IFNULL兜底:

SELECT IFNULL(SUM(amount), 0) AS total_amount FROM sales_order WHERE ...;

AVG同理,如果所有值都是NULL,返回NULL。逻辑上没问题,但程序端要做好防空处理。

2.4 MIN和MAX:不只是数字,字符串和日期也能比大小

很多人以为MIN和MAX只能用于数值列,其实字符串和日期类型同样适用。字符串按字典序比较,日期按时间先后比较。

实际业务中有个很典型的用法:查每个用户最早的下单时间。用MIN(created_at)就能实现,不用ORDER BY再加LIMIT。还有人用它配合GROUP BY来取“每组最早的一条记录”,这个后面讲分组时会展开。

还有一个冷门但实用的写法——用MAX(id)来判重。比如同步数据时想知道某张表最大自增ID到哪了,直接SELECT MAX(id)就行,比SELECT * ORDER BY id DESC LIMIT 1要高效,因为MAX可以直接走索引的末尾节点,不需要排序。

提一句容易被误解的:聚合函数能不能和普通列一起出现在SELECT里?比如:

SELECT customer_id, MAX(amount) FROM sales_order;

这条SQL在MySQL的默认配置下能执行,但结果是不确定的,customer_id不一定对应amount最大的那一行。在严格模式下(ONLY_FULL_GROUP_BY),MySQL 5.7及以后会直接报错。这是一个必须养成的习惯:SELECT中的普通列必须出现在GROUP BY子句中,或者被聚合函数包裹。

3. 分组查询:GROUP BY不只是“加一个分组条件”那么简单

3.1 GROUP BY的执行顺序决定了你的SQL能不能跑对

分组查询的核心是GROUP BY,它的执行逻辑在SQL语句的整体流程中处于一个很关键的位置。一条完整的SQL执行顺序大致是:

FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

这个顺序解释了三个常见疑问:

第一,WHERE是在分组之前过滤的。所以WHERE里不能使用聚合函数作为条件,比如“筛选出订单金额大于100的顾客”,这个条件是在分组前对原始行进行过滤的。而HAVING是在分组之后对组进行过滤的,所以HAVING里可以使用聚合函数。

第二,SELECT的别名不能在WHERE中使用,但可以在ORDER BY中使用,因为SELECT在ORDER BY之前执行。

第三,GROUP BY之后SELECT能出现什么列,取决于分组依据。如果按customer_id分组,那么每组有唯一的customer_id,但amount有很多个值,直接SELECT amount没有意义。

现在看一个完整的例子:统计每个客户的订单总额和下单次数,只保留有效订单,最后按金额倒序。

SELECT customer_id, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM sales_order WHERE status = 1 GROUP BY customer_id HAVING total_amount > 1000 ORDER BY total_amount DESC;

执行流程拆解:

  • FROM sales_order:加载表数据
  • WHERE status = 1:筛掉无效订单
  • GROUP BY customer_id:把同一个客户的记录归为一组
  • 聚合计算:COUNT和SUM针对每组分别计算
  • HAVING total_amount > 1000:筛掉总额不足1000的组
  • SELECT输出:取需要的列
  • ORDER BY:按总额排序

这里有个注意点:HAVING里用了SELECT别名total_amount,MySQL允许HAVING引用SELECT别名,但标准SQL里更稳妥的写法是用聚合函数本身,即HAVING SUM(amount) > 1000。两种写法MySQL都支持,但理解它们的作用位置才是关键。

3.2 WHERE与HAVING:谁先执行、谁走索引、谁拖慢全表

对于初学者,最容易犯的错误就是把HAVING当成“能用在WHERE后面的条件”,在WHERE里写SUM(amount) > 1000,直接语法报错。反之,把本来应该在WHERE里过滤的条件放到HAVING里,SQL能跑,但性能可能会差很多。

为什么性能会差?因为WHERE在分组前过滤,减少了进入分组的数据量;HAVING在分组后过滤,意味着所有数据都必须先分组、聚合,然后才轮到它起作用。举个例子:

-- 推荐:先过滤状态,再分组 SELECT customer_id, COUNT(*) FROM sales_order WHERE status = 1 GROUP BY customer_id; -- 不推荐:所有状态都分组后再过滤 SELECT customer_id, COUNT(*) FROM sales_order GROUP BY customer_id HAVING ...;

能放到WHERE的条件,绝不放HAVING。这是分组查询优化的第一条铁律。

还有一个细节:WHERE条件如果在索引列上(比如status字段有索引),过滤阶段就能走索引,大幅减少需要分组的数据量。HAVING里的条件通常是对聚合结果的判断,无法走索引,只能内存中过滤。

3.3 GROUP BY与DISTINCT:去重这件事,选错就出事

GROUP BY可以用来去重,DISTINCT也可以。两者结果在某些场景相同,但使用场景完全不同。

比如查询所有不重复的客户ID:

SELECT DISTINCT customer_id FROM sales_order; SELECT customer_id FROM sales_order GROUP BY customer_id;

结果一样,但语义不同:DISTINCT是“去重后显示”,GROUP BY是“按这个字段分组聚合”。如果只是去重而没有聚合需求,用DISTINCT更直观。如果还需要统计每个客户的订单数,那必须用GROUP BY。

有个经典问题:GROUP BY和DISTINCT的性能哪个好?没有一个绝对答案。在MySQL 8.0里,如果GROUP BY不需要聚合计算,优化器有时会把GROUP BY转换为DISTINCT来执行。如果你只需要去重,直接写DISTINCT语义更清晰;如果你需要分组+聚合,就别想用DISTINCT实现了。

另外,DISTINCT和聚合函数可以组合,比如COUNT(DISTINCT customer_id)用来统计去重后的客户数。这也是个非常常用的写法,但注意它会对DISTINCT字段做排序或哈希操作,数据量大时成本不低,不要滥用。

3.4 分组后的排序问题:为什么ORDER BY有时候会失效

分组查询里一个常见的困扰是:我想取分组后每组最新的一条记录,直接GROUP BY + ORDER BY created_at DESC却拿不到预期结果。

看这个例子:

-- 意图:查出每个客户最近一笔订单 SELECT customer_id, order_no, MAX(created_at) FROM sales_order GROUP BY customer_id;

这条SQL的问题是MAX(created_at)只返回了日期,order_no不一定是对应最新订单的订单号。因为分组后每一组只有一行,order_no如果不在GROUP BY里,MySQL只会从这一组里随机挑一个值返回(在非严格模式下)。也就是说,order_no和MAX(created_at)之间没有关联关系。

正确的解法有几种。最简单的是子查询:先查出每个客户的最大时间,再用JOIN关联回原表取完整记录。

SELECT s.customer_id, s.order_no, s.created_at FROM sales_order s JOIN ( SELECT customer_id, MAX(created_at) AS max_time FROM sales_order GROUP BY customer_id ) t ON s.customer_id = t.customer_id AND s.created_at = t.max_time;

如果同一客户在同一秒下了多笔订单,这个写法可能返回多行,需要根据业务加上其他条件(比如用MAX(id)替代MAX(created_at))。这是一个非常典型的“分组取最新”问题,在订单系统、消息系统里极其常见,后面联合查询部分还会再提一次。

3.5 多列分组与空值分组:GROUP BY的边界场景

GROUP BY可以接多个列,比如按分类和状态两个维度统计:

SELECT category, status, COUNT(*), SUM(amount) FROM sales_order GROUP BY category, status;

多列分组时,MySQL把多个列的值当作一个组合来看待,只有所有列的值都相同才归为一组。这在统计报表中非常有用,比如“统计每个分类下有效和取消订单各有多少”。

另一个容易忽略的边界:GROUP BY列包含NULL。MySQL会把所有NULL值归为一组,显示在结果里。如果你不希望NULL单独成组,可以先用IFNULL或COALESCE把NULL替换成具体值,再分组:

SELECT IFNULL(category, '未知'), COUNT(*) FROM sales_order GROUP BY IFNULL(category, '未知');

这个细节在数据清洗场景中很实用,因为数据库里很多字段允许NULL,统计时如果不处理,报表上会莫名其妙多出一行NULL分组。

4. 联合查询:UNION把结果拼起来,JOIN把表拼起来

4.1 UNION与UNION ALL:一字之差,一次全表扫描的代价

联合查询(UNION)是把多个SELECT的结果纵向拼接在一起,要求每个SELECT的列数相同,且对应列的数据类型兼容。UNION和UNION ALL的区别,最核心的一句话:UNION会去重,UNION ALL不会。

看个实际例子。假设有两张结构相同的表,一张是2024年的订单历史表,一张是2025年的订单表,我要合并查询两年的客户ID列表:

SELECT customer_id FROM sales_order_2024 UNION SELECT customer_id FROM sales_order_2025;

如果用UNION,结果里的重复客户会被合并成一个;如果业务上需要“每个客户出现次数”,就必须用UNION ALL,因为UNION的隐式去重会破坏计数。

性能上,UNION比UNION ALL多一步去重操作,MySQL需要为结果集排序或建临时表来消除重复行,数据量大时差异很明显。所以一个优化经验是:如果你确定两个子查询的结果不会有重复,或者重复不影响业务,一律用UNION ALL。反过来,如果你确实需要去重,但可以用DISTINCT在子查询内部完成,也比UNION去重更可控。

还有一个排序的坑:如果要对UNION后的整体结果排序,ORDER BY必须放在最后一个SELECT后面,而不是每个子查询里单独写ORDER BY。而且单独用ORDER BY配合LIMIT时,要搞清楚LIMIT是对单个子查询还是对最终结果生效。MySQL的规则是,子查询里写ORDER BY会被优化器忽略,除非配合LIMIT才有意义。

(SELECT customer_id, amount FROM sales_order_2024 ORDER BY amount DESC LIMIT 10) UNION ALL (SELECT customer_id, amount FROM sales_order_2025 ORDER BY amount DESC LIMIT 10) ORDER BY amount DESC LIMIT 20;

上面这个写法表示:分别取两张表金额最高的10条,合并后再按金额排序取前20条。这种写法在排行榜场景非常实用,但一定要注意两边括号的使用和LIMIT的位置。

4.2 JOIN在此文中的定位:和UNION互补,别混为一谈

严格来说,JOIN(连接查询)属于多表查询的范畴,和UNION的“纵向拼接”不同,JOIN是“横向拼接”——把两张表的列按关联条件合在一起。虽然热搜词里单列了联合查询,但实际业务中UNION经常和JOIN组合使用。

一个典型场景:要查每个客户的最新一笔订单,同时显示客户姓名。这时需要先GROUP BY找出每个客户最新订单时间,再JOIN客户表取姓名,再JOIN订单表取完整订单信息。三张表联动,SQL会复杂不少,但逻辑上是清晰的分步:

SELECT c.customer_name, t.order_no, t.amount, t.created_at FROM customer c JOIN ( SELECT customer_id, order_no, amount, created_at FROM sales_order s JOIN ( SELECT customer_id, MAX(created_at) AS max_time FROM sales_order GROUP BY customer_id ) m ON s.customer_id = m.customer_id AND s.created_at = m.max_time ) t ON c.customer_id = t.customer_id;

第一层子查询负责找出每个客户的最新订单,第二层子查询负责把这笔订单的具体信息带出来,最后JOIN客户表补上客户姓名。这种“分组 + 子查询 + JOIN”的链式写法在处理报表类需求时几乎绕不开。

JOIN连接本身还需要注意连接类型:INNER JOIN只保留两表匹配的行,LEFT JOIN保留左表全部行、右表不匹配的补NULL。很多人在统计时用错连接类型,导致计数虚高或数据缺失。比如统计每个分类的订单数,如果用INNER JOIN关联分类表,某个一直没有订单的分类会被丢掉,报表上就少了一个分类。这种问题在业务里很难一眼发现,所以写之前要想清楚:从哪个表作为主体出发,用哪种连接方式。

4.3 IN与EXISTS:联合查询的另一种“联动”思路

在讨论联合查询时,很多人忽略了一个替代方案:IN和EXISTS子查询。它们也能实现“根据另一个集合来判断当前行是否满足条件”的需求,但执行方式不同。

比较两个写法:

-- 查询有订单的客户的姓名 SELECT customer_name FROM customer WHERE customer_id IN (SELECT customer_id FROM sales_order); -- 等价写法:EXISTS SELECT customer_name FROM customer c WHERE EXISTS ( SELECT 1 FROM sales_order s WHERE s.customer_id = c.customer_id );

IN子查询会把子查询结果集缓存起来,然后逐行判断;EXISTS是逐行对外层表做循环,每次判断子查询是否有返回。MySQL优化器对两者都会改写,实际性能差别在特定场景下存在,但最靠谱的方法是看执行计划EXPLAIN。

一个相对通用的经验:子查询结果集小(几百几千行)用IN没问题;外层表小、子查询表大且有索引,用EXISTS往往更优。但这只是经验之谈,真实项目还是以执行计划为准。

4.4 UNION与JOIN组合的真实业务案例:月度报表拼接

把三个查询能力放在一个案例里看。一个常见需求:按月汇总每个分类的销售总额,把1月和2月的数据拼接成一张报表,同时补充分类名称。

步骤拆解:

  1. 子查询A:统计1月各分类销售额
  2. 子查询B:统计2月各分类销售额
  3. UNION ALL:合并两个结果
  4. JOIN分类表:补分类名称
SELECT c.category_name, t.month_label, t.total_amount FROM category c JOIN ( SELECT category, '2025-01' AS month_label, SUM(amount) AS total_amount FROM sales_order WHERE created_at >= '2025-01-01' AND created_at < '2025-02-01' GROUP BY category UNION ALL SELECT category, '2025-02', SUM(amount) FROM sales_order WHERE created_at >= '2025-02-01' AND created_at < '2025-03-01' GROUP BY category ) t ON c.category_id = t.category ORDER BY t.month_label, t.total_amount DESC;

这个案例把聚合(SUM)、分组(GROUP BY)、联合(UNION ALL)、连接(JOIN)全部串在一起,同时也是月度报表的一种标准范式。我在实际项目中写过很多类似的SQL,一个通用的优化思路是:能用一条SQL完成的报表统计,就不要拆成多次查询在应用层拼接。数据库擅长集合运算,把计算下推到SQL里,不仅能减少网络开销,还能利用索引加速。

5. 组合查询的底层优化:从执行计划到索引利用

5.1 EXPLAIN:看到查询是如何被执行的,而不是猜

前面讲了这么多,最终落到性能上,离不开一个工具——EXPLAIN。在SQL前面加EXPLAIN,MySQL会返回执行计划,标明用了哪张表、走了哪个索引、扫描了多少行、有没有使用临时表或文件排序。

看一个分组查询的执行计划:

EXPLAIN SELECT customer_id, COUNT(*), SUM(amount) FROM sales_order WHERE status = 1 GROUP BY customer_id;

结果里重点看几列:

  • type:访问类型,从好到差依次是system、const、eq_ref、ref、range、index、ALL。出现ALL说明全表扫描,需要警惕。
  • possible_keys:可能用到的索引。
  • key:实际用到的索引。
  • rows:预估扫描行数。
  • Extra:额外信息,如果出现Using temporary或Using filesort,说明查询有临时表或文件排序,通常是性能瓶颈。

对于GROUP BY查询,MySQL 8.0之前最典型的优化是:如果GROUP BY的列上有索引,优化器可以直接利用索引的有序性完成分组,避免临时表和文件排序。比如在sales_order表上建一个(customer_id, status)的联合索引,上面的查询性能会好很多。

ALTER TABLE sales_order ADD INDEX idx_customer_status (customer_id, status);

注意联合索引的列顺序:customer_id在前,status在后,这与查询中WHERE和GROUP BY的使用顺序有关。联合索引的基本原则是“最左前缀”,查询条件里使用索引列的顺序需要和索引定义顺序一致才能充分利用。

5.2 临时表与文件排序:分组查询的两大隐形杀手

分组查询性能差的根源,通常集中在两个地方:临时表和文件排序。MySQL做GROUP BY时,如果无法利用索引的有序性,就需要把中间结果放进内存临时表;数据量大时临时表会溢出到磁盘,性能急剧下降。文件排序同理,GROUP BY结果需要排序时,如果数据量超过sort_buffer_size的设置,就会用磁盘文件排序。

这两个问题的直接应对手段就是上面说的索引。以GROUP BY customer_id为例,customer_id上有索引,MySQL扫描索引时数据本身就是按customer_id排好序的,分组操作可以直接顺序完成,既不需要临时表,也不需要排序。这就是为什么“分组字段建索引”能带来明显收益。

另外补充一个容易被忽视的细节:WHERE条件中的过滤字段如果能走索引,可以大幅减少GROUP BY要处理的数据量。所以联合索引的设计要同时考虑WHERE和GROUP BY两个环节。这也是为什么很多生产环境里的慢查询,最终优化方案都是加一个联合索引,而不是改写SQL。

5.3 聚合查询与索引:COUNT和SUM能走覆盖索引吗

聚合查询的索引利用有一个很实用的场景:覆盖索引。如果查询只需要从索引中就能取到所有数据,不需要回表,性能会有数量级的提升。

比如上面的分组统计:

SELECT customer_id, COUNT(*), SUM(amount) FROM sales_order GROUP BY customer_id;

如果有一个(customer_id, amount)的联合索引,MySQL可以从这个索引中直接读取customer_id和amount两列完成分组和聚合,完全不需要回表查主键。在EXPLAIN的Extra列会显示Using index,这就是覆盖索引的标志。

因此,在设计报表类查询的索引时,一个常见套路是:把WHERE过滤字段、GROUP BY分组字段、SELECT聚合字段全部塞进一个联合索引。索引占用空间会增加,但查询性能收益远大于写放大成本。

5.4 数据量增大时:聚合结果过大怎么办

当GROUP BY分组数量达到数万甚至数十万时,即使索引利用得当,聚合计算本身的开销也不小。这时候可以考虑分层聚合:先在明细表上做一次粗粒度汇总,存到统计表里,查询时直接读统计表。

比如订单表按天存储,每天有几十万行。如果要看“每个客户每月的总订单额”,可以每天凌晨跑一个定时任务,把当天的数据按客户+天聚合好存到汇总表。查询时直接对汇总表做GROUP BY,数据量从百万级降到几千级,速度自然快很多。

这种“预聚合”的思路在BI报表、数据仓库场景里非常常见。MySQL本身虽然不像ClickHouse那样为分析而生,但通过合理的分层聚合和索引设计,也能支撑千万级数据的日常报表查询。

6. 写在后面:几条我踩过坑总结出来的SQL习惯

最后分享几条我在实际项目中总结的经验,不一定写在教科书里,但绝对能帮你少走弯路。

第一,写聚合查询时,永远先想清楚是“对行过滤”还是“对组过滤”。WHERE干前者,HAVING干后者,位置错了要么报错,要么性能白丢。

第二,能用UNION ALL就别用UNION。除非业务明确要求去重,否则默认不加去重操作。等我排查一个慢SQL时发现,仅仅是UNION去重导致的临时表排序,就让查询从200ms变成了3秒。

第三,分组取最新记录,别总想着ORDER BY和LIMIT。正确姿势是子查询先取MAX,再关联回原表。这个坑在面试里出现频率极高,在实际业务里也几乎每周都能遇到。

第四,聚合函数返回NULL这件事,代码里要防。IFNULL(SUM(x), 0)这种写法看着啰嗦,但能帮你避免线上空指针告警。

第五,每次写完组合查询,养成跑EXPLAIN的习惯。看看有没有Using temporary、Using filesort,有没有走全表扫描。一次执行计划的检查,可能比调半天代码都管用。

SQL不是写出来能跑就算完,能跑得对、跑得快,才是合格的水平。聚合、分组、联合这三个能力,属于那种“看起来简单,用好了能顶半边天”的数据库基本功,值得多花时间打磨。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询