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 列存如何跳过无关数据?
列存能跳过无关数据,靠的是三个层次的能力:
- 列级跳过:只读需要的列,其他列直接跳过。这是投影下推的基础。
- 块级跳过:每列数据按块组织,每个块有统计信息(min/max/null count)。过滤时先看块级统计,不满足条件的块直接跳过。
- 行级跳过:块内数据可以进一步用索引或位图过滤,精确定位到需要的行。
我习惯把这三层跳过比作「筛子」:第一层筛掉整列,第二层筛掉整块,第三层筛掉无关行。每层筛完,剩下的数据量都大幅减少。
个人经验:块级跳过是列存最实用的优化。我建议你在设计列存表时,按常用过滤字段排序数据,让块内数据的 min/max 范围尽量小。这样块级跳过的命中率会更高。
4.4 两种下推的协同工作
投影下推和谓词下推不是孤立的,它们协同工作,效果叠加。
流程是这样的:
- 第一步,投影下推确定需要读取的列集合
- 第二步,谓词下推在过滤列上做块级跳过,确定需要读取的行范围
- 第三步,根据行范围,只读取目标列中对应的数据块
说白了,就是先确定「读哪几列」,再确定「读哪几行」,最后只读「这几列中这几行」的数据。每一步都在减少 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 倍。
嗯,这就是列存能实现秒级查询响应的核心原因。不是硬件变快了,而是需要读的数据量变少了。
一句话总结:投影下推让你只读需要的列,谓词下推让你只读需要的行。列存把这两个优化发挥到了极致,因为它的存储结构天然支持按列、按块、按行三级跳过。无关数据?直接跳过,连读都不读。