为什么数据库比文本文件快?

文本为什么更慢?

文本文件慢的原因,主要在于会多出很多数据类型解析的动作。

举个例子,设想一下把文本“12345" 转成内存二进制整数 12345 的过程:

1. 先设结果的初始值为 0

2. 拆出字符“1”,解析出数值 1,将初值 0 乘以 10 加上这个 1 得到数值 1

3. 再拆出字符“2”,解析出数值 2,把刚才的 1 乘以 10 和这个 2 相加得到数值 12

4. 再拆出字符“3”,解析出数值 3,把刚才的 12 再乘以 10 加上这个 3 得到数值 123

5. …

有些 C 程序员知道用函数 atoi() 可以实现字串到整数的转换,仅仅一句代码,看似非常简单,但其实背后的步骤非常多,CPU 要干很多事才能完成这个动作,耗时并不短。实际过程中还要判断可能出现的非法字符(比如不是数字的字符),比上面描述的步骤还要更复杂得多。

整数还是最简单的数据类型,如果是实数还要处理小数点,字符串解析时要考虑转义字符和引号匹配,日期的解析更是要麻烦得多,因为格式种类太多,2018/1/10 和 10-1-2018 都是常见的合法日期格式,甚至还有 Jan-10 2018 这种,要正确解析,就得尝试用多种格式去匹配,CPU 耗时很严重。

理解了文本慢的原因,也就理解了数据库快的原因。数据库当然不会用文本存储数据,而是采用二进制的形式,这时候已经将数据类型等信息保存好了,读取时不需要再解析数据类型,所以性能更好。

二进制文件会更快

即然文本慢的原因是数据类型的解析,我们如果把数据存成二进制文件,是不是可以获得与数据库一样的性能呢?

答案是肯定的,而且相对数据库,二进制文件的性能更高!

因为数据库通常都要提供数据更新的能力,这就会产生影响性能的因素。

1. 紧凑性与压缩手段

数据库要考虑数据的更新,一般会采用段页式的分块存储,在存储数据时不会把分块完全填满,而会留一小部分空白区用于后续的修改动作。这样,占用的硬盘空间就会比不考虑更新动作的文件更大一点。

因为要更新数据,也很难实现数据压缩。比如把小整数存成较短字节的方案,如果采用了这种方案,一旦这个小整数被改成了大整数,原来的空间就存不下了,就要把后续数据都向后移动,这会使数据更新成本过高,所以一般数据库都不采用压缩手段,而直接根据数据类型分配空间,也会造成空间的浪费,极端情况会出现占用空间大于文本的现象。

而二进制文件则没有这方面的限制,不仅可以将数据紧凑存储,还可以放心地进行压缩从而获得更高的读取性能(减少硬盘时间)。不过数据的压缩率不是越高越好,解压缩需要耗费 CPU 时间,过于追求压缩率会导致 CPU 时间过长反倒影响性能。理想的压缩方案,要在硬盘空间的减少和 CPU 的消耗中取得某种平衡。

2. 事务一致性带来的复杂性

许多商业数据库还会同时支持 OLTP 业务,在读取数据时要提供一致性的能力,这会使访问数据的动作复杂度变大很多。同一条数据,由于其它事务的写操作,可能出现多个备份,在读取时数据库要根据事务的启动时刻找到正确的那一个,这是个非常麻烦的动作,对性能影响很大。而二进制文件则没有这方面的限制。

3. 分段不灵活

数据库按块存储的结构对于分段也不够自由,不能像文件那样可以实施更灵活的并行手段,也会导致数据库的性能表现弱于二进制文件。

4.IO 性能差

数据库普遍还有一个 IO 性能不佳的问题,数据在数据库中运算时性能不错,但某些复杂运算用 SQL 不容易写,要通过口取出来再运算,而这个接口通常都非常慢。实测的情况表明,从数据库中取数的性能经常可能会比从文本文件中取数还慢,相对二进制文件就更差了。

通过比较,我们看到二进制文件相对数据库有很多优势。但在实际应用中却很少使用,这是为什么呢?

原因在于文件(包括二进制文件)最大的缺点:可计算性太差

数据不是存起来就完事儿,要使用才能产生价值,本质上就是计算。我们知道数据库不仅能存储,还能计算,不仅能算还挺方便,使用 SQL 这种专门的数据库计算语言处理数据很方便,分组汇总、关联计算简单一句就能搞定。而文件却不具备这样的能力,要计算文件数据还要借助其他编程语言完成,实现难度很大。像大部分应用都在采用的 Java,由于缺少相应的结构化数据计算类库,要实现 SQL 式的集合运算代码量很大,简单的分组汇总也要写几十行,更别提复杂一些的计算了。

除了可计算性,还需要有一套相对通用的二进制文件的存储方式,如适合的压缩算法、有效的分段方案等。业界在方面并没有标准(相对来讲文本文件就通用得多了),这些工作如果每次都要从头来做,那经常就不如直接使用数据库来的方便了。

开源 SPL 提供可计算的通用二进制文件

有了开源 SPL,就可以利用二进制文件的高性能了。

开源 SPL 提供了通用的二进制文件存储方式,以及基于这些文件的计算能力,从而可以充分发挥二进制文件的优势,高效完成数据处理。

SPL 的二进制文件

集文件

集文件是 SPL 提供的基础二进制数据格式,不仅采用了压缩技术(占用空间更小读取更快),存储了数据类型(无需解析数据类型读取更快),还支持可追加数据的倍增分段机制,利用分段策略很容易实现并行计算,进一步提升计算性能。

