29

我有两个巨大的表,每个表大约有 1 亿条记录,恐怕我需要在两者之间执行内部联接。现在,两张表都非常简单;这是描述:

生物实体表:

  • BioEntityId (int)
  • 名称(nvarchar 4000,虽然这是一个矫枉过正)
  • 类型 ID (int)

EGM 表(一个辅助表,实际上是批量导入操作的结果):

  • EMGId (int)
  • PID(整数)
  • 名称(nvarchar 4000,虽然这是一个矫枉过正)
  • 类型 ID (int)
  • LastModified(日期)

我需要获得一个匹配的名称,以便将 BioEntityId 与 EGM 表中的 PId 相关联。最初,我尝试使用单个内部连接来完成所有操作,但查询似乎花费的时间太长,并且数据库的日志文件(在简单恢复模式下)设法耗尽了所有可用的磁盘空间(刚刚超过 200 GB,当数据库占用18GB时),如果我没记错的话,等待两天后查询会失败。我设法阻止日志增长(现在只有 33 MB),但是查询已经连续运行了 6 天,而且看起来不会很快停止。

我在一台相当不错的计算机(4GB RAM,Core 2 Duo (E8400) 3GHz,Windows Server 2008,SQL Server 2008)上运行它,我注意到计算机每隔 30 秒(给予或接受)偶尔会卡顿一段时间几秒钟。这使得它很难用于其他任何事情,这真的让我很紧张。

现在,这是查询:

 SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
 FROM EGM INNER JOIN BioEntity 
 ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId

我手动设置了一些索引;EGM 和 BioEntity 都有一个包含 TypeId 和 Name 的非聚集覆盖索引。但是,查询运行了 5 天,也没有结束,所以我尝试运行 Database Tuning Advisor 以使其正常工作。它建议删除我的旧索引并创建统计信息和两个聚集索引(每个表上一个,只包含我觉得很奇怪的 TypeId - 或者只是简单的愚蠢 - 但我还是试了一下)。

它已经运行了 6 天,我仍然不知道该怎么办......有什么想法吗?我怎样才能使它更快(或者,至少,有限)?

更新: -好的,我已取消查询并重新启动服务器以使操作系统重新启动并运行-我正在使用您提出的更改重新运行工作流,特别是将 nvarchar 字段裁剪为更小的大小并交换“like”对于“=”。这至少需要两个小时,所以我稍后会发布更多更新

更新 2(格林威治标准时间下午 1 点,2009 年 11 月 18 日): - 估计的执行计划显示表扫描的成本为 67%,然后是 33% 的哈希匹配。接下来是 0% 并行度(这不是很奇怪吗?这是我第一次使用估计的执行计划,但这个特别的事实让我大吃一惊),0% 哈希匹配,更多 0% 并行度,0% 顶部,0 % 表插入,最后是另一个 0% 选择。似乎索引是垃圾,正如预期的那样,所以我将制作手动索引并丢弃蹩脚的建议索引。

4

16 回答 16

18

我不是 SQL 调优专家,但在我所知道的任何数据库系统中,在 VARCHAR 字段上加入数亿行听起来都不是一个好主意。

您可以尝试向每个表添加一个整数列,并在 NAME 字段上计算一个哈希值,以便在引擎必须查看实际的 VARCHAR 数据之前将可能的匹配项获得一个合理的数字。

于 2009-11-17T16:26:55.093 回答
11

对于巨大的连接,有时明确选择一个loop join可以加快速度:

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
FROM EGM 
INNER LOOP JOIN BioEntity 
    ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId

与往常一样,发布您的估计执行计划可以帮助我们提供更好的答案。

编辑:如果两个输入都已排序(它们应该是带有覆盖索引的),您可以尝试MERGE JOIN

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
FROM EGM 
INNER JOIN BioEntity 
    ON EGM.name LIKE BioEntity.Name AND EGM.TypeId = BioEntity.TypeId
OPTION (MERGE JOIN)
于 2009-11-17T16:25:40.807 回答
8

首先,100M 行连接一点也不不合理或不常见。

但是,我怀疑您看到的性能不佳的原因可能与 INTO 子句有关。这样,您不仅要进行连接,还要将结果写入新表。您对日志文件增长如此之大的观察基本上证实了这一点。

要尝试的一件事:移除 INTO 并查看它的性能。如果性能合理,那么要解决写入缓慢的问题,您应该确保您的数据库日志文件位于与数据不同的物理卷上。如果不是这样,磁盘磁头在读取数据和写入日志时会抖动(大量寻道),并且您的性能会崩溃(可能低至其他情况的 1/40 到 1/60) )。

于 2009-11-18T04:32:09.100 回答
6

也许有点离题,但是:“我注意到计算机每隔 30 秒(给或取)偶尔会卡顿几秒钟。”

