sql server查询
写SQL查询,十次有八次栽在NULL上。上周同事查订单表,条件写的是WHERE 优惠金额 <> 100,结果漏了几百条记录。原因很简单——那些优惠金额为NULL的行,压根不会进入比较运算。NULL不等于任何值,连它自己都不等于。要捞全,得写成WHERE 优惠金额 <> 100 OR 优惠金额 IS NULL。这种坑,文档里写得清清楚楚,但真到写的时候,手比脑子快。
SQL Server的查询优化器有个脾气:它不看你写的顺序,只看它认为的最优路径。你写SELECT * FROM 订单 WHERE 客户ID = 5,它可能先走索引再回表,也可能直接全表扫。想看它到底怎么干的,按Ctrl+M调出执行计划。那个图形化界面比文字版直观得多,箭头粗细代表数据量,百分比告诉你哪一步最烧资源。我见过一个查询,光排序就占了87%的开销,加个索引立马降到3%。
JOIN不是越多越好
三张表关联是常态,五张表开始吃力,八张表以上基本是设计出了问题。每多一个JOIN,优化器要评估的组合就翻倍。更麻烦的是,INNER JOIN和LEFT JOIN混用时,条件写在ON里还是WHERE里,结果完全不同。ON里过滤的是被关联表的行,WHERE过滤的是最终结果集。把LEFT JOIN的右表条件写到WHERE里,等于把外连接降级成了内连接——这个错误我犯过不止一次。
子查询和CTE(公用表表达式)的选择也让人纠结。相关子查询(correlated subquery)逐行执行,数据量一大就慢得离谱。改成JOIN或者用CROSS APPLY,性能经常能差出几十倍。CTE写起来舒服,逻辑清晰,但SQL Server不会自动物化CTE结果——它只是语法糖,每次引用都重新计算。递归CTE倒是真有用,查组织架构树、物料清单这类层级数据,比游标干净多了。
分页查询的两种写法
ROW_NUMBER() OVER (ORDER BY 创建时间 DESC)套一层子查询,是SQL Server 2005到2012时代的标配。写法啰嗦,但兼容性好。2012之后有了OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY,简洁不少。不过要注意,OFFSET分页在数据量大时性能会线性下降——它得先扫过前面所有行。 keyset分页(记住上一页最后一条的ID,用WHERE ID > 上次最大ID)才是真快,但只适合连续翻页的场景,跳页就废了。
聚合函数和GROUP BY的组合也有讲究。COUNT(DISTINCT 列)在大表上慢得让人想砸键盘,因为要排序去重。如果只是判断存在性,EXISTS比COUNT(*)快得多——前者找到第一条就停,后者要数完所有行。还有HAVING和WHERE的区别:WHERE在分组前过滤,HAVING在分组后过滤。能写在WHERE里的条件,别放到HAVING里。
参数嗅探:好心和坏结果
存储过程第一次执行时,优化器会根据传入的参数值生成执行计划,然后缓存起来。下次调用,哪怕参数分布完全不同,也复用这个计划。比如第一次传了客户ID=1(只有10条订单),计划走了索引查找;第二次传客户ID=999(有50万条订单),还是走索引查找,结果慢成狗。解决办法有几种:OPTION (RECOMPILE)每次重编译,OPTION (OPTIMIZE FOR UNKNOWN)用平均分布估算,或者把参数赋值给局部变量再查询。各有代价,看场景选。
查询提示(query hint)是最后的手段。WITH (NOLOCK)能避免读锁等待,但会读到脏数据——财务对账时用这个,等着挨骂吧。OPTION (MAXDOP 1)强制单线程,有时候并行计划反而更慢,特别是小数据量加复杂聚合的时候。这些提示别乱加,优化器大多数时候比人聪明,只是偶尔犯轴。
最后说个观察:很多慢查询的根源不在SQL本身,而在表结构。缺少索引、索引碎片率高、统计信息过期——这些问题不解决,查询怎么写都快不了。我习惯先看执行计划里的扫描操作,全表扫描或索引扫描出现得越少越好。实在绕不过去,再回头检查统计信息的最后更新时间。那个日期要是几个月前的,先更新统计信息再说别的。









