为什么我的SELECT DISTINCT TOP N查询会扫描整个表?


27

我遇到了一些SELECT DISTINCT TOP N查询,这些查询似乎没有被SQL Server查询优化器优化。让我们从一个简单的例子开始:一个带有两个交替值的百万行表。我将使用GetNums函数生成数据:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

对于以下查询:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server仅通过扫描表的第一个数据页就可以找到两个不同的值,但是它会扫描所有数据。SQL Server为什么不扫描直到找到所需数量的不同值?

对于此问题,请使用以下测试数据,该数据包含1000万行,并在块中生成10个不同的值:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

带有聚集索引的表的答案也可以接受:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

以下查询扫描表中的所有1000万行。我怎么能得到不能扫描整个桌子的东西?我正在使用SQL Server 2016 SP1。

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

游标甚至可以工作10个
狗仔队

Answers:


29

看起来有三种不同的优化器规则可以执行DISTINCT上述查询中的操作。以下查询引发错误,表明该列表是详尽无遗的:

SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);

Msg 8622,第16级,状态1,第1行

由于此查询中定义的提示,查询处理器无法生成查询计划。重新提交查询,而不指定任何提示,也无需使用SET FORCEPLAN。

GbAggToSort将分组汇总(区别)实现为一种独特的排序方式。这是一个阻塞运算符,它将在产生任何行之前从输入读取所有数据。GbAggToStrm将group-by聚合实现为流聚合(在这种情况下,也需要输入排序)。这也是一个阻塞运算符。GbAggToHS实现为哈希匹配,这是我们从问题中的错误计划中看到的结果,但可以实现为哈希匹配(聚合)或哈希匹配(流程不同)。

哈希匹配(流不同)运算符是解决此问题的一种方法,因为它不会阻塞。一旦找到足够多的不同值,SQL Server应该能够停止扫描。

Flow Distinct逻辑运算符扫描输入,删除重复项。Distinct运算符在产生任何输出之前会消耗掉所有输入,而Flow Distinct运算符返回从输入中获得的每一行(除非该行是重复的,在这种情况下将被丢弃)。

为什么问题中的查询使用哈希匹配(聚合)而不是哈希匹配(流不同)?随着表中不同值数量的变化,我希望哈希匹配(流不同)查询的成本会减少,因为对需要扫描到表的行数的估计应该减少。我希望哈希匹配(聚合)计划的成本会增加,因为它需要构建的哈希表会变得更大。一种调查方法是创建计划指南。如果我创建了两个数据副本,但对其中一个应用了计划指南,则我应该能够针对同一数据并列比较哈希匹配(聚合)与哈希匹配(不同)。请注意,我无法通过禁用查询优化器规则来做到这一点,因为同一规则适用于两个计划(GbAggToHS)。

这是获取我所遵循的计划指南的一种方法:

DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;

CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;

-- run this query
SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

获取计划句柄并使用它来创建计划指南:

-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM 
sys.dm_exec_query_stats AS qs   
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st  
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;

EXEC sp_create_plan_guide_from_handle 
'EVIL_PLAN_GUIDE', 
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;

计划指南仅适用于确切的查询文本,因此让我们将其从计划指南中复制回去:

SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';

重置数据:

TRUNCATE TABLE X_PLAN_GUIDE_TARGET;

INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

应用计划指南为查询获取查询计划

SELECT DISTINCT TOP 10 VAL  FROM X_PLAN_GUIDE_TARGET  OPTION (MAXDOP 1)

它具有我们想要的测试数据哈希匹配(流不同)运算符。请注意,SQL Server希望从表中读取所有行,并且估计成本与具有哈希匹配项(聚合)的计划完全相同。我所做的测试表明,当计划的行目标大于或等于SQL Server从表中期望的不同值的数量时,两个计划的成本是相同的,在这种情况下,可以直接从统计。不幸的是(对于我们的查询),当成本相同时,优化器会选择哈希匹配(聚合),而不是哈希匹配(流不同)。因此,我们离所需计划有0.0000001个魔术优化器单位。