这种行为是廉价 RAID5 阵列(或者可能是单个磁盘)在复制(并且您的查询主要复制数据)千兆字节信息时的特征。

有关问题的更多信息 - 您不能将查询划分为更小的块吗?喜欢以 A、B 等开头的名称或特定范围内的 ID?这可以大大减少事务/锁定开销。

于 2009-11-17T22:20:07.277 回答
4

我会尝试删除“LIKE”运算符;因为您似乎没有进行任何通配符匹配。

于 2009-11-17T16:26:50.210 回答
3

按照建议,我会散列名称以使连接更合理。如果可能的话,我会强烈考虑在批次导入期间通过查找来分配 id,因为这将消除以后进行连接的需要(并且可能会反复执行这种低效的连接)。

我看到你在 TypeID 上有这个索引 - 如果这是有选择性的,这将非常有帮助。此外,将具有名称哈希的列添加到同一索引中:

SELECT EGM.Name
       ,BioEntity.BioEntityId
INTO AUX 
FROM EGM 
INNER JOIN BioEntity  
    ON EGM.TypeId = BioEntity.TypeId -- Hopefully a good index
    AND EGM.NameHash = BioEntity.NameHash -- Should be a very selective index now
    AND EGM.name LIKE BioEntity.Name
于 2009-11-17T16:44:03.833 回答
2

我可能提供的另一个建议是尝试获取数据的子集,而不是一次处理所有 100 M 行来调整您的查询。这样您就不必花太多时间等待查询何时完成。然后,您可以考虑检查查询执行计划,这也可能为手头的问题提供一些见解。

于 2009-11-17T16:54:04.360 回答
1

1 亿条记录是巨大的。我想说要使用大型数据库,您需要专用的测试服务器。在执行类似查询的同时使用同一台机器做其他工作是不切实际的。

您的硬件功能相当强大,但要获得如此大的连接才能正常运行,您需要更多的功能。一个 8GB 的​​四核系统将是一个好的开始。除此之外,您必须确保您的索引设置正确。

于 2009-11-17T16:26:07.690 回答
1

你有任何主键或索引吗?可以分阶段选择吗?即名称如'A%',名称如'B%'等。

于 2009-11-17T21:39:13.000 回答
1

我手动设置了一些索引;EGM 和 BioEntity 都有一个包含 TypeId 和 Name 的非聚集覆盖索引。但是,查询运行了 5 天,也没有结束,所以我尝试运行 Database Tuning Advisor 以使其正常工作。它建议删除我的旧索引并创建统计信息和两个聚集索引(每个表上一个,只包含我觉得很奇怪的 TypeId - 或者只是简单的愚蠢 - 但我还是试了一下)。

