SQL:内部连接两个大型表

时间:2009-11-17 16:19:26

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

我有两个大表,每个表有大约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 %table insert和最后另一个0%select into。似乎索引是垃圾,正如预期的那样,所以我将制作手动索引并丢弃糟糕的建议。

16 个答案:

答案 0 :(得分:17)

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

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

答案 1 :(得分:7)

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

答案 2 :(得分:7)

首先,100M行连接根本不合理或不常见。

但是,我怀疑你所看到的糟糕表现的原因可能与INTO条款有关。有了它,您不仅要进行连接,还要将结果写入新表。 您对日志文件增长如此巨大的观察基本上是对此的确认。

要尝试的一件事:删除INTO并查看其执行情况。如果性能合理,那么要解决慢速写入问题,应确保数据库日志文件位于与数据不同的物理卷上。如果不是这样,磁头会在读取数据并写入日志时捶打(大量搜索),并且你的性能将会崩溃(可能会低至其原来的1/40到1/60) )。

答案 3 :(得分:6)

可能有点偏离主题,但是: “我注意到计算机偶尔每隔30秒就会卡住(给予或接受)几秒钟。”

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

有关问题的更多信息 - 您无法将查询分区为较小的块吗?类似以A,B等开头的名称或特定范围内的ID?这可能会大大降低事务/锁定开销。

答案 4 :(得分:4)

我试着删除'LIKE'运算符;因为你似乎没有做任何通配符匹配。

答案 5 :(得分: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

答案 6 :(得分:2)

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

答案 7 :(得分:1)

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

你的硬件功能相当强大,但对于那些大得体的连接来说,你需要更多的功能。一个8GB的四核系统将是一个良好的开端。除此之外,您必须确保您的索引设置正确。

答案 8 :(得分:1)

你有任何主键或索引吗?你能分阶段选择它吗?即名称如'A%',其中名称如'B%'等

答案 9 :(得分:1)

  

我手动设置了一些索引; EGM和BioEntity都有一个包含TypeId和Name的非聚集覆盖索引。但是,查询运行了五天,它也没有结束,所以我尝试运行Database Tuning Advisor来使事情发挥作用。它建议删除我的旧索引并创建统计信息和两个聚簇索引(每个表一个,只包含我发现相当奇怪的TypeId - 或者只是简单的愚蠢 - 但我还是试了一下)。

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

在我们的环境中,我们有一张大约有1000万至2000万条记录的表格。我们做了很多类似于你的查询,我们在一列或两列上组合了两个数据集。为每个外键添加索引应该会对您的表现有所帮助。

请记住,有1亿条记录,这些索引需要很多的磁盘空间。但是,看起来性能在这里很关键,所以它应该是值得的。

ķ。斯科特有一篇非常好的文章here,它更深入地解释了一些问题。

答案 10 :(得分: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

根据许多因素,它仍会运行很长时间(尤其是因为您将数据复制到新表中?)但这应该花费不到几天。

答案 11 :(得分:1)

为什么是nvarchar?最佳实践是,如果您不需要(或期望需要)unicode支持,只需使用varchar。如果您认为最长的名称不超过200个字符,我会将该列设为varchar(255)。我可以看到推荐给你的散列成本很高的场景(看起来这个数据库是插入密集型的)。但是,如果使用这么多的大小,以及名称的频率和随机性,在大多数情况下,索引都会在哈希(依赖于哈希)或名称的索引上快速分段。

我将如上所述更改名称列,并生成聚簇索引TypeId,EGMId / BioentityId(任一表的代理键)。然后你可以很好地加入TypeId,而Name上的“粗略”连接将有更少的循环。要查看此查询可能运行多长时间,请尝试使用TypeIds的一小部分,这应该可以估算运行时间(尽管可能会忽略缓存大小,内存大小,硬盘传输速率等因素)。

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

答案 12 :(得分:1)

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

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

这并不妨碍您尝试使用索引,但如果您需要等待6天才能获得结果,我认为这可以证明资源花费在探索其他可能的选项上。

我的2美分

答案 13 :(得分:0)

由于您没有要求数据库进行任何花哨的关系操作,因此您可以轻松编写此脚本。不要使用大量简单的查询来查杀数据库,而是尝试导出两个表(您可以从备份中获取脱机副本吗?)。

导出表后,编写脚本以执行此简单连接。它需要大约相同的执行时间,但不会杀死数据库。

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

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

答案 14 :(得分:0)

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

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

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

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

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

答案 15 :(得分:0)

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

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

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

关于校验和的想法也很好,但我自己并没有那么多玩过。

在任何情况下,这样一个巨大的哈希匹配都不会像批量循环连接一样好。如果你能得到合并加入,那就太棒了......

相关问题