解决此问题的一种方法是降低行目标。如果从优化器的角度出发的行目标小于行的唯一计数,我们可能会得到哈希匹配(流不同)。这可以通过OPTIMIZE FOR查询提示来完成:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

对于此查询,优化器会创建一个计划,就好像查询只需要第一行,而执行查询时,它会返回前10行。在我的机器上,此查询从X_10_DISTINCT_HEAP299毫秒中扫描892800行,并在250毫秒CPU时间和2537逻辑读取中完成。

请注意,如果统计信息仅报告一个不同的值(针对偏斜数据的抽样统计信息可能会发生这种情况),则此技术将不起作用。但是,在这种情况下,您的数据不太可能足够密集地打包以使用此类技术进行验证。扫描表中的所有数据可能不会造成很多损失,特别是如果可以并行进行的话。

解决此问题的另一种方法是通过夸大SQL Server期望从基表中获得的估计的不同值的数量。这比预期的要难。应用确定性函数可能无法增加结果的不同计数。如果查询优化器知道该数学事实(一些测试表明至少是出于我们的目的),则应用确定性函数(包括所有字符串函数)将不会增加不同行的估计数量。

许多不确定的功能没有任何工作,包括明显的选择NEWID()RAND()。但是,LAG()此查询有技巧。查询优化器期望针对该LAG表达式的1000万个不同的值,这将鼓励哈希匹配(流不同)计划

SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);

在我的计算机上,此查询从X_10_DISTINCT_HEAP1165 ms 扫描892800行并在1109 ms的CPU时间和2537的逻辑读取中完成查询,因此LAG()增加了相当多的相对开销。@Paul White建议尝试对此查询进行批处理模式处理。在SQL Server 2016上,即使使用,也可以进行批处理模式处理MAXDOP 1。获取行存储表批处理模式的一种方法是加入空CCI,如下所示:

CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);

CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;

SELECT DISTINCT TOP 10 VAL
FROM
(
    SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
    FROM X_10_DISTINCT_HEAP
    LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);

该代码将导致此查询计划

Paul指出,我不得不更改查询以使用它,LAG(..., 1)因为LAG(..., 0)它似乎不符合Window Aggregate优化的条件。此更改将经过时间减少到520 ms,将CPU时间减少到454 ms。

请注意,这种LAG()方法不是最稳定的一种。如果Microsoft针对该功能更改了唯一性假设,则它可能不再起作用。与旧版CE的估算值不同。同样,针对堆的这种优化也不是一个好主意。如果重建表,则可能会出现最坏的情况,即几乎所有行都需要从表中读取。

对于具有唯一列的表(例如问题中的聚集索引示例),我们有更好的选择。例如,我们可以通过使用SUBSTRING始终返回空字符串的表达式来欺骗优化器。SQL Server认为SUBSTRING不会更改的不同值的数量,因此,如果将其应用于唯一列(例如PK),则估计的不同行的数量为1000万。以下查询获取哈希匹配(流不同)运算符:

SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);

在我的机器上,此查询扫描来自900000行 X_10_DISTINCT_CI 333 ms中并在297 ms CPU时间和3011逻辑读取中完成。

总而言之,SELECT DISTINCT TOP NN> =估计的表中不同的行数时,查询优化器似乎假定将从表中读取所有行。哈希匹配(聚合)运算符的成本可能与哈希匹配(流不同)运算符的成本相同,但优化器始终选择聚合运算符。当足够多的不同值位于表扫描开始处附近时,这可能导致不必要的逻辑读取。诱使优化器使用散列匹配(流不同)运算符的两种方法是使用OPTIMIZE FOR提示降低行目标或使用唯一列LAG()SUBSTRING在唯一列上增加估计的不同行数。


12

您已经正确回答了自己的问题。