SPL 将文本转换成集文件非常简单:


A

1

=file("data.btx").export@b(file("data.txt").cursor@t())

通过游标 cursor 流式读取文本内容并写出成二进制集文件 data.btx,其中选项 @b 代表生成二进制文件。如果针对已有二进制文件进行追加写入,增加一个选项 @a 即可,即:

=file("data.btx").export@ab(file("data.txt").cursor@t())

生成的二进制文件天然具备上述讨论的优良特性,比如倍增分段技术,即将数据进行分块存储,并行计算的时候直接基于分块数据计算会很方便,但是要考虑数据追加的问题,SPL 设计了一种倍增分块的方案,保证数据可以不断追加的同时分块还可以顺利完成。当然这些对应用人员来讲都是透明的,直接使用就好。取某个分段数据很简单:

=file("data.btx").cursor@b(;23:100) //表示取100段中的第23段。

组表

组表是 SPL 提供列存、索引机制的文件存储格式,在参与计算的列数(字段)较少时列存会有巨大优势。组表除了支持列存,实现了 minmax 索引外,还支持倍增分段机制,这样不仅能享受到列存的优势,也更容易并行提升计算性能。

组表的生成和使用:


A

B

1

=file("data.ctx")

=file("date.btx")

2

=A1.create(…)

=A2.append(B1.cursor@b())

3

=A1.open()


4

=A3.cursor(…;;4:10)


5

=A4.fetch(1)


和集文件不同,组表需要事先创建后才能追加数据(A2 创建,B2 追加数据)创建时要先指明数据结构(A2 中的…部分),这有点类似数据库中的表需要先 CREATE TABLE。A4 类似前面产生了不同分段的游标,注意要多写一个分号。组表创建时会缺省使用列存,读取时在参数中(A4 中的…)写上用到的字段名,就能利用列存优势减少读取量了。

SPL 除了提供高效的二进制文件存储,还有很多其他特性方便数据处理。

基于二进制文件直接计算

SPL 提供了完备的计算能力,基于二进制文件,通过 SPL 敏捷语法和高性能算法就可以高效实现数据处理,不仅写的简单,运算效率也更高。

比如常见的 TOPN 与组内 TOPN 计算:


A


1

=file(“data.ctx”).create().cursor()


2

=A1.groups(;top(10,amount))

金额在前 10 名的订单

3

=A1.groups(area;top(10,amount))

每个地区金额在前 10 名的订单

SPL 还提供了丰富的计算类库,常规与复杂计算用 SPL 实现都很简单。


A

B

1

=T("/data/scores.btx")


2

=A1.select(CLASS==10)

过滤

3

=A1.groups(CLASS;min(English),max(Chinese),sum(Math))

分组汇总

4

=A1.sort(CLASS:-1)

排序

5

=T("/data/students.btx").keys(SID)


6

=A1.join(STUID,A5,SNAME)

关联

7

=A6.derive(English+ Chinese+ Math:TOTLE)

追加列

对于文件计算,SPL 还提供了相当 SQL92 标准的 SQL 支持,对于熟悉使用 SQL 的人员可以直接使用 SQL 查询文件(文本、二进制都可以):

$select * from d:/Orders.btx where Client in ('TAS','KBRO','PNS')

复杂些的 with 都支持:

$select t.Client, t.s, ct.Name, ct.address from
(select Client ,sum(amount) s from d:/Orders.btx group by Client) t
left join ClientTable ct on t.Client=ct.Client

SPL 的敏捷语法和过程计算还非常适合完成复杂计算,比如基于股票记录计算某只股票最长连涨天数 可以这样写:


A

1

=T("/data/stock.btx")

2

=A1.group@i(price<price[-1]).max(~.len())-1

再比如,根据用户登录记录列出每个用户最近一次登录间隔:


A


1

=T(“/data/ulogin.btx”)


2

=A1.groups(uid;top(2,-logtime))

最后2个登录记录

3

=A2.new(uid,#2(1).logtime-#2(2).logtime:interval)

计算间隔

不仅如此,SPL 还对接多样性数据源,想到想不到的都支持。

更进一步,SPL 还可以进行多源混合计算,特别适合完成 T+0 查询。二进制文件适合存储不再修改的大量历史数据,而少量实时可能修改的热数据仍然存储在数据库中,如果要全量查询就要跨文件和数据库查询,这时就可以利用 SPL 的跨源计算能力和数据路由功能,根据计算需求选择对应的数据源以及跨数据源混合计算。如冷热数据分离后,基于 SPL 进行冷热数据混查,同时对上层应用透明,实现 T+0 查询。

可集成

在集成性方面,SPL 提供了标准 JDBC 和 ODBC 接口供应用调用。特别地,对于 Java 应用可以将 SPL 作为嵌入引擎集成到应用中,使得应用本身就具备基于文件的强计算能力。

JDBC 调用 SPL 代码示例:

…
Class.forName("com.esproc.jdbc.InternalDriver");

Connection conn =DriverManager.getConnection("jdbc:esproc:local://");

CallableStatement st = conn.prepareCall("{call splscript(?, ?)}");

st.setObject(1, 3000);

st.setObject(2, 5000);

ResultSet result=st.execute();
…

SPL 是解释执行的,天然支持热切换,这对 Java 体系下的应用是重大利好。基于 SPL 的数据计算逻辑编写、修改和运维都不需要重启,实时生效,开发运维更加便捷。

SPL 不仅直接提供了高效的二进制文件存储,还提供了强大的文件计算能力,在诸多对性能要求较高的分析型场景下可以发挥巨大效力,是数据库很好的替代。