同一索引操作的不同估计行?

different estimated rows on same index operation?

简介和背景

我必须优化一个简单的查询(下面的示例)。重写几次后,我发现同一索引操作的估计行数因查询的编写方式而异。

最初查询进行了聚簇索引扫描,因为生产中的 table 包含一个二进制列 table 相当大(大约 100 GB)并且完整的 table 扫描执行时间太长。

问题

为什么同一索引操作的估计行数不同(示例将显示)?优化器在这里做什么?

示例数据库 - 我正在使用 SQL Server 2008 R2

我试图创建一个非常简单的作品版本 tables 来显示行为。

-- CREATE THE SAMPLE TABLES
----------------------------
CREATE TABLE dbo.MasterTable(
    MasterId    smallint NOT NULL,
    Name        varchar(5) NOT NULL,
    CONSTRAINT PK_MasterTable PRIMARY KEY CLUSTERED (MasterId ASC)
) ON  [PRIMARY]

GO

CREATE TABLE dbo.DetailTable(
    DetailId    bigint IDENTITY(1,1) NOT NULL,
    MasterId    smallint NOT NULL,
    Name        nvarchar(50) NOT NULL,
    CreateDate  datetime NOT NULL,
    CONSTRAINT PK_DetailTable PRIMARY KEY CLUSTERED (DetailId ASC)
) ON  [PRIMARY]

GO

ALTER TABLE dbo.DetailTable
    ADD  CONSTRAINT FK1
    FOREIGN KEY(MasterId) REFERENCES dbo.MasterTable (MasterId)

GO

CREATE NONCLUSTERED INDEX IX_DetailTable
    ON dbo.DetailTable( MasterId ASC, Name ASC )

GO

-- INSERT SOME SAMPLE DATA
----------------------------
SET NOCOUNT ON
GO

-- These are some Codes. In our system we always use these codes to search for "types" of data.

INSERT INTO dbo.MasterTable (MasterId, Name)
VALUES (1, 'N1'), (2, 'N2'), (3, 'N3'), (4, 'N4'), (5, 'N5'), (6, 'N6'), (7, 'N7'), (8, 'N8')

GO

-- ADD ROWS TO THE DETAIL TABLE
-- Takes about 1 minute to run
-- Don't care about the logic, it's just to get a distribution similar to production system
----------------------------
declare @x int = 1
DECLARE @MasterID INT
while (@x <= 400000)
begin
    SET @MasterID = ABS(CHECKSUM(NEWID())) % 8 + 1

    INSERT INTO dbo.DetailTable(MasterId,Name,CreateDate)
    VALUES(
        CASE
            WHEN @MasterID IN (1, 3, 4) AND @x % 20 != 0 THEN 2
            WHEN @MasterID IN (5, 6) AND @x % 20 != 0 THEN 7
            WHEN @MasterID = 8 AND @x % 100 != 0 THEN 7
            ELSE @MasterID
        END,
        NEWID(),
        DATEADD(DAY, - ABS(CHECKSUM(NEWID())) % 1000, GETDATE())
)

SET @x = @x + 1
end

go
-- DO THE INDEX AND STATISTIC MAINTENANCE
----------------------------
alter index all on dbo.DetailTable reorganize
alter index all on dbo.MasterTable reorganize
update statistics dbo.DetailTable WITH FULLSCAN
update statistics dbo.MasterTable WITH FULLSCAN
go

准备工作完成,下面开始查询

我们先看一下统计,看看RANGE_HI_KEY=8,有489个EQ_ROWS

-- CHECK THE STATISTICS
----------------------------
dbcc show_statistics ('dbo.DetailTable', IX_DetailTable)
GO

现在我们进行查询。第一个是我必须优化的原始查询。 执行时请激活当前执行计划。 看看操作"index seek (nonclustered) [DetailTable].[IX_DetailTable]"

-- ORIGINAL QUERY
----------------------------
SELECT d.DetailId
FROM dbo.DetailTable d
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

GO

-- FORCESEEK
----------------------------
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

GO

-- Actual: 489, Estimated 50.000


-- TABLE VARIABLE
----------------------------
DECLARE @MasterId AS TABLE( MasterId SMALLINT )
INSERT INTO @MasterId (MasterId)
SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN @MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

GO

-- Actual: 489, Estimated 40.000

-- TEMP TABLE
----------------------------
CREATE TABLE #MasterId( MasterId SMALLINT )
INSERT INTO #MasterId (MasterId)
    SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'

