加入前过滤表

Jam*_*son 6 performance sql-server sql-server-2012 query-performance

我有一个存储过程,它使用 Id 列表填充临时表 #employee_benefits。该表最终大约有 10,000 行长。下面的查询然后从一个名为 EmployeeBenefitData 的表中进行选择,该表有大约 400 万行。

SELECT  ebd.EmployeeBenefitDataId, ebd.EmployeeBenefitId, ebd.[DataDefinitionId]
FROM    #employee_benefits eb
INNER JOIN EmployeeBenefitData ebd ON eb.EmployeeBenefitId = ebd.EmployeeBenefitId
Run Code Online (Sandbox Code Playgroud)

瓶颈是 EmployeeBenefitData 表上的索引扫描。它首先进行索引扫描,然后将其加入临时表。临时表充当过滤器,这意味着在连接之前扫描所有数据的效率非常低。我添加了以下代码以将扫描更改为搜索并显着减少所需的读取量。

DECLARE @MinEmpBenId INT, @MaxEmpBenId INT

SELECT @MinEmpBenId = MIN(EmployeeBenefitId), @MaxEmpBenId = MAX(EmployeeBenefitId)
FROM #employee_benefits

SELECT  ebd.EmployeeBenefitDataId, ebd.EmployeeBenefitId, ebd.[DataDefinitionId],
        dd.TypeId, dd.DataDefinitionId, dd.Name, ebd.[Value], ebd.[Date], ebd.[Text]
FROM    #employee_benefits eb
INNER JOIN EmployeeBenefitData ebd ON eb.EmployeeBenefitId = ebd.EmployeeBenefitId
INNER JOIN DataDefinition dd ON ebd.DataDefinitionId = dd.DataDefinitionId
WHERE   ebd.EmployeeBenefitId >= @MinEmpBenId AND ebd.EmployeeBenefitId <= @MaxEmpBenId
Run Code Online (Sandbox Code Playgroud)

它对客户端统计数据产生了巨大的影响

总执行时间 74, 1794
服务器回复的等待时间 11, 11

我的问题是:这是好的做法吗?为什么优化器不这样做?

更新 我应该提到临时表在 EmployeeBenefitID 上有一个聚集索引

Mar*_*ith 6

这是好的做法吗?

在这种情况下,我会说是的。我可能还会添加一个OPTION (RECOMPILE)让它“嗅探”变量值。最佳计划可能会因较大表中与此范围匹配的行的比例而异。

它为优化器提供了一个潜在有用的额外路径,据我所知,它并不是查询优化器自己做的事情。最接近的是,通过合并连接,当任一输入完成时,它将停止处理输入。因此,这意味着它可能会避免完全扫描。

唯一想到的缺点是如果计算最小/最大范围值本身可能很昂贵(但如果您用作过滤器的表在该列上建立索引,这应该非常便宜)。

我创建了两个测试表

CREATE TABLE EmployeeBenefitData(EmployeeID INT PRIMARY KEY);

CREATE TABLE FilteredEmployee(EmployeeID INT PRIMARY KEY);
Run Code Online (Sandbox Code Playgroud)

并使用 1 到 4,000,000 的整数(6,456 页)加载 EmployeeBenefitData

和 FilteredEmployee 的整数从 2,000,000 AND 2,010,000(19 页)

然后运行以下形式的 6 个查询

DECLARE @E1 INT,
        @E2 INT

SELECT @E1 = FE.EmployeeID,
       @E2 = EBD.EmployeeID
FROM   FilteredEmployee FE
       INNER LOOP JOIN EmployeeBenefitData EBD
         ON FE.EmployeeID = EBD.EmployeeID
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

通过颠倒两个表的顺序并尝试所有三种连接类型LOOPMERGE, 来组成 6 个排列HASH

结果如下

+------------+-------------+-------+----------------+-----------+---------------+----------+
| Left Table | Right Table | Join  | EBD Scan Count | EBD reads | FE Scan Count | FE reads |
+------------+-------------+-------+----------------+-----------+---------------+----------+
| FE         | EBD         | Loop  |              0 |     30637 |             1 |       19 |
| EBD        | FE          | Loop  |              1 |      6456 |             0 |  8250009 |
| FE         | EBD         | Merge |              1 |      3257 |             1 |       19 |
| EBD        | FE          | Merge |              1 |      3257 |             1 |       19 |
| FE         | EBD         | Hash  |              1 |      6456 |             1 |       19 |
| EBD        | FE          | Hash  |              1 |      6456 |             1 |       19 |
+------------+-------------+-------+----------------+-----------+---------------+----------+
Run Code Online (Sandbox Code Playgroud)

上图说明了合并连接的要点,因为它“仅”扫描大表的一半以上。它仍然首先读取从 1 到 1,999,999 的所有行并丢弃它们。

用 a 重复实验WHERE EBD.EmployeeID BETWEEN 2000000 AND 2010000给出以下结果。

+------------+-------------+-------+----------------+-----------+---------------+----------+
| Left Table | Right Table | Join  | EBD Scan Count | EBD reads | FE Scan Count | FE reads |
+------------+-------------+-------+----------------+-----------+---------------+----------+
| FE         | EBD         | Loop  |              0 |     30637 |             1 |       19 |
| EBD        | FE          | Loop  |              1 |        21 |             0 |    20636 |
| FE         | EBD         | Merge |              1 |        21 |             1 |       19 |
| EBD        | FE          | Merge |              1 |        21 |             1 |       19 |
| FE         | EBD         | Hash  |              1 |        21 |             1 |       19 |
| EBD        | FE          | Hash  |              1 |        21 |             1 |       19 |
+------------+-------------+-------+----------------+-----------+---------------+----------+
Run Code Online (Sandbox Code Playgroud)

唯一没有从附加范围谓词中受益的查询是较大表位于嵌套循环连接内部的查询。

这当然并不奇怪,因为该计划(下面的计划 1)是由使用FilteredEmployee.

在此处输入图片说明

计划 1 也是优化器“自然”选择的,没有范围谓词。有了范围谓词,它选择了一个不同的合并连接计划,在不扫描不必要的行的情况下寻找相关的索引范围,并且成本显着降低(计划 2)