列存储索引无法缓解聚合倾斜,需通过数据分布优化、执行计划干预及有序输入触发Stream Aggregate来解决;低基数分组、隐式转换、LOB类型和谓词未下推是主要诱因。

聚合结果倾斜时,列存储索引本身不缓解,得靠数据分布和执行计划干预
列存储索引不会自动“均衡”聚合结果分布。所谓“倾斜”,本质是 GROUP BY 键值分布极不均匀(比如 90% 的行分到同一个 status = 'Pending' 组),导致 Hash Aggregate 运算符中一个线程处理远超平均的数据量。此时即使开了批处理模式,CPU 利用率也会严重不均,整体耗时拉长。
关键判断:先确认是不是真倾斜——在 SSMS 中执行 SET STATISTICS XML ON,查看执行计划里 Hash Aggregate 节点的 ActualRows 和 ActualRebinds。若某次 Rebind 返回数万行,而其他 Rebind 多为个位数,就是典型倾斜。
- 避免在 GROUP BY 中使用低基数列(如只有 2–3 个取值的 flag 列),除非业务强依赖;否则改用高选择性列或组合列(如
GROUP BY status, YEAR(order_date)) - 对高频倾斜键做预过滤:例如已知
status = 'Pending'占比极高,可拆成两路查询:SELECT ... WHERE status != 'Pending' GROUP BY ...+ 单独查status = 'Pending'的聚合,再UNION ALL - 禁用并行(
OPTION (MAXDOP 1))有时反而更快——因为单线程 Stream Aggregate 可规避哈希桶分配不均问题,前提是输入已按分组列排序(见下一条)
Stream Aggregate 能绕过倾斜,但只在严格有序输入时出现
Stream Aggregate 不建哈希表,而是逐行归并,天然不受键值分布影响。但它不会被“强制启用”,只在优化器确信上游数据已按 GROUP BY 列物理排序时才选它。
- 必须有覆盖索引:例如
GROUP BY customer_id, order_date,就要建CREATE INDEX IX_cust_date ON dbo.orders (customer_id, order_date),且不能含计算列或隐式转换 - 避免破坏排序链:不要写
GROUP BY UPPER(status)或WHERE id = '123'(int 列被转字符串),这类操作会切断索引顺序传递 - 显式
ORDER BY有用但有限:加GROUP BY status ORDER BY status可能触发 Sort + Stream Aggregate,但 Sort 本身开销大,仅适用于小结果集
批处理模式下,LOB 类型或字符串聚合函数会直接退化为行模式
哪怕表有聚集列存储索引、兼容级别 160、CPU 支持 AVX2,只要聚合涉及 NVARCHAR(MAX)、JSON_OBJECTAGG 或 STRING_AGG,执行计划里 Hash Aggregate 节点的 BatchMode 属性一定是 false——这时你看到的“快”,其实是列存储压缩带来的 I/O 减少,不是批处理加速。
- 检查字段类型:用
SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'YourTable'确认分组/聚合字段不是TEXT、NVARCHAR(MAX)、XML - 替换 JSON/STRING_AGG:如果只是拼接 ID 列表,改用
CONCAT+ 子查询;若必须用STRING_AGG,考虑先用 CTE 或临时表把原始数据按分组预聚合成小结果集,再在外层调用 - 警惕隐式转换:
WHERE code = 123对VARCHAR列会触发转换,导致谓词无法下推,连带影响后续聚合是否进批模式
行组消除失效时,“聚合快”只是假象
看起来 SELECT COUNT(*) FROM orders_cs WHERE status = 'Shipped' 很快,但如果实际扫描了全部行组(查 sys.dm_db_column_store_row_group_physical_stats 发现 deleted_rows 高但 size_in_bytes 没降),说明谓词根本没下推,只是靠列压缩解压快“蒙混过关”。一旦加上 GROUP BY,性能断崖下跌。
- 验证谓词是否下推:运行查询后立即查
SELECT object_id, row_group_id, deleted_rows, size_in_bytes FROM sys.dm_db_column_store_row_group_physical_stats WHERE object_id = OBJECT_ID('orders_cs'),若多数行组deleted_rows > 0但size_in_bytes接近满值,就说明过滤没生效 - 修复插入顺序:用
SELECT * INTO #staging FROM orders ORDER BY status,再CREATE CLUSTERED COLUMNSTORE INDEX,比重建索引更可靠 - 别依赖 REORGANIZE:它不重排行组内数据,只合并删除标记,对倾斜无改善
真正卡住性能的,往往不是“有没有列存储索引”,而是“谓词能否下推”和“GROUP BY 输入是否有序”这两件事没对齐。一个 UPPER(col) = 'X' 就能让整条批处理流水线停摆,而一个没对齐的索引顺序会让 Stream Aggregate 永远不会出现在执行计划里。










