4. 查询加速:投影下推与谓词下推,为什么列存能跳过无关数据?

好,咱们进入正题。上一章聊了列存的基础存储结构,这一章我们聊聊它怎么让查询飞起来的——说白了,就是两个核心优化:投影下推和谓词下推。

你想想看,一个查询来了,数据库要干两件事:
第一,确定要读哪些列(投影)。
第二,确定要读哪些行(谓词过滤)。

行存怎么做?它得把整行数据拉出来,再扔掉不需要的列和行。列存呢?它可以直接跳过那些无关的数据块。嗯,这就是差距的根源。

4.1 投影下推:只读你需要的列

先看一个简单查询:

SELECT name, age FROM users WHERE city = 'Beijing';

这个查询只关心 name、age、city 三列。但行存表有 20 列,每行数据都完整存储。数据库不得不把整行数据从磁盘读到内存,再从中提取出这三列。剩下的 17 列数据,白白读了一遍,又白白扔掉。

我在项目中遇到过这样一个场景:一个用户行为表有 50 多列,业务方只查两三个字段做聚合。行存下,每次查询都要扫全表,IO 开销大得吓人。后来换成列存,只读那两三列的数据块,查询时间从分钟级降到了秒级。

列存下,投影下推的逻辑很简单:

  • 解析 SQL,提取出需要的列列表
  • 只加载这些列对应的数据块
  • 其他列的数据块,碰都不碰

核心收益:列存天然支持按列读取,投影下推让 IO 量从「全行宽度」降为「所需列宽度」。列数越多,收益越明显。

举个例子,假设一张表有 100 列,每行 1KB。查询只取 3 列(约 30 字节)。行存要读 1KB/行,列存只读 30 字节/行。IO 量差了 30 多倍。你想想看,这差距有多大。

4.2 谓词下推:过滤条件提前执行

投影下推解决了「读哪些列」的问题。谓词下推解决的是「读哪些行」的问题。

还是那个查询:

SELECT name, age FROM users WHERE city = 'Beijing';

行存的做法:把整张表的所有行都读出来,逐行判断 city = 'Beijing',符合条件的保留,不符合的丢弃。这意味着大量无关数据被读入内存,然后被过滤掉。

列存的做法:只读取 city 列的数据块,在数据块级别就做过滤。不满足条件的行,对应的 name 和 age 数据块根本不用读。

我曾经踩过一个坑:早期用列存时,以为列存会自动做谓词下推,结果发现查询还是很慢。排查后发现,SQL 里写了 WHERE UPPER(city) = 'BEIJING',函数包裹导致下推失效。嗯,这里要注意——谓词下推对表达式有要求,不是所有过滤条件都能下推。

4.3 列存如何跳过无关数据?

列存能跳过无关数据,靠的是三个层次的能力:

  1. 列级跳过:只读需要的列,其他列直接跳过。这是投影下推的基础。
  2. 块级跳过:每列数据按块组织,每个块有统计信息(min/max/null count)。过滤时先看块级统计,不满足条件的块直接跳过。
  3. 行级跳过:块内数据可以进一步用索引或位图过滤,精确定位到需要的行。

我习惯把这三层跳过比作「筛子」:第一层筛掉整列,第二层筛掉整块,第三层筛掉无关行。每层筛完,剩下的数据量都大幅减少。

个人经验:块级跳过是列存最实用的优化。我建议你在设计列存表时,按常用过滤字段排序数据,让块内数据的 min/max 范围尽量小。这样块级跳过的命中率会更高。

4.4 两种下推的协同工作

投影下推和谓词下推不是孤立的,它们协同工作,效果叠加。

流程是这样的:

  • 第一步,投影下推确定需要读取的列集合
  • 第二步,谓词下推在过滤列上做块级跳过,确定需要读取的行范围
  • 第三步,根据行范围,只读取目标列中对应的数据块

说白了,就是先确定「读哪几列」,再确定「读哪几行」,最后只读「这几列中这几行」的数据。每一步都在减少 IO 量。

下面这张图展示了完整的执行流程:

投影下推与谓词下推协同流程 投影下推 + 谓词下推 协同流程 SQL 查询解析 投影下推 确定需要读取的列集合 谓词下推 确定需要读取的行范围 列级跳过:只读目标列 块级跳过:过滤数据块 协同执行 只读目标列中目标行的数据块 IO 量大幅减少 → 秒级响应 每一步都在减少 IO 量:列级跳过 → 块级跳过 → 行级跳过,三层过滤后只剩目标数据

4.5 行存 vs 列存:下推效果对比

为了让你更直观地理解差距,我整理了一个对比表:

对比维度 行存 列存
投影下推 必须读整行,再丢弃无关列 只读目标列的数据块
谓词下推 读所有行到内存再过滤 在数据块级别提前过滤
块级跳过 不支持(行存无块级统计) 利用 min/max 跳过无关块
IO 量(100列取3列) 100% 数据量 约 3% 数据量
过滤效率 线性扫描,无法跳过 层级跳过,效率指数级提升

避坑提醒:我曾经遇到一个案例,同事在列存表上用了 SELECT *,结果投影下推完全失效,所有列都被加载。列存虽然存储效率高,但 SELECT * 会破坏投影下推的优势。我建议你始终明确指定需要的列,别偷懒用星号。

4.6 实际场景中的效果

说个真实数据。我之前维护过一个广告点击日志系统,表有 80 列,每天新增 5 亿行。业务查询通常是:

SELECT campaign_id, clicks, cost 
FROM ad_logs 
WHERE date = '2024-01-15' AND platform = 'iOS';

行存下,这个查询要扫描全表 80 列 × 5 亿行,IO 量大约 400GB。即使有索引,也要回表读取整行数据,实际 IO 在 200GB 以上。

列存下,投影下推只读 3 列,谓词下推在 date 和 platform 列上做块级跳过。实际读取的数据量只有 3 列中满足条件的行,大约 500MB。IO 量从 200GB 降到 500MB,差了 400 倍。

嗯,这就是列存能实现秒级查询响应的核心原因。不是硬件变快了,而是需要读的数据量变少了。

一句话总结:投影下推让你只读需要的列,谓词下推让你只读需要的行。列存把这两个优化发挥到了极致,因为它的存储结构天然支持按列、按块、按行三级跳过。无关数据?直接跳过,连读都不读。