本节摘要:SELECT 是最常被轻视的语句——它看起来只是"取几列",实际上每个子句都有明确的物理落点:列清单决定列裁剪的范围、WHERE 决定过滤发生在哪一端、DISTINCT 引入一次隐藏 Shuffle、ORDER BY 独占一个阶段、LIMIT 在多数场景走取数捷径。本节用对照实验把"写法一变、计划就变"变成可验证的手感。
拿一条带全部常见子句的查询当解剖对象:
SELECT region, COUNT(DISTINCT customer_id) AS buyers FROM mall.orders WHERE dt = '2026-01-16' AND amount > 50 GROUP BY region HAVING COUNT(DISTINCT customer_id) > 100 ORDER BY buyers DESC LIMIT 20;
七个子句,物理落点各不相同:
同一个查询,七个子句在三处 Map 端、两次 Shuffle、一个 Reduce 端之间各就各位。读懂落点,就读懂了代价。
COUNT 普通列与 COUNT DISTINCT 列的翻译完全不同。普通 COUNT 配合 Map 端聚合:每个 Map 任务数自己的行数,网络上传的只是"每任务一个数"。DISTINCT 要求去重——同一个 customer_id 的行必须相遇才能判重,于是 Shuffle 键从"region 单键"变成"region 加 customer_id 组合键":先按组合键分布去重聚合成 region 加 customer_id 粒度的计数,再按 region 二次聚合。一次查询,两轮 Shuffle。
EXPLAIN 里它的指纹是两个 Group By Operator 串联,第一个的 keys 含 customer_id,第二个只剩 region,中间隔着一次 ReduceSink。代价直觉:DISTINCT 列基数越高,第一轮 Shuffle 的体量越大。
近似替代方案在精度允许时值得一试:COUNT DISTINCT 换成近似函数(如 approx_distinct,各版本名称略异),用 HyperLogLog 思路把去重压成每任务一份小状态,Shuffle 体量骤降,报表场景误差通常在百分之一量级内。
ORDER BY 保证全局有序,物理上只能让全部结果涌向单个 Reduce 任务排序——全局有序与并行计算天然冲突,结果集大时这个单点任务就是瓶颈。SORT BY 只保证每个 Reduce 任务内部有序,不保证全局;DISTRIBUTE BY 决定哪些行进哪个任务。三者组合成"分区内有序"的写法:
SET mapreduce.job.reduces = 4; SELECT customer_id, amount FROM mall.orders WHERE dt = '2026-01-16' DISTRIBUTE BY customer_id SORT BY amount DESC;
同一 customer 的行进同一任务、任务内按金额排序——下游做"每客户取前 N"这类需求时,这个组合比全局 ORDER BY 便宜得多。真正的"每组前 N"标准解法是窗口函数(下一节展开),但理解 DISTRIBUTE BY 与 SORT BY 的物理含义,是理解窗口函数翻译的前置课。
LIMIT 与 ORDER BY 组合时,Hive 有取数捷径:排序段按方向只保留前 N 行(Top-N 优化),排序任务不必物化全部结果。EXPLAIN 里会看到 Reduce 端带 limit 相关标记。但注意 LIMIT 无 ORDER BY 时只是"任取 N 行",各任务扫一小段即停,代价极低——这是抽样检查的廉价写法,但结果不具分布代表性(想要代表性抽样回看第 4.3 节的桶抽样)。
理论说尽,不如亲手做三个实验,每个都只改一处写法,EXPLAIN 对比计划差异。
实验一:星号改显式列。
EXPLAIN SELECT * FROM mall.orders WHERE dt = '2026-01-16'; EXPLAIN SELECT order_id, amount FROM mall.orders WHERE dt = '2026-01-16';
看第二个计划的 Select Operator expressions:只剩两列。若表是 ORC,没列出的列连磁盘都不读;TextFile 下也省掉序列化与网络。列数多的表上,这一处改写的收益经常超过任何参数调优。
实验二:子查询位置移动。
-- 写法 A 先过滤再连接 SELECT c.region, o.amount FROM (SELECT * FROM mall.orders WHERE dt = '2026-01-16' AND amount > 50) o JOIN mall.customers c ON o.customer_id = c.customer_id; -- 写法 B 先连接再过滤 SELECT c.region, o.amount FROM mall.orders o JOIN mall.customers c ON o.customer_id = c.customer_id WHERE o.dt = '2026-01-16' AND o.amount > 50;
两份 EXPLAIN 几乎一致——谓词下推规则会把 B 的条件下沉。优化器抹平了很多书写差异,这是现代 SQL 引擎的仁慈;但下推有边界(第 2 章讲过的双侧谓词推不动),重要查询仍要看计划确认,而不是假设仁慈永远在线。
实验三:LEFT JOIN 加 WHERE 的语义陷阱。
-- 写法 A 右表条件放 WHERE 连接语义被破坏 SELECT o.order_id, c.region FROM mall.orders o LEFT JOIN mall.customers c ON o.customer_id = c.customer_id WHERE c.region = 'north'; -- 写法 B 右表条件放 ON 保留左表全部行 SELECT o.order_id, c.region FROM mall.orders o LEFT JOIN mall.customers c ON o.customer_id = c.customer_id AND c.region = 'north';
A 的计划里,region 条件出现在 Join 之后(右表未匹配行的 region 是 NULL,过滤把它们全部丢弃),LEFT JOIN 实际退化成 INNER JOIN;B 的计划里条件参与连接过程,左表行保得住。同一意图、写法不同,计划与结果双变——这是 SQL 审计里最高频的语义事故之一。
把本节收进一份 SELECT 审查清单,与第 3 章六问互补: