技术开发 频道

达梦数据库7.0版新特性之批量处理技术

  【IT168 技术】在前两篇文章中,我们为大家介绍了达梦7.0数据库的两个新特性:水平分区动态性能监视。本篇文章里,我们继续为大家介绍达梦7.0新特性——批量处理技术。

  数据库系统对性能的关注,一般停留在系统IO管理、查询优化器、并发执行等方面,很少关注执行的迭代模型与表达式计算所消耗的时间。

  数据库的执行计划通常以树型的数据结构表示,树中的节点被称为操作符,每个操作符完成一个特定的功能。树的叶节点用于进行数据扫描,从物理存储位置抽取记录,然后向上返回给上层节点。上层节点对记录进行加工,继续向上返回,或者再次向下要求新的记录。这就是数据库引擎典型的执行方式。这个过程中,通常不断地在各个操作符间跳来跳去,消耗大量的CPU时间。而表达式计算也是一个影响性能的重要因素,数据库系统通常把表达式翻译成内部中间语言,然后用一个虚拟机来解释执行。执行的过程本来就比较慢,如果处理的记录非常多,则解释执行的次数和CPU时间就非常可观。

  达梦数据库针对执行的迭代模型和表达式计算两个方面,重新设计了执行器,对一次一条记录的迭代模型修改为一次一批数据,单条记录的表达式计算修改为数组运算,希望能借助这两个改进,实现性能的提升。这里用TPC-H Q1为例,来检验一下新版达梦执行器的改进效果。

  TPC-H是一个决策支持的基准测试标准,它衡量数据库管理系统对OLAP应用或者说即席复杂查询的处理能力。这类查询通常需要大量的全表扫描,对海量数据进行过滤,分类处理,而且一般不能借助索引定位来加速查询。TPC-H标准中,定义了22个查询语句。其中第一个查询Q1是这样的:

select     l_returnflag,
l_linestatus,
sum(l_quantity) as sum_qty,
sum(l_extendedprice) as sum_base_price,
sum(l_extendedprice*(1-l_discount)) as sum_disc_price,
sum(l_extendedprice*(1-l_discount)*(1+l_tax)) as sum_charge,
avg(l_quantity) as avg_qty,
avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc,
count(*) as count_order
from lineitem
where l_shipdate
<= date' 1998-12-01' - interval '90' day
group by l_returnflag, l_linestatus
order by l_returnflag, l_linestatus;

   为了检验达梦的处理性能,用1G规模的TPC-H数据进行验证。在1G规模下, LINEITEM表有6001215行,其中 (l_shipdate <= date' 1998-12-01' - interval '90' day)只能过滤掉大约不到2%的数据。

  在装载完数据后,用disql先看一下LINEITEM的定义和记录数:

SQL>select tabledef('SYSDBA', 'LINEITEM');
select tabledef('SYSDBA', 'LINEITEM');
--------------------------------------------------------
CREATE TABLE "SYSDBA"."LINEITEM"
(
"L_ORDERKEY"
INT NOT NULL,
"L_PARTKEY"
INT,
"L_SUPPKEY"
INT,
"L_LINENUMBER"
INT NOT NULL,
"L_QUANTITY"
FLOAT,
"L_EXTENDEDPRICE"
FLOAT,
"L_DISCOUNT"
FLOAT,
"L_TAX"
FLOAT,
"L_RETURNFLAG"
CHAR(1),
"L_LINESTATUS"
CHAR(1),
"L_SHIPDATE" DATE,
"L_COMMITDATE" DATE,
"L_RECEIPTDATE" DATE,
"L_SHIPINSTRUCT"
CHAR(25),
"L_SHIPMODE"
CHAR(10),
"L_COMMENT"
VARCHAR(44),
CLUSTER
PRIMARY KEY("L_ORDERKEY", "L_LINENUMBER"));

1 rows got
time used:
3.882(ms) clock tick:1611336.
SQL
>select count(*) from lineitem;
select count(*) from lineitem;
---------------------
              6001215

1 rows got
time used:
4.874(ms) clock tick:2208198.
SQL
>

   测试机器是一台普通的兼容机,CPU为INTEL E7400,主频为2.8Ghz,内存为2G,操作系统是Windows XP。

  第一次执行运行时间为11899ms。考虑到第一次运行,数据都在硬盘上,需要做I/O操作, 因此速度比较慢,第二次的执行如下:

A            F            3.7734107000E+007  5.6586554401E+010
5.3758257135E+010  5.5909065223E+010  2.5522005853E+001  3.8273129735E+004
4.9985295838E-002                1478493
N            F            
9.9141700000E+005  1.4875047104E+009
1.4130821681E+009  1.4696492232E+009  2.5516471921E+001  3.8284467761E+004
5.0093426674E-002                  38854
N            O            
7.4476040000E+007  1.1170172970E+011
1.0611823031E+011  1.1036704387E+011  2.5502226770E+001  3.8249117989E+004
4.9996586054E-002                2920374
R            F            
3.7719753000E+007  5.6568041381E+010
5.3741292685E+010  5.5889619120E+010  2.5505793613E+001  3.8250854626E+004
5.0009405830E-002                1478870
4 rows got
time used:
1554.677(ms) clock tick:58122033.
SQL
>

   我们看到第二遍的执行为1554ms。

  Q1的执行计划比较简单,使用explain 命令,得到下面的计划:

#NSET2: [0, 0, 0]
  #PRJT2:
[0, 0, 0]; exp_num(10), is_atom(FALSE)
    #SORT2:
[0, 0, 0]; key_num(2), is_distinct(FALSE)
      #HAGR2:
