具有冗余谓词的 LEFT JOIN 比 CROSS JOIN 执行得更好?

LEFT JOIN With Redundant Predicate Performs Better Than a CROSS JOIN?

我正在查看其中两个语句的执行计划,我有点困惑为什么 LEFT JOIN 语句比 CROSS JOIN 语句执行得更好:

Table 定义:

CREATE TABLE [Employee] (
    [ID]                int             NOT NULL    IDENTITY(1,1),
    [FirstName]         varchar(40)     NOT NULL,
    CONSTRAINT [PK_Employee] PRIMARY KEY CLUSTERED ([ID] ASC)
);

CREATE TABLE [dbo].[Numbers] (
    [N] INT IDENTITY (1, 1) NOT NULL,
    CONSTRAINT [PK_Numbers] PRIMARY KEY CLUSTERED ([N] ASC)
); --The Numbers table contains numbers 0 to 100,000.

问题中的查询,我向每个员工加入一个 'day':

DECLARE @PeriodStart AS date = '2019-11-05';
DECLARE @PeriodEnd AS date = '2019-11-05';

SELECT E.FirstName, CD.ClockDate
FROM Employee E
    CROSS JOIN (SELECT DATEADD(day, N.N, @PeriodStart) AS ClockDate 
                FROM Numbers N 
                WHERE N.N <= DATEDIFF(day, @PeriodStart, @PeriodEnd)
        ) CD
WHERE E.ID > 2000;

SELECT E.FirstName, CD.ClockDate
FROM Employee E
    LEFT JOIN (SELECT DATEADD(day, N.N, @PeriodStart) AS ClockDate 
                FROM Numbers N 
                WHERE N.N <= DATEDIFF(day, @PeriodStart, @PeriodEnd)
        ) CD ON CD.ClockDate = CD.ClockDate
WHERE E.ID > 2000;

执行计划: https://www.brentozar.com/pastetheplan/?id=B139JjPKK

如您所见,根据优化器,带有看似冗余谓词的第二个(左联接)查询的成本似乎比第一个(交叉联接)查询的成本低得多。当期间日期跨越多天时也是如此。

奇怪的是,如果我将 LEFT JOIN 的谓词更改为不同的东西,例如 1 = 1,它会像 CROSS APPLY 一样执行。我还尝试将 LEFT JOIN 的 SELECT 部分更改为 SELECT N 并加入 CD.N = CD.N ......但这似乎也表现不佳。

根据执行计划,第二个查询的索引查找仅从 Numbers table 中读取 3000 行,而第一个查询的读取次数是其 10 倍。第二个查询的索引查找也有这个谓词(我假设它来自 LEFT JOIN):

dateadd(day,[Numbers].[N] as [N].[N],[@PeriodStart])=dateadd(day,[Numbers].[N] as [N].[N],[@PeriodStart])

我想了解为什么第二个查询似乎执行得更好,即使我不会例外?这与我加入 DATEADD 函数的结果有关吗? SQL 是否在加入之前评估 DATEADD 的结果?

这些查询得到不同估计的原因,即使计划几乎相同并且可能花费相同的时间,似乎是因为 DATEADD(day, N.N, @PeriodStart) 可以为空,因此 CD.ClockDate = CD.ClockDate 基本上只是验证结果不为空。优化器看不到它始终为非空,因此因此降低了行估计值。


但在我看来,您查询中的主要性能问题是您每次都select查询整个数字table。相反,您应该 select 您需要的行数

SELECT E.FirstName, CD.ClockDate
FROM Employee E
    CROSS JOIN (
        SELECT TOP (DATEDIFF(day, @PeriodStart, @PeriodEnd) + 1)
            DATEADD(day, N.N, @PeriodStart) AS ClockDate
        FROM Numbers N
        ORDER BY N.N
    ) CD
WHERE E.ID > 2000;

使用此技术,如果您想将行数与查询的其余部分相关联,您甚至可以使用 CROSS APPLY (SELECT TOP (outerValue)

有关数字 table 的更多提示,请参阅 Itzik Ben-Gan's excellent series