您说您在两个表中的 TypeId 上都创建了一个聚集索引,尽管看起来您已经在每个表上都有一个主键(分别为 BioEntityId 和 EGMId)。您希望您的 TypeId 成为这些表上的聚集索引。您希望将 BioEntityId 和 EGMId 聚集在一起(这将按照磁盘上聚集索引的顺序对数据进行物理排序。您希望在用于查找的外键上使用非聚集索引。即 TypeId。尝试使主键聚集,并在仅包含 TypeId 的两个表上添加非聚集索引。

在我们的环境中,我们有一个大约有 10-20 百万条记录的表。我们做了很多与您类似的查询,我们将两个数据集组合在一列或两列上。为每个外键添加索引应该对您的性能有很大帮助。

请记住,对于 1 亿条记录,这些索引将需要大量磁盘空间。但是,性能似乎是这里的关键,所以它应该是值得的。

K. Scott在这里有一篇非常好的文章,它更深入地解释了一些问题。

于 2009-11-17T21:46:01.120 回答
1

在这里重申之前的一些帖子(我会投票赞成)......

TypeId 的选择性如何?如果您在 100M+ 行中只有 5、10 甚至 100 个不同的值,那么索引对您没有任何帮助 - 特别是因为您无论如何都要选择所有行。

我建议在两个表中的 CHECKSUM(Name) 上创建一个列似乎不错。也许使它成为一个持久的计算列:

CREATE TABLE BioEntity
 (
   BioEntityId  int
  ,Name         nvarchar(4000)
  ,TypeId       int
  ,NameLookup  AS checksum(Name) persisted
 )

然后像这样创建一个索引(我会使用集群,但即使是非集群也会有所帮助):

CREATE clustered INDEX IX_BioEntity__Lookup on BioEntity (NameLookup, TypeId)

(检查 BOL,在可能适用于您的环境的计算列上构建索引有一些规则和限制。)

在两个表上都完成,如果像这样修改它应该提供一个非常有选择性的索引来支持您的查询:

SELECT EGM.Name, BioEntity.BioEntityId INTO AUX
 FROM EGM INNER JOIN BioEntity 
 ON EGM.NameLookup = BioEntity.NameLookup
  and EGM.name = BioEntity.Name
  and EGM.TypeId = BioEntity.TypeId

取决于许多因素,它仍然会运行很长时间(尤其是因为您要将多少数据复制到新表中?)但这应该不到几天的时间。

于 2009-11-17T22:04:13.017 回答
1

为什么是 nvarchar?最佳实践是,如果您不需要(或期望需要)Unicode 支持,只需使用 varchar。如果您认为最长的名称少于 200 个字符,我会将该列设为 varchar(255)。我可以看到向您推荐的散列会很昂贵的场景(看起来这个数据库是插入密集型的)。但是,由于具有如此大的大小以及名称的频率和随机性,在大多数情况下,您的索引将在散列(取决于散列)或名称上索引的情况下迅速变得碎片化。

我将按上述方式更改名称列,并创建聚集索引 TypeId、EGMId/BioentityId(任一表的代理键)。然后你可以在 TypeId 上很好地加入,而 Name 上的“粗略”加入将有更少的循环。要查看此查询可能运行多长时间,请尝试对您的 TypeId 的一个非常小的子集进行测试,这应该会给您一个运行时间的估计值(尽管它可能会忽略缓存大小、内存大小、硬盘传输速率等因素)。

编辑:如果这是一个持续的过程,您应该在两个表之间强制执行外键约束,以便将来导入/转储。如果它不是持续的,散列可能是你最好的。

于 2009-11-17T22:31:32.980 回答
1

我会尝试在框外解决问题,也许有一些其他算法可以比数据库更好更快地完成这项工作。当然,这完全取决于数据的性质,但有一些非常快的字符串搜索算法(Boyer-Moore、ZBox 等)或其他数据挖掘算法(MapReduce ?)通过精心设计数据导出,可以弯曲问题以适应更优雅和更快的解决方案。此外,可以更好地并行化问题,并通过一个简单的客户端利用您周围系统的空闲周期,有框架可以帮助解决这个问题。

它的输出可能是一个 refid 元组列表,您可以使用它来更快地从数据库中获取完整数据。

This does not prevent you from experimenting with index, but if you have to wait 6 days for the results I think that justifies resources spent exploring other possible options.

my 2 cent

于 2009-11-19T03:46:37.643 回答
0

由于您不要求数据库执行任何花哨的关系操作,因此您可以轻松编写脚本。与其用一个庞大而简单的查询来杀死数据库,不如尝试导出这两个表(你能从备份中获取离线副本吗?)。

导出表后,编写一个脚本来为您执行这个简单的连接。执行所需的时间大致相同,但不会杀死数据库。

由于数据的大小和查询运行的时间长度,您不会经常这样做,因此离线批处理是有意义的。

对于脚本,您需要索引较大的数据集,然后遍历较小的数据集并查找大型数据集索引。运行时间为 O(n*m)。

于 2009-11-17T17:08:09.223 回答
0

我想知道,执行时间是由连接还是数据传输占用的。

假设您的 Name 列中的平均数据大小为 150 个字符,那么您实际上将有 300 个字节加上每条记录的其他列。将其乘以 1 亿条记录,您将获得大约 30GB 的数据来传输给您的客户。您是远程运行客户端还是在服务器本身上运行?也许您正在等待将 30GB 的数据传输到您的客户端……

编辑:好的,我看到你正在插入 Aux 表。数据库恢复模式的设置是什么?

为了调查硬件方面的瓶颈,限制资源是读取数据还是写入数据可能会很有趣。例如,您可以开始运行 Windows 性能监视器并捕获队列的长度以读取和写入磁盘。

理想情况下,您应该将 db 日志文件、输入表和输出表放在单独的物理卷上以提高速度。

于 2009-11-18T10:51:16.773 回答
0

如果散列匹配消耗了太多资源,则分批进行查询,例如一次 10000 行,“遍历”TypeID 列。您没有说 TypeID 的选择性,但想必它的选择性足以进行这么小的批量,并且一次完全覆盖一个或多个 TypeID。您还在批次中寻找循环连接,因此如果您仍然获得哈希连接,则要么强制循环连接,要么减少批处理大小。

在简单恢复模式下,使用批处理还可以防止您的 tran 日志变得非常大。即使在简单恢复模式下,像您这样的巨大连接也会消耗大量空间,因为它必须保持整个事务处于打开状态,而在执行批处理时,它可以为每个批处理重用日志文件,将其大小限制为所需的最大一批操作。

如果您确实需要加入名称,那么您可能会考虑一些将名称转换为 ID 的辅助表,基本上是临时修复非规范化设计(如果您无法永久修复它)。

关于校验和的想法也可以很好,但我自己并没有玩太多。

无论如何,如此巨大的哈希匹配不会像批处理循环连接那样执行。如果你能得到一个合并加入,那就太棒了......

于 2009-11-19T02:50:40.123 回答