批量随机键值查询测试
【摘要】
当数据量巨大时,使用大批量随机键值集获取对应记录集合,不仅仅考验数据库软件本身,更在于程序员对数据的理解!如何在硬件资源有限的情况下将性能发挥到极致?点击:批量随机键值查询测试,来乾学院一探究竟!
本次测试主要针对集算器组表索引实现的批量键值取数性能,并与 Oracle 进行同规模运算对比。
一、测试环境
处理器 |
Intel(R) Xeon(R) CPU E5-2670 @ 2.60GHz两颗 |
内存 |
64G |
硬盘 |
SAS 1TB |
操作系统 |
centos6.8(64 位) |
二、数据描述
2.1数据结构
字段 |
类型 |
备注 |
id |
long |
1000000000001开始自增 |
data |
string |
随机字符串(长度为 180 字节) |
2.2数据规模
按以上数据结构,造出 6 亿条记录的行存组表文件和对应的索引文件:
类型 |
文件名 |
大小 |
组表 |
id_600m.ctx |
111G |
索引 |
id_600m.ctx__id_idx |
14G |
三、测试过程
3.1生成测试文件
3.1.1 建组表
A |
B |
|
1 |
1234567890qwertyuiopasdfghjklzxcvbnm |
|
2 |
=file("id_600m.ctx") |
|
3 |
=A2.create@r(#id,data) |
|
4 |
for 6000 |
=to((A4-1)*100000+1,A4*100000).new(~+1000000000000:id,rands(A1,180):data) |
5 |
=A3.append(B4.cursor()) |
A1:包含 26 个英文字母和 10 个阿拉伯数字的字符串。
A2、A3:建立结构为 (id,data) 的组表文件,@r 选项表示使用行式存储方式。
A4:循环 6000 次,循环体B4、B5,每次生成 10 万条对应结构的记录,并追加到组表文件。
执行后,生成组表文件:id_600m.ctx
3.1.2 建索引
A |
|
1 |
=file("id_600m.ctx ") |
2 |
=A1.create().index(id_idx;id) |
A2:根据组表文件的 id 列,建立组表索引。
执行后,生成组表的索引文件:id_600m.ctx__id_idx
3.2查询测试
A |
|
1 |
=file("id_600m.ctx").create() |
2 |
=10000.(1000000000000+(rand(600000000)+1)).sort() |
3 |
=now() |
4 |
=A1.icursor(A2.contain(id),id_idx).fetch() |
5 |
=interval@ms(A3,now()) |
A2:循环一万次,每次获取对应组表文件 id 列中的随机一个,并排序。(可能会有少量重复值,但对测试影响不大)
A4:在组表的 icursor()这个函数中,使用索引 id_idx,以条件 A2.contain(id) 来过滤组表。集算器会自动识别出 A2.contain(id) 这个条件可以使用索引,并会自动将 A2 的内容排序后从前向后查找。
3.3奇怪的现象
原本希望多次执行后,求得一个平均值作为测试结果。但是发现每执行完毕一次该测试代码,都会比上一次执行快一些,这里列出从第一次执行该代码后的 5 次测试查询耗时:
次数 |
查询耗时(毫秒) |
1 |
79778 |
2 |
78391 |
3 |
78186 |
4 |
76192 |
5 |
74244 |
手动一次次点击设计器中的执行按钮,并记录下查询耗时,太费劲了。为了找出规律,将代码改为以下形式:
A |
B |
|
1 |
||
2 |
for |
=file("id_600m.ctx").create() |
3 |
=10000.(1000000000000+(rand(600000000)+1)).sort() |
|
4 |
=now() |
|
5 |
=B2.icursor(B3.contain(id),id_idx).fetch() |
|
6 |
=interval@ms(B4,now()) |
|
7 |
>A1=A1|B6 |
B7:将循环体中 icursor() 函数每一次查询的耗时,在 A1 中追加记录下来。
执行过程中,观察 A1 中新追加的查询耗时与上一次的比较,发现经过大约 350 次循环后接近极限值 25 秒。再后续近千次循环中,查询耗时也都是如此,基本稳定。
难道是集算器对数据进行了缓存?抱着怀疑的态度,重启了集算器设计器,再次执行了查询代码。发现重启后第一次的查询耗时也是 25 秒。这样看来提速的原因和集算器本身并没有什么直接的关系了。
另一方面,可以想到基于目前测试的数据量,能够在短时间内完成查询,部分数据可能已经装载至内存,那么很可能是 linux 操作系统的文件缓存造成了这个现象。重启服务器后,再通过集算器设计器来执行查询,发现耗时又开始从 80 秒左右慢慢减少了。
进一步的测试中,使用了 linux 的 free 命令查看系统内存使用情况。发现每完成一次组表的查询,其中的 cached 一项就会变大。而随着 cached 慢慢的变大,查询的耗时又逐步减少。
3.4index@3的使用
在网络上查询了一些资料,了解到 Linux 会存在缓存内存,通常叫做 Cache Memory。就是之前使用 free 命令看到其中的 cached 一项,执行 free -h:
total |
used |
free |
shared |
buffers |
cached |
|
Mem: |
62G |
62G |
519M |
9.1M |
10M |
45G |
当我们读写文件的时候,Linux 内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是 Cache Memory(缓存内存)。即使我们的程序运行结束后,Cache Memory 也不会自动释放。这就会导致我们在 Linux 系统中程序频繁读写文件后,我们会发现可用物理内存会很少。其实这个缓存内存在我们需要使用内存的时候会自动释放,所以我们不必担心没有内存可用。并且手动去释放 Cache Memory 也是有办法的,但此处不再详细探讨。
这个函数涉及数据量有 111G,比机器的物理内存 64G 更大,显然不可能把所有数据都缓存到内存中,那么到底缓存了哪些数据后就能稳定地提高查询性能呢?是不是可以事先就把需要这些数据先缓存起来以获得高性能?请教了高手后,发现果然还有选项可以来预先缓存索引的索引。在使用 icursor()函数查询之前,对组表索引使用了 T.index@3(idx)。代码如下:
A |
|
1 |
=file("id_600m.ctx").create() |
2 |
=now() |
3 |
=A1.index@3(id_idx) |
4 |
=interval@ms(A2,now()) |
5 |
=10000.(1000000000000+(rand(600000000)+1)).sort() |
6 |
=now() |
7 |
=A1.icursor(A5.contain(id),id_idx).fetch() |
8 |
=interval@ms(A6,now()) |
集算器的索引有个分级缓存,@3 的意思是将索引的第三级缓存先加载进内存。经过 index@3 预处理,第一遍查询时间也能达到上面查询数百次后才能达到的极限值。
四、与 Oracle 对比
测试环境、数据结构和规模与上文一致,测试对象如下:
产品 |
版本 |
Oracle数据库 |
Oracle Database 12c Release 1(12.1.0.2.0) |
集算器 |
集算器 V2018 |
Oracle建表语句为:
create table ctx_600m (id number(13),data varchar2(200));
数据由集算器生成同结构的文本文件后,使用 Oracle 的 SqlLoader 导入表中。
Oracle建索引语句为:
create unique index idx_id_600m on ctx_600m(id);
使用 Oracle 进行批量随机取数测试时,我们使用这样的 SQL:
select * from ctx_600m where id in (…)
使用单线程连接 Oracle 进行查询的集算器脚本为:
A |
|
1 |
=10000.(1000000000000+rand(600000000)+1).sort() |
2 |
=A1.group((#-1)\1000) |
3 |
=connect("oracle12c") |
4 |
=now() |
5 |
=A2.(A3.query("select * from ctx_600m where id in (?)",~)).conj() |
6 |
=interval@ms(A4,now()) |
7 |
>A3.close() |
由于 oracle 的 in 个数有限制,脚本中进行分批执行后合并。
使用 10 线程连接 Oracle 进行查询的集算器脚本为:
A |
B |
|
1 |
=10000.(1000000000000+rand(600000000)+1).sort() |
|
2 |
=A1.group((#-1)\1000) |
|
3 |
=now() |
|
4 |
fork A2 |
=connect("oracle12c") |
5 |
=B4.query("select * from ctx_600m where id in (?)",A4) |
|
6 |
>B4.close() |
|
7 |
=A4.conj() |
|
8 |
=interval@ms(A3,now()) |
使用单线程对行存组表进行查询的集算器脚本为:
A |
|
1 |
=file("id_600m.ctx").create() |
2 |
=now() |
3 |
=A1.index@3(id_idx) |
4 |
=interval@ms(A2,now()) |
5 |
=10000.(1000000000000+(rand(600000000)+1)).sort() |
6 |
=now() |
7 |
=A1.icursor(A5.contain(id),id_idx).fetch() |
8 |
=interval@ms(A6,now()) |
使用 10 线程对行存组表进行查询的集算器脚本为:
A |
B |
|
1 |
=file("id_600m.ctx").create() |
|
2 |
=now() |
|
3 |
=A1.index@3(id_idx) |
|
4 |
=interval@ms(A2,now()) |
|
5 |
=10000.(1000000000000+(rand(600000000)+1)).sort() |
|
6 |
=A5.group((#-1)\1000) |
|
7 |
=now() |
|
8 |
fork A6 |
=A1.icursor(A8.contain(id),id_idx) |
9 |
=B8.fetch() |
|
10 |
return B9 |
|
11 |
=A8.conj() |
|
12 |
=interval@ms(A7,now()) |
从 6 亿条数据总量中取 1 万条批量随机键值,在都建立索引的测试结果:
耗时(毫秒) |
|||
单线程 |
多线程(10 线程) |
||
Oracle |
集算器组表 |
Oracle |
集算器组表 |
117322 |
20745 |
39549 |
10975 |
五、列存索引测试
集算器列存采用了数据分块并压缩的算法,这样对于遍历运算来讲,访问数据量会变小,也就会具有更好的性能。但对于基于索引随机取数的场景,由于要有额外的解压过程,而且每次取数都会针对整个分块,运算复杂度会高很多。因此,从原理上分析,这时候的性能应当会比行存要差。
上述代码中把生成组表的 create() 函数不用 @r 选项,即可生成列存文件。重复上面的运算,单线程情况下 6 亿行中取 1 万行耗时为 129120 毫秒,比行存方式慢了 6 倍多。不过平均到一行也只有 13 毫秒,对于大多数单条取数的场景仍然有足够的实用性。
同一份数据不能在遍历运算和随机取数这两方面都达到最优性能,在实际应用中就要根据需求做一下取舍了,一定要追求各种运算的极限性能时,可能就要把数据冗余多份了。
六、索引冗余机制
集算器确实也提供了冗余索引机制,可以用于提高列存数据的随机访问性能,代码如下:
A |
|
1 |
=file("id_600m.ctx") |
2 |
=A1.create().index(id_data_idx;id;data) |
在对组表建立索引时,当 index 函数有数据列名参数,如本例 A2 中的 data,就会在建索引时把数据列 data 复制进索引。当有多个数据列时,可以写为:index(id_idx;id;data1,data2,…)
因为在索引中做了冗余,索引文件也自然会较大,本文中测试的列存组表和索引冗余后的文件大小为:
类型 |
文件名 |
大小 |
组表 |
id_600m.ctx |
105G |
索引 |
id_600m.ctx__id_data_idx |
112G |
当数据复制进索引后,实际上读取时不再访问原数据文件了。
从 6 亿条数据总量中取 1 万条批量随机键值,完整的测试结果对比:
耗时(毫秒) |
|||||
单线程 |
多线程(10 线程) |
||||
Oracle |
行存索引 |
冗余索引 |
Oracle |
行存索引 |
冗余索引 |
117322 |
20745 |
19873 |
39549 |
10975 |
9561 |
?