我只是想补充一点,最有效的方法实际上是扫描整个表-如果可以将其组织为列存储“堆”

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

简单查询:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

然后给出:

执行计划

表'X_10_DISTINCT_HEAP'。扫描计数1
 逻辑读取0,物理读取0,预读读取0, 
 大逻辑读取66,lob物理读取0,lob预读读取0。
表'X_10_DISTINCT_HEAP'。段读取为13,段跳过0。

 SQL Server执行时间:
   CPU时间= 0毫秒,经过的时间= 11毫秒。

哈希匹配(流不同)当前无法以批处理模式执行。由于从批处理到行处理的(不可见的)昂贵的过渡,使用此方法的方法要慢得多。例如:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

给出:

流程不同的执行计划

表'X_10_DISTINCT_HEAP'。扫描计数1
 逻辑读取0,物理读取0,预读读取0, 
 lob逻辑读取20,lob物理读取0,lob预读读取0。
表'X_10_DISTINCT_HEAP'。段读为4,段跳过为0。

 SQL Server执行时间:
   CPU时间= 640毫秒,经过的时间= 680毫秒。

这比将表组织为行存储堆时要慢。


4

这是尝试使用递归CTE模拟重复的部分扫描(类似于但不同于跳过扫描)的尝试。由于我们没有索引,其目的(id)是避免对表进行排序和多次扫描。

它做了一些技巧来绕过一些递归CTE限制:

  • 没有TOP允许递归部分。我们使用子查询ROW_NUMBER()代替。
  • 我们不能对常量部分有多个引用,也不能对递归部分使用LEFT JOIN或使用NOT IN (SELECT id FROM cte)。要绕过,我们构建了一个VARCHAR字符串,该字符串累积所有id值,类似于STRING_AGG或类似于hierarchyID,然后与进行比较LIKE

对于rextester.com上的堆(假设列名为id),请使用test-1

如测试所示,这不能避免多次扫描,但是当在前几页中找到不同的值时执行“确定”。但是,如果值分布不均匀,则可能会对表的大部分进行多次扫描-这样会导致性能下降。

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

并将表聚集(CI上unique_key)时,在rextester.com上使用test-2

这使用聚簇索引(WHERE x.unique_key > ct.unique_key)避免多次扫描:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;

该解决方案存在一个相当细微的性能问题。找到第N个值后,最终在表上进行了额外的查找。因此,如果前10个值有10个不同的值,它将寻找第11个不存在的值。最后,您需要进行额外的全面扫描,并且实际上总共有1000万个ROW_NUMBER()计算。我这里有一个解决方法,可以将机器上的查询速度提高20倍。你怎么看?brentozar.com/pastetheplan/?id=SkDhAmFKe
Joe Obbish

2

为了完整起见,解决此问题的另一种方法是使用OUTER APPLY。我们可以OUTER APPLY为需要查找的每个不同的值添加一个运算符。这在概念上与ypercube的递归方法相似,但是有效地手工编写了递归。优点之一是,我们可以TOP在派生表中使用它而不是ROW_NUMBER()解决方法。一个主要的缺点是查询文本随着N增加而变长。

这是针对堆的查询的一种实现:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

是上述查询的实际查询计划。在我的计算机上,此查询在713毫秒(625毫秒的CPU时间和12605逻辑读取)中完成。我们每10万行获得一个新的不同值,因此我希望此查询能够扫描900000 * 10 * 0.5 = 4500000行。从理论上讲,此查询应从另一个答案中对该逻辑进行五次逻辑读取:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

该查询进行了2537次逻辑读取。2537 * 5 = 12685,非常接近12605。

对于具有聚集索引的表,我们可以做得更好。这是因为我们可以将最后一个集群键值传递到派生表中,以避免两次扫描相同的行。一种实现:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

