检索条件越加越多时,先判断的不该是“要不要统一检索”,而是这些条件在查询里各自出现在哪一步。过滤类条件决定扫描范围,排序类条件决定要不要全局归并再取前 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_time | 2 |
| 聚合类 | GROUP BY + 聚合函数 | 按天按渠道汇总成交金额与笔数 | stat_date、channel、amount、order_id | 2 个过滤 + 1 个分组 |
分类完成后,先扫一眼时间字段:过滤类用 biz_date,聚合类用 stat_date,如果两者是同一粒度,共表的基础就比较稳;如果一个是明细时间戳、一个是统计日,共表后时间列无法同时服务两类查询。
在固定观察窗口内记录每类查询的表现
这一步的目的是拿到判断拆不拆表的输入数据。窗口要固定:同一时间段、同一批 SQL、同一份数据版本、同样的并发。可以用慢查询日志或对单条 SQL 做执行计划分析(例如在 Hologres 里对目标 SQL 执行 explain analyze,观察扫描行数与算子耗时)。只填实际观察到的值,不要填预期值。
查询类别 | 并发数 | 耗时区间 | 数据量级 | 结果条数
过滤类 | | | |
排序类 | | | |
聚合类 | | | |
记录规则:
1. 同一个窗口内跑 2~3 遍,取稳定区间,不取最好的一次
2. 数据量级写实际扫描范围(分区/日期区间),不写全表总量
3. 结果条数必须记,它是后面回验是否改错的基准
4. 三类查询分别记录,混在一起看不出是哪一类在拖判断哪些条件适合放在同一份数据上
避免为每个条件各建一份数据,那样写入链路和一致性成本会迅速上升。判断规则是三项同时成立才建议共表:条件是否共享同一时间粒度、是否共享同一实体粒度、是否共享同一更新频率。逐条对照:
- 商户维度 + 订单明细,都按天落库、都按商户聚合 → 时间粒度:是;实体粒度:是(商户/订单同属一个业务实体链);更新频率:是(同为 T+1 或同为实时写入)→ 可以共表。
- 订单明细(按天,实时写入)+ 渠道日报(按天,T+1 批写)→ 时间粒度:是;实体粒度:部分是;更新频率:否 → 建议分开,日报单独落地。
- 按 update_time 取最新的变更流 + 按 stat_date 做月度汇总 → 时间粒度:否;实体粒度:是;更新频率:否 → 建议拆开,实时表只保留变更所需列。
只要“更新频率”这一列出现否,共表后往往会为了补齐一份数据而引入额外的合并逻辑,得不偿失。
给出拆表与列存组合的判断表
Hologres 表默认按列存组织,适合宽表扫描和聚合;行存更适合按主键少量列的点查。列存属性可以按查询形态微调:分布键影响数据落在哪个分片,排序键(clustering_key)让范围过滤更有序,时间列(event_time_column)帮助按时间裁剪。下面这张表用于对照自己的查询形态,结论以自己环境观察为准。
| 查询形态 | 是否拆表 | 理由(延迟 / 数据量 / 更新频率) | 列存配合 |
|---|---|---|---|
| 高频点查过滤(少量列 + 主键) | 可拆出小表 | 延迟敏感,列多会放大扫描;数据量小、更新频繁 | 考虑行存取向,或列存 + 分布键对齐主键 |
| 大范围过滤(时间 + 维度) | 不拆,留在明细表 | 数据量大但列裁剪有效;更新频率与明细一致 | 列存 + 排序键(时间/维度)+ 时间列 |
| 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 原样保留,改动前跑一遍留底,改动后再跑同一批,同一窗口、同一并发,只比较耗时与结果条数。结果条数不一致说明拆分逻辑或过滤条件写错了,比耗时更值得先查。
| 查询类别 | 改动前耗时 | 改动后耗时 | 改动前结果条数 | 改动后结果条数 | 是否一致 |
|---|---|---|---|---|---|
| 过滤类 | |||||
| 排序类 | |||||
| 聚合类 |
如果只有某一类变快、另外两类持平或变差,说明拆分只对部分形态有效,可以按形态继续分化,而不是一次性全表重做。回验时至少跑两遍,避免把缓存或偶发波动当成结构性改善;改动是否值得保留,取决于三类查询在自己环境里的整体表现,而不取决于单次数字。