视图上最大(日期)查询的奇怪查询计划
Strange query plan on max(date) query on a View
我有一个观点,其中包括 4 年 tables:
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
每个 table 具有以下结构:
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]
第一个,当 运行 每年 table,立即 运行s。
第二个,似乎要花很长时间。这个查询计划,好像很奇怪。
计划显示每年两次索引扫描 table。
每年table的计划是我期望看到的:
最后,非索引日期列上的计划也是我期望看到的(聚集索引扫描)。每个 table 上的聚簇索引扫描。此查询 运行 秒,约 3 分钟,符合预期。
这里有什么问题?我缺少一些反模式?为什么非聚集索引上的索引扫描按照 live 计划进行了 2 次?我希望视图能够像个人 table 一样快速响应。
郑重声明,我运行在 SQL Server 2017 上执行此操作。
这看起来像是一个优化器限制。我已提交 a suggestion that this should be improved.
一个更简单的例子是
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
table 然后计算 MAX
的 MAX
-es - 与
相同
SELECT MAX(MaxX)
FROM
(
SELECT MAX(X) AS MaxX FROM T1
UNION ALL
SELECT MAX(X) AS MaxX FROM T1
) T
启用 ScalarGbAggToTop
规则后,计划现在看起来像这样
它有效地执行以下操作...
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 NULL
和 NULL
分支使用向后排序。并且不执行任何额外的探索以查看是否有更有效的访问路径。
这意味着在所有行都是 NULL
或 NOT NULL
的病态情况下,其中一次扫描将最终读取 table 中的所有行(如果适用于所有 4 table,则在您的情况下为 50 亿)。即使存在 NULL
和 NOT NULL
的混合,IS NULL
分支正在进行向后扫描的事实也是次优的,因为 NULL
在 [=68= 中排在第一位] 服务器将位于索引的开头。
首先添加 NOT NULL
分支似乎基本上没有必要,因为如果没有它,查询会 return 得到相同的结果。我想它只需要它知道是否显示消息
Warning: Null value is eliminated by an aggregate or other SET
operation.
但我怀疑你关心这个。在这种情况下,添加显式 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
部分进行查找并向后读取(在从每个 table 读取第一行后停止)
我有一个观点,其中包括 4 年 tables:
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
每个 table 具有以下结构:
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]
第一个,当 运行 每年 table,立即 运行s。
第二个,似乎要花很长时间。这个查询计划,好像很奇怪。
计划显示每年两次索引扫描 table。
每年table的计划是我期望看到的:
最后,非索引日期列上的计划也是我期望看到的(聚集索引扫描)。每个 table 上的聚簇索引扫描。此查询 运行 秒,约 3 分钟,符合预期。
这里有什么问题?我缺少一些反模式?为什么非聚集索引上的索引扫描按照 live 计划进行了 2 次?我希望视图能够像个人 table 一样快速响应。
郑重声明,我运行在 SQL Server 2017 上执行此操作。
这看起来像是一个优化器限制。我已提交 a suggestion that this should be improved.
一个更简单的例子是
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
table 然后计算 MAX
的 MAX
-es - 与
SELECT MAX(MaxX)
FROM
(
SELECT MAX(X) AS MaxX FROM T1
UNION ALL
SELECT MAX(X) AS MaxX FROM T1
) T
启用 ScalarGbAggToTop
规则后,计划现在看起来像这样
它有效地执行以下操作...
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 NULL
和 NULL
分支使用向后排序。并且不执行任何额外的探索以查看是否有更有效的访问路径。
这意味着在所有行都是 NULL
或 NOT NULL
的病态情况下,其中一次扫描将最终读取 table 中的所有行(如果适用于所有 4 table,则在您的情况下为 50 亿)。即使存在 NULL
和 NOT NULL
的混合,IS NULL
分支正在进行向后扫描的事实也是次优的,因为 NULL
在 [=68= 中排在第一位] 服务器将位于索引的开头。
首先添加 NOT NULL
分支似乎基本上没有必要,因为如果没有它,查询会 return 得到相同的结果。我想它只需要它知道是否显示消息
Warning: Null value is eliminated by an aggregate or other SET operation.
但我怀疑你关心这个。在这种情况下,添加显式 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
部分进行查找并向后读取(在从每个 table 读取第一行后停止)