如何提示SQL Server中的多对多联接?


9

我有3个“大”表,它们连接在一对列(均为int)上。

  • Table1拥有约2亿行
  • Table2拥有约150万行
  • Table3拥有约600万行

每个表都有一个聚集索引Key1Key2以及再得一列。Key1具有低基数并且非常偏斜。WHERE子句中始终引用它。条款中Key2从未提及WHERE。每个联接都是多对多的。

问题在于基数估计。每个连接的输出估计值变而不是变大。当实际结果达到数百万时,最终得出的结果估计只有几百个。

我有什么办法让行政长官提示做出更好的估计?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

我尝试过的解决方案:

  • 在创建多列统计Key1Key2
  • 创建大量已过滤的统计信息Key1(这很有帮助,但是我最终在数据库中获得了数千个用户创建的统计信息。)

掩盖的执行计划(抱歉掩盖不好)

就我而言,结果有900万行。新的CE估计有180行;旧版CE估计有6100行。

这是一个可重现的示例:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Answers:


5

需要明确的是,优化器已经知道这是一个多对多联接。如果强制合并联接并查看估计的计划,则可以看到联接运算符的属性,该属性会告诉您联接是否可以是多对多的。您需要在此处解决的问题是增加基数估计,大概是为了让您针对遗漏的查询部分获得更有效的查询计划。

的第一件事,我会尝试是放的结果从加入Object3Object5到一个临时表。对于您发布的计划,它仅是51393行上的一列,因此它几乎不占用tempdb中的任何空间。您可以在临时表上收集完整的统计信息,仅此一项就足以获取足够准确的最终基数估计。收集完整的统计信息Object1也可能会有所帮助。当您从右向左遍历计划时,基数估计值通常会变得更糟。

如果这样不起作用,ENABLE_QUERY_OPTIMIZER_HOTFIXES如果您尚未在数据库或服务器级别启用查询提示,则可以尝试查询提示。Microsoft将SQL Server 2016的影响计划的性能修复锁定在该设置之后。其中一些与基数估计有关,因此也许您会很幸运,并且其中一项修复将有助于您的查询。您也可以尝试使用带有FORCE_LEGACY_CARDINALITY_ESTIMATION查询提示的旧式基数估算器。使用旧版CE,某些数据集可能会得到更好的估计。

作为最后的选择,您可以使用Adam Machanic的MANY()函数手动增加基数估计值,直到您喜欢的任何因素。我在另一个答案中谈到了该问题,但看起来该链接已失效。如果您有兴趣,我可以尝试挖掘一些东西。


亚当的make_parallel功能已被用来帮助缓解问题。我看看many。似乎是一个非常粗糙的创可贴。
史蒂文·希伯

2

SQL Server统计信息仅包含统计信息对象前列的直方图。因此,您可以创建过滤后的统计信息,以提供的值直方图Key2,但仅在带有的行中提供Key1 = 1。在每个表上创建这些过滤的统计信息会固定估计值,并导致您期望测试查询执行以下操作:每个新联接都不会影响最终基数估计(在SQL 2016 SP1和SQL 2017中均已确认)。

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

如果没有这些经过过滤的统计信息,SQL Server将采用基于启发式的方法来估算联接的基数。以下白皮书对SQL Server使用的一些启发式方法进行了很好的高级描述:使用SQL Server 2014 Cardinality Estimator优化查询计划

例如,在USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')查询中添加提示将更改联接包含启发式,以假定Key1谓词和Key2联接谓词之间存在某种关联(而不是独立性),这可能对您的查询有利。对于最终的测试查询,此提示将基数估计从从1,175增加到7,551,但是20,000与过滤后的统计信息所产生的正确行估计相比,仍然有很多不足。

我们在类似情况下使用的另一种方法是将数据的相关子集提取到#temp表中。尤其是现在,由于SQL Server的更新版本不再急于将#temp表写入磁盘,因此使用这种方法已经取得了不错的效果。您对多对多联接的描述意味着您的情况下的每个#temp表都相对较小(或至少小于最终结果集),因此这种方法可能值得尝试。

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2

我们广泛使用过滤后的统计信息,但是我们Key1在每个表的每个值上使它们一个。我们现在有数千个。
史蒂文·希伯

2
@StevenHibble很好的一点是,成千上万的过滤统计数据可能会使管理变得困难。(我们也已经看到它会对计划的编译时间产生负面影响。)它可能不适合您的用例,但是我还添加了另一种#temp表方法,该方法已经成功使用了好几次。
Geoff Patterson

-1

触手可及。除了尝试,没有其他依据。

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
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.