【问题标题】:SQL: Inner joining two massive tablesSQL:内部连接两个海量表
【发布时间】:2010-12-17 12:36:53
【问题描述】:

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

BioEntity 表:

  • BioEntityId (int)
  • 名称(nvarchar 4000,虽然这有点矫枉过正)
  • TypeId (int)

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

  • EMGId (int)
  • PId (int)
  • 名称(nvarchar 4000,虽然这有点矫枉过正)
  • TypeId (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 的非聚集覆盖索引。然而,查询运行了五天,它也没有结束,所以我尝试运行 Database Tuning Advisor 以使事情正常工作。它建议删除我的旧索引并创建统计信息和两个聚集索引(每个表上一个,只包含我觉得很奇怪的 TypeId - 或者只是简单的愚蠢 - 但我还是试了一下)。

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

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

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

【问题讨论】:

  • 只是好奇...为什么您需要返回 100+ 百万行以及您打算如何处理所有这些数据??
  • 您的 4k 名称字段中存储的最大值是多少?如果它大大小于 4k,则减少每个表中的大小。
  • 它应该比 4k 小得多,只是认为因为它是一个 Nvarchar 字段,它并不重要......我想我可能可以将它减少到少于 200 个字符,也许。
  • @Hal - 是的,我可以看到,但我认为没有任何程序能够吞下这么多数据并存活下来。必须缺少“where”子句。
  • 真的应该是海量数据; “where”子句通过强制限制数据本身的性质(在 TypeId 字段中定义)来应用。

标签: sql sql-server sql-server-2008 query-optimization inner-join


【解决方案1】:

对于大型连接,有时明确选择 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)

【讨论】:

  • 我现在取消查询,看看SQL Server能不能起死回生,给我们方案……
  • 好的,服务器死机,重启,晚上重做工作流;立即发布结果
  • inner loop join 使用更少的内存和更多的cpu?
【解决方案2】:

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

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

【讨论】:

  • 哈哈,是的,告诉他 StackOverflow 说你也需要一台新的 AlienWare 笔记本电脑!
  • 还有两个 30 英寸的显示器。有很多数据需要查看
【解决方案3】:

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

【讨论】:

  • 不是真的不,我也尝试过使用等号字符(“=”),但它看起来并不乐观。我去换,谢谢!
  • 如果没有通配符,LIKE 应该优化为“=”。
【解决方案4】:

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

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

【讨论】:

  • 校验和可以工作,但根据 NAME 中数据的性质,您可能能够使用更快的哈希算法(也许 NAME 在前十个字符中往往是唯一的,或者类似那个)。
  • 如果 Name 被设置并且从不改变,哈希将只需要计算一次,因此哈希计算速度可能不那么重要。
  • 也许是这样,也许没有对哈希算法进行合理的优化。从 CHECKSUM 开始是一个很好、简单的解决方案。尽管如此,仍有超过一亿条记录。 . .
【解决方案5】:

按照建议,我会对名称进行哈希处理以使连接更加合理。如果可能的话,我会强烈考虑在批次导入期间通过查找来分配 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

【讨论】:

  • 我将在此过程中进一步尝试,我现在需要探索估算计划。谢谢:)
【解决方案6】:

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

【讨论】:

  • 这和拥有完全正确、最小的可能索引(可能是另一个预处理步骤)是易处理性的关键。
【解决方案7】:

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

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

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

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

【讨论】:

    【解决方案8】:

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

    【讨论】:

    • 我有 PK(EMGId 和 BioEntityId)并且索引发布在问题中
    【解决方案9】:

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

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

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

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

    K。 Scott 有一篇很好的文章 here,它更深入地解释了一些问题。

    【讨论】:

    • 我知道。我这样做了,但结果并不是我所期望的。我试了一下,因为 SQL Server Database Tuning Advisor 建议这样做;还是觉得很傻
    【解决方案10】:

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

    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
    

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

    【讨论】:

    • 是的,TypeId 中只有一百左右的项目。感谢您的意见,我会尽快尝试...我 2 岁的 MBP 刚刚死在我身上,我要去商店了:/
    【解决方案11】:

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

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

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

    【讨论】:

    • 所以这篇文章因为发布了我已经问过的相同内容而获得了投票?
    • Thirster42,我发帖时没有看到你的答案(否则我会参考你的帖子)。投票有什么问题?你丢东西了吗?
    【解决方案12】:

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

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

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

    【讨论】:

    • 不能确定,尽管可能就足够了
    【解决方案13】:

    首先,100M 行的连接并非不合理或不常见。

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

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

    【讨论】:

      【解决方案14】:

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

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

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

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

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

      【讨论】:

      • 恢复模式设置为简单;学到了艰难的方法:) 关于单独的物理卷的简单而合乎逻辑的建议,我只使用一个 HDD。谢谢!顺便说一下,我现在发布执行计划估算
      【解决方案15】:

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

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

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

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

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

      【讨论】:

        【解决方案16】:

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

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

        这并不妨碍您尝试使用索引,但如果您必须等待 6 天才能获得结果,我认为有理由将资源用于探索其他可能的选项。

        我的 2 美分

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-04-29
          • 2020-03-27
          • 1970-01-01
          • 2012-04-25
          • 1970-01-01
          • 2011-01-28
          相关资源
          最近更新 更多