不能直接在同一个SELECT里混用窗口函数和DISTINCT,因为DISTINCT作用于最终结果集,而窗口函数在逻辑查询处理阶段早于DISTINCT执行,导致先全量计算窗口值再冗余去重,引发内存与排序开销激增。

为什么不能直接在同一个SELECT里混用窗口函数和DISTINCT
因为语义冲突:DISTINCT作用于最终结果集,而窗口函数(如ROW_NUMBER()、COUNT() OVER())是在逻辑查询处理阶段早于DISTINCT执行的。数据库必须先算完所有窗口值、生成完整结果集,再做去重——这导致中间数据量爆炸,尤其当原始表大、分区多时,内存和排序开销翻倍。
常见错误写法:SELECT DISTINCT user_id, COUNT(*) OVER (PARTITION BY user_id) FROM orders。它看似“只取不同user_id”,实则先对全表每行计算COUNT,再按两列去重,完全浪费计算资源。
用CTE或子查询把窗口计算和去重分层剥离
把窗口逻辑提前固化到中间结果,再对外层结果做精准过滤或聚合,避免重复扫描和冗余字段传递。
- 需要每个用户最新一条订单?写成:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) SELECT user_id, order_id, amount FROM ranked WHERE rn = 1;
- 需要统计“去重用户数 / 总订单数”?别在主查询套
COUNT(DISTINCT),改用:WITH user_orders AS ( SELECT user_id, COUNT(*) AS cnt_per_user FROM orders GROUP BY user_id ) SELECT COUNT(*) * 1.0 / (SELECT COUNT(*) FROM orders) AS ratio FROM user_orders;
替代DISTINCT的三种轻量级方案
多数场景下,你真正要的不是“去重”,而是“按某字段唯一取一行”或“统计唯一基数”。直接换策略更高效:
- 字段唯一取行 → 用
ROW_NUMBER() OVER (PARTITION BY x ORDER BY y)+WHERE rn = 1,配合(x, y)联合索引可跳过排序 - 统计唯一值个数 → 用
COUNT(DISTINCT x),但确保x有索引;若只是判断是否存在,用EXISTS比SELECT DISTINCT x快得多 - WHERE条件已锁定结果 → 如
SELECT DISTINCT a,b FROM t WHERE a=1 AND b='ok',直接改写为SELECT a,b FROM t WHERE a=1 AND b='ok' LIMIT 1,避免全表去重流程
MySQL 8.0+ 和 PostgreSQL 的关键差异点
MySQL 8.0 不支持命名WINDOW子句,多个窗口函数若PARTITION BY和ORDER BY不完全一致(哪怕只是ORDER BY ts vs ORDER BY ts ASC),就会触发多次独立排序;PostgreSQL 则能更好复用排序结果。
另外,RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(默认框架)在日期/状态码等高重复值字段上性能极差,务必显式改成ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW——这个细节在跨数据库迁移时最容易被忽略,且几乎不报错,只默默拖慢查询。


