是上述查询的实际查询计划。在我的计算机上,此查询在154毫秒内完成,具有140毫秒的CPU时间和3203逻辑读取。这似乎比OPTIMIZE FOR对聚簇索引表的查询要快一些。我没想到,所以我尝试更仔细地评估性能。我的方法是没有结果集运行的每个查询十倍,并在汇总数据看,从sys.dm_exec_sessionssys.dm_exec_session_wait_stats。会话56是APPLY查询,会话63是OPTIMIZE FOR查询。

输出sys.dm_exec_sessions

╔════════════╦══════════╦════════════════════╦═══════════════╗
 session_id  cpu_time  total_elapsed_time  logical_reads 
╠════════════╬══════════╬════════════════════╬═══════════════╣
         56      1360                1373          32030 
         63      2094                2091          30400 
╚════════════╩══════════╩════════════════════╩═══════════════╝

APPLY查询的cpu_time和elapsed_time似乎有明显的优势。

输出sys.dm_exec_session_wait_stats

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
 session_id            wait_type             waiting_tasks_count  wait_time_ms  max_wait_time_ms  signal_wait_time_ms 
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
         56  SOS_SCHEDULER_YIELD                             340             0                 0                    0 
         56  MEMORY_ALLOCATION_EXT                            38             0                 0                    0 
         63  SOS_SCHEDULER_YIELD                             518             0                 0                    0 
         63  MEMORY_ALLOCATION_EXT                            98             0                 0                    0 
         63  RESERVED_MEMORY_ALLOCATION_EXT                  400             0                 0                    0 
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

OPTIMIZE FOR查询还有一个附加的等待类型RESERVED_MEMORY_ALLOCATION_EXT。我不知道这意味着什么。它可能只是散列匹配(流量不同)运算符中开销的度量。无论如何,不​​值得担心CPU时间相差70毫秒。


1

我认为您
对此有一个答案,为什么这可能是解决该问题的一种方式,但
我知道执行起来很混乱,但是执行计划指出,前2名仅占成本的84%

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;

这段代码在我的机器上花了5秒钟。看起来,联接到表变量会增加很多开销。在最终查询中,表变量被扫描892800次。该查询花费了1359 ms的CPU时间和1374 ms的经过时间。绝对超出我的预期。向主表变量添加主键似乎有所帮助,尽管我不确定为什么。可能还有其他可能的优化。
Joe Obbish

-4

我认为您需要退后一步,客观地看您的问题,以了解您所看到的。

查询优化程序如何选择前10个不同的值而不先确定完整的不同值列表?

Select Distinct需要全表(或覆盖索引)扫描,以识别其结果集。考虑一下-表中的最后一行可能包含一个从未见过的值。

选择不同是一种非常钝的武器。


2
并不是的。如果我扫描一个表并且前20行有10个不同的值,为什么我需要继续扫描表的其余部分?
ypercubeᵀᴹ

2
当我只要求10个时,为什么还要继续寻找?它已经找到10个不同的值,应该停止。那是问题的问题。
ypercubeᵀᴹ

3
为什么前N个搜索需要先查看整个结果集?如果它具有10个不同的值,那么您只关心它就可以停止寻找其他值。如果必须对整个结果集进行排序以知道哪个“是”另一个故事的前10个,但是如果您只希望10个不同的值而不关心哪个10个,则没有逻辑上的要求就可以得到整个结果集。
汤姆五世-莫妮卡团队

2
试想一下,您自己的任务是返回请求的集合。要求您提供千万个不同的前十个值,并且不指示您遵循任何排序顺序。如果在看完例如前100个值之后得出结果,您是否会感到自己有义务遍历整个值集?那将是毫无意义的。现在在数据库产品中实现该逻辑是另一回事,但是您似乎在逻辑上认为有必要扫描整个表以查找此问题,而事实并非如此。
Andriy M

4
@Marco:我不同意,这一个答案。碰巧的是,回答者不同意问题的前提,并回答了他/她认为是OP的误解。
Andriy M
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.