当前位置: 首页 > 科技观察

如何使JOIN运行得更快?

时间:2023-03-16 00:32:33 科技观察

JOIN一直是数据库性能优化的难题。一旦一个查询非常快,一旦涉及到几个JOIN,性能就会急剧下降。而且JOIN涉及的表越大越多,性能提升就越难。其实让JOIN跑得快的关键是对JOIN进行分类。分类后,可以利用各类JOIN的特点来优化性能。JOIN的分类有过SQL开发经验的同学都知道,绝大部分JOIN都是等价JOIN,即关联条件相等的JOIN。非等价的JOIN要少见得多,大多数情况下也可以转化为等价的JOIN,所以我们只能讨论等价的JOIN。等价JOIN可以分为两大类:外键关联和主键关联。外键关联是指用一张表的非主键字段关联另一张表的主键。前者称为事实表,后者称为维度表。比如下图中,订单表是事实表,客户表、产品表、员工表是维度表。外键表是多对一的关系,是非对称的。事实表和维度表的位置不能互换。需要注意的是,这里所说的主键指的是逻辑主键,即在表中具有唯一值,可以用来唯一确定某条记录的字段(或字段组)。数据库表上可能没有建立主键。主键关联是指将一张表的主键与另一张表的主键或部分主键相关联。比如下图中的客户和VIP客户的关系,订单表和订单明细。Customers和VIP客户是按主键关联的,这两个表是同一个维度表。该命令是使用主键关联部分主键的详细信息。我们称订单表为主表,明细表为子表。同一个维度表是一对一的关系。而且同维表是对称的,两个表的状态是一样的。主子表是一对多的关系,不对称,方向明确。如果你仔细观察,你会发现这两种类型的JOIN都涉及到主键。不涉及主键的JOIN将导致多对多关系,这在大多数情况下没有商业意义。也就是说,以上两种JOIN几乎涵盖了所有具有业务意义的JOIN。如果能利用JOIN总是涉及主键的特性进行性能优化,就可以解决这两种JOIN,也就意味着大部分的JOIN性能问题都解决了。而SQL对JOIN的定义并没有涉及到主键,只是对两张表进行笛卡尔积,然后根据一定的条件进行过滤。这个定义简单而广泛,足以描述几乎所有事物。但是如果严格按照这个定义来实现JOIN,在性能优化上就没有办法利用主键的特性了。SPL改变了JOIN的定义,专门针对这两类JOIN,利用主键的特性来减少计算量,从而达到性能优化的目的。让我们来看看SPL是如何做到的。外键关联如果事实表和维表不是太大,都可以加载到内存中。SPL提供了一种外键寻址方式:先将事实表中的外键字段值转换为对应维表记录的地址,然后在引用维表字段时,直接通过该地址进行检索即可。以前面的订单表和员工表为例,假设这两个表已经读入内存。外键寻址的工作机制如下:针对order表中一条记录r的eid字段,在employee表中找到eid字段值对应的记录,得到其内存地址a,然后替换eid字段r的值与a。order表中的所有记录都这样转换,外键寻址就完成了。这时当order表的记录r需要引用employee表的字段时,直接使用eid字段存储的地址a取出employee表的记录和字段即可,这样相当于在常数时间内获取employee表的字段,而无需去employee表查找。事实表和维表可以在系统启动的时候读入内存,外键寻址可以一次性完成,即预关联。这样在后续的关联计算中,可以直接使用事实表外键字段中的地址来取维表记录,完成高性能的JOIN计算。SQL通常使用HASH算法进行内存连接,需要计算比较HASH值,性能会比直接使用地址读取差很多。SPL之所以能够实现外键寻址,是利用了维表关联字段为主键的特性。在上面的例子中,关联的字段eid是employee表的主键,是唯一的。order表中的每个eid只对应一条员工记录,所以每个eid都可以转化为它唯一对应的员工记录的地址。但是,如果JOIN的SQL定义中没有对主键进行约定,则无法确定事实表中外键关联的维表记录是唯一的,可能关联多条记录。对于order表中的记录,eid值是没有办法唯一对应一条员工记录的,所以无法实现外键寻址。而且SQL没有记录地址的数据类型,所以每次关联还是需要计算HASH值比较。当只有两个表JOIN时,外键寻址和HASH关联的区别不是很明显。这是因为JOIN并不是最终目的,JOIN之后还会有很多其他的操作,JOIN本身消耗的时间比例比较小。但是事实表往往有多个维表,甚至维表也有很多层。例如,订单与产品相关联,产品与供应商相关联,供应商与城市相关联,城市与国家相关联,等等。当关联表较多时,外键寻址的性能优势会更加明显。下面的测试比较了不同关联表数时SPL和Oracle的性能差异。可以看出,当表很多的时候,外键寻址的优势是相当明显的:只有维表可以加载到内存中,而事实表需要外存的情况下,SPL提供了外键序列化的方法:convert将事实表中的外键字段值预先写入维表中对应记录的序号中。关联计算时,批量读取新的事实表记录,然后使用序号检索对应的维表记录。以上面的订单表和产品表为例,假设产品表已经加载到内存中,订单表存储在外存中。外键序列化的过程如下:先读入一批订单数据,假设某条记录r中的pid对应内存中product表中的第i条记录。我们要将r中的pid字段值转换为i。对这批订单记录完成这样的转换后,在进行关联计算时,从外部存储中批量读取订单数据。对于记录r,可以直接使用内存中product表中的location,根据pid值检索对应的记录,也避免了查找动作。数据库通常是将小表读入内存,然后批量读取大表的数据,使用哈希算法做内存连接,需要计算哈希值并进行比较。而SPL采用序号定位直接读取,不做任何比较,性能优势明显。虽然事先将事实表的外键字段转换成序号需要一定的代价,但是这种预计算只需要做一次,并且可以在多个外键关联中复用。SPL外键序列化也是利用了维表关联字段为主键的特点。前面说过,SQL中JOIN的定义没有主键约定,不能利用这个特性来序列化外键。此外,SQL使用了无序集合的概念。即使我们提前将外键序列化,数据库也无法利用这个特性。无法在无序集合上使用序号快速定位机制。最快的方法是使用索引搜索。而且数据库不知道外键是序列化的,还是计算HASH值比较。在下面的测试中,在不同并行数的情况下,对比SPL和Oracle完成大事实表和小维度表关联计算的速度,SPL比Oracle快3到8倍。测试结果如下图所示:如果维表很大,需要外存,而事实表很小,可以加载到内存中,SPL提供了大维表查找机制。如果维度表和事实表都很大,SPL使用单边堆算法。对于维表过滤后关联的情况,SPL提供了索引复用、比对序列等方法。当数据量大到需要分布式计算时,如果维表很小,SPL使用重写维表的机制在集群节点上复制维表;如果维表很大,则采用簇维表的方式来保证随机访问。这两种方法都可以有效避免Shuffle动作。相比之下,在SQL系统下无法区分维表,HASH拆分方式需要对两张表进行shuffle动作,网络传输量要大很多。主键关联主键关联涉及的表一般都比较大,需要存储在外存中。SPL为此提供了一种有序合并的方法:将外存表预先按照主键顺序存储,关联时依次取出数据进行合并计算。以customer和VIPcustomer两张表的innerjoin为例,假设两张表事先已经按照主键cid依次存储在外存中。链接时,分别从两个表的游标中读取记录,将cid值一一比较。如果cids相等,则将两张表的记录合并为结果游标的一条记录返回。如果不相等,则cid较小的游标重新读取记录,继续判断。重复这些动作,直到任意一张表的数据都取出来,返回的游标就是JOIN的结果。对于两个大表的关联,数据库通常使用哈希堆算法,复杂度是乘法的。有序归并算法的复杂度是相加的,性能会好很多。而且,当数据库对大数据进行外存操作时,哈希分区会产生对缓存文件的读写动作。有序合并算法只需要依次遍历两张表,不需要使用外存缓存,可以大大减少IO量,具有巨大的性能优势。提前按主键排序虽然成本高,但是可以一次性搞定,以后可以一直用merge算法实现JOIN,性能可以有很大的提升。同时,SPL也提供了在有额外数据时保持数据整体顺序的解决方案。这种JOIN的特点是关联字段是主键或者是主键的一部分,有序合并算法就是根据这个特点设计的。因为不管是同维表还是主分表,关联的字段都不会是主键以外的字段,所以我们可以按照主键的顺序对关联表进行排序存储,就会有没有冗余。外键关联没有这个特性,不能使用orderedmerge。具体来说,由于事实表的关联字段不是主键,所以会有多个外键字段参与关联。我们不可能让同一个事实表同时按多个字段排序。SQL的JOIN定义不区分JOIN类型。如果不假设有些JOIN总是针对主键,那么从算法层面就没办法利用主键关联的特性。而且前面提到,SQL是基于无序集合的概念,数据库并没有刻意保证数据的物理顺序,所以很难实现有序的合并算法。orderedmerge算法的优点是易于切分和并行化。以oid关联的订单和订单明细为例。如果把两张表按照记录条数粗略分成4个section,那么order中第二section的oid可能会出现在details的第三section中。类似的错位会导致错误的计算结果。SPL再次利用主键oid的有序性,提供了同步切分机制来解决这个问题:先将有序的订单表分成4段,然后找出每段起始记录和结束记录的oid值,形成4个区间,同时划分list也分为4个同步段。这样两张表对应的段在并行计算的时候就不会错位了。由于明细表也是按oid排序的,所以可以根据起止oid快速定位,不会降低有序合并的性能。传统的HASH分堆技术更难实现并行。当多线程的HASH子堆需要同时向某个子堆写入数据,造成共享资源冲突时;而下一步要实现某组堆的关联,会消耗大量的资源。内存,不可能实现大的并行量。实际测试证明,在同样的情况下,我们测试了两个大表的主键关联。结果是SPL比Oracle快了近3倍:除了有序合并,SPL还提供了很多高性能算法,全面提升主键关联JOIN计算速度。包括:附表机制,可以将多个表统一存储,在减少存储数据量的同时,也相当于提前完成了关联,不需要再进行比较;关联定位算法,先过滤再关联,避免全表遍历,性能更好等。当数据量不断增加,需要多服务器集群时,SPL提供了多表机制,将需要关联的大表按照主键分布到集群节点上。同一个主键的数据在同一个节点上,避免了分机之间的数据传输,不会有Shuffle动作。回顾与总结回顾以上两种JOIN在各个场景下的情况,根据情况使用SPL提供的高性能算法,可以利用不同类型JOIN的特点来提速,让JOIN运行得更快。SQL对上述JOIN场景的处理是通用的,所以没有办法根据不同JOIN的特点来实现这些高性能的算法。例如:当事实表和维度表都加载到内存时,SQL只能根据key值进行HASH和比较,不能使用地址直接对应;SQL数据表乱序,大表按主键关联时不能有序合并,只能用HASH切堆,可能有多个缓存,性能有些不可控.在并行计算方面,SQL单表计算很容易实现分段并行。通常,当多表关联操作只能事先固定时,很难实现同步动态切分。确定并行度。集群计算也是如此。理论上,SQL不区分维度表和事实表。实现大表JOIN,必然会出现占用大量网络资源的HASHShuffle动作。当集群节点数量过多时,网络传输带来的延迟会超过节点数量多带来的好处。SPL设计并应用了一种新的计算和存储模型,可以从原理和实现上解决SQL的这些问题。针对JOIN的不同分类和场景,程序员可以有针对性地使用上述高性能算法,以获得更快的计算速度,使JOIN运行得更快。