怎样用 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 序号关联方法可以有效加速多表关联,从而避免宽表。