先按查询形态拆表再谈统一检索——Hologres 里列存与检索条件的组合取舍

文章导读
检索条件越加越多时,先判断的不该是“要不要统一检索”,而是这些条件在查询里各自出现在哪一步。过滤类条件决定扫描范围,排序类条件决定要不要全局归并再取前 N 条,聚合类条件决定中间结果怎么收缩。三类混在同一份宽表上,扫描量、排序代价和更新代价会互相牵制;拆表或调整列存属性,都是为了把彼此冲突的形态分开。可以先按下面的顺序做:分类 → 记录表现 → 判断能否共表 → 组合取舍 → 同一批查询回验。
📋 目录
  1. 一 把现有查询按形态分成过滤、排序、聚合三类
  2. 二 在固定观察窗口内记录每类查询的表现
  3. 三 判断哪些条件适合放在同一份数据上
  4. 四 给出拆表与列存组合的判断表
  5. 五 用同一批查询验证改动前后的差别
A A

检索条件越加越多时,先判断的不该是“要不要统一检索”,而是这些条件在查询里各自出现在哪一步。过滤类条件决定扫描范围,排序类条件决定要不要全局归并再取前 N 条,聚合类条件决定中间结果怎么收缩。三类混在同一份宽表上,扫描量、排序代价和更新代价会互相牵制;拆表或调整列存属性,都是为了把彼此冲突的形态分开。可以先按下面的顺序做:分类 → 记录表现 → 判断能否共表 → 组合取舍 → 同一批查询回验。

判断方向:过滤、排序、聚合三类查询里,只有共享同一时间粒度、同一实体粒度、同一更新频率的条件,才建议继续放在同一份数据上;否则拆成服务不同查询形态的表,再配合列存属性(排序键、时间列、分布键)收敛扫描范围。适用场景是单表检索条件持续增加、查询延迟开始分化。操作上先分类和记录,再改一份数据,最后用同一批 SQL 对比耗时与结果条数。结论以自己环境的观察为准。

把现有查询按形态分成过滤、排序、聚合三类

不要笼统记“某个查询慢”,而是记清楚条件写在 SQL 的哪一步。过滤条件出现在 WHERE、JOIN 关联键、IN 列表里;排序条件出现在 ORDER BY 加 LIMIT;聚合条件出现在 GROUP BY 和聚合函数的输入列上。同一张表上跑的三类查询,扫描方式其实不一样。

查询形态条件落点业务查询描述(按自己业务替换)涉及字段条件数量
过滤类WHERE / JOIN 键查某商户某天一批订单的状态merchant_id、biz_date、order_id 列表3
排序类ORDER BY + LIMIT取某门店最近若干条变更记录shop_id、update_time2
聚合类GROUP BY + 聚合函数按天按渠道汇总成交金额与笔数stat_date、channel、amount、order_id2 个过滤 + 1 个分组

分类完成后,先扫一眼时间字段:过滤类用 biz_date,聚合类用 stat_date,如果两者是同一粒度,共表的基础就比较稳;如果一个是明细时间戳、一个是统计日,共表后时间列无法同时服务两类查询。

先按查询形态拆表再谈统一检索——Hologres 里列存与检索条件的组合取舍

在固定观察窗口内记录每类查询的表现

这一步的目的是拿到判断拆不拆表的输入数据。窗口要固定:同一时间段、同一批 SQL、同一份数据版本、同样的并发。可以用慢查询日志或对单条 SQL 做执行计划分析(例如在 Hologres 里对目标 SQL 执行 explain analyze,观察扫描行数与算子耗时)。只填实际观察到的值,不要填预期值。

查询类别 | 并发数 | 耗时区间 | 数据量级 | 结果条数
过滤类   |        |          |          |
排序类   |        |          |          |
聚合类   |        |          |          |

