为什么这样更快,使用安全吗?(字母表中的第一个字母在哪里)

Joh*_*hnF 10 sql-server optimization sql-server-2017

长话短说,我们正在使用非常大的人员表中的值更新小型人员表。在最近的测试中,此更新需要大约 5 分钟才能运行。

我们偶然发现了看似最愚蠢的优化方法,但它似乎完美无缺!相同的查询现在可以在不到 2 分钟的时间内运行并完美地产生相同的结果。

这是查询。最后一行被添加为“优化”。为什么查询时间急剧减少?我们错过了什么吗?这会导致将来出现问题吗?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
Run Code Online (Sandbox Code Playgroud)

技术说明:我们知道要测试的字母列表可能需要更多的字母。我们也意识到使用“DIFFERENCE”时明显的误差幅度。

查询计划(常规): https : //www.brentozar.com/pastetheplan/?
id = rypV84y7V 查询计划(带“优化”):https : //www.brentozar.com/pastetheplan/?id=r1aC2my7E

Ran*_*gen 9

这取决于您的表中的数据,您的索引,.... 如果无法比较执行计划 / io + 时间统计信息,就很难说。

我期望的区别是在两个表之间的 JOIN 之前发生的额外过滤。在我的示例中,我将更新更改为选择以重用我的表。

带有“优化”的执行计划 在此处输入图片说明

执行计划

您清楚地看到过滤操作正在发生,在我的测试数据中没有过滤掉记录,因此没有进行任何改进。

没有“优化”的执行计划 在此处输入图片说明

执行计划

过滤器消失了,这意味着我们将不得不依靠连接来过滤掉不需要的记录。

其他原因 更改查询的另一个原因/结果可能是,更改查询时创建了一个新的执行计划,这恰好更快。这方面的一个例子是引擎选择了不同的 Join 运算符,但这只是在这一点上的猜测。

编辑:

得到两个查询计划后澄清:

查询从大表中读取 550M 行,并将它们过滤掉。 在此处输入图片说明

这意味着谓词是进行大部分过滤的谓词,而不是搜索谓词。导致数据被读取,但返回的方式更少。

使 sql server 使用不同的索引(查询计划)/添加索引可以解决这个问题。

那么为什么优化查询没有同样的问题呢?

因为使用了不同的查询计划,使用扫描而不是查找。

在此处输入图片说明 在此处输入图片说明

没有做任何搜索,但只返回 4M 行来处理。

下一个区别

不考虑更新差异(优化查询上没有更新任何内容)在优化查询上使用哈希匹配:

在此处输入图片说明

而不是对非优化的嵌套循环连接:

在此处输入图片说明

当一张表小而另一张大时,嵌套循环是最好的。由于它们都接近相同的大小,我认为在这种情况下哈希匹配是更好的选择。

概述

优化的查询 在此处输入图片说明

Optimized query的plan具有并行性,使用hash match join,需要做的residual IO过滤较少。它还使用位图来消除不能产生任何连接行的键值。(也没有更新)

非优化查询 在此处输入图片说明 non-Optimized query的plan没有并行性,使用嵌套循环join,需要对550M记录做residual IO过滤。(也正在更新中)

你能做些什么来改进非优化查询?

  • 更改索引以在键列列表中包含 first_name 和 last_name:

    CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name on dbo.largeTableOfPeople(birth_date,first_name,last_name) include(id)

但是由于函数的使用和这个表很大,这可能不是最佳解决方案。

  • 更新统计数据,使用重新编译来尝试获得更好的计划。
  • 将 OPTION 添加(HASH JOIN, MERGE JOIN)到查询中
  • ...

测试数据 + 使用的查询

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Run Code Online (Sandbox Code Playgroud)


Mar*_*ith 8

不清楚第二个查询实际上是一种改进。

执行计划包含的 QueryTimeStats 显示的差异比问题中陈述的要小得多。

慢速计划的运行时间为257,556 ms(4 分 17 秒)。190,992 ms尽管并行度为 3,但快速计划的运行时间为(3 分 11 秒)。

此外,第二个计划在数据库中运行,在该数据库中加入后没有工作要做。

第一个计划

在此处输入图片说明

第二个计划

在此处输入图片说明

因此,额外的时间可以很好地解释为更新 350 万行所需的工作(更新运算符中定位这些行、锁定页面、将更新写入页面和事务日志所需的工作不可忽略)

如果在将 like 与 like 进行比较时这实际上是可重现的,那么解释是您在这种情况下很幸运。

具有 37 个IN条件的过滤器只消除了表中 4,008,334 行中的 51 行,但优化器认为它会消除更多

在此处输入图片说明

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 
Run Code Online (Sandbox Code Playgroud)

这种不正确的基数估计通常是一件坏事。在这种情况下,它产生了一个不同形状(和平行)的计划,尽管由于大量低估导致散列溢出,但显然(?)对你更有效。

如果没有TRIMSQL Server 能够将其转换为基本列直方图中的范围区间,并提供更准确的估计,但TRIM它只能依靠猜测。

猜测的性质可能会有所不同,但对单个谓词的估计LEFT(TRIM(largeTbl.last_name), 1)在某些情况下*只是估计为table_cardinality/estimated_number_of_distinct_column_values

我不确定究竟是什么情况 - 数据的大小似乎起了一定作用。我能够像这里一样使用宽固定长度数据类型重现这一点,但得到了不同的、更高的猜测varchar(仅使用了 10% 的平坦猜测和估计的 100,000 行)。@Solomon Rutzky指出,如果varchar(100)用尾随空格填充,因为char使用较低的估计值

IN列表扩展为ORSQL Server 使用指数退避,最多考虑 4 个谓词。因此,219.707估计值如下。

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Run Code Online (Sandbox Code Playgroud)