SQL查询总出错?搞懂SELECT执行顺序,告别WHERE和GROUP BY报错
我以前写了一个看似简单的查询:“统计每个店铺销售额大于5000元的订单数量”。我写的是:
sql
SELECT shop_name, COUNT(*) FROM orders WHERE amount > 5000 GROUP BY shop_name;
结果返回的是空。我检查了半天,发现列里存的是字符串,包含“¥”符号。但这还不是最坑的。后来我学会了WHERE不能用在聚合后的结果上,又写了:
sql
SELECT shop_name, COUNT(*) AS order_cnt FROM orders GROUP BY shop_name WHERE order_cnt > 5000;
结果直接报错。原因是我根本不懂语句的执行顺序——WHERE在GROUP BY之前执行,那时候还没有这个别名。
很多SQL初学者写的查询,逻辑上觉得对,但执行结果总是不对。根本原因是:人脑是从左到右读SQL,但数据库是从下到上、从右到左执行SQL。这一章彻底拆解语句的完整执行顺序,搞懂它,你就能写出正确的复杂查询,也能快速定位报错原因。
学习前准备:已完成MySQL环境搭建,导入电商订单样例数据(包含字段:, , , , ),无其他要求。
语句执行顺序的核心意义
为什么执行顺序是所有SQL查询的基础
SQL语句的书写顺序和实际执行顺序完全不同。书写顺序是:
sql
SELECT DISTINCT 列名
FROM 表名
JOIN 另一张表
WHERE 条件
GROUP BY 分组列
HAVING 分组后条件
ORDER BY 排序列
LIMIT 数量;
但数据库的执行顺序是:
不懂这个顺序,你会遇到三类高频问题:
在WHERE中用了聚合函数(如SUM() > 1000),报错
在WHERE中用了阶段才产生的别名(如WHERE total > 1000),报错
对GROUP BY的结果再筛选时用了WHERE而不是,结果错误
真实踩坑案例
有一次我统计“每个店铺下单次数超过100次的店铺”,写了:
sql
SELECT shop_name, COUNT(*) AS order_cnt
FROM orders
GROUP BY shop_name
WHERE order_cnt > 100;
报错: '' in 'where '。因为WHERE在第2步执行,而在第5步才产生。正确应该用。
我的踩坑经历:刚学会GROUP BY时,我想筛选出总金额大于10000的店铺,写成了WHERE SUM(amount) > 10000,结果报错。折腾了一个下午才明白WHERE不能跟聚合函数,要用HAVING。从那以后,我把执行顺序图贴在工位旁边。
执行顺序全拆解(整体框架)
SQL语句按执行优先级从高到低排列如下:
执行步骤
子句
作用
简单理解
FROM / JOIN
确定数据来源
先把要用的表都找出来,关联好
WHERE
按行筛选
把不需要的行扔掉
GROUP BY
分组
把相同值的行合并成一组
按组筛选
把不需要的组扔掉
选择列、计算表达式
挑出要看的列,算别名、聚合
去重
去掉重复行
ORDER BY
排序
按指定列排顺序
LIMIT
限制行数
只取前N行
步骤1:FROM / JOIN阶段
核心逻辑
确定查询的数据来源,如果有JOIN,则在此时将多张表按照关联条件合并成一张虚拟表。
电商场景实操
假设有两张表:(订单表)和users(用户表),需要关联查询。
sql
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.user_id;
执行过程:
从表取出所有行
对每一行,在users表中找到匹配的行
将匹配的两行数据横向拼接,形成一张临时虚拟表(包含两张表的所有列)
分步操作
步骤1:确认当前数据库中有和users表。
sql
SHOW TABLES;
步骤2:执行关联查询,不写WHERE和GROUP BY,观察结果行数。
sql
SELECT COUNT(*) FROM orders o JOIN users u ON o.user_id = u.user_id;
预期结果:返回关联后的总行数(可能小于行数,如果有些用户不在users表中)。
避坑提醒
如果JOIN条件写错(如漏了关联字段),会产生笛卡尔积,行数爆炸。
LEFT JOIN和INNER JOIN结果不同,要根据业务需求选择。
在FROM阶段,表的别名(如o、u)已经生效,可以在后续所有子句中使用。
步骤2:WHERE阶段
核心逻辑
对FROM/JOIN产生的虚拟表按条件进行行级过滤。只有满足条件的行才会进入下一步。
电商场景实操
查询2025年6月已支付的订单。
sql
SELECT *
FROM orders
WHERE order_status = '已支付'
AND create_time >= '2025-06-01'
AND create_time < '2025-07-01';
执行过程:
从表中逐行检查和
只有两列都满足条件的行才保留
分步操作
步骤1:先不加WHERE,看全表数据量。
sql
SELECT COUNT(*) FROM orders;
步骤2:加上WHERE,看过滤后数据量。
sql
SELECT COUNT(*) FROM orders
WHERE order_status = '已支付'
AND create_time >= '2025-06-01';
预期结果:行数明显减少。
避坑提醒
WHERE中不能使用聚合函数(SUM、AVG、COUNT等),因为聚合是在后面的GROUP BY阶段才执行。
中不能使用阶段才产生的别名。比如 * 0.9 AS ,不能在WHERE中用。
字符串比较注意大小写和空格,MySQL默认不区分大小写但区分末尾空格。
我的踩坑经历:有一次在WHERE里写create_time = '2025-06-01',但表中create_time是DATETIME类型,包含时分秒。结果只查到了0点0分0秒的数据,漏了当天的其他订单。后来改成create_time >= '2025-06-01' AND create_time < '2025-06-02'才正确。

