SQL Server中等价DISTINCT查询性能差异及优化器问题咨询
一、先给结论:两个查询完全等价
我们先把两个查询的SQL再贴出来对比:
-- Q1 select distinct o1.category, (select count(*) from orders o2 where order_date = 1 and o1.category = o2.category) from orders o1
-- Q2 select o1.category, (select count(*) from orders o2 where order_date = 1 and o1.category = o2.category) from (select distinct category from orders) o1
不管orders表里的数据怎么分布,这两个查询最终返回的结果都是一模一样的——都是所有唯一category对应的、order_date=1的同类别订单数量。
从逻辑上拆解:
- Q1是先扫描整个
orders表,对每一行的category执行一次关联子查询统计符合条件的订单数,最后用distinct把重复的category结果去重; - Q2是先提取
orders表中所有唯一的category集合,再对每个唯一值执行关联子查询统计数量。
两者的最终输出没有任何区别,不存在反例。
二、为啥SQL Server 2016优化器不给Q1生成Q2的高效计划?
这其实是查询优化器的改写能力边界问题,核心原因有这么几个:
改写的成本收益权衡
查询优化器会尝试对SQL进行逻辑改写,但它需要权衡通用场景下的收益。Q1这种“全表扫描+每行子查询+最后去重”的写法,优化器要识别出“可以先去重再执行子查询”,但这种改写不是在所有场景都划算——比如如果category字段几乎没有重复值,提前去重反而会增加额外的排序/哈希去重开销。优化器不会为了某一种数据分布场景就默认执行这种改写。相关子查询的处理限制
Q1里的子查询是相关子查询(依赖外部查询的o1.category),优化器处理这类子查询时,通常会先尝试将其转换为连接操作,但distinct的存在让这个转换逻辑变得复杂。而Q2的写法直接把“去重”和“统计”拆分成两个独立步骤,相当于给优化器明确指明了执行路径,它能立刻意识到只需要处理唯一的category值,无需对全表每一行都执行一次子查询。索引匹配的路径问题
当你创建了ix_orders_kat (category, order_date)这个复合索引后,Q2可以直接利用索引的有序性快速获取唯一的category集合(去重成本极低),同时子查询也能通过这个索引快速定位order_date=1的同类别数据。但Q1的写法没有给优化器足够的提示,它仍然优先选择“扫描全表+每行执行子查询”的常规路径,没有触发“先去重再统计”的改写逻辑,自然无法利用这个索引的性能优势。
三、聊聊声明式SQL的局限性
SQL确实是声明式语言,你只需要描述“想要什么结果”,不用关心“如何获取结果”,但优化器的能力并不是无限的。它依赖于内置的规则集和成本估算模型来选择执行计划,当你的SQL写法没有明确引导它走向高效路径时,它可能会选择一个“足够好但不是最优”的计划。这也是为什么资深SQL开发者会刻意调整SQL写法——本质上是帮优化器减少决策复杂度,直接给它指明最优的执行方向。
内容的提问来源于stack exchange,提问作者Radim Bača

