使用JOIN有效地更新表


8

我有一个表,其中包含住户的详细信息,而另一个表中包含与住户相关的所有人员的详细信息。对于家用表,我有一个主键,它使用两列定义[tempId,n]。对于人员表,我有一个使用其3列定义的主键[tempId,n,sporder]

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

 ALTER TABLE dbo.persons
 ADD PERID INT IDENTITY
 CONSTRAINT [UQ dbo.persons HHID] UNIQUE;

现在,我的下一步是将每个人与相应的家庭相关联,即:将a映射[PERID][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;

我总共有1928783户家庭记录和5239842人记录。当前执行时间非常长。

现在,我的问题是:

  1. 是否可以进一步优化此查询?更一般而言,优化联接查询的经验法则是什么?
  2. 是否有另一个查询构造可以在更短的执行时间内达到我想要的结果?

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

Answers:


19

我很确定表定义与此相似:

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)
);

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

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

UPDATE STATISTICS dbo.households 
WITH 
    ROWCOUNT = 1928783, 
    PAGECOUNT = 25000;

查询计划分析

您现在拥有的查询是:

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;

这会产生效率低下的计划:

预设方案

该计划中的主要问题是哈希联接和排序。两者都需要内存授权(哈希联接需要构建哈希表,排序需要空间以在排序进行时存储行)。计划资源管理器显示此查询已被授予765 MB:

内存授予

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

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

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

哈希输入

排序输入的估算值不太准确:

排序输入

您将不得不使用Profiler进行检查,但是在这种情况下,我怀疑这种排序会溢出到tempdb中。散列表也有可能溢出,但这不太明确。

请注意,为此查询保留的内存在哈希表和排序之间分配,因为它们同时运行。“内存分数”计划属性显示每个操作预期使用的内存授权的相对数量。

为什么排序和散列?

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

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

在这种情况下,优化程序的成本核算模型确定哈希联接是三个选项(哈希,合并,嵌套循环)中最便宜的。

改善绩效

成本模型并不一定总是正确。它倾向于高估并行合并联接的成本,尤其是随着线程数量的增加。我们可以通过查询提示强制进行合并联接:

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);

这将产生一个不需要太多内存的计划(因为合并联接不需要哈希表):

合并计划

有问题的排序仍然存在,因为合并联接仅保留其联接键(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);

该查询的计划是:

串行嵌套循环计划

该查询计划被优化程序的成本模型认为是最差的,但它确实具有一些非常理想的功能。首先,嵌套循环联接不需要内存授予。其次,它可以保留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);

通过QUERYTRACEON提示启用跟踪标志8649 会产生以下计划:

并行嵌套循环计划

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

在我的文章《强制并行查询执行计划》中有关并行性的更多信息:


1

查看您的查询计划,您真正的问题可能不是联接本身,而是实际的更新过程。

据我所知,您可能正在更新数据库中的所有人员记录并更新索引(我看不到它还有其他索引,所以我不知道这是否可能是一个因素)

如果这是一项一次性的任务,则可以禁用索引,运行更新并重建索引吗?

填充数据后,可以在查询中添加where子句以仅更新那些需要更新的记录

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.