记录规则:
1. 同一个窗口内跑 2~3 遍,取稳定区间,不取最好的一次
2. 数据量级写实际扫描范围(分区/日期区间),不写全表总量
3. 结果条数必须记,它是后面回验是否改错的基准
4. 三类查询分别记录,混在一起看不出是哪一类在拖

判断哪些条件适合放在同一份数据上

避免为每个条件各建一份数据,那样写入链路和一致性成本会迅速上升。判断规则是三项同时成立才建议共表:条件是否共享同一时间粒度、是否共享同一实体粒度、是否共享同一更新频率。逐条对照:

先按查询形态拆表再谈统一检索——Hologres 里列存与检索条件的组合取舍
  • 商户维度 + 订单明细,都按天落库、都按商户聚合 → 时间粒度:是;实体粒度:是(商户/订单同属一个业务实体链);更新频率:是(同为 T+1 或同为实时写入)→ 可以共表。
  • 订单明细(按天,实时写入)+ 渠道日报(按天,T+1 批写)→ 时间粒度:是;实体粒度:部分是;更新频率:否 → 建议分开,日报单独落地。
  • 按 update_time 取最新的变更流 + 按 stat_date 做月度汇总 → 时间粒度:否;实体粒度:是;更新频率:否 → 建议拆开,实时表只保留变更所需列。

只要“更新频率”这一列出现否,共表后往往会为了补齐一份数据而引入额外的合并逻辑,得不偿失。

给出拆表与列存组合的判断表

Hologres 表默认按列存组织,适合宽表扫描和聚合;行存更适合按主键少量列的点查。列存属性可以按查询形态微调:分布键影响数据落在哪个分片,排序键(clustering_key)让范围过滤更有序,时间列(event_time_column)帮助按时间裁剪。下面这张表用于对照自己的查询形态,结论以自己环境观察为准。

先按查询形态拆表再谈统一检索——Hologres 里列存与检索条件的组合取舍
查询形态是否拆表理由(延迟 / 数据量 / 更新频率)列存配合
高频点查过滤(少量列 + 主键)可拆出小表延迟敏感,列多会放大扫描;数据量小、更新频繁考虑行存取向,或列存 + 分布键对齐主键
大范围过滤(时间 + 维度)不拆,留在明细表数据量大但列裁剪有效;更新频率与明细一致列存 + 排序键(时间/维度)+ 时间列
Top-N 排序视排序字段是否与过滤共享排序需要全局归并,延迟随结果集增大;更新频率高时归并代价更明显排序键前置,尽量让 ORDER BY 与排序键同序
聚合类(按天按维度)建议拆出汇总表更新频率与明细不同,数据量小、复用高列存 + 按分组维度做分布键,减少跨分片聚合
-- 调整列存属性的通用骨架,字段与键按自己表替换
-- 放在建表或改表阶段执行,执行后用 explain 观察是否走了排序键
call set_table_property('dwd_order_detail', 'orientation', 'column');
call set_table_property('dwd_order_detail', 'distribution_key', 'merchant_id');
call set_table_property('dwd_order_detail', 'clustering_key', 'biz_date,merchant_id');
call set_table_property('dwd_order_detail', 'event_time_column', 'biz_date');

用同一批查询验证改动前后的差别

验证方式:把分类阶段整理的那批 SQL 原样保留,改动前跑一遍留底,改动后再跑同一批,同一窗口、同一并发,只比较耗时与结果条数。结果条数不一致说明拆分逻辑或过滤条件写错了,比耗时更值得先查。

查询类别改动前耗时改动后耗时改动前结果条数改动后结果条数是否一致
过滤类
排序类
聚合类

如果只有某一类变快、另外两类持平或变差,说明拆分只对部分形态有效,可以按形态继续分化,而不是一次性全表重做。回验时至少跑两遍,避免把缓存或偶发波动当成结构性改善;改动是否值得保留,取决于三类查询在自己环境里的整体表现,而不取决于单次数字。