【性能优化】2.2 [外存数据集] 集文件及倍增分段
2.2 集文件及倍增分段
文本文件使用字符来编码数据,通用性虽然比较好,但性能很差。要把字符转换成可以计算的数值,还需要较多的运算量,日期时间类数据还需要很复杂的解析判断过程。如果直接把数据按它在内存中的形式直接存储出去,在读出时就没必要再做转换工作了,就能获得好得多的性能。
这就是二进制的格式。
二进制格式的缺点在于无法直接阅读,业界也没有通行的二进制文件格式标准。
SPL 提供了一种简单的二进制格式,称为集文件。
A |
|
1 |
=file("data.btx").export@b(file("data.txt").cursor@t()) |
2 |
=file("data.btx").export@ab(file("data.txt").cursor@t()) |
可以文本文件读出转换成集算器,使用 @a 选项可以在已有的集算器后面追加数据。
读者可以尝试一下使用文本文件和集文件的做同样运算的性能对比,比如简单做一个行数的统计:
A |
|
1 |
=file("data.btx").export@b(file("data.txt").cursor@t()) |
2 |
=file("data.btx").export@ab(file("data.txt").cursor@t()) |
集文件游标不需要再用 @t 选项,它一定会带有字段名。
和文本文件类似,要提高二进制文件的运算性能,分段后并行也是一个重要手段。但二进制文件和文本文件不同,它没有每行之间的分隔符,而且任何取值的字节都可能出现在存储的字段值中,不能使用某个特殊取值用来分隔字段或记录。事实上,没有分隔符也是二进制文件的优势,因为很多数据类型占用的字节数是确定的,没必要再浪费存储一个分隔符(一个整数可能占 4 字节,如果增加一个分隔符占 1 字节,就会把存储量扩大 25% 之多)。
那么这时候我们怎么做分段?
早期的二进制文件会使用定长记录,也就是每条记录的长度相同,这样就可以用乘法计算每第 n 条记录所在的字节位置。 早期数据库也是这种办法,所以会要求用户在创建数据表时写清每个字段有多宽,就是为了留好位置。因为不同记录实际占用字段数可能相差很大,这种办法就要求按最大的那个预留空间,浪费严重,现在已经很少采用,现代数据库目前多用不定长的 varchar 代替 char 了。
另一个办法是仿照文本文件,人为设置一个分隔符来分隔两条记录。但任何字节取值都可能是实际的数据,简单用某个字符作为分隔符是很可能出错的。所以一般会有一连串字节,比如连续 16 个 255 表示记录的结束,一般正常的数据不会有这种情况,这样就不会错位了。但这同样会造成不小的浪费,而且不能完全杜绝出错的可能性。
目前主流的办法是分块,每 N 条记录或写够一定字节数作为一块。块内数据不再拆分,分段是在所有的块中进行。只要块的数量足够多,分段并行的效果就不会差,而数据量大的情况确实会有很多分块;如果分块数比较少,说明数据量不大,也没必要分段并行。
分块的麻烦之处主要在于需要有个块的索引信息,记录总共有多少块,每一块从哪里开始等等。随着数据的追加,块的数量还会不断增加,这个索引的长度也会变化。如果是数据库系统或大数据平台,那可以专门有一套索引管理机制。但如果在普通文件系统中实现,就需要单独再有一个存储索引的文件,使用起来就不方便了。
SPL 采用了一种称为倍增分段的方法在文件系统中实现了单文件可追加分块方案。
在文件头预留空间可以存储 1024 个(也可以是其它数)块索引信息,然后在其后追加实际数据。起始认为一条记录占一块,每追加一条记录就意味增加了一个块,追加后要在索引区填入块的信息。追加到 1024 条记录时,所有索引块都被填满。
如果再有追加记录的动作,则认为两条记录占一块,将 1024 的索引块减半,第 1 块和第 2 块合并成新的第 1 块,第 3 块和第 4 块合并成新的第 2 块、….、第 1023 块第 1024 块合并成新的第 512 块,从第 513 到第 1024 块的索引全部清空。因为实际数据是连续存储的,只要把每索引块中数据块的起始位置改一下就行了。
现在有了 512 个空索引块,可以再追加 512*2 条新记录。继续追加到总共 2048 记录后,所有的索引块又被填满。如果要再追加记录,则再做一次刚才的调整,改成每 4 条记录一块,同样把当前的第 1、第 2 块合并成新的第 1 块、…。又可以空出 512 个块索引以继续追加。
追加可以一直进行,分块大小会不断翻倍,全其中数据一直是连续,而且没有多余的分隔符浪费空间。总的分块数在 512-1024 之间变化(一开始记录数小于 512 时除外),也满足分块数足够多的要求。文件头预留给索引空间是固定的,不会随着数据追加而变大,一个单文件就可以实现分块分段的机制了。而且,这时候还很容易数出总共有多少记录(用乘法后,只要数最后一块有多少记录)。
实际上,SPL 不会一上来就使用倍增分段,如果总记录数很少(比如小于 1024),则不会分段,当记录数超过一定数量时才会执行分段方案。这样,如果向集文件写入几条记录,并不会多出一个看起来有点大的文件头。而数据量大之后 ,多出来的文件头相比之下就不明显了。
对于应用人员来讲,这些都是透明的,只要简单追加后,集文件就能实现分段的计算。
A |
B |
C |
|
1 |
=file("data.btx") |
||
2 |
=A1.cursor@b(;4:10) |
=A1.cursor@b(;5:10) |
=A1.cursor@b(;23:100) |
3 |
=A2.fetch(1) |
=B2.fetch(100) |
=C2.fetch() |
访问语法和文本文件基本相同,但分段数超过 1024 对于集文件没有意义了。
想了解下集文件块内各条记录是怎么区分的呢?
一条取完就是下一条,没有分隔符
这个一条一条是怎么确定的呢?没有分隔符怎么才能知道一条读完了?
每种数据类型有确定的编码方式,读之前就知道会占多少字节,字段数量也是确定的,读够字段数就算结束了。
可以自己看看源代码去理解
集文件中,记录的每个字段数据,是定长存储么
不是,它会压缩,不同数据的压缩比不一样