【问题标题】:How to avoid SQL Server using default row estimate for missing values?如何避免 SQL Server 对缺失值使用默认行估计?
【发布时间】:2021-02-02 16:44:30
【问题描述】:

我有一个包含几百万行的表,其中有一个 Status 列,这是非聚集索引中的第二列。 状态为char(10),包含“New”、“Processing”、“Processed”和“Failed”

轮询函数检查新行

SELECT TOP 1 ... FROM Table WHERE firstColumnInIdex = 1 AND Status = 'New' ORDER BY Id

(实际上是对状态“处理中”的更新和其他一些差异,但在这里无关紧要)

查询使用非聚集索引,但行估计约为行的 30%,因此内存授予在 GB 范围内。

我的测试表明问题出在统计数据上。由于表中通常没有状态为“New”的行,因此统计信息中不存在“New”(表示数百万“已处理”和数千“失败”)。如果在统计信息中找不到该值,SQL Server 似乎会采用默认估计值,在这种情况下约为 30% 的行。

我在表中添加了一行,状态为“新建”,并使用FULLSCAN NORECOMPUTE 创建了新的统计信息。 (所以它变成数百万“已处理”,数千“失败”和 1“新”)

现在行估计是 1 行,查询成本从 82 下降到 6,内存授予很小。

(删除统计数据再次导致 30%)

虽然这个技巧解决了问题,但感觉就像是某天可能会停止工作的 hack(例如,某些未来的 dba 会发现这些过时的统计数据并删除/更新它)。

有没有更好的方法来解决这个问题?例如

  • 改用整数状态?
  • 使用外键或约束让 SQL Server 知道“新建”状态?

版本是 2016SP1

【问题讨论】:

  • 您能否将“新”行与所有其他行存储在单独的表中,并在处理后将它们移至主表?
  • @MJH,是的,但是我可能需要一个将它们组合起来的视图,以获得未开始、正在进行、最近完成等的完整图片。或者触发将新行的 ID 存储在单独的表(或者只是在表中保留一个虚拟的“新”行以获得正确的统计信息)。
  • 您确定状态字段是CHAR(10) - 一个固定长度字段而不是 VARCHAR?另请注意,“处理中”(11 个字符)不适合它。
  • @seanb, 'Processing' 是 10 个字符 :-)
  • 哈哈哎呀。你是对的。

标签: sql-server sql-execution-plan


【解决方案1】:

我觉得有用的是filtered index

假设这是一个队列并且事情从状态“新”开始,你

  • 选择一个或所有“新”行(获取 PK ID)
  • 对这些 ID 采取行动
  • 根据 ID 更新状态

在这些情况下,您可以创建一个过滤索引,它基本上只是所有状态为“新”的行的最新列表。

CREATE NONCLUSTERED INDEX ix_myindex ON [myTable] 
([ID])
WHERE (Status = 'New')

注意 - 索引将非常“热门”,例如,有很多变化(一旦它们不再是“新的”,它们就会从索引中删除)。

但是,我们的想法是保持小到无关紧要。

确保索引具有识别相关行(例如,您的 PK)所需的所有字段,以使其尽可能简单/小,并查看它是否有效。

更新以下评论

这些问题可能与“升序关键问题”有关 - 请随时研究和审查。

我可能在上面犯了一个小错误 - 如果您实际包含要过滤的字段,通常过滤索引会更好地工作。因此,以下可能会更好。

CREATE NONCLUSTERED INDEX ix_myindex ON [myTable] 
([ID], [Status])
WHERE (Status = 'New')

关于解决方案中的方法 - 我们将完全忽略统计数据。相反,我们实际上创建了一个具有相关行数的临时表,而这些行数将限制基数估计。

为了进行测试,我有一个名为“test”的表,它有大约 150 万行,带有一个 ID PK 和 4 个带有 UUID 的列(基本上是随机数据)。

我用它来创建一个带有状态列的新表“test2”。其中大约 80% 的状态为“已处理”,10% 的状态为“处理中”,10% 的状态为“失败”。

然后我插入一个状态为“新”的新行。请注意,统计信息不会更新

但是,我随后使用过滤索引来识别相关行,方法是将它们放入临时表中 - 并使用该表进行进一步处理。

设置

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
*/

这是我的存储过程 - 它首先标记适当的行(将它们的状态更改为“处理中”)并记录它们的 ID。

然后这些 ID 用于处理表中的行,然后再次将状态更新为“已处理”。

为了简洁起见,我没有包括任何交易或错误检查。

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; 以查看处理统计信息
  • 还可以设置“包括实际执行计划”以查看估计值与实际值等
EXEC UpdateTest2News

这是一个经过清理的版本统计数据 - 非常好。

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.

这是执行计划/等等,估计值与实际值也很好。


注意 - 它确实记住/缓存了执行计划,当你有大量不同数量的“新”行时,这可能会变成一个问题。

如果需要,您可以将 OPTION (RECOMPILE) 放在存储过程中的语句 2 或 3 上,这样它就会采用新的行数估计值。

如果需要,命令 UPDATE STATISTICS test2 (IX_test2_StatusNew) WITH fullscan 也很容易运行(因为该索引中几乎没有行) - 这可能对您的情况有所帮助。

【讨论】:

  • 初始测试表明它有效,但我必须包含查询中的所有列,否则它将用于聚集索引。谢谢。明天我会再测试一下,然后再标记为答案
  • 一种替代方法是从索引中提取 PK,例如,将 PK 插入临时表(与原始表具有相同的 PK),然后使用该临时表在原始表的连接中。如果您正在执行复杂的处理,则此临时表可能已经存在于您的进程中。我并不是说这种方法更好,但可能 - 这真的取决于您的具体情况以及添加宽索引的问题有多大。
  • 我对此进行了更多测试,不幸的是部分索引不起作用:-(。必须有一个包含状态“新”的统计信息,否则仍使用默认值。只需在一个“新”过滤器没有任何区别。
  • 目前正在考虑添加一个触发器来将新行的索引放入第二个表中。只是看起来很奇怪,我以前从未听说过其他人有这个问题。
  • 谢谢,您的新代码或多或少是我正在做的。我的代码中的额外过滤必须有一些东西导致它。我不会花更多时间在上面,您的测试代码也对我有用,所以我将其标记为答案。 (对我来说最简单的解决方案是在表中保留一个状态为“新”的虚拟行,因为其他原因永远不会被击中)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多