跨库运算的手段

出于各种原因,完整数据有时会存储在多个数据库中,这种情况就要进行跨库的混合运算。跨库运算的手段种类多样原理不同,下面将对比这些手段,尤其是代码写法、部署配置、性能方面的差异。

DBLink

DBLink是Oracle提供的跨库运算手段,可以直接使用Oracle SQL语法,代码因此简单易懂。

比如:订单表Orders在MySQL库里,需要先汇总出每个SellerId的销售额,再和Oracle库的员工表Employees关联,将销售员的名字和销售额对应起来。

只需编写SQL:select e.name, o. subtotal from Employees e, (select SellerId, sum(Amount) subtotal from Orders@mysql1 group by SellerId) o where o.Sellerid=e.EId

SQL中的mysql1为MySQL的数据源名,表和数据源的关系可以简单地用@来表示。

DBLink的代码虽然简单,但配置起来却很复杂,不仅要安装不同厂商的组件,还要注意版本之间的搭配,之后还需在多处进行配置,除了少数配置项有界面,大多数配置项只能手工修改文件。主要配置步骤有:

1.      分别配置好Oracle Client和 MySQL ODBC,确保可单独访问Oracle和MySQL。

2.      安装MySQL数据库网关,即Oracle Database Gateway for ODBC。

3.      启动Oracle Heterogeneous Service(HA)及相关的Agent。

4.      在Oracle中新建异构数据库ODBC配置文件,在该文件中填入MySQL ODBC相关信息。

5.      在listener.ora和tnsnames.ora里分别配置MySQL数据源,两者须一致。

6.      使用DDL命令建立指向MySQL的DBlink(比如叫mysql1)。

DBLink的性能较差,主要是因为不能很好地利用其它数据库的计算能力。可能是出于语法兼容的考虑,Oracle会把涉及远程表的计算翻译为两部分:在远程服务器执行的SQL,即无函数的条件查询、计算列、取全部数据这三种;其他绝大部分计算都转为本地执行,包括带函数的查询或计算列、排序、分组汇总等等。这样一来,有很多在远端可以很快完成的计算,都不得不转移到本地来缓慢执行,比如分组汇总这种结果比源数据小的计算,也要额外花费传输成本,把源数据取到本地再算。

上面的例子中,select SellerId, sum(Amount) subtotal from Orders@mysql1 group by SellerId是涉及远程表的计算。如果在远程完成这个计算,只需把很小的结果集传到本地就可以,但Oracle会把它翻译为2部分,远程实际执行的是select SellerId, Amount from Orders,也就是取全部源数据;从远程取来数据存为本地临时表(假设叫$Orders)之后,本地再执行select SellerId, sum(Amount) subtotal from $Orders group by SellerId。

除了Oracle之外,DB2(Federated Database)和MSSQL(Linked Server)也有类似的跨库计算手段,由于原理相近,因此优缺点也是类似的。

Scala

Scala的设计初衷虽然是通用开发语言,但实际上多应用于结构化数据计算。Scala提供了三种数据类型( DataFrame、DataSet、RDD)和两种计算语法(DSL、SQL),都可以实现跨库计算,且互相可以转换。比如,前面提到的计算可以写成:

package test
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.DataFrame
object testJoin {
  def   main(args: Array[String]): Unit = {
    //create   spark session on local
    val spark   = SparkSession.builder()
         .master("local")
         .appName("example")
         .getOrCreate()
    //load Employees   from oracle
    val Employees   = spark.read
         .format("jdbc")
         .option("url",   "jdbc:oracle:thin:@127.0.0.1:1521:ORCL")
         .option("query",   "select EId,Name from employees")
         .option("user",   "scott")
         .option("password",   "password")
         .load()  
    //load Orders   group from MySql
    val O =   spark.read
         .format("jdbc")
         .option("url",   "jdbc:mysql://127.0.0.1:3306/mysql1")
         .option("query",   "select SellerId, sum(Amount) subtotal from Orders group by SellerId")
         .option("user",   "root")
         .option("password",   "password")
         .load()  
    //join using   dataframe
    val   join=O.join(Employees,O("SellerId")===Employees("EId"),"Inner")
         .select("Name","subtotal")
    join.show()
  }
}

可以看出,Spark整体代码虽然较长,但核心用于计算的代码却很简单。另外,Spark底层是JAVA,二者互相调用的代码也很简单。

Scala跨库计算的配置比Oracle容易得多,只需将数据库驱动Jar包加入类路径。从这一点可以看出,Scala的跨库计算更加开放更加专业,而Oracle的跨库计算封闭且不成熟。

相对来讲,Scala性能表现更好,因为它可以利用数据库的计算能力。从上面代码可以看出来,涉及远程表的计算可以真正在远程执行,比如MySQL端执行的是select SellerId, sum(Amount) subtotal from Orders group by SellerId。如此便只需返回少量的数据集,而不是全量表数据,传输速度因此显著加快。

