6.1 SELECT的物理含义:投影与过滤的落点


6.1 SELECT 的物理含义:投影与过滤的落点

本节摘要: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;

七个子句,物理落点各不相同:

  • FROM 与 WHERE:dt 条件换算成分区裁剪(路径区只剩一个目录);amount 条件变成 Map 端 Filter(第 2 章的下推规则);
  • SELECT 列清单:决定列裁剪范围——本查询只需要三列,orders 表其余列在扫描层就被丢弃(列存格式下不读盘);
  • GROUP BY region:region 成为 Shuffle 分区键,配合两段式聚合;
  • COUNT DISTINCT customer_id:这个组合比普通聚合贵得多,下面单独拆;
  • HAVING:聚合后过滤,发生在 Reduce 端拿到聚合结果之后;
  • ORDER BY buyers DESC:全局排序,独立成段——所有聚合结果过一次全量 Shuffle 到单个 Reduce 任务排序;
  • LIMIT 20:排序后的截断,通常伴随取数捷径。

同一个查询,七个子句在三处 Map 端、两次 Shuffle、一个 Reduce 端之间各就各位。读懂落点,就读懂了代价

COUNT DISTINCT:隐藏在聚合里的第二次 Shuffle

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 与 SORT BY:全局与局部

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 章六问互补:

  1. 星号改显式列了吗(列裁剪是免费的提速);
  2. DISTINCT 用在什么基数上,是否可近似;
  3. ORDER BY 是真需求还是习惯,结果集多大,能否换 SORT BY 加 DISTRIBUTE BY;
  4. LIMIT 有没有配合 ORDER BY 走 Top-N;
  5. LEFT JOIN 的右表条件在 ON 还是在 WHERE,语义确认过吗;
  6. 子查询嵌套层数是否必要,优化器抹平了书写差异吗(EXPLAIN 确认)。

本节要点回顾

  • 落点意识:投影在扫描层、过滤多在 Map 端、聚合跨两端、排序独占一段,每个子句都有物理地址;
  • COUNT DISTINCT 双 Shuffle:组合键去重加二次聚合,高基数时考虑近似函数;
  • 全局有序是单点:ORDER BY 汇聚单任务,大结果集换 DISTRIBUTE BY 加 SORT BY;
  • LIMIT 双面性:配 ORDER BY 走 Top-N 捷径,无序 LIMIT 是廉价非代表抽样;
  • 下推抹平书写差异但有边界:写法 B 不必然慢,但重要查询必须 EXPLAIN 验证;
  • LEFT JOIN 陷阱:右表条件放 WHERE 会让外连接退化成内连接,计划与结果双变化。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U