多个COUNT(DISTINCT)会让查询变慢,因为数据库为每列单独启动去重流程,分别构建哈希表或排序结构,无法复用中间结果,导致IO、CPU和内存开销翻倍;优化方式是将各字段去重下沉至独立子查询预聚合,再轻量JOIN。

COUNT(DISTINCT) 多次出现,数据库大概率会为每个单独启动一次去重流程——不是“慢一点”,而是IO、CPU、内存开销直接翻倍。
为什么多个 COUNT(DISTINCT) 会让查询变慢
常见错误是写类似 COUNT(DISTINCT user_id), COUNT(DISTINCT order_id), COUNT(DISTINCT product_id) 这样的语句。数据库不会“合并去重”,而是分别构建三套哈希表或排序结构:每列都扫一遍全表(或分组内数据),中间结果无法复用。EXPLAIN 中反复看到 Using temporary; Using filesort 就是典型信号。
- MySQL/PostgreSQL 对每个
COUNT(DISTINCT col)都走独立聚合通道,不共享状态 - 若字段无索引,全表扫描触发三次;有索引也难避免多次随机访问
- 在 GROUP BY 场景下更糟:10 万个分组 × 3 次去重 = 30 万次哈希操作
用子查询预聚合替代多字段并列
把高成本的去重下沉到子查询,外层只做轻量 JOIN。这是最通用、跨数据库兼容的解法。
原始写法(低效):
SELECT channel, COUNT(DISTINCT user_id), COUNT(DISTINCT order_id), COUNT(DISTINCT product_id) FROM sales WHERE dt = '2026-09-15' GROUP BY channel;
优化后(推荐):
SELECT t1.channel, t1.user_cnt, t2.order_cnt, t3.product_cnt FROM ( SELECT channel, COUNT(DISTINCT user_id) AS user_cnt FROM sales WHERE dt = '2026-09-15' GROUP BY channel ) t1 JOIN ( SELECT channel, COUNT(DISTINCT order_id) AS order_cnt FROM sales WHERE dt = '2026-09-15' GROUP BY channel ) t2 ON t1.channel = t2.channel JOIN ( SELECT channel, COUNT(DISTINCT product_id) AS product_cnt FROM sales WHERE dt = '2026-09-15' GROUP BY channel ) t3 ON t1.channel = t3.channel;
- 每个子查询只扫一次数据,且可利用
(channel, user_id)等联合索引加速 - 结果集极小(比如几百个 channel),JOIN 几乎无压力
- 务必在每个子查询里加相同过滤条件(如
WHERE dt = ...),否则分区裁剪失效
PostgreSQL / MySQL 8.0+ 可用 CTE 提升可读性
逻辑不变,但用 WITH 把预聚合步骤显式拆开,避免嵌套过深。
WITH user_agg AS ( SELECT channel, COUNT(DISTINCT user_id) AS user_cnt FROM sales WHERE dt = '2026-09-15' GROUP BY channel ), order_agg AS ( SELECT channel, COUNT(DISTINCT order_id) AS order_cnt FROM sales WHERE dt = '2026-09-15' GROUP BY channel ) SELECT u.channel, u.user_cnt, o.order_cnt FROM user_agg u JOIN order_agg o ON u.channel = o.channel;
- CTE 不是视图,不会物化(除非加
MATERIALIZED),性能和子查询等价 - 调试时可单独运行每个 CTE,快速定位哪一列去重最慢
- 注意 PostgreSQL 中若 CTE 被多次引用,可能重复执行——此时应改用临时表
大数据量下慎用 CONCAT 模拟多列 DISTINCT
有人试图用 COUNT(DISTINCT CONCAT(user_id, '|', order_id)) 合并计算,但风险极高。
- 分隔符
|若出现在原始数据中(比如 user_id 是 JSON 字符串),会导致不同组合被误判为相同 - CONCAT 结果不可索引,无法利用覆盖索引加速
- 字符串拼接本身有 CPU 开销,尤其字段长或 NULL 多时
- 真正需要多列组合去重(如
(user_id, order_id)),应直接用原生语法:COUNT(DISTINCT user_id, order_id)(MySQL 8.0.22+)或COUNT(DISTINCT (user_id, order_id))(PostgreSQL)
真正卡点不在语法怎么写,而在是否让去重发生在最小可行数据集上——先过滤、再分组、最后去重,顺序错一步,代价就翻几倍。

