上面的代码是全内存计算,只适合数据量较小的情况。如果数据库返回的数据量较大,还应开启内外存自动交换功能,具体代码是O. persist()。应该注意的是,Scala鼓励内存计算,在外存计算方面并不成熟,很容易发生故障,因此不推荐这种用法。

Calcite

Calcite是Apache软件基金会旗下的开源项目,其目标是“one size fits all”,即一种查询引擎适应所有计算平台和数据源。Calcite支持多种风格的SQL(并非实际执行的方言SQL),比如,用MySQL风格实现前面的跨库计算:

package Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class testCalcite {
    public   static void main(String[] args)throws Exception {
        Properties config = new Properties();
        config.put("model", "d:\\ds.json");
//using   built-in lexical policy of the MySQL style
        config.put("lex", "MYSQL");
    String   sql ="select e.name, o. subtotal from orcl.Employees e, (select   SellerId, sum(Amount) subtotal from mysql1.Orders group by SellerId) o where   o.Sellerid=e.EId";
Connection   con = DriverManager.getConnection("jdbc:calcite:", config));
Statement   stmt = con.createStatement();
ResultSet   rs = stmt.executeQuery(sql));
if(con!=   null) con.close();
    }
}

代码中的orcl和mysql1是数据源名,分别指向Oracle和MySQL。可以看到,Calcite语法简单易懂,和普通JDBC在表面上没有太大差别。

Calcite的配置也很简单,除了将数据库jar包加入类路径之外,只需编写一个数据源文件,即上面代码中的ds.json。

在计算性能方面,Calcite和Oracle类似,都不能真正利用异构数据库的计算能力,因此表现较差。Calcite的风格化SQL语句经过层层翻译,最后会分为2部分,一部分是在数据库上执行的计算方言(比如MySQL的SQL),另一部分是获取数据库的计算结果后,在Calcite执行的本地计算。计算方言的翻译工作难度很大,需要兼容不同的数据库,只能翻译得简单些,比如group by只翻译成取记录,而不是实质的分组汇总,这就浪费了数据库的计算能力。本地计算的翻译工作相对受控,也没有兼容性的顾虑,可以尽量多翻译一些,比如真正的分组汇总。

Calcite还有一个缺点:只能做全内存计算,数据量大的容易内存溢出。

集算器 SPL

集算器 SPL是专业的开源结构化计算引擎,也具有鲜明的one size fits all特性,但集算器 SPL更“轻”, 层次更少,语法更简单。比如,前面提到的计算可以写成脚本文件(cross.dfx):

A
1 =orcl.query("select EId,Name from employees")
2 =mysql1.query("select SellerId, sum(Amount) subtotal from Orders group by   SellerId")
3 =join(A1:O,SellerId; A2:E,EId)
4 =A3.new(O.Name,E.subtotal)

上述脚本可在IDE中执行或调试,也可以JDBC的形式嵌入JAVA,如下:

package Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class test1 {
public static void main(String[]   args)throws Exception {
Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection   =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement =   connection.createStatement();
ResultSet result = statement.executeQuery("call   cross()");
if(connection != null)   connection.close();
}
}

简单计算也可直接将计算语句写在JAVA代码中,而无须脚本文件。比如下面这样:

ResultSet result = statement.executeQuery("=join(orcl.query(\"select EId,Name from   employees\"):O,SellerId; mysql1.query(\"select SellerId,   sum(Amount) subtotal from Orders group by   SellerId\"):E,EId).new(O.Name,E.subtotal)");

在计算性能方面,SPL与Scala类似,都可以很好地利用数据库的计算能力,因此计算性能较佳。比如上面代码中,MySQL只需返回少量的数据集,而不是全量表数据。

如果数据量较大,还可改为游标进行计算。游标是SPL专门为大数据量计算设计的,成熟稳定。比如按Employees表的Dept字段分组,对Orders表的Amount字段汇总:

A
1 =orcl.cursor("select EId, Dept from   Employees order by EId")
2 =mysql1.cursor("select SellerId, Amount from   Orders order by SellerId")
3 =joinx(A2:O,SellerId; A1:E,EId)
4 =A3.groups(E.Dept;sum(O.Amount))

上面代码还利用了有序归并进行关联,性能会更好。

对于分库等特殊的跨库计算,SPL还支持并行计算语法,可显著提升性能:

A B C
1 =[connect("mysql1"),connect("mysql2"),connect("mysql3")] /连接多个mysql
2 select * from orders where   amount>=10000 /SQL
3 fork A1 =A3.query(A2) /并行执行SQL
4 =A3.conj() /合并结果

集算器跨库计算的配置较为简单,只需将数据库驱动Jar包加入类路径,并设置数据库的数据源名。

通过上述比较可以看出:在语法表达方面,各种跨库运算手段都比较简单,在配置方面,除DBLink外都比较方便;在计算性能上,Scala和SPL更加优秀;在计算丰富和成熟性上,SPL更胜一筹。