怎样用 esProc 加速 JOIN 以避免宽表
在数据分析应用中,数据库多表关联的 SQL 语句很复杂,JOIN 性能也不理想,多表往往会被整合成宽表。比如图中订单表和多个维表就可能会被转换为宽表:
但是,宽表也存在很多问题,比如数据冗余量很大、不符合范式要求容易出错、维表数据变化时要刷新整个宽表等等。而且,宽表计算性能也未必就比多表关联好。
esProc 专门针对此类关联设计了序号关联方法,可以大幅提升 JOIN 的性能,这样就能避免使用宽表了。
下面,我们以订单表和维表为例,比较 esProc 多表方案和 MYSQL 数据库多表、宽表方案的性能。订单表是事实表,数据量一千万。其他表都是维表,数据量不大。
测试环境:VMWARE 虚拟机,8 核 CPU,8G 内存,SSD 硬盘。操作系统是 Win11,MYSQL 版本是 8.0。
先下载 esProc https://www.esproc.com/download-esproc/,用标准版就可以了。
安装 esProc 后,试一下 IDE 是否可以正常访问数据库。先把 MYSQL 数据库的 JDBC 放到目录 "[安装目录]\common\jdbc",这是 esProc 的类路径之一:
在 esProc 中建立 MYSQL 数据源:
返回到数据源界面并连接刚才配置的数据源,如果数据源名变成粉色,说明配置成功。
在 IDE 中新建脚本,编写 SPL 语句,连接数据库,执行 SQL 读入 city 表数据:
A |
B |
|
1 |
=connect("mysql16") |
|
2 |
=A1.query("select * from city") |
按 ctrl-F9 执行,可以在 IDE 右边看到 A2 的执行结果,很方便。
接下来完成数据准备,把历史数据从数据库导出到 esProc 的高性能文件:
A |
|
1 |
=connect("mysql16") |
2 |
=A1.query("select * from city").keys@i(cityid) |
3 |
=file("city.btx").export@b(A2) |
4 |
=A1.query("select * from supplier").keys@i(sid) |
5 |
=file("supplier.btx").export@b(A4.new(sid,sname,A2.pfind(cityid):cityid)) |
6 |
=A1.query("select * from customer").keys@i(cid) |
7 |
=file("customer.btx").export@b(A6) |
8 |
=A1.query("select * from product").keys@i(pid) |
9 |
=file("product.btx").export@b(A8.new(pid,pname,ptype,A6.pfind(sid):sid)) |
10 |
=A1.cursor("select * from orders") |
11 |
=A10.new(oid,A6.pfind(cid):cid,A8.pfind(pid):pid,odate,amount) |
12 |
=file("orders.ctx").create@y(oid,cid,pid,odate,amount) |
13 |
=A12.append(A11) |
14 |
>A1.close(),A12.close() |
A2-A9 取出各维表,维表数据量不大,存入集文件 btx 比较方便。supplier 的 cityid 字段、product 的 sid 字段要用 pfind 函数转换成对应维表中的位置序号。
事实表 orders 比较大,A10 建立事实表的数据库游标。
A11 利用 pfind 函数把 cid 和 pid 转换为对应表中的位置序号。
A12 新建一个组表,A13 把 A11 游标的数据写入组表。
这里,esProc 把事实表 orders 中的维度字段 cid,pid 转换为维表的序号,实现序号化。计算时预先把维表装入内存并完成关联。再将事实表数据分批读入内存,用维度字段值(也就是维表序号),直接取内存维表对应位置的记录,速度很快。
维表 supplier 和 product 的维度字段 cityid、sid,同样要完成序号化。
数据准备完成后,要进行 esProc 系统初始化,把维表数据预先加载到内存中,并完成关联:
A |
B |
|
1 |
=file("customer.btx").import@b() |
=env(customer,A1) |
2 |
=file("product.btx").import@b() |
=env(product,A2) |
3 |
=file("supplier.btx").import@b().keys@i(sid) |
=env(supplier,A3) |
4 |
=file("city.btx").import@b().keys@i(cityid) |
=env(city,A4) |
5 |
=supplier.run(cityid=city(cityid)) |
=product.run(sid=supplier(sid)) |
A1 到 B4 读入各维表数据,放到全局变量中。
A5 完成供应商表和城市表的关联,由于预先完成了 supplier 中 cityid 字段的序号化,这个字段的值就是 city 表的序号,可以直接取 city 表中对应序号的记录。B5 完成产品表和供应商表的关联,也是用序号方式。
关联后,产品表的 sid 字段被赋值为供应商表的记录对象,供应商表的 cityid 字段也被赋值为城市表的记录对象。
初始化完成后,就可以利用 esProc 代替宽表实现查询了,比如把供应商城市是芝加哥的订单,按照客户分组统计金额,结果要包括客户名称:
A |
|
1 |
=file("orders.ctx").open() |
2 |
=A1.cursor@m(oid,cid,pid,amount;cid:customer:#,pid:product:#) |
3 |
=A2.select(pid.sid.cityid.cityname=="Chicago").groups(cid;cid.cname,sum(amount):total) |
4 |
>A1.close() |
A2 中 cid:customer:# 的写法,表示利用序号 #,在游标上完成订单与客户的关联。关联后,cid 和 pid 字段都被转换为对应表的记录对象。
A3 就可以很方便的计算了,可以采用对象属性的方式写代码,比如 pid.sid.cityid.cityname。
这个脚本实际执行时间是 1.3 秒。
A2 的 @m 表示按照 option 里配置的并行选项进行多线程计算。
需要把这个并行选项打开。
MYSQL 多表方案的 SQL:
SELECT c.cid,c.cname,SUM(o.amount)
FROM
orders o
JOIN
customer c ON o.cid = c.cid
JOIN
product p ON o.pid = p.pid
JOIN
supplier s ON p.sid = s.sid
JOIN
city ci ON s.cityid = ci.cityid
WHERE
ci.cityname = 'Chicago'
GROUP BY
c.cid, c.cname
执行时间:28 秒
MYSQL 宽表方案的 SQL:
SELECT cid,cname,SUM(amount)
FROM
orders_all
WHERE
cityname = 'Chicago'
GROUP BY
cid, cname
执行时间:33 秒。
代码比多表关联简单,但性能反而有所下降,可能是宽表列数更多,计算时需要读取更多的数据,导致速度变慢。
我们再看另一个计算,按照供应商分组统计订单数量,结果要包括供应商名称和所在城市。前面 esProc SPL 在 A2 的基础上,可以方便的完成各种计算,只要改一下 A3 代码就可以了:
=A2.groups(pid.sid;pid.sid.sname,pid.sid.cityid.cityname,count(1))
执行时间:1 秒
MYSQL 多表方案:
SELECT s.sid,s.sname,c.cityname,COUNT(o.oid)
FROM
orders o
JOIN
product p ON o.pid = p.pid
JOIN
supplier s ON p.sid = s.sid
JOIN
city c ON s.cityid = c.cityid
GROUP BY
s.sid, s.sname, c.cityname, c.statename;
执行时间:36 秒
MYSQL 宽表方案:
SELECT sid,sname,cityname,statename,count(oid)
FROM
orders_all
GROUP BY
sid, sname, cityname, statename
执行时间:44 秒
测试结果:
MYSQL 多表 |
MYSQL 宽表 |
esProc |
|
计算一 |
28 秒 |
33 秒 |
1.3 秒 |
计算二 |
36 秒 |
44 秒 |
1 秒 |
esProc 多表关联计算性能完胜 MYSQL 多表、宽表方案,代码也很简单,完全可以避免宽表。
数据准备时,从数据库导出数据时间较长,但这个操作是一次性的,时间长一些也无妨。后续订单表有增量数据的时候,只要定期对组表继续追加即可。
不过维表有数据变更的时候会麻烦一些,需要重新生成 btx,重新加载到内存中。而且事实表序号化的结果依赖于维表的记录次序,如果维表发生插入删除等变化,就要重新生成一次事实表,维护管理成本相对较高。
实际上,针对固定不变的历史数据的计算场景就非常多了,也有不少多表关联亟需提高性能。这些场景采用 SPL 序号关联方法可以有效加速多表关联,从而避免宽表。
英文版