问题描述
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% 的成本,有利于第二次查询
执行计划:
解决方法
当您使用表变量(或 TVP,这是同一件事)时,SQL Server 使用一个固定的估计值,即它只会从中获取 1 行(更多内容见下文),基数为 1。这意味着它假定 SiloId
连接过滤器是非常有选择性的,它会优先考虑它并执行嵌套循环连接以仅获取那些行,然后在 Time
上进行过滤。
而当您使用常量时,确切的大小是硬编码的。无论出于何种原因(可能是糟糕的统计数据),它都假设 Time
更具选择性,因此优先于其他过滤器。
表变量计划失败的地方是当它或主表中有很多行时,因为那样你会得到很多键查找,这可能很慢。
理想情况下,您希望编译器预先知道表变量的大小。您可以通过多种方式执行此操作,例如 Brent Ozar explains:
- 跟踪标志 2453,如果基数非常不同,这会导致重新编译(如果您可以冒 TF 风险,这是个好主意)
-
OPTION (RECOMPILE)
(每次都会重新编译,这本身可能效率低下) - 临时表(不能作为参数)