基于视图的max(date)查询的奇怪查询计划

问题描述 投票:1回答:1

我有一个包含4个年表的视图:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE VIEW [dbo].[BGT_BETWAYDETAILS]
WITH SCHEMABINDING
AS
SELECT [bwd_BetTicketNr] ,
    [bwd_LineID] [int] ,
    [bwd_ResultID] [bigint] NOT NULL,
    [bwd_DateModified]  ,
    [bwd_DateModifiedTrunc]  ,
    [bwd_LineMaxPayout] 
FROM   [dbo].[BGT_BETWAYDETAILS_2020]
UNION ALL
SELECT [bwd_BetTicketNr] ,
    [bwd_LineID] [int] ,
    [bwd_DateModified]  ,
    [bwd_DateModifiedTrunc]  ,
    [bwd_LineMaxPayout] 
FROM   [dbo].[BGT_BETWAYDETAILS_2019]
UNION ALL
SELECT [bwd_BetTicketNr] ,
    [bwd_LineID] [int] ,
    [bwd_DateModified]  ,
    [bwd_DateModifiedTrunc]  ,
    [bwd_LineMaxPayout] 
FROM   [dbo].[BGT_BETWAYDETAILS_2018]
UNION ALL
SELECT [bwd_BetTicketNr] ,
    [bwd_LineID] [int] ,
    [bwd_DateModified]  ,
    [bwd_DateModifiedTrunc]  ,
    [bwd_LineMaxPayout] 
FROM   [dbo].[BGT_BETWAYDETAILS_2017];
GO

每个表具有以下结构:

CREATE TABLE [dbo].[BGT_BETWAYDETAILS_2020]
(
    [bwd_BetTicketNr] [bigint] NOT NULL,
    [bwd_LineID] [int] NOT NULL,
    [bwd_ResultID] [bigint] NOT NULL,
    [bwd_DateModified] [datetime] NULL,
    [bwd_DateModifiedTrunc] [date] NULL,
    [bwd_LineMaxPayout] [decimal](18, 4) NULL,

    CONSTRAINT [CSTR__BGT_BETWAYDETAILS_2020_CKEY] 
        PRIMARY KEY CLUSTERED ([bwd_BetTicketNr] ASC, [bwd_LineID] ASC, [bwd_ResultID] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                          IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                          ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

我在上面添加了非聚集索引

CREATE NONCLUSTERED INDEX [NCI__DATEMODIFIED] 
ON [dbo].[BGT_BETWAYDETAILS_2020] ([bwd_DateModifiedTrunc] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                          SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, 
                          ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

我正在运行以下3个查询:

SELECT COALESCE(MAX([bwd_DateModifiedTrunc]), '2019-01-01') AS next_date
FROM [dbo].[BGT_BETWAYDETAILS_2020]

SELECT COALESCE(MAX([bwd_DateModifiedTrunc]), '2019-01-01') AS next_date
FROM [dbo].[BGT_BETWAYDETAILS]

SELECT COALESCE(CAST(MAX([bwd_DateModified]) AS date), '2019-01-01') AS next_date
FROM [dbo].[BGT_BETWAYDETAILS]

第一个,在每个年度表上运行时,立即运行。

第二个,似乎要永远坚持下去。此查询计划似乎很奇怪。

enter image description here

该计划在每个年表上显示两次索引扫描。

每个年度表的计划是我希望看到的:

enter image description here

最后,非索引日期列上的计划也是我希望看到的(聚集索引扫描)。对每个表进行聚集索引扫描。此查询将在大约3分钟内运行。

enter image description here

这里是什么问题?我缺少一些反模式吗?为什么根据实时计划对非聚集索引进行了两次索引扫描?我希望该视图能够像单个表一样快速地响应。

出于记录,我正在SQL Server 2017上运行它。

sql-server sql-execution-plan
1个回答
2
投票
一个简单的例子是

CREATE TABLE T1(X INT NULL UNIQUE CLUSTERED); CREATE TABLE T2(X INT NULL UNIQUE CLUSTERED); INSERT INTO T1 OUTPUT INSERTED.X INTO T2 SELECT TOP 100000 NULLIF(ROW_NUMBER() OVER (ORDER BY 1/0),1) FROM sys.all_objects o1, sys.all_objects o2;

然后

WITH CTE AS
(
SELECT X FROM T1
UNION ALL
SELECT X FROM T2
)
SELECT MAX(X)
FROM CTE
OPTION (QUERYRULEOFF ScalarGbAggToTop)

这将禁用查询优化器规则ScalarGbAggToTop,并且查询计划在每个单独的表上执行MAX,然后计算MAX -es的MAX-等同于

SELECT MAX(MaxX)
FROM 
(
SELECT MAX(X) AS MaxX FROM T1 
UNION ALL
SELECT MAX(X) AS MaxX FROM T1 
) T

enter image description here

启用ScalarGbAggToTop规则后,该计划现在看起来像这样

enter image description here

正在有效地执行以下操作...

SELECT MAX(MaxX) FROM (SELECT MAX(X) AS MaxX FROM (SELECT TOP 1 X FROM T1 WHERE X IS NULL UNION ALL SELECT TOP 1 X FROM T1 WHERE X IS NOT NULL ORDER BY X DESC) T1 UNION ALL SELECT MAX(X) AS MaxX FROM (SELECT TOP 1 X FROM T2 WHERE X IS NULL UNION ALL SELECT TOP 1 X FROM T2 WHERE X IS NOT NULL ORDER BY X DESC) T2) T0

...,但是效率很低。运行上面的SQL将给出一个带有搜索的计划,并且每个分支仅读取一行。

ScalarGbAggToTop生成的计划仅对流聚合计划进行了最小的更改。看起来像是从那里进行扫描并对其应用向后排序,然后对NOT NULLNULL分支都使用向后排序。并且不执行任何其他探索以查看是否存在更有效的访问路径。

这意味着在所有行均为NULLNOT NULL的病理情况下,一次扫描最终将读取表中的所有行(在您的情况下为30亿)。即使NULLNOT NULL混合在一起,IS NULL分支正在执行向后扫描的事实也是次优的,因为NULL在SQL Server中首先排序,因此将在索引的开头。

首先添加NOT NULL分支似乎不被保证,因为如果没有它,查询将返回相同的结果。

添加显式WHERE ... NOT NULL解决了该问题。

WITH CTE AS ( SELECT X FROM T1 UNION ALL SELECT X FROM T2 ) SELECT MAX(X) FROM CTE WHERE X IS NOT NULL ;

现在它在索引的NOT NULL部分中进行查找并向后读(在从每个表中读取第一行之后停止)

enter image description here

语义在最终结果方面是相同的。行为上有一个非常细微的差别,即您将不再看到消息

警告:通过合计或其他SET消除了空值操作。

但是我怀疑有人在乎这个。
© www.soinside.com 2019 - 2024. All rights reserved.