步骤3:GROUP BY阶段
核心逻辑
按照指定的列对数据进行分组,每个分组内的行具有相同的分组列值。分组后,后续的聚合函数(SUM、COUNT、AVG等)会在每个组内计算。
电商场景实操
按店铺分组统计GMV和订单量。
sql
SELECT shop_name, SUM(amount) AS gmv, COUNT(*) AS order_cnt
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
执行过程:
先通过WHERE筛选出已支付订单
再按分组,将同一个店铺的所有订单归为一组
对每个组分别计算SUM()和COUNT(*)
分步操作
步骤1:只分组不聚合,观察中间结果(实际上不能直接看,但可以用模拟)。
sql
SELECT shop_name, GROUP_CONCAT(amount) AS amounts
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
步骤2:加上聚合函数,得到最终统计。
sql
SELECT shop_name, SUM(amount), COUNT(*)
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
避坑提醒
子句中出现的非聚合列,必须出现在GROUP BY子句中。否则MySQL会返回不确定的值(在其他数据库中会报错)。
GROUP BY可以在分组列上使用表达式,如GROUP BY DATE()。
分组后,只能选择分组列和聚合结果,不能选择其他非聚合列。
步骤4:阶段
核心逻辑
对GROUP BY产生的分组结果进行组级过滤。只有满足条件的分组才会保留。
与WHERE的核心区别
对比维度
WHERE
执行时机
在GROUP BY之前
在GROUP BY之后
过滤对象
原始行
分组后的组
能否使用聚合函数
不能
能否使用列别名
不能(别名未生成)
能(已执行)
电商场景实操
查询GMV大于10000的店铺。
sql
SELECT shop_name, SUM(amount) AS gmv
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name
HAVING gmv > 10000;
注意:这里用了别名gmv,因为在之后执行,别名已经生效。
分步操作
步骤1:先不加,查看所有店铺的GMV。
sql
SELECT shop_name, SUM(amount) AS gmv
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
步骤2:加上 gmv > 10000,只保留大额店铺。
避坑提醒
如果不需要分组,只是要筛选聚合结果,WHERE用不上,只能用。
会降低查询性能,因为需要先完成分组和聚合。尽量在WHERE阶段过滤掉更多行,减少分组数据量。
我的踩坑经历:我曾经写HAVING shop_name = '女装旗舰店',虽然语法正确,但性能很差。因为HAVING在分组后执行,即使只有一个店铺,也先全部分组。正确的做法是把shop_name = '女装旗舰店'放到WHERE里。
步骤5:阶段
核心逻辑
确定最终要返回的列,计算表达式和别名。此时可以访问前面步骤产生的所有列(包括分组列和聚合结果)。
电商场景实操
计算每个店铺的客单价(GMV / 订单数)。
sql
SELECT
shop_name,
SUM(amount) AS gmv,
COUNT(*) AS order_cnt,
SUM(amount) / COUNT(*) AS avg_order_value
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
执行过程:阶段计算SUM() / COUNT(*),并给列起别名。
分步操作
步骤1:先写基础查询,不加计算列。
sql
SELECT shop_name, SUM(amount), COUNT(*)
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
步骤2:添加别名和计算列。
sql
SELECT
shop_name,
SUM(amount) AS gmv,
COUNT(*) AS order_cnt,
ROUND(SUM(amount) / COUNT(*), 2) AS avg_price
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name;
避坑提醒
别名在当前阶段生成,可以在ORDER BY、LIMIT中使用,但不能在同一个的其他列中使用(如 AS a, a * 0.9 AS b会报错,因为a还未生成)。
尽量使用有意义的别名,不要用a、b。
步骤6-8: / ORDER BY / LIMIT阶段
阶段
去除结果集中完全相同的行。因为是在之后执行,所以可以基于列别名去重。
电商场景:查询有过购买记录的用户ID(去重)。
sql
SELECT DISTINCT user_id FROM orders WHERE order_status = '已支付';
执行过程:先查询出所有已支付订单的用户ID(可能有重复),然后去掉重复值。
避坑提醒:会对所有选择的列组合去重,不是只对第一列。 , 会去掉(, )完全相同的行。
ORDER BY阶段
对结果集进行排序。可以使用列名、表达式或别名。
电商场景:按GMV降序排列店铺。
sql
SELECT shop_name, SUM(amount) AS gmv
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name
ORDER BY gmv DESC;
执行过程:在之后,gmv别名已经生成,可以直接用于排序。
避坑提醒:
排序的列不一定在中,但通常建议包含以便理解。
对大量数据排序会消耗内存,建议配合LIMIT使用。
LIMIT阶段
限制返回的行数,常用于分页或查看前N条记录。
电商场景:查看GMV最高的3个店铺。
sql
SELECT shop_name, SUM(amount) AS gmv
FROM orders
WHERE order_status = '已支付'
GROUP BY shop_name
ORDER BY gmv DESC
LIMIT 3;
执行过程:先排序,再取前3行。
避坑提醒:
LIMIT是最后执行的,所以不会影响前面步骤的处理行数,只是截取最终结果。
分页查询用LIMIT , count,注意从0开始。
综合实操案例:618大促订单数据多条件分组统计
案例背景























