不用 SQL 的数据仓库
当前绝大部分数据仓库都会采用 SQL,SQL 发展了几十年已经成为数据库界的标准语言,用户量巨大,所以支持 SQL 对于数据仓库来讲也是很正常的。但是,在当代大数据背景下,业务复杂度节节攀升,在以计算为主要任务的数据仓库场景下,SQL 似乎越来越不够用了。典型表现是一些数据仓库开始集成 Python 的能力,将 Python 这样的非 SQL 语言融入到数据仓库中。且不论两种风格迥异的开发语言是否能很好融合互补,单看这样的趋势已经足够表现出业界对 SQL 能力的一些质疑。
我们这里要介绍一种非 SQL 型数据仓库 esProc,由于没有使用 SQL 作为查询语言(而是 SPL),可以暂且将其看成一种新型数据仓库。
为什么 esProc 不再使用 SQL 了呢?
要回答这个问题,我们还要继续为什么数据仓库有了 SQL 还要引入 Python 的话题。引入 Python 要解决哪些问题呢?
我们知道,SQL 对过程计算的支持很差,即使有了 CTE 语法在描述复杂计算时仍然十分复杂,经常要嵌套多层且反复关联。同时,SQL 的集合是无序的,非常不擅长有序计算,涉及序运算经常会写得很繁琐甚至很难写出来。SQL 本身的语言特性注定不善于完成某些复杂计算,而这类计算在数据仓库类的数据分析型场景中并不少见。比如在计算电商漏斗分析(浏览商品、加购物车、下单、付款等多步的用户流失率)的场景用 SQL 就很难实现。而这样涉及多个前后步骤、结果反复使用的场景用 Python 这样支持分步和有序运算的语言实现则要简单得多。
也就是说,SQL 的能力是缺失的。
但是,如果因此引入 Python 等第三方语言来弥补能力上的缺失,又会带来技术栈的复杂问题。还是那句话,Python 能不能弥补这些缺失尚且不论,两者能否很好融合也不管,但是多种技术风格带来的系统复杂度上升,从而导致高开发成本和运维成本是跑不掉的了。
除了能力方面的欠缺,采用 SQL 的关系数据库还存在封闭性的问题。
数据库的作用主要是交易(TP),这就需要众多约束保证数据一致等,因此数据入库时只有满足条件才能进来,进来才能使用。这种只有装入内部才能使用的特性我们称之为封闭性。数据仓库是基于数据库发展而来的,封闭性的特点也继承了下来。
封闭性对于 TP 业务是非常重要的。但是,对于以分析计算为主的 AP 业务,这个封闭性不仅毫无意义,甚至还有很大缺点。封闭性要求数据进入内部才能使用,这会导致多个数据库之间的数据无法进行任意组合和运算,这就极大限制了数据仓库的应用场景。
不仅如此,现代数据应用的数据源十分广泛,除了不同数据库,经常会面对五花八门的数据来源和类型。封闭的 SQL 数据库不能针对库外的数据开放其计算能力,就只能把外部数据先导入才能计算。这会增加一步 ETL 动作,增大工作量并加大数据库负担的同时,还丧失了数据的实时性。这些外部数据的格式常常不规范,导入进有强约束的数据库并不是一件很容易的事。而且,即使做 ETL 也要先把未整理的数据先入库才能利用数据库的计算能力,结果把 ETL 做成 ELT,加大数据库负担。
封闭性还不方便用户自由实施空间换时间。我们知道,存储资源相对于计算资源要便宜得多,如果我们针对不同计算目标把数据以多种方式冗余存储,就可能获得更好的查询体验。但是,SQL 要用表来存储数据,自建表太多又会导致元数据变大,严重增加运维成本。表数量太多还会导致数据仓库出现容量和性能问题,面临扩容压力。很多大型机构的中央数据仓库中会有成千上万的中间表,积累多年而不敢删除,数据库容量、性能、运维压力都很大。
SQL 在性能方面也不理想。
我们知道,SQL 的执行效率取决于数据库优化引擎的优化程度,好的数据库会根据 SQL 的计算目标(而非字面意思)选择更高效的执行方式。但这种自动优化机制仅对简单的情况下有效,一旦 SQL 变得稍复杂优化引擎就不起作用了,只能根据 SQL 的字面表达去执行,结果性能陡降。像前面提到的漏斗分析,有人用 SQL 写出 3 步的漏斗计算到数据库执行,结果性能低到不可用的程度。关于 SQL 性能不佳的情况相信诸位在实际业务中并不少见,很多跑批场景一个 SQL 跑个把小时的情况比比皆是,这些都是 SQL 性能不高引起的。
SQL 能力不足,加上封闭性又导致使用沉重,性能也不高。这是当前 SQL 型数据仓库面临的主要问题。
在 SQL 的基础上引入 Python 能力也不能解决问题。除了我们上面提到的技术栈复杂问题会导致使用和运维成本过高外,Python 还没法获得高性能。
本身 Python 对大数据计算的支持就比较差,对于超出内存容量的数据计算并没有提供相应的外存计算类型(如游标),导致处理大数据异常复杂。同时,Python 并不支持真正的多线程并行,Python 的并行是伪并行,对于 CPU 来说就是串行,甚至比串行还慢,难以充分利用现代 CPU 多核的优势。
更主要的是,由于 Python 要基于 SQL 数据库表进行运算,这些表(存储)为数据库私有外界无法干预。但很多高性能计算是需要根据计算目标来组织数据的,如将数据按照关联字段排序就可以使用更高效的有序归并算法,存储无法干预就不能实现这些算法,性能自然也无法得到保证。此外,Python 读取数据库数据还涉及 IO 成本,这些都会导致计算性能不高。
看来要解决 SQL 的这些问题就只能抛弃 SQL 了。
其实非 SQL 的计算技术一直都有,比较典型的代表是 Spark。Spark 诞生之初使用 Scala 作为编程语言,再借助其大规模的分布式计算能力大有革命 SQL 的态势。不过很遗憾,随着使用的深入我们发现 Spark 并没有取代 SQL 的能力(实现繁琐、性能低),加之 Scala 的使用难度,让 Spark 又不得不回到 SQL 的怀抱。
接下来我们来看看非 SQL 数据仓库 esProc 的能力,会有哪些不同。
esProc SPL
esProc 数据仓库的形式化语言是 SPL,并没有使用业界普遍采用的 SQL。原因就在于 SQL 能力缺失、体系封闭、性能不高,而 SPL 能很好解决这些问题。
能力完备
首先,SPL 天然支持过程计算。
过程计算可以有效降低复杂业务的实现难度,同样的 100 行代码,分成 100 个语句还是只有 1 个语句,其复杂度完全不是一个层面的。SQL 虽然在 CTE 语法和存储过程的支持下具备了一定程度的过程化,但仍远远不够。SPL 在这方面提供了天然支持,将复杂计算分解成多步从而降低实现难度。
其次,SPL 提供了更丰富的数据类型和运算支持。
相比 SQL 没有显著的记录数据类型(单条记录会被 SQL 作为只有一条记录的临时表处理,也就是个单成员的集合),SPL 提供了专业的结构化数据对象序表,并在序表的基础上提供了丰富的计算类库,从而使得 SPL 具备了完善且简单的结构化数据处理能力。
比如部分常规计算:
Orders.sort(Amount) // 排序
Orders.select(Amount*Quantity>3000 && like(Client,"*S*")) // 过滤
Orders.groups(Client; sum(Amount)) // 分组
Orders.id(Client) // 去重
join(Orders:o,SellerId ; Employees:e,EId) // 连接
有了过程化和序表的支持,SPL 就可以完成更加丰富的运算。比如对有序运算的支持 SPL 就更为直接和彻底。还有分组运算,SPL 可以保留分组子集,即集合的集合,这样可以很方便完成我们分组后对分组结果的进一步操作。相比之下,SQL 没有显式的集合数据类型,无法返回集合的集合这类数据,不能实现独立的分组,就只能强迫分组和聚合作为一个整体来计算了。
此外,SPL 对聚合运算也有新的理解,聚合结果除了常见的单值 SUM、COUNT、MAX、MIN 等之外,也可以是个集合。比如常常出现的 TOPN 计算,SPL 也看作和 SUM、COUNT 一样的聚合计算,既可以针对全集也可以针对分组子集。
其实 SPL 还有很多特性不仅比 SQL 更加完善,相对 Python 也更加丰富。像离散性可以让构成数据表的记录游离于数据表外独立反复使用;普遍集合支持任何数据构成的集合并参与运算;连接运算区分了三种不同连接类型可以因地制宜;……。
有了这些完备的计算能力,不仅代码编写简单,更不需要借助其他计算能力,技术栈简单,在一个体系内就可以搞定所有问题。
体系开放
不同于 SQL 数据库需要数据先入库再计算(封闭性),SPL 面对多样性数据源时可以直接计算,具备良好的开放性。
SPL 没有传统数据仓库中“库“的概念,也没有元数据概念,更没有约束。任何可访问到的数据源都可以看作 SPL 的数据,并可被直接计算。计算前不需要先“入库”,计算后也可以用接口写出目标数据源中,不需要刻意“出库”。
SPL 对常见的数据源都封装了访问接口,如各种关系数据库(JDBC 数据源)、MongoDB、HBase、HDFS、HTTP/Restful、…,以及 SalesForces、SAP BW、…。这些数据源在逻辑上地位基本相同,访问后都可以单独或混合计算,不同之处仅仅在于访问接口以及不同接口表现出来的性能。
文件存储
在数据存储方面,SPL 与传统数据仓库也有很大不同。
SPL没有元数据,直接采用文件存储,可以使用任意开放文件类型,SPL 为了保证计算性能还设计了专门的二进制文件格式。
目前 SPL 提供了两种文件类型:集文件和组表。集文件采用了压缩技术(占用空间更小读取更快),存储了数据类型(无需解析数据类型读取更快),支持可追加数据的倍增分段机制,利用分段策略很容易实现并行计算,保证计算性能。组表支持列式存储,在参与计算的列数(字段)较少时会有巨大优势。组表上还实现了索引,同时支持倍增分段,这样不仅能享受到列存的优势,也更容易并行提升计算性能。
存储和计算不再绑定还很方便实现存算分离,进而实施弹性计算,也更易于云化。
文件存储的成本更低,在 AP 类计算场景下用户可以随意设计空间换时间的方案,无非就是多存几个文件,即使冗余数据文件多到上万(现代文件系统处理这个规模的文件数据很轻松)也完全没有负担。而且使用文件系统的树状结构很容易分门别类管理这些数据文件,运维成本更低。
延伸阅读: 跑在文件系统上的数据仓库
高性能
基于灵活的文件存储,我们就可以根据计算目标灵活设计数据组织(存储)形式以实现高性能。除了高性能存储支持外,SPL 还提供了诸多大数据与高性能计算机制和算法支持。
SPL 首先在运算能力上提供了游标计算来应对超出内存容量的大数据计算。
=file("orders.txt").cursor@t(area,amount).groups(area;sum(amount):amount)
同时还为内外存计算都提供了并行计算支持。简单增加一个 @m 选项就可以实现并行充分利用 CPU 多核的能力,非常方便:
=file("orders.txt").cursor@tm(area,amount;4).groups(area;sum(amount):amount)
除了游标和并行计算外,SPL 还内置了很多高性能算法。像 TOPN 运算,SPL 把这种运算和普通的聚合运算同样看待后,写出来的取前 N 名的语句中就不会有排序动作,执行效率因此更高。
类似的,SPL 还提供了很多这样的高性能算法。包括:
- 内存计算类的二分法、序号定位、位置索引、哈希索引、多层序号定位、……
- 外存查找类的二分法、哈希索引、排序索引、带值索引、全文检索、……
- 遍历计算类的延迟游标、遍历复用、多路并行游标、有序分组汇总、序号分组、……
- 外键关联类的外键地址化、外键序号化、索引复用、对位序列、单边分堆、……
- 归并与连接类的有序归并、分段归并、关联定位、附表、……
- 多维分析类的部分预汇总、时间段预汇总、冗余排序、布尔维序列、标签位维度、……
- 集群计算类的集群复组表、复写维表、分段维表、冗余与备胎容错、负载均衡、……
有了高性能文件存储和高性能算法的支持,esProc 在实际应用中经常获得比传统 SQL 数据仓库几倍到几十倍,有的甚至达到上千倍的性能提升。
看起来,相对于 SQL,SPL 就没有劣势?
这当然不可能,这世界上没有全面都好的东西。
SQL 经过几十年的积累发展,很多数据库都拥有很强的优化引擎。对于适合用 SQL 完成的简单场景运算,可以将普通程序员写出来的慢语句优化出较好的性能,从这个意义上讲,对程序员的要求相对较低。某些场景(比如多维分析)已经被优化多年,某些 SQL 引擎也可以跑出相当好的极致性能。
相比之下,SPL 没有做多少自动优化的功能,要跑出高性能,几乎全靠程序员写出低复杂度的代码。程序员需要经过一定的培训和练习来熟悉 SPL 的理念和库函数,多一个上手的门槛。而且 SPL 目前是用 Java 实现的,虽然获得了兼容性好、移植性强以及易于云化的好处,但也受限于 JVM 而无法充分利用 CPU 和内存。某些简单运算场景下的性能还是赶不上被充分优化的 SQL 引擎。
数据仓库不一定非要 SQL,还有 SPL。
英文版