内存计算指数据事先存储于内存,各步骤中间结果不落硬盘的计算方式,适合性能要求较高,并发较大的情况。
HANA、TimesTen等内存数据库可实现内存计算,但这类产品价格昂贵结构复杂实施困难,总体拥有成本较高。本文介绍的集算器同样可实现内存计算,而且结构简单实施方便,是一种轻量级内存计算引擎。
下面就来介绍一下集算器实现内存计算的一般过程。
一、 启动服务器 集算器有两种部署方式:独立部署、内嵌部署,区别首先在于启动方式有所不同。
l 独立部署 作为独立服务部署时,集算器与应用系统分别使用不同的JVM,两者可以部署在同一台机器上,也可分别部署。应用系统通常使用集算器驱动(ODBC或JDBC)访问集算服务,也可通过HTTP访问。
n Windows下启动独立服务,执行“安装目录\esProc\bin\esprocs.exe”,然后点击“启动”按钮。
n Linux下应执行“安装目录/esProc/bin/ServerConsole.sh”。 启动服务器及配置参数的细节,请参考:http://doc.raqsoft.com.cn/esproc/tutorial/fuwuqi.html。
l 内嵌部署 作为内嵌服务部署时,集算器只能与JAVA应用系统集成,两者共享JVM。应用系统通过JDBC访问内嵌的集算服务,无需特意启动。
详情参考http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html。
二、 加载数据 加载数据是指通过集算器脚本,将数据库、日志、WebService等外部数据读入内存的过程。
比如Oracle中订单表如下:
订单ID(key)
| 客户ID
| 订单日期
| 运货费
| 10248
| VINET
| 2012-07-04
| 32.38
| 10249
| TOMSP
| 2012-07-05
| 11.61
| 10250
| HANAR
| 2012-07-08
| 65.83
| 10251
| VICTE
| 2012-07-08
| 41.34
| 10252
| SUPRD
| 2012-07-09
| 51.3
| …
| …
| …
| …
| 订单明细如下:
订单ID(key)(fk)
| 产品ID(key)
| 单价
| 数量
| 10248
| 17
| 14
| 12
| 10248
| 42
| 9
| 10
| 10248
| 72
| 34
| 5
| 10249
| 14
| 18
| 9
| 10249
| 51
| 42
| 40
| …
| …
| …
| …
| 将上述两张表加载到内存,可以使用下面的集算器脚本(initData.dfx):
| A
| 1
| =connect("orcl")
| 2
| =A1.query("select 订单ID,客户ID,订单日期,运货费 from 订单").keys(订单ID)
| 3
| =A1.query@x("select 订单ID,产品ID,单价,数量 from 订单明细") .keys(订单ID,产品ID)
| 4
| =env(订单,A2)
| 5
| =env(订单明细,A3)
| A1:连接Oracle数据库。
A2-A3:执行SQL查询,分别取出订单表和订单明细表。query@x表示执行SQL后关闭连接。函数keys可建立主键,如果数据库已定义主键,则无需使用该函数。
A4-A5:将两张表常驻内存,分别命名为订单和订单明细,以便将来在业务计算时引用。函数env的作用是设置/释放全局共享变量,以便在同一个JVM下被其他算法引用,这里将内存表设为全局变量,也就是将全表数据保存在内存中,供其他算法使用,也就实现了内存计算。事实上,对于外存表、文件句柄等资源也可以用这个办法设为全局变量,使变量驻留在内存中。
脚本需要执行才能生效。
对于内嵌部署的集算服务,通常在应用系统启动时执行脚本。如果应用系统是JAVA程序,可以在程序中通过JDBC执行initData.dfx,关键代码如下:
1. com.esproc.jdbc.InternalConnection con=null;
2. try {
3. Class.forName("com.esproc.jdbc.InternalDriver");
4. con =(com.esproc.jdbc.InternalConnection)DriverManager.getConnection("jdbc:esproc:local://");
5. ResultSet rs = con.executeQuery("call initData()");
6. } catch (SQLException e){
7. out.println(e);
8. }finally{
9. if (con!=null) con.close();
10. }
| 这篇文章详细介绍了JAVA调用集算器的过程http://doc.raqsoft.com.cn/esproc/tutorial/bjavady.html
如果应用系统是JAVA WebServer,那么需要编写一个Servlet,在Servlet的init方法中通过JDBC执行initData.dfx,同时将该servlet设置为启动类,并在web.xml里进行如下配置:
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.myCom.myProject.myServlet </servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
|
对于独立部署的集算服务器,JAVA应用系统同样要用JDBC接口执行集算器脚本,用法与内嵌服务类似。区别在于脚本存放于远端,所以需要像下面这样指定服务器地址和端口:
st = con.createStatement();
st.executeQuery("=callx(\“initData.dfx\”;[\“127.0.0.1:8281\”])");
| 如果应用系统非JAVA架构,则应当使用ODBC执行集算器脚本,详见http://doc.raqsoft.com.cn/esproc/tutorial/odbcbushu.html
对于独立部署的服务器,也可以脱离应用程序,在命令行手工执行initData.dfx。这种情况下需要再写一个脚本(如runOnServer.dfx):
| A
| 1
| =callx(“initData.dfx”;[“127.0.0.1:8281”])
| 然后在命令行用esprocx.exe调用runOnServer.dfx:
D:\raqsoft64\esProc\bin>esprocx runOnServer.dfx
| Linux下用法类似,参考http://doc.raqsoft.com.cn/esproc/tutorial/minglinghang.html
三、 执行运算获得结果 数据加载到内存之后,就可以编写各种算法进行访问,执行计算并获得结果,下面举例说明:以客户ID为参数,统计该客户每年每月的订单数量。
该算法对应的Oracle中的SQL语句如下:
select to_char(订单日期,'yyyy') AS 年份,to_char(订单日期,'MM') AS 月份, count(1) AS 订单数量
from 订单
where客户ID=?
group by to_char(订单日期,'yyyy'),to_char(订单日期,'MM')
| 在集算器中,应当编写如下业务算法(algorithm_1.dfx)
| A
| 1
| =订单.select@m(客户ID==pCustID).groups(year(订单日期):年份, month(订单日期):月份;count(1):订单数量)
| 为方便调试和维护,也可以分步骤编写:
| A
| 1
| =订单.select@m(客户ID==pCustID)
| 2
| =A1.groups(year(订单日期):年份, month(订单日期):月份;
count(1):订单数量)
| A1:按客户ID过滤数据。其中,“订单”就是加载数据时定义的全局变量,pCustID是外部参数,用于指定需要统计的客户ID,函数select执行查询。@m表示并行计算,可显著提高性能。
A2:执行分组汇总,输出计算结果。集算器默认返回有表达式的最后一个单元格,也就是A2。如果要返回指定单元的值,可以用return语句
当pCustID=”VINET”时,计算结果如下:
年份
| 月份
| 订单数量
| 2012
| 7
| 3
| 2012
| 8
| 2
| 2012
| 9
| 1
| 2013
| 11
| 4
|
需要注意的是,假如多个业务计算都要对客户ID进行查询,那不妨在加载数据时把订单按客户ID排序,这样后续业务算法中就可以使用二分法进行快速查询,也就是使用select@b函数。具体实现上,initData.dfx中SQL应当改成:
=A1.query("select 订单ID,客户ID,订单日期,运货费 from 订单 order by 客户ID")
| 相应的,algorithm_1.dfx中的查询应当改成:
=订单.select@b(客户ID==pCustID)
|
执行脚本获得结果的方法,前面已经提过,下面重点说说报表,这类最常用的应用程序。
由于报表工具都有可视化设计界面,所以无需用JAVA代码调用集算器,只需将数据源配置为指向集算服务,在报表工具中以存储过程的形式调用集算器脚本。
对于内嵌部署的集算服务器,调用语句如下:
call algorithm_1(”VINET”)
| 由于本例中算法非常简单,所以事实上可以不用编写独立的dfx脚本,而是在报表中直接以SQL方式书写表达式:
=订单.select@m(客户ID==”VINET”).groups(year(订单日期):年份, month(订单日期):月份;count(1):订单数量)
| 对于独立部署的集算服务器,远程调用语句如下:
=callx(“algorithm_1.dfx”,”VINET”;[“127.0.0.1:8281”])
|
有时,需要在内存进行的业务算法较少,而web.xml不方便添加启动类,这时可以在业务算法中调用初始化脚本,达到自动初始化的效果,同时也省去编写servlet的过程。具体脚本如下:
| A
| B
| 1
| if !ifv(订单)
| =call("initData.dfx")
| 2
| =订单.select@m(客户ID==pCustID)
|
| 3
| =A2.groups(year(订单日期):年份, month(订单日期):月份;
count(1):订单数量)
|
| A1-B1:判断是否存在全局变量“订单明细”,如果不存在,则执行初始化数据脚本initData.dfx。
A2-A3:继续执行原算法。
四、 引用思维 前面例子用到了select函数,这个函数的作用与SQL的where语句类似,都可进行条件查询,但两者的底层原理大不相同。where语句每次都会复制一遍数据,生成新的结果集;而select函数只是引用原来的记录指针,并不会复制数据。以按客户查询订单为例,引用和复制的区别如下图所示:
可以看到,集算器由于采用了引用机制,所以计算结果占用空间更小,计算性能更高(分配内存更快)。此外,对于上述计算结果还可再次进行查询,集算器中新结果集同样引用最初的记录,而SQL就要复制出很多新记录。
除了查询之外,还有很多集算器算法都采用了引用思维,比如排序、集合交并补、关联、归并。
五、 常用计算 回顾前面案例,可以看到集算器语句和SQL语句存在如下的对应关系:
计算
| SQL
| 集算器
| 查询
| select
| select
| 条件
| Where….订单.客户ID=?
| 订单ID.客户ID==pCustID
| 分组汇总
| group by
| groups
| 日期函数
| to_char(订单日期,'yyyy')
| year(订单日期)
| 别名
| AS 年份
| :年份
| 事实上,集算器支持完善的结构化数据算法,比如:
l GROUP BY…HAVING
| A
|
| 1
| =订单.groups(year(订单日期):年份;count(1):订单数量).select(订单数量>300)
|
|
l ORDER BY…ASC/DESC
| A
|
| 1
| =订单.sort(客户ID,-订单日期)
| /排序只是变换了记录指针的次序,并没有复制记录
| l DISTINCT
| A
|
| 1
| =订单.id(year(订单日期))
| /取唯一值
| 2
| =A1.(客户ID)
| /所有出现值
| 3
| =订单.([ year(订单日期),客户ID])
| /组合的所有出现值
| l UNION/UNION ALL/INTERSECT/MINUS
| A
|
| 1
| =订单.select(运货费>100)
|
| 2
| =订单.select([2011,2012].pos(year(订单日期))
|
| 3
| =A2|A3
| /UNION ALL
| 4
| =A2&A3
| /UNION
| 5
| =A2^A3
| /INTERSECTION
| 6
| =A2\A3
| /DIFFERENCE
| 与SQL的交并补不同,集算器只是组合记录指针,并不会复制记录。 l SELECT … FROM (SELECT …)
| A
|
| 1
| =订单.select(订单日期>date("2010-01-01"))
| /执行查询
| 2
| =A1.count()
| /对结果集再统计
| l SELECT (SELECT … FROM) FROM
| A
|
| 1
| =订单.new(订单ID,客户.select(客户ID==订单.客户ID).客户名)
| /客户表和订单表都是全局变量
| l CURSOR/FETCH 游标有两种用法,其一是外部JAVA程序调用集算器,集算器返回游标,比如下面脚本:
| A
|
| 1
| =订单.select(订单日期>=date("2010-01-01")).cursor()
|
| JAVA获得游标后可继续处理,与JDBC访问游标的方法相同。 其二,在集算器内部使用游标,遍历并完成计算。比如下面脚本:
| A
| B
|
|
| 1
| =订单.cursor()
|
|
|
| 2
| for A1,100
| =A2.select(订单日期>=date("2010-01-01"))
| /每次取100条运算
|
| 3
|
| …
|
|
|
集算器适合解决复杂业务逻辑的计算,但考虑到简单算法占大多数,而很多程序员习惯使用SQL语句,所以集算器也支持所谓“简单SQL”的语法。比如algorithm_1.dfx也可写作:
| A
| 1
| $() select year(订单日期) AS 年份,month(订单日期) AS 月份,count(1) AS 订单数量
From {订单}
where订单.客户ID='VINET'
group by year(订单日期),month(订单日期)
| 上述脚本通用于任意SQL,$()表示执行默认数据源(集算器)的SQL语句,如果指定数据源名称比如$(orcl),则可以执行相应数据库(数据源名称是orcl的Oracle数据库)的SQL语句。
from {}语句可从任意集算器表达式取数,比如:from {订单.groups(year(订单日期):年份;count(1):订单数量)}
from 也可从文件或excel取数,比如:from d:/emp.xlsx
简单SQL同样支持join…on…语句,但由于SQL语句(指任意RDB)在关联算法上性能较差,因此不建议轻易使用。对于关联运算,集算器有专门的高性能实现方法,后续章节会有介绍。
简单SQL的详情可以参考:http://doc.raqsoft.com.cn/esproc/func/dbquerysql.html#db_sql_
六、 有序引用 SQL基于无序集合做运算,不能直接用序号取数,只能临时生成序号,效率低且用法繁琐。集算器与SQL体系不同,能够基于有序集合运算,可以直接用序号取数。例如:
| A
|
| 1
| =订单.sort(订单日期)
| /如果加载时已排序,这步可省略
| 2
| =A1.m(1).订单ID
| /第一条
| 3
| =A1.m(-1).订单ID
| /最后一条
| 4
| =A1.m(to(3,5))
| /第3-5条
| 函数m()可按指定序号获取成员,参数为负表示倒序。参数也可以是集合,比如m([3,4,5])。而利用函数to()可按起止序号生成集合,to(3,5)=[3,4,5]。
前面例子提到过二分法查询select@b,其实已经利用了集算器有序访问的特点。
有时候我们想取前 N名,常规的思路就是先排序,再按位置取前N个成员,集算器脚本如下:
=订单.sort(订单日期).m(to(100))
| 对应SQL写法如下:
select top(100) * from 订单 order by 订单日期 --MSSQL
select * from (select * from 订单 order by 订单日期) where rownum<=100 --Oracle
| 但上述常规思路要对数据集大排序,运算效率很低。除了常规思路,集算器还有更高效的实现方法:使用函数top。
函数top只排序出订单日期最早的N条记录,然后中断排序立刻返回,而不是常规思路那样进行全量排序。由于底层模型的限制,SQL不支持这种高性能算法。
函数top还可应用于计算列,比如拟对订单采取新的运货费规则,求新规则下运货费最大的前100条订单,而新规则是:如果原运货费大于等于1000,则运货费打八折。
集算器脚本为:
=订单.top(-100;if(运货费>=1000,运货费*0.8,运货费))
|
|