如何避免 SQL 服务器对缺失值使用默认行估计?
How to avoid SQL Server using default row estimate for missing values?
我有一个 table 有几百万行,其中 Status
列是非聚集索引中的第 2 列。
状态为 char(10)
,包含“新建”、“处理中”、“处理中”和“失败”
轮询函数检查新行
SELECT TOP 1 ... FROM Table WHERE firstColumnInIdex = 1 AND Status = 'New' ORDER BY Id
(其实是对状态“Processing”的更新和一些其他的区别,不过这里没关系)
查询使用非聚集索引,但行估计是行的 ~30%,因此内存授予在 GB 范围内。
我的测试显示问题出在统计数据上。由于 table 中通常没有状态为“新建”的行,因此统计信息中不存在“新建”(表示数百万个“已处理”和数千个“失败”)。 SQL 服务器似乎采用默认估计,在本例中为 ~30% 的行,如果在统计信息中未找到该值。
我在 table 中添加了一行,状态为“新建”,并使用 FULLSCAN NORECOMPUTE
创建了新的统计信息。 (所以它变成了数百万个“已处理”,数千个“失败”和 1 个“新”)
现在,行估计为 1 行,查询成本从 82 下降到 6,并提供少量内存。
(删除统计数据再次导致 30%)
虽然这个技巧解决了这个问题,但感觉就像是一个 hack,可能有一天会停止工作(例如,一些未来的 dba 发现了这个过时的统计数据并且 deletes/updates 它)。
有没有更好的方法解决这个问题?例如
- 改用整数状态?
- 让 SQL 服务器知道带有外键或约束的“新建”状态?
版本为 2016SP1
我觉得有用的一件事是filtered index。
假设这是一个队列并且事情以状态 'new' 开始,你
- Select 一或所有 'new' 行(获取 PK ID)
- 根据这些 ID 采取行动
- 根据ID更新状态
在这些情况下,您可以创建一个过滤索引,它基本上只是状态为 'new'.
的所有行的 up-to-date 列表
CREATE NONCLUSTERED INDEX ix_myindex ON [myTable]
([ID])
WHERE (Status = 'New')
注意 - 索引将非常 'hot' 例如,有很多更改(一旦它们不再 'new',它们就会从索引中删除)。
然而,我们的想法是尽可能小,这并不重要。
确保索引包含识别相关行(例如,您的 PK)所需的所有字段,以使其尽可能保持 simple/small,并查看它是否有效。
更新以下评论
这些问题可能与 'Ascending key problem' 有关 - 请随时研究和审查。
我可能在上面犯了一个小错误 - 如果您实际包含要过滤的字段,过滤索引通常会工作得更好。所以下面的可能更好
CREATE NONCLUSTERED INDEX ix_myindex ON [myTable]
([ID], [Status])
WHERE (Status = 'New')
关于解决方案中的方法 - 我们的想法是我们将完全忽略统计数据。相反,我们实际上创建了一个具有相关行数的临时 table,这些将限制基数估计。
为了测试,我有一个名为 'test' 的 table,它有大约 150 万行,ID PK 和 4 列 UUID(本质上是随机数据)。
我用它来创建一个带有状态栏的新 table 'test2'。其中大约 80% 的状态为 'Processed',10% 的状态为 'Processing',10% 的状态为 'Failed'.
然后我插入状态为 'New' 的新行。请注意,统计数据 不会更新 。
但是,我随后使用过滤索引来识别相关行,方法是将它们放入临时 table - 并使用该 table 进行进一步处理。
设置
IF OBJECT_ID (N'test2', N'U') IS NOT NULL DROP TABLE dbo.Test2;
GO
CREATE TABLE [dbo].[test2](
[ID] [int] NOT NULL,
[Status] [varchar](12) NULL,
[col2] [varchar](100) NULL,
[col3] [varchar](100) NULL,
[col4] [varchar](100) NULL,
[col5] [varchar](100) NULL,
CONSTRAINT [PK_test2] PRIMARY KEY CLUSTERED ([ID] ASC)
);
GO
CREATE NONCLUSTERED INDEX [IX_test2_StatusNew] ON [dbo].[test2] ([ID] ASC, [Status] ASC)
WHERE ([Status]='New');
GO
INSERT INTO dbo.Test2 (ID, Status, Col2, Col3, Col4, Col5)
SELECT ID, CASE WHEN ID % 12 < 10 THEN 'Processed' WHEN ID % 12 = 10 THEN 'Processing' ELSE 'Failed' END,
Col2, Col3, Col4, Col5
FROM dbo.Test;
GO
CREATE STATISTICS [S_Status] ON [dbo].[test2]([Status]);
GO
DBCC SHOW_STATISTICS ('dbo.Test2', 'S_Status');
/*
RANGE_HI_KEY RANGE_ROWS EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
Failed 0 141420 0 1
Processed 0 1417080 0 1
Processing 0 141420 0 1
*/
这是我的存储过程 - 它首先标记适当的行(将它们的状态更改为 'Processing')并记录它们的 ID。
然后使用 ID 处理 table 中的行,然后再次将状态更新为 'Processed'。
为简洁起见,我没有包含任何交易或 error-checking。
CREATE PROCEDURE UpdateTest2News
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #IDs_to_process (ID int PRIMARY KEY);
UPDATE test2
SET Status = 'Processing'
OUTPUT deleted.ID
INTO #IDs_to_process
WHERE Status = 'New';
UPDATE test2
SET Col2 = NEWID(),
Col3 = NEWID(),
Col4 = NEWID(),
Col5 = NEWID()
FROM test2
INNER JOIN #IDs_to_Process IDs ON test2.ID = IDs.ID;
UPDATE test2
SET Status = 'Processed'
FROM test2
INNER JOIN #IDs_to_Process IDs ON test2.ID = IDs.ID;
END;
然后我在 Test2 中添加一个新行(状态为 'New')。检查统计数据时,它们没有改变(没有发生足够的变化来强制更新)。
SELECT TOP 1 ID FROM dbo.test2 ORDER BY ID DESC; -- Getting the latest value for next step
/* Max ID = 1699920 */
INSERT INTO dbo.Test2 (ID, Status, Col2, Col3, Col4, Col5)
SELECT 1699921, 'New', NULL, NULL, NULL, NULL;
DBCC SHOW_STATISTICS ('dbo.Test2', 'S_Status');
/* Same as above */
DBCC SHOW_STATISTICS ('dbo.Test2', 'IX_test2_StatusNew');
/* No records represented in stats */
GO
现在,最后的步骤
- 运行
SET STATISTICS TIME, IO ON;
查看处理统计数据
- 同时设置 'Include actual execution plan' 以查看估计值与实际值等
EXEC UpdateTest2News
这里有一个 cleaned-up 版本统计数据 - 非常好。
Stats summary
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
Table '#IDs_to_process___...________________0000000000BC'. Scan count 0, logical reads 2
Table 'test2'. Scan count 1, logical reads 7
Table 'Worktable'. Scan count 1, logical reads 5
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 14 ms.
SQL Server parse and compile time:
CPU time = 25 ms, elapsed time = 25 ms.
Table 'test2'. Scan count 0, logical reads 11
Table '#IDs_to_process________...__________0000000000BC'. Scan count 1, logical reads 2
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 593 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
Table 'test2'. Scan count 0, logical reads 3
Table '#IDs_to_process_____...______0000000000BC'. Scan count 1, logical reads 2
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 45 ms.
SQL Server Execution Times:
CPU time = 61 ms, elapsed time = 683 ms.
这是执行 plan/etc,估计与实际也很好。
注意 - 它 执行 remember/cache 执行计划,当您的 'new' 行数大不相同时,这可能会成为一个问题。
如果需要,您可以将 OPTION (RECOMPILE)
放在存储过程中的语句 2 或 3 上,以便采用新的行数估计值。
此外,如果需要,命令 UPDATE STATISTICS test2 (IX_test2_StatusNew) WITH fullscan
对于 运行 来说是微不足道的(因为该索引中几乎没有行)- 这可能对您的情况有所帮助。
我有一个 table 有几百万行,其中 Status
列是非聚集索引中的第 2 列。
状态为 char(10)
,包含“新建”、“处理中”、“处理中”和“失败”
轮询函数检查新行
SELECT TOP 1 ... FROM Table WHERE firstColumnInIdex = 1 AND Status = 'New' ORDER BY Id
(其实是对状态“Processing”的更新和一些其他的区别,不过这里没关系)
查询使用非聚集索引,但行估计是行的 ~30%,因此内存授予在 GB 范围内。
我的测试显示问题出在统计数据上。由于 table 中通常没有状态为“新建”的行,因此统计信息中不存在“新建”(表示数百万个“已处理”和数千个“失败”)。 SQL 服务器似乎采用默认估计,在本例中为 ~30% 的行,如果在统计信息中未找到该值。
我在 table 中添加了一行,状态为“新建”,并使用 FULLSCAN NORECOMPUTE
创建了新的统计信息。 (所以它变成了数百万个“已处理”,数千个“失败”和 1 个“新”)
现在,行估计为 1 行,查询成本从 82 下降到 6,并提供少量内存。
(删除统计数据再次导致 30%)
虽然这个技巧解决了这个问题,但感觉就像是一个 hack,可能有一天会停止工作(例如,一些未来的 dba 发现了这个过时的统计数据并且 deletes/updates 它)。
有没有更好的方法解决这个问题?例如
- 改用整数状态?
- 让 SQL 服务器知道带有外键或约束的“新建”状态?
版本为 2016SP1
我觉得有用的一件事是filtered index。
假设这是一个队列并且事情以状态 'new' 开始,你
- Select 一或所有 'new' 行(获取 PK ID)
- 根据这些 ID 采取行动
- 根据ID更新状态
在这些情况下,您可以创建一个过滤索引,它基本上只是状态为 'new'.
的所有行的 up-to-date 列表CREATE NONCLUSTERED INDEX ix_myindex ON [myTable]
([ID])
WHERE (Status = 'New')
注意 - 索引将非常 'hot' 例如,有很多更改(一旦它们不再 'new',它们就会从索引中删除)。
然而,我们的想法是尽可能小,这并不重要。
确保索引包含识别相关行(例如,您的 PK)所需的所有字段,以使其尽可能保持 simple/small,并查看它是否有效。
更新以下评论
这些问题可能与 'Ascending key problem' 有关 - 请随时研究和审查。
我可能在上面犯了一个小错误 - 如果您实际包含要过滤的字段,过滤索引通常会工作得更好。所以下面的可能更好
CREATE NONCLUSTERED INDEX ix_myindex ON [myTable]
([ID], [Status])
WHERE (Status = 'New')
关于解决方案中的方法 - 我们的想法是我们将完全忽略统计数据。相反,我们实际上创建了一个具有相关行数的临时 table,这些将限制基数估计。
为了测试,我有一个名为 'test' 的 table,它有大约 150 万行,ID PK 和 4 列 UUID(本质上是随机数据)。
我用它来创建一个带有状态栏的新 table 'test2'。其中大约 80% 的状态为 'Processed',10% 的状态为 'Processing',10% 的状态为 'Failed'.
然后我插入状态为 'New' 的新行。请注意,统计数据 不会更新 。
但是,我随后使用过滤索引来识别相关行,方法是将它们放入临时 table - 并使用该 table 进行进一步处理。
设置
IF OBJECT_ID (N'test2', N'U') IS NOT NULL DROP TABLE dbo.Test2;
GO
CREATE TABLE [dbo].[test2](
[ID] [int] NOT NULL,
[Status] [varchar](12) NULL,
[col2] [varchar](100) NULL,
[col3] [varchar](100) NULL,
[col4] [varchar](100) NULL,
[col5] [varchar](100) NULL,
CONSTRAINT [PK_test2] PRIMARY KEY CLUSTERED ([ID] ASC)
);
GO
CREATE NONCLUSTERED INDEX [IX_test2_StatusNew] ON [dbo].[test2] ([ID] ASC, [Status] ASC)
WHERE ([Status]='New');
GO
INSERT INTO dbo.Test2 (ID, Status, Col2, Col3, Col4, Col5)
SELECT ID, CASE WHEN ID % 12 < 10 THEN 'Processed' WHEN ID % 12 = 10 THEN 'Processing' ELSE 'Failed' END,
Col2, Col3, Col4, Col5
FROM dbo.Test;
GO
CREATE STATISTICS [S_Status] ON [dbo].[test2]([Status]);
GO
DBCC SHOW_STATISTICS ('dbo.Test2', 'S_Status');
/*
RANGE_HI_KEY RANGE_ROWS EQ_ROWS DISTINCT_RANGE_ROWS AVG_RANGE_ROWS
Failed 0 141420 0 1
Processed 0 1417080 0 1
Processing 0 141420 0 1
*/
这是我的存储过程 - 它首先标记适当的行(将它们的状态更改为 'Processing')并记录它们的 ID。
然后使用 ID 处理 table 中的行,然后再次将状态更新为 'Processed'。
为简洁起见,我没有包含任何交易或 error-checking。
CREATE PROCEDURE UpdateTest2News
AS
BEGIN
SET NOCOUNT ON;
CREATE TABLE #IDs_to_process (ID int PRIMARY KEY);
UPDATE test2
SET Status = 'Processing'
OUTPUT deleted.ID
INTO #IDs_to_process
WHERE Status = 'New';
UPDATE test2
SET Col2 = NEWID(),
Col3 = NEWID(),
Col4 = NEWID(),
Col5 = NEWID()
FROM test2
INNER JOIN #IDs_to_Process IDs ON test2.ID = IDs.ID;
UPDATE test2
SET Status = 'Processed'
FROM test2
INNER JOIN #IDs_to_Process IDs ON test2.ID = IDs.ID;
END;
然后我在 Test2 中添加一个新行(状态为 'New')。检查统计数据时,它们没有改变(没有发生足够的变化来强制更新)。
SELECT TOP 1 ID FROM dbo.test2 ORDER BY ID DESC; -- Getting the latest value for next step
/* Max ID = 1699920 */
INSERT INTO dbo.Test2 (ID, Status, Col2, Col3, Col4, Col5)
SELECT 1699921, 'New', NULL, NULL, NULL, NULL;
DBCC SHOW_STATISTICS ('dbo.Test2', 'S_Status');
/* Same as above */
DBCC SHOW_STATISTICS ('dbo.Test2', 'IX_test2_StatusNew');
/* No records represented in stats */
GO
现在,最后的步骤
- 运行
SET STATISTICS TIME, IO ON;
查看处理统计数据 - 同时设置 'Include actual execution plan' 以查看估计值与实际值等
EXEC UpdateTest2News
这里有一个 cleaned-up 版本统计数据 - 非常好。
Stats summary
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
Table '#IDs_to_process___...________________0000000000BC'. Scan count 0, logical reads 2
Table 'test2'. Scan count 1, logical reads 7
Table 'Worktable'. Scan count 1, logical reads 5
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 14 ms.
SQL Server parse and compile time:
CPU time = 25 ms, elapsed time = 25 ms.
Table 'test2'. Scan count 0, logical reads 11
Table '#IDs_to_process________...__________0000000000BC'. Scan count 1, logical reads 2
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 593 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.
Table 'test2'. Scan count 0, logical reads 3
Table '#IDs_to_process_____...______0000000000BC'. Scan count 1, logical reads 2
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 45 ms.
SQL Server Execution Times:
CPU time = 61 ms, elapsed time = 683 ms.
这是执行 plan/etc,估计与实际也很好。
注意 - 它 执行 remember/cache 执行计划,当您的 'new' 行数大不相同时,这可能会成为一个问题。
如果需要,您可以将 OPTION (RECOMPILE)
放在存储过程中的语句 2 或 3 上,以便采用新的行数估计值。
此外,如果需要,命令 UPDATE STATISTICS test2 (IX_test2_StatusNew) WITH fullscan
对于 运行 来说是微不足道的(因为该索引中几乎没有行)- 这可能对您的情况有所帮助。