使用 JOIN 有效地更新表

sri*_*amn 8 join sql-server optimization execution-plan

我有一张表,其中包含家庭的详细信息,而另一个表则包含与这些家庭相关的所有人员的详细信息。对于家庭表,我使用其中的两列定义了一个主键 - [tempId,n]。对于 person 表,我有一个使用其 3 列定义的主键[tempId,n,sporder]

使用主键上的聚集索引所指示的排序,我为每个家庭[HHID]和每个人的[PERID]记录生成了一个唯一 ID (下面的代码片段用于生成 PERID]:

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;
Run Code Online (Sandbox Code Playgroud)

现在,我的下一步是将每个人与相应的家庭相关联,即;将 a 映射[PERID]到 a [HHID]。两个表之间的人行横道基于两列[tempId,n]。为此,我有以下内部连接语句。

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n;
Run Code Online (Sandbox Code Playgroud)

我总共有1928783个户籍记录和5239842个人记录。执行时间目前非常高。

现在,我的问题:

  1. 是否可以进一步优化此查询?更一般地说,优化连接查询的拇指规则是什么?
  2. 是否有另一个查询构造可以以更好的执行时间实现我想要的结果?

我已将SQL Server 2008 为整个脚本生成的执行计划上传到 SQLPerformance.com

Pau*_*ite 20

我很确定表定义接近于此:

CREATE TABLE dbo.households
(
    tempId  integer NOT NULL,
    n       integer NOT NULL,
    HHID    integer IDENTITY NOT NULL,

    CONSTRAINT [UQ dbo.households HHID] 
        UNIQUE NONCLUSTERED (HHID),

    CONSTRAINT [PK dbo.households tempId, n]
    PRIMARY KEY CLUSTERED (tempId, n)
);

CREATE TABLE dbo.persons
(
    tempId  integer NOT NULL,
    sporder integer NOT NULL,
    n       integer NOT NULL,
    PERID   integer IDENTITY NOT NULL,
    HHID    integer NOT NULL,

    CONSTRAINT [UQ dbo.persons HHID]
        UNIQUE NONCLUSTERED (PERID),

    CONSTRAINT [PK dbo.persons tempId, n, sporder]
        PRIMARY KEY CLUSTERED (tempId, n, sporder)
);
Run Code Online (Sandbox Code Playgroud)

我没有这些表或您的数据的统计信息,但以下内容至少会正确设置表基数(页数是猜测):

UPDATE STATISTICS dbo.persons 
WITH 
    ROWCOUNT = 5239842, 
    PAGECOUNT = 100000;

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;
Run Code Online (Sandbox Code Playgroud)

查询计划分析

您现在的查询是:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n;
Run Code Online (Sandbox Code Playgroud)

这产生了相当低效的计划:

默认计划

该计划的主要问题是散列连接和排序。两者都需要内存授权(散列连接需要构建一个散列表,并且在排序过程中排序需要空间来存储行)。计划资源管理器显示此查询被授予 765 MB:

内存授予

这是用于一个查询的大量服务器内存!更重要的是,根据行数和大小估计,在执行开始之前,此内存授予是固定的。

如果在执行时内存不足,至少一些哈希和/或排序数据将写入物理tempdb磁盘。这被称为“溢出”,它可能是一个非常缓慢的操作。您可以使用 Profiler 事件Hash WarningsSort Warnings跟踪这些溢出(在 SQL Server 2008 中)。

哈希表的构建输入的估计非常好:

哈希输入

排序输入的估计不太准确:

排序输入

您必须使用 Profiler 进行检查,但我怀疑在这种情况下排序会溢出到tempdb。哈希表也有可能溢出,但这不太明确。

请注意,为该查询保留的内存在哈希表和排序之间分配,因为它们是并发运行的。Memory Fractions 计划属性显示每个操作预期使用的内存授予的相对量。

为什么要排序和散列?

排序由查询优化器引入,以确保行以聚集键顺序到达聚集索引更新运算符。这促进了对表的顺序访问,这通常比随机访问更有效。

散列连接是一个不太明显的选择,因为它的输入大小相似(无论如何都是近似值)。当一个输入(构建哈希表的那个)相对较小时,哈希连接是最好的。

在这种情况下,优化器的成本模型确定散列连接是三个选项(散列、合并、嵌套循环)中成本较低的一个。

提高性能

成本模型并不总是正确的。它往往会高估并行合并连接的成本,尤其是随着线程数量的增加。我们可以使用查询提示强制合并连接:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (MERGE JOIN);
Run Code Online (Sandbox Code Playgroud)

这会产生一个不需要那么多内存的计划(因为合并连接不需要哈希表):

合并计划

有问题的排序仍然存在,因为合并连接仅保留其连接键(tempId,n)的顺序,但聚集键是(tempId,n,sporder)。您可能会发现合并连接计划的性能并不比散列连接计划好。

嵌套循环加入

我们也可以尝试嵌套循环连接:

UPDATE P
SET HHID = H.HHID
FROM dbo.households AS H
JOIN dbo.persons AS P
    ON P.tempId = H.tempId
    AND P.n = H.n
OPTION (LOOP JOIN);
Run Code Online (Sandbox Code Playgroud)

此查询的计划是:

串行嵌套循环计划

这个查询计划被优化器的成本模型认为是最糟糕的,但它确实有一些非常理想的特性。首先,嵌套循环连接不需要内存授权。其次,它可以保留Persons表中的键顺序,因此不需要显式排序。您可能会发现此计划执行得相对较好,甚至可能已经足够好。

并行嵌套循环

嵌套循环计划的最大缺点是它在单个线程上运行。这个查询很可能受益于并行性,但优化器认为在这里这样做没有任何好处。这也不一定正确。不幸的是,没有内置的查询提示来获取并行计划,但有一种未公开的方式:

UPDATE t1
  SET t1.HHID = t2.HHID
  FROM dbo.persons AS t1
  INNER JOIN dbo.households AS t2
  ON t1.tempId = t2.tempId AND t1.n = t2.n
OPTION (LOOP JOIN, QUERYTRACEON 8649);
Run Code Online (Sandbox Code Playgroud)

使用QUERYTRACEON提示启用跟踪标志 8649会生成以下计划:

并行嵌套循环计划

现在我们有一个避免排序的计划,不需要额外的连接内存,并有效地使用并行性。您应该会发现此查询的性能比替代查询要好得多。

我的文章Forcing a Parallel Query Execution Plan 中有关并行性的更多信息: