使用 CTE 优化时态表
Posted
技术标签:
【中文标题】使用 CTE 优化时态表【英文标题】:Optimize temporal table with CTE 【发布时间】:2019-07-26 03:29:56 【问题描述】:我创建临时表以设置级别:
CREATE TABLE [#DesignLvl]
(
[DesignKey] INT,
[DesignLevel] INT
);
WITH RCTE AS
(
SELECT
*,
1 AS [Lvl]
FROM
[Design]
WHERE
[ParentDesignKey] IS NULL
UNION ALL
SELECT
[D].*,
[Lvl] + 1 AS [Lvl]
FROM
[dbo].[Design] AS [D]
INNER JOIN
[RCTE] AS [rc] ON [rc].[DesignKey] = [D].[ParentDesignKey]
)
INSERT INTO [#DesignLvl]
SELECT
[DesignKey], [Lvl]
FROM
[RCTE]
创建后,我在非常大的查询中用作 LEFT JOIN:
SELECT...
FROM..
LEFT JOIN [#DesignLvl] AS [dl] ON d.DesignKey = dl.DesignKey
WHERE ...
查询有效,但性能下降,现在查询太慢。有什么办法可以优化这张表吗?
CTE的执行计划
我尝试将 CLUSTERED 索引添加为:
CREATE TABLE [#DesignLvl]
(
[DesignKey] INT,
[DesignLevel] INT
);
CREATE CLUSTERED INDEX ix_DesignLvl
ON [#DesignLvl] ([DesignKey], [DesignLevel]);
也试试:
CREATE TABLE [#DesignLvl]
( [DesignKey] INT INDEX IX1 CLUSTERED ,
[DesignLevel] INT INDEX IX2 NONCLUSTERED );
但我得到相同的结果,执行需要很长时间
【问题讨论】:
查看执行计划以告诉您问题所在。您需要为表添加索引。 我运行执行计划并且问题出现在 CTE 函数中,我在几秒钟内用照片更新我的问题@Brad 您需要在Design
上使用前导列 ParentDesignKey
的覆盖索引,因此不必为每个父行扫描它
我该怎么做? @马丁史密斯
您应该将新索引添加到 [dbo].[Design] 而不是 [#DesignLvl]; [dbo].[Design] 的主键是什么?
【参考方案1】:
性能可能会变慢,因为dbo.Design
表上的聚集索引是在嵌套循环内访问的。根据成本估算,数据库花费了 66% 的时间来扫描该索引。循环只会让事情变得更糟。
见related question
考虑将dbo.Design
上的索引更改为非聚集索引,或者尝试使用非聚集索引创建另一个临时表并将其用于递归查询:
CREATE TABLE [#DesignTemp]
(
ParentDesignKey INT,
DesignKey INT
);
-- Insert the data, then create the index.
INSERT INTO [#DesignTemp]
SELECT
ParentDesignKey,
DesignKey
FROM [dbo].[Design];
COMMIT;
-- Try this index, or create indexes for individual columns if the plan works better at high volumes.
CREATE NONCLUSTERED INDEX ix_DesignTemp1 ON [#DesignTemp] (ParentDesignKey, DesignKey);
CREATE TABLE [#DesignLvl]
(
[DesignKey] INT,
[DesignLevel] INT
);
WITH RCTE AS
(
SELECT
*,
1 AS [Lvl]
FROM
[DesignTemp]
WHERE
[ParentDesignKey] IS NULL
UNION ALL
SELECT
[D].*,
[Lvl] + 1 AS [Lvl]
FROM
[DesignTemp] AS [D]
INNER JOIN
[RCTE] AS [rc] ON [rc].[DesignKey] = [D].[ParentDesignKey]
)
INSERT INTO [#DesignLvl]
SELECT
[DesignKey], [Lvl]
FROM
[RCTE];
【讨论】:
【参考方案2】:您的问题不完整,“查询很慢”,但是查询的哪一部分很慢?
CTEQuery
或 LEFT JOIN in really big query
我认为需要大查询的脚本,以及详细信息, 比如哪个表包含多少行,它们的数据类型等。
抛出关于大查询的更多细节。
如果有任何 UDF 参与连接条件,请告诉我们。
你为什么left join
临时表?为什么不INNER JOIN
单独测试性能或 CTE 和 Big Query。
一旦在递归部分使用[D].[ParentDesignKey] is not null
,
SELECT
[D].*,
[Lvl] + 1 AS [Lvl]
FROM
[dbo].[Design] AS [D]
INNER JOIN
[RCTE] AS [rc] ON [rc].[DesignKey] = [D].[ParentDesignKey]
and [D].[ParentDesignKey] is not null
注意:在 CTE 中仅使用那些需要的列。
如果可以Pre- Calculate [Lvl]
,因为Recursive CTE
性能特别差,涉及到很多记录。
每个 CTE 查询平均会处理多少行?
如果临时表的容量超过100 rows
,则可以在其上创建聚集索引,
CREATE CLUSTERED INDEX ix_DesignLvl
ON [#DesignLvl] ([DesignKey], [DesignLevel]);
如果您没有在连接条件中使用[DesignLevel]
,则从索引中删除。
另外,显示表[dbo].[Design]
的索引以及DesignKey和ParentDesignKey的少量数据。
获得Index Scan
的原因有多种,其中之一是Selectivity of Key
。
那么一个DesignKey
可以有多少行,一个ParentDesignKey
可以有多少行?
所以取决于上面的答案Create Composite Clustered Index
在表[dbo].[Design]
的两个键上
所以考虑到我的答案不完整,我会相应地更新它。
【讨论】:
【参考方案3】:根据我在 this article 上发布的测试,与递归 CTE 相比,基于集合的循环可以提高性能。
DECLARE @DesignLevel int = 0;
INSERT INTO [#DesignLvl]
SELECT [DesignKey], 1
FROM [RCTE];
WHILE @@ROWCOUNT > 0
BEGIN
SET @DesignLevel += 1;
INSERT INTO [#DesignLvl]
SELECT [D].[DesignKey], dl.DesignLevel
FROM [dbo].[Design] AS [D]
JOIN [#DesignLvl] AS [dl] ON [dl].[DesignKey] = [D].[ParentDesignKey]
WHERE dl.DesignLevel = @DesignLevel;
END;
【讨论】:
我试过了,但没有用,执行查询的结果相同【参考方案4】:试试@table,你用内存临时表而不是磁盘临时表查询
declare @DesignLvl table
(
[DesignKey] INT,
[DesignLevel] INT
);
WITH RCTE AS
(
SELECT
*,
1 AS [Lvl]
FROM
[Design]
WHERE
[ParentDesignKey] IS NULL
UNION ALL
SELECT
[D].*,
[Lvl] + 1 AS [Lvl]
FROM
[dbo].[Design] AS [D]
INNER JOIN
[RCTE] AS [rc] ON [rc].[DesignKey] = [D].[ParentDesignKey]
)
INSERT INTO @DesignLvl
SELECT
[DesignKey], [Lvl]
FROM
[RCTE]
可能有点帮助,我们在谈论多少行以及什么 sql server 版本? @@版本?
【讨论】:
【参考方案5】:正如其他人所说,您的查询的哪一部分很慢并不完全清楚。我们也不知道记录的数量(可能是 100 条,也可能是 1 亿条)或实际时间(您可能会认为 10 秒加载数百万行很慢?!?)。
我们也不知道您的really big query
有多“难”;据我们所知,如果没有 LEFT OUTER JOIN
也可能会很慢。
无论如何,为了更好地了解,如果你运行这个会发生什么:(未经测试的代码,你可能需要修复一些东西)
DECLARE @level int = 0,
@rowcount int
-- create working table to calculate levels
SELECT lvl = @level,
D.[DesignKey]
INTO #hierarchy
FROM [Design] D
WHERE D.[ParentDesignKey] IS NULL
SELECT @rowcount = @@ROWCOUNT
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - Loaded ' + Convert(nvarchar, @rowcount) + N' level ' + Convert(nvarchar, @rowcount) + ' records...'
CREATE UNIQUE CLUSTERED INDEX uq0 ON #hierarchy (lvl, [DesignKey])
WHILE @rowcount > 0
BEGIN
INSERT #hierarchy
SELECT lvl = @level + 1,
D.[DesignKey]
FROM #hierarchy t
JOIN [Design] D
ON D.[ParentDesignKey] = t.[DesignKey]
WHERE t.lvl = @level
SELECT @rowcount = @@ROWCOUNT,
@level = @level + 1
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - Loaded ' + Convert(nvarchar, @rowcount) + N' level ' + Convert(nvarchar, @rowcount) + ' records...'
END
GO
-- we now have a lvl value for each DesignKey but the index is backwards for future use; so add index in the other direction
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - re-indexing...'
CREATE UNIQUE INDEX uq1 ON #hiearchy ([DesignKey]) INCLUDE (lvl) WITH (FILLFACTOR = 100)
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - done.'
GO
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - Starting query...'
-- actual use:
;WITH DesignLvlCTE
AS (SELECT h.lvl, D.*
FROM [Design] D
JOIN #hierarchy h
ON h.[DesignKey] = D.[DesignKey])
SELECT...
INTO #result -- leave this in to exclude overhead time of client
FROM..
LEFT JOIN DesignLvlCTE AS [dl] ON d.DesignKey = dl.DesignKey
WHERE ...
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - Done fetching data.'
-- get results
SELECT * FROM #result
PRINT Convert(nvarchar, CURRENT_TIMESTAMP, 113) + ' - Done.'
-- DROP TABLE #result
【讨论】:
【参考方案6】:你试过memory optimized tables吗?我在类似的过程中使用了它们(递归 CTE),我得到了惊人的结果。在 SQL Server 2017 中也应该包含在标准版中。您首先需要为内存优化数据创建一个文件组:
ALTER DATABASE MyDB
ADD FILEGROUP mem_data CONTAINS MEMORY_OPTIMIZED_DATA;
GO
ALTER DATABASE MyDB
ADD FILE (NAME = 'MemData', FILENAME = 'D:\Data\MyDB_MemData.ndf') TO FILEGROUP mem_data;
然后你创建(或转换)你的表:
CREATETABLE dbo.MemoryTable
(
Col1 INT IDENTITY PRIMARY KEY
...
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
【讨论】:
【参考方案7】:您是否尝试过将SELECT *
更改为仅SELECT DesignLevel
,我发现对于宽行这足以更改执行计划以选择使用急切假脱机进行索引扫描:
WITH RCTE AS
(
SELECT
[DesignKey],
1 AS [Lvl]
FROM
[Design]
WHERE
[ParentDesignKey] IS NULL
UNION ALL
SELECT
[D].[DesignKey],
[Lvl] + 1 AS [Lvl]
FROM
[dbo].[Design] AS [D]
INNER JOIN
[RCTE] AS [rc] ON [rc].[DesignKey] = [D].[ParentDesignKey]
)
INSERT INTO [#DesignLvl]
SELECT
[DesignKey], [Lvl]
FROM
[RCTE]
计划和测试 SQL 可以在这里找到:https://www.brentozar.com/pastetheplan/?id=BymxTD4wV
【讨论】:
【参考方案8】:问题可能在于设计表很大,并且在没有任何主要过滤条件的情况下将其与自身连接导致扫描整个表。
因为您只对像 designkey 和 parentdesignkey 这样的极少数列感兴趣,请尝试将数据填充查询(插入 #designlvl)拆分为多个部分。
确保你有一个索引(designkey,parentdesignkey)
INSERT INTO #DesignLevel
SELECT DISTINCT DesignKey, 1 FROM Design WHERE ParentDesignKey IS NULL
INSERT INTO #DesignLevel
SELECT DISTINCT ParentDesignKey, Lvl+1 FROM Design WHERE ParentDesignKey is NOT NULL
【讨论】:
【参考方案9】:确保 DesignKey.ParentDesignKey 和 #DesignLv1.DesignKey 中没有空值 列,如果是这样,请尽可能使用不是空约束。我见过空值来创建交叉连接。
如果设计表是一个被频繁写入的事务表,请经常在此表上重建索引。
按顺序在 Design.DesignKey 和 Design.ParentDesignKey 上创建一个非聚集索引。
在#DesignLvl DesignKey 上创建非聚集索引。
如果设计表很大(> 1000 万行)和一大堆列,请创建一个索引视图,其中包含仅用于此查询的不同列并使用它。
检查系统事件日志以了解具有 tempdb 的磁盘上的磁盘读写故障,并且(您应该将 tempdb 放在 RAID 1 或 RAID 10 阵列上,因为它们已针对高写入应用程序进行了优化。)来自 (https://searchsqlserver.techtarget.com/tip/SQL-Server-tempdb-best-practices-increase-performance )
【讨论】:
以上是关于使用 CTE 优化时态表的主要内容,如果未能解决你的问题,请参考以下文章