目标
不做基数画像就做存储设计,本质是披着架构图的猜测。四 个取值且其中一个占 94% 的列、两百万唯一值的用户列、380 种设备的列,是三种完全不同的设计输入——而且每一个都能用一条查询量出来。让度量先行,索引与约束才有依据。
步骤要点
- 用 DuckDB 对每个关键列算去重数、空值数与总数,得到一张列统计表作为设计输入。
- 对低基数列继续测频率偏斜:某值占 94% 意味着全量 btree 无选择性,而只针对稀有值的部分索引反而出色。
- 按量出的数字导出设计:status 加 CHECK 约束不建全量索引,failed 建部分索引只扫 6% 行,user_id 与时间建复合索引服务范围查询。
- 抽样统计与全量对比验证:全量去重数与样本在预期范围内一致,画像可以外推后再落设计。
- 把画像 SQL 作为 schema 变更的附件入库,记录采样日期与比例,下次重设计从证据出发。
排错提示
去重计数爆内存就对高基数列用 APPROX_COUNT_DISTINCT 或抽样估计,精确计数只留给驱动枚举决策的低基数列;样本说 4 个状态生产出现 6 个,是样本漏了稀有值,状态全集从约束流程枚举,把基数下界当下界对待;频率偏斜随季节变就在旺季与淡季各画像一次再选部分索引谓词,十月稀有的状态一月可能主导。
适用边界
基数只是一个维度,更新频率与行宽同样影响索引选择,需一并权衡;抽样统计随数据增长漂移,量级里程碑时要安排画像重跑。
代码示例
column distincts nulls top frequency
-------- --------- ----- ---------------------
status 4 0% completed 94.1%
user_id 2.1M 0% (flat)
device 380 1.9% iOS 41.2%
informed design:
status -> CHECK constraint, no btree (4 values: useless)
failed -> partial index (6% of rows, real selectivity)
user+time -> composite (created, user_id) for range queries
vs. the previous design: btree(status) -- full-scans anyway