与 SQL Server 中的硬编码值相比,WHERE IN 使用表类型的性能要好得多

问题描述

我有 2 个基本相同的查询(至少如果我没有遗漏什么的话)。

DECLARE @siloIds SiloIdsTableType
INSERT INTO @siloIds VALUES 
(1),(2),(3)

-- Query 1
SELECT *
FROM [Transaction]
WHERE 
      SiloId IN (1,2,3)
  AND Time > '2000-02-01'

-- Query 2
SELECT *
FROM [Transaction]
WHERE 
       SiloId IN (select SiloId from @siloIds)
   AND Time > '2000-02-01'

我在想不能击败在查询本身中声明的常量,但显然第一个查询比第二个查询慢几倍。似乎 sql 服务器不够聪明,无法为硬编码值提供一个好的计划,或者我在这里遗漏了什么?

似乎不应该使用 where in 列表很长,而 TVP 应该总是受到青睐

附言我在查询中使用千值而不是 1,3

P.P.S.我在 SiloId ASC Time ASC 上有一个非聚集索引,但似乎第一个查询由于某种原因没有使用它来支持聚集索引扫描。

P.P.P.S.执行计划分担 14% 到 86% 的成本,有利于第二次查询

执行计划:

enter image description here

解决方法

当您使用表变量(或 TVP,这是同一件事)时,SQL Server 使用一个固定的估计值,即它只会从中获取 1 行(更多内容见下文),基数为 1。这意味着它假定 SiloId 连接过滤器是非常有选择性的,它会优先考虑它并执行嵌套循环连接以仅获取那些行,然后在 Time 上进行过滤。

而当您使用常量时,确切的大小是硬编码的。无论出于何种原因(可能是糟糕的统计数据),它都假设 Time 更具选择性,因此优先于其他过滤器。

表变量计划失败的地方是当它或主表中有很多行时,因为那样你会得到很多键查找,这可能很慢。

理想情况下,您希望编译器预先知道表变量的大小。您可以通过多种方式执行此操作,例如 Brent Ozar explains:

  • 跟踪标志 2453,如果基数非常不同,这会导致重新编译(如果您可以冒 TF 风险,这是个好主意)
  • OPTION (RECOMPILE)(每次都会重新编译,这本身可能效率低下)
  • 临时表(不能作为参数)