SELECT d.DetailId
FROM dbo.DetailTable d --WITH (FORCESEEK)
INNER JOIN #MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

-- Actual 489, Estimated 489

DROP TABLE #MasterId

GO

分析和最终问题

请看操作"index seek (nonclustered) [DetailTable].[IX_DetailTable]"

上面脚本中的注释显示了我获得的估计行数和实际行数的值。

在我们的生产环境中,这个 table 有 3300 万行,上面查询中的估计行数在 300 万到 1600 万之间。

总结一下:

  1. 当在 DetailTable 和 MasterTable 之间进行连接时,估计行数为 12.5%(master table 中有 8 个值,这是有道理的,有点...)

  2. 当在 DetailTable 和 table 变量之间进行连接时,估计行数为 10%

  3. 当在 DetailTable 和临时 table 之间进行连接时,估计行数与实际行数完全相同

问题是为什么这些值不同?

统计数据是最新的,进行估算应该很容易。

我就是想了解一下。

因为没有人回答我会尽量给出答案:

请不要强迫优化器跟随你

(1) 对您原始查询的解释:

SELECT d.DetailId
FROM dbo.DetailTable d
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'

为什么这个查询很慢?

这个查询很慢,因为你的索引没有覆盖这个查询, 两个查询都使用索引扫描而不是加入 "Hash join":

为什么要扫描整行以查找 mastertable? 因为 Master table 上的索引在 MasterId 列上,而不是在 Name 列上。

为什么扫描整行以获得详细信息table?因为这里也有索引 (DETAILID) "CLUSTERED" AND (MasterId ASC, Name ASC) "NON CLUSTERED"
不在 Createdate 列上。

拥有一个 NONCLUSTERED 索引将有助于此特定查询的 ON 列 (CREATEDATE,MasterId) 查询。

如果您的 Master table 也很大,您可以在 (Name) 列上创建 NONCLUSTERED 索引。

(2) 关于FORCESEEK的解释:

-- 力求

SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN dbo.MasterTable m ON d.MasterId = m.MasterId
WHERE m.Name = 'N8'
AND d.CreateDate > '20150312 11:00:00'
GO

为什么优化器估计有 50,000 行?

您在此处加入列 d.MasterId = m.MasterId,并且您正在强制优化器选择在细节 table 上查找,所以 optizer 使用 INDEX IX_DetailTable () 加入你的 Mastertable 使用 LOOP join 。

自从优化器选择循环连接将 MAster table 的所有行(实际上是一个)连接到 Detail table 所以它将从主table中选择一个键,然后寻找整个索引,然后将匹配的值传递给进一步的迭代器。

因此优化器选择每个值的平均行数。 第 40000 列中的 8 个唯一值 table 基数(行)所以 40000 / 8 是估计的 50,000 行(足够公平)。

(3) -- TABLE 变量

这是您的查询:

DECLARE @MasterId AS TABLE( MasterId SMALLINT )
INSERT INTO @MasterId (MasterId)
SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'
SELECT d.DetailId
FROM dbo.DetailTable d WITH (FORCESEEK)
INNER JOIN @MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

GO

Statatictic 不维护 table 变量,因此优化器不知道有多少行(因此它估计 1 行)它会处理以产生一个好的计划, 这里估计的行也是 1,实际的行也是 1,恭喜!!

但是优化器如何估计“40.000”ROWS

我个人从来没有检查过这个,因为这个问题我做了服务器测试,但不知道优化器如何计算估计的行数,所以如果有人来启发我们就太好了。

(4) -- 温度 TABLE

您的查询

CREATE TABLE #MasterId( MasterId SMALLINT )
INSERT INTO #MasterId (MasterId)
    SELECT MasterID FROM dbo.MasterTable WHERE Name = 'N8'

SELECT d.DetailId
FROM dbo.DetailTable d --WITH (FORCESEEK)
INNER JOIN #MasterId m ON d.MasterId = m.MasterId
WHERE d.CreateDate > '20150312 11:00:00'

-- Actual 489, Estimated 489
DROP TABLE #MasterId

此处优化器选择的查询计划与在 table 变量中选择的查询计划相同,但不同之处在于 统计数据确实在 temp tables 上维护,因此在查询优化器中有一个公平的 idia 它实际要加入的行。 "N8" 键有 8 个,dbo.DetailTable 中 8 的估计行数为 489。