[0, 0, 0]; grp_num(2), sfun_num(8)
        #PRJT2:
[1548, 2280461, 0]; exp_num(7), is_atom(FALSE)
          #SLCT2:
[1548, 2280461, 0]; LINEITEM.L_SHIPDATE <= var3
            #CSCN2:
[1548, 0, 0]; INDEX33555477(LINEITEM)
time used:
68.545(ms) clock tick:16177761.
SQL
>

   这个计划没有分支,从下到上是一个线性链表,依次为:

  ■ CSCN:聚集索引扫描
  ■ SLCT:选择, 对数据进行过滤,过滤条件为LINEITEM.L_SHIPDATE <= var3
  ■ PRJT:投影, 计算7个表达式
  ■ HAGR:HASH 分组
  ■ SORT:排序

  最后是PRJT和NSET,分别处理最后的投影并进行结果集合的收集。

  达梦系统在默认情况下,打开了监控开关,可以直接进行分析。在动态表V$SQL_HISTORY中,记录了每一次查询的信息。先查出Q1第二次查的执行ID:

Select top 1 exec_id, top_sql_text from v$sql_history
where top_sql_text like%lineitem%’;

  确认其exec_id 为3。

  在动态表V$SQL_NODE_HISTORY和V$SQL_NODE_NAME, 可以得到exec_id为3的每一个操作符号的执行次数和时间,时间单位为微秒。

    SQL>    Select a.name, b.n_enter, b.time_used
2       From v$sql_node_name a, v$sql_node_history b
3       Where a.type$ = b.type$ and b.exec_id = 3
4       Order by 3;
        
Select a.name, b.n_enter, b.time_used
        
From v$sql_node_name a, v$sql_node_history b
        
Where a.type$ = b.type$ and b.exec_id = 3
        
Order by 3;
NAME                         N_ENTER             TIME_USED
------------------------ ----------- ---------------------
DLCK                               2                     3
PRJT2                              
4                     5
CTX                                
1                    33
NSET2                              
3                    74
SORT2                              
4                   240
SLCT2                          
12006                137613
PRJT2                          
12006                164071
HAGR2                          
6005                349937
CSCN2                          
6003                881910
9 rows got
time used:
79.808(ms) clock tick:23274343.
SQL
>

   查询结果里,多了 DLCK和CTX操作符号,是系统自动添加的字典封锁和上下文切换操作符,不用管它,重要的是表中后面的四个。我们观察到,用于扫描LINEITEM的CSCN运行了6003次,总共执行时间为882毫秒,分组运算是执行了6005次,消耗了350毫秒,而选择与投影则都执行了12006次,各自消耗了100多个毫秒。

  显然,最慢的是CSCN操作符,它的任务是把6001215条物理记录解析成系统执行器能处理的内部数据。由于在这里使用了标准的行存储模式,因此物理记录的解析消耗了大部分时间,如果能提升解析的速度,则应该可以提升性能。次慢是HASH分组运算。表达式计算量最大的选择与投影,则性能相当好。

  为了总体评估达梦的Q1性能,在同样环境下对比测试了其他系统。测试时,多次执行Q1, 以消除IO的影响,ORACLE完成Q1需要约9秒,SQL SERVER 2005大约是12秒,而PostgreSQL则更慢,大概要16秒。也没有发现在这些系统中,有类似达梦系统可以分析单个操作符具体执行时间的手段。

  新版本的达梦数据库对类似Q1这类计算密集型查询,进行了相当大的优化,取得了惊人的进步。那么达梦是如何做到这一点的呢?

  仔细观察操作符的执行分析,发现最底层的扫描操作符CSCN,居然只执行了约6000次,而LINEITEM表却有约6000000行记录。也就是说平均每一次扫描,返回了1000行的记录。而传统的数据库管理系统,操作符号一次只能处理一条记录,每取得一条记录,就把控制权传递给上层节点。其迭代模型为:

  ■ Open 初始化,申请资源
  ■ Next 取一行数据
  ■ Close 释放资源

  达梦对这个迭代模型做了重大改进,引入了达梦独有的批量技术。原来的迭代模型的缺陷在于每传递一条记录就发生一次控制权的转移,程序执行的表现就是发生一次或多次的函数调用和返回。当记录相当多的时候,这些函数的调用与返回,消耗了大量的CPU时间。而通过批量技术,比如在这个例子中,一次处理1000条记录,则把控制权转移的CPU时间减少到几乎是原来的1/1000,基本上可以忽略不计的程度。

  表达式计算很密集的投影与选择操作,对约600万行记录的处理总共花了约300毫秒,平均每条记录的处理时间为50纳秒。这么高的处理速度,同样是因为采用了批量技术。达梦对表达式运算的设计目标,是希望能达到接近C语言的性能。下面这个程序片段,揭示了达梦数据库内部对整数数组与整数常量值的加法运算:

           for (i = 0; i < n; i++)
      {
            res_tmp
= (lint64)opr1_arr[i] + (lint64)opr2;
            
if (res_tmp != (lint)res_tmp && null_arr[i] != 0)
                
return EC_DATA_OVERFLOW;
            res_arr
[i]  = (lint)res_tmp;
      }

   这里的n就是批量的尺寸。这个C语言的循环操作,完成了n个记录的加法运算,其性能显然比一次计算一条记录要快得多。而且,以数组形式存放的计算数据,物理上都紧挨在一起,而且是连续访问,这一特性非常适合利用现代CPU的2级高速缓存,减少主存的访问次数,以进一步提升性能。

0
相关文章