SQL 提速:两个大数据表在主键上做连接
【摘要】
从原理上分析 SQL 语句慢的原因,用代码示例给出提速办法。点击了解SQL 提速:两个大数据表在主键上做连接
问题描述
主键连接是非常常见的情况,如订单和订单明细。但数据库并不区分关联的字段是什么,一般都会采用 HASH JOIN 算法计算,常常出现大表连接非常慢的现象。这是因为 HASH JOIN 本质上是内存运算,数据量巨大的时候需要做分堆缓存才能把大数据 JOIN 转换成若干小数据 JOIN,IO 开销很大,而且 HASH 运气不好时还可能多次缓存。
提速方案
一、有序归并计算连接
若两表都对主键有序,就可以使用归并算法实现连接,有序归并算法只需要对两个表依次遍历,不必借助缓存,可以大幅降低 IO 量。而且有序归并复杂度很低,是加法级的,而 HASH JOIN 是乘法级的。
如果两表无序,我们要预先按主键排序。排序成本很高,每次都排序会导致总体性能还不如传统算法。幸运的是,仅是主键(或部分主键)之间关联的场景就非常多,能解决这个场景应用面就足够广了。事实上,其他有意义的关联也都和主键相关,我们会另行讨论。
预先按照主键排序虽然慢,但是一次性的,而且只需要保持主键有序这一种存储即可,没有冗余。
二、新增数据追加
新增数据并不总是按主键继续有序,所以不能简单的追加到有序数据的末尾。而直接将有序数据和新增数据一起重新做常规大排序,会非常耗时。
我们可以将新增数据排序后,和原有序数据一起,也用低成本的有序归并算法生成新的有序数据。这样整体复杂度相当于把所有数据读写一遍,可以避免常规大排序中产生很多临时外存缓存的现象,从而获得更好的性能。
更进一步,可以另外保持一个小规模的有序数据 (以下称为补数据)。新增数据排序后和补数据归并,原有序数据不变。经过适当的时间后,补数据积累到合适大小时,再和原有序数据归并。做连接计算时,从有序数据和补数据中分别读取,归并后再和其他表做连接,性能会比只有一份有序数据时下降一些,但仍能利用有序实现快速连接计算。
这个适当时间的确定,与新增数据的周期有关。比如每天都有新增数据,则每个月做一次原有序数据和补数据的归并。补数据不会超过一个月的数据量,原有序数据存储一个月之前的所有数据。也就是说补数据可能会比原有序数据小很多,所以每天归并的数据量相对较小,很快就能完成数据追加。每个月才需要完成一次全量有序归并,耗时长一些也可以接受了。
代码示例
第一步,数据预处理,有序存储。
以表 A 为例,表 B 的代码类似。
A |
|
1 |
=file("A-original.ctx") |
2 |
=A1.open().cursor().sortx(a).new(a,…) |
3 |
=file("A.ctx").create(#a,…) |
4 |
=A3.append@i(A2) |
A1:转换前的原始组表 A-original.ctx。
A2:打开组表 A-original.ctx 建立游标,按照字段 a 排序,结果游标将字段 a 放在前面。
A3:新建组表 A.ctx,字段名前面加了 #,即表明该组表对 a 字段有序。
A4:将排好序的数据输出到组表 A.ctx 中。
第二步,有序归并实现连接。
A |
|
1 |
=file("A.ctx").open().cursor(a,…) |
2 |
=file("B.ctx").open().cursor(b,…) |
3 |
=joinx(A1:A,a;A2:B,b) |
4 |
… |
A1、A2:打开组表 A.ctx 和 B.ctx,定义游标。
A3:采用有序归并算法,两个游标按照字段 a 和 b 做内连接,返回游标。类似的,joinx@f 表示全连接,joinx@1 表示左连接。
A4:继续对有序归并的结果游标做其他计算。
第三步,新增数据追加。
假设 A 表每天的新增数据存储在 A_new.btx 中,且字段名称、顺序与 A.ctx 相同。
A |
B |
|
1 |
=file("A.ctx").open() |
|
2 |
=file("A_new.btx").cursor@b().sortx(a) |
|
3 |
if (day(now())==1) |
>A1.reset() |
4 |
>A1.append@a(A2) |
A1:打开组表 A.ctx。
A2:定义 A_new.btx 的游标并排序,通常每天新增数据量不大,这里虽然用 sortx,但实际上经常会是内存排序,速度很快。
A3:判断日期是否为 1 号,如果不是则执行 A4,用 append@a 只在补数据上归并。如果是 1 号,那么就执行 B3,用 reset 把原有序数据和补数据有序归并成新的有序数据。