Yor*_*rik 6 join sql-server execution-plan sql-server-2017
我面临 SQL Server 生成非最佳执行计划的问题:嵌套循环连接并寻找维度表并对其执行 2M 读取。
排序操作估计是 100 行而不是 450 K 行,可能会影响计划选择:
NestedLoop:https ://www.brentozar.com/pastetheplan/ ? id = B110MZ2Pm或 NestedLoop 计划
这是在测试数据库中。我们有一个具有相同架构和几乎相同数据的附加数据库。
运行完全相同的查询(均来自 SSMS)使用哈希联接和维度表扫描(32K 读取)生成不同的计划:
HashJoin:https ://www.brentozar.com/pastetheplan/ ? id = r1Jm7b2D7或 哈希计划
我需要帮助来理解和解决问题。
我可以通过提示 Hash Joint 来解决这个问题,但是同一实例上的 2 个相似的 DB 生成不同的计划没有任何意义。
更新 #1:我发现估计的成本是不同的所以当 SQL Server 并行执行时,它会选择一个散列连接。
用单线程会嵌套循环。
更新 #2:在从同一个表中进行 SELECT 时发生了同样的问题。取决于列数(估计成本)。当我减少列数时,执行计划陷入嵌套循环并寻找维度表。
在您的一个环境中获得串行嵌套循环连接计划而在另一个环境中获得散列连接的原因似乎有三个。根据您提供的信息,最佳修复涉及查询提示或将查询拆分为两部分。
您的环境之间的差异
一个环境在您的 CCI 中有 480662 行,而另一个环境有 686053 行。我不会称之为几乎相同。您的环境之间似乎也存在硬件或配置差异,或者至少您很不走运。251 MB 估计数据的串行排序的 IO 成本为 0.0037538 个单位。351 MB 估计数据的并行排序具有 23.1377 个单位的 IO 成本,即使它被并行性打折扣。引擎预计会为并行计划溢出相对大量的数据。这样的差异会导致不同环境之间的计划不同。
优化器错误地应用了行目标成本降低,这可能有利于嵌套循环连接计划
嵌套循环计划的成本就好像只需要从排序中输出 100 行:
但是,查询在SELECT
子句中包含以下内容:COUNT(*) OVER ()
引擎必须读取所有行才能为聚合生成正确的结果。这确实是实际计划中发生的情况,索引查找执行了 450k 次而不是 100 次。这种成本降低似乎发生在各种版本上(我测试回 2016 SP1 基础),在两个 CE 上,具有许多不同的窗口函数,以及批处理模式和行模式。这是产品中的一个限制,导致此处的查询计划不理想。
您的串行嵌套循环连接可能符合并行性(取决于您的 CTFP),您可能想知道为什么优化器没有找到成本更低的并行计划。优化器具有启发式,可防止并行批处理模式排序成为嵌套循环连接(必须在行模式下运行)的第一个子项。问题是并行批处理模式排序会将所有行放在单个线程上,这不适用于并行嵌套循环连接。将排序移动为循环连接的父级不会导致索引查找的估计执行次数减少(由于优化器问题)。因此,即使 CTFP 设置为默认值 5,您也很可能会得到一个串行计划。
这是您问题的重现,我无法将其上传到 PasteThePlan,因为它不支持我的 SQL Server 版本:
drop table if exists cci_216665;
create table cci_216665 (
SORT_ID BIGINT,
JOIN_ID BIGINT,
COL1 BIGINT,
COL2 BIGINT,
COL3 BIGINT,
INDEX CCI CLUSTERED COLUMNSTORE
);
INSERT INTO cci_216665 WITH (TABLOCK)
SELECT TOP (500000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 50
, 0, 0, 0
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);
drop table if exists YEAH_NAH;
CREATE TABLE dbo.YEAH_NAH (ID INT, FILLER VARCHAR(20), PRIMARY KEY (ID));
INSERT INTO dbo.YEAH_NAH WITH (TABLOCK)
SELECT TOP (50) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, 'CHILLY BIN'
FROM master..spt_values t1;
GO
-- takes 780 ms of CPU with nested loops
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;
-- takes 111 ms of CPU with hash join
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (HASH JOIN);
Run Code Online (Sandbox Code Playgroud)
解决问题的最直接方法是将查询一分为二。这是一种方法:
SELECT COUNT(*)
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID;
SELECT TOP (100) *
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;
Run Code Online (Sandbox Code Playgroud)
在我的机器上,这实际上比散列连接计划快,但您可能看不到相同的结果。一般来说,我对像您这样的查询的第一次尝试是OVER
在只需要前 100 行时避免没有子句的窗口聚合。
一个合理的替代方法是使用DISABLE_OPTIMIZER_ROWGOAL
SQL Server 2016 SP1 中引入的使用提示。对于这种类型的查询,行目标存在问题,因此此提示直接解决了问题,而无需依赖统计信息或类似内容。我认为这是一个相对安全的提示。
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (USE HINT('DISABLE_OPTIMIZER_ROWGOAL'));
Run Code Online (Sandbox Code Playgroud)
这会在我的机器上产生一个散列连接计划。