PostgreSQL中COUNT(DISTINCT (col1, col2))因NULL导致结果偏低,应改用子查询:SELECT COUNT(*) FROM (SELECT DISTINCT col1, col2 FROM t) AS s;该写法语义准确、兼容性强,且正确处理NULL。

COUNT(DISTINCT (col1, col2)) 在 PostgreSQL 中不报错但结果不对
PostgreSQL 从 9.5 开始支持 COUNT(DISTINCT (col1, col2)) 这种写法,表面看是“多字段去重计数”,但实际语义是:对 (col1, col2) 构成的行值(row type)做唯一性判断。问题在于——只要任意一个字段为 NULL,整行就会被判定为不参与去重(类似 NULL = NULL 不成立),导致统计结果偏低。
- 例如
(1, NULL)和(1, NULL)不会被视为相同行,COUNT(DISTINCT (a,b))会把它们算作两个不同值 - 而
SELECT DISTINCT a, b FROM t却能把这两行合并(标准 SQL 行去重行为) - 所以不能依赖
COUNT(DISTINCT (a,b))做业务上需要的“组合维度唯一计数”
用子查询 + COUNT(*) 替代,确保语义准确
最稳妥、跨版本兼容的做法是显式构造去重后的中间集,再统计行数:
SELECT COUNT(*) AS unique_count FROM (SELECT DISTINCT region, product_type FROM sales) AS t;
这种写法明确表达了“先按 region 和 product_type 联合去重,再数有多少组”,和业务理解完全一致。注意以下几点:
- 别漏掉别名
AS t,否则 PostgreSQL 会报错subquery in FROM must have an alias - 如果字段含
NULL,DISTINCT会把所有(x, NULL)归为一组(符合预期),不会像元组语法那样失效 - 在大表上,可考虑给
(region, product_type)加复合索引,PostgreSQL 的Bitmap Index Scan或Index Only Scan可能跳过堆扫描
用 ROW() 构造显式可比较的结构(仅限需处理 NULL 的边缘场景)
如果你确实需要把 NULL 当作有效值参与组合去重(比如 “(A, NULL)” 和 “(A, NULL)” 必须算作同一类),又不想用 COALESCE 硬编码占位符,可以用 ROW() 配合类型强制:
SELECT COUNT(DISTINCT ROW(region, product_type)::TEXT) AS unique_count FROM sales;
但要注意:
-
ROW(a,b)::TEXT生成形如(A,B)或(A,)的字符串,NULL会被转成字符串NULL,从而参与比较 - 性能比原生
DISTINCT差很多,因为要构造文本、哈希、去重,大数据量下内存和 CPU 开销明显上升 - 不如直接用
COALESCE(region, 'NULL_VAL')+COALESCE(product_type, 'NULL_VAL')组合,更可控也更容易加索引
真正容易被忽略的是:多字段去重不是“拼接”也不是“元组比较”,而是基于 SQL 标准的行级唯一性判定;PostgreSQL 的 DISTINCT (a,b) 和 SELECT DISTINCT a,b 行为不等价,尤其在 NULL 存在时。别图省事写一行就跑,先确认 NULL 是否业务有效值,再选子查询还是显式转换。

















