关系数据库的查询优化始终是一个重要而实际的问题,在那些以查询为主的应用系统中,这几乎是一个成败攸关的问题。但迄今为止,关于这个问题的讨论中所提出的种种解决方案大致可分为两大类,即利用硬件体系结构上的优势及DBMS对并行处理的支持能力的一类方案及完全由应用设计来处理的方案。在本文作者以前所发表的文章中曾推荐过利用临时中介表和表更新方法和快查询处理的策略。在同一篇文章中,我们也曾提到有可能利用程序变换支持查询优化的想法。所有这些建议和想法都属于应用设计类的处理办法,这些方法从某种意义上说有一定的一般性。但是,实际应用不断地提出这样或那样难而“怪”的问题,这些问题极富挑战性,用常规方法往往要以很昂贵的系统资源为代价才有望解决。
本文的目的是向读者介绍一种由E.Birger等人首先提出的方法,即加速查询处理的特征函数法。这个方法适用于大多数SQL的数据库系统,如果这类系统还包括为数不多的几个(最少为2个)内部函数,如abs()及sign()等,则这个方法就是直接可用的了。在E.Birger等人关于这个方法的研究报告中,曾给出很多极有难度而又很典型的查询要求及其求解办法,其中包括分技条件查询、求行内量的边界值、求直方图、表转置、求中位值、有序集的等段截分以及去边界值问题等。这些问题的共性是,若用常规方法求解,系统无论在存储开销上还是处理开销上都很大,而某些问题(如中值)的求解还相当难。本文将重述这些有趣的查询问题及其解决方案。同时,我们还将讨论“特征函数”作为一种使能技术的其他一些应用可能。
2.特征函数及其表示
特征函数是来自点集拓扑学的一个纯数学概念,集合S的特征函数定义如下:
1 若x? S
d s(x)= (0)
0 若x? S
在这里,任意元素x是否属于集合S,决定函数取不同的值。同时,这里也隐含了一个前提,即任何元素的集合S为范围的归属是完全确定的,不存在元素x的归属不明的情况。显而易见,特征函数是一种识别(或判定)装置。正是这一特性,使它能够成为数据库查询中选择准则的一种等价(和更有效的)替换成分。因此,我们说特征函数是加速查询的实施技术。
为了更直接地针对数据库查询问题,我们将特征函数的一般形式变换成如下的“数据库版本”:
1 若a=ture
d (a)= (1)
0 若a=false
其中α是布尔表达式。当构成布尔表达式的算术表达式由表属性及数据库内部函数组成时,特征函数的选择作用就很清楚了。
众所周知,一般关系数据库采用三值逻辑,即布尔表达式有可能取不确定值(“maybe”)。但为了简化表达并因此突出特征函数在加速查询中的本质作用,本文不考虑表属性取不确定值的情形。另外,实现特征函数的数据库(内部)函数(我们称之为特征函数的“元函数”)会因系统和我们主观选择上的不同而不同。例如,Sybase的Transact SQL有两个很有用的内部函数abs()和sign(),可以直接作为特征函数的元函数。若A和B是任意两个表属性,则
d [A!=B]=abs(sign(A-B)) (2)
为了使元函数有定义,表属性必须是数值变量。因此,除有特别声明而外,本文将一概假定所有举例和一般性讨论中的表属性为非空数值变量。等式(2)可从元函数的定义
abs(A)=|A| (3)
-1 若A<0
sign(A)= 0 若A=0 (4)
+1 若A>0
直接推导出来。一般地,经abs()和sign()而实现的特征函数是
d [A=B]=1-abs(sign(A-B))
d [A!=B]=abs(sign(A-B)) (5)
d [A d [A<=B]=sign(1-sign(A-B))
d [A>B]=1-sign(1-sign(A-B))
d [A>=B]=sign(1+sign(A-B)))
此外,设α和b 是任意布尔表达式,则
d [NOTα]=1-d [α]
d [αANDb ]=d [α]*d [b ] (6)
d [αOR b ]=sign(d [α]+d [b ])
这里的A和B是表属性,为非空数值量。等式(5)给出了6个最简单的特征函数的元函数表示,但这并不是唯一的表示,还可能其他的表示方法。等式(6)是布尔算子的一般导出规则。对于由最简型式的关系表达式构成的布尔表达式而言,等式(5)和(6)就构成其特征函数的实现规则。对于一般布尔表达式,等式(5)和(6)也是导出其特征函数的基础。一般而言,由(5)和(6)可以推演出一个特征函数类,某些特征函数直接对应于SQL的选择算子。例如,形如d [A<=X<=B]的特征函数显然与判定变量X是否在闭区间[A,B]中有关。利用(5)中的第4个特征函数及(6)中的第2个导出规则,
d [A<=X<=B]
=d [(A<=X)AND(X<=B)]
=d [A<=X]*d [X<=B]
=sign(1-sign(A-X))*sign(1-sign(x-B)) (7)
显而易见,等式(7)右端的区则算术表达式恰是选择算子BETWEEN的一种等价表达。可以仿照上述方法得到其他三个与区间值有关的特征函数,即δ[A
3. 实例分析
为了说明以上引入的特征函数在加速查询处理中的作用,让我们具体分析一个实例。
试考察一个描述学生收入状况的表 Students(name,status,parentincome,selfincome)(8)
其中name是主键,属性status是一种标法量,当status取值1时,表明学生的收入完全来自父母,当status取值0时,表明学生的收入完全是自己劳动所得。针对这个表,假定我们想得到形如(name,income)的查询结果,其中income或为学生自己的收入(当相应的status取0值时)或是来自父母的收入(当相应的status取值为1时)。
从表students的结构及查询结果的语义分析,完成查询的常规方法应当是
SELECT name,income=parentincome
FROM student
WHERE student=1 UNION (9)
SELECT name , income=selfincome
FROM student
WHERE student=0
这是一个很自然、很直白的查询表达,但同时也是一个非常低效和非常耗费资源的表达。执行这个查询的一般过程是:首先分别执行由算子UNION所连结的两个子查询,然后产生一个存放查询中间结果的临时表并将两个子查询的结果存入以这个临时表中,第3步对临时表作排序以便消除可能存在的重复值。至此,才得到最终的查询结果。在这样的处理中,除对整个表students要遍历两次而且要对中间结果作排序处理,处理上的烦杂和资源的消耗都是显而易见的。查询(9)唯一的优点,是它表达上的自然直白,谁都想得到。
对本例而言,还有更紧凑和更有效的查询表达。例如,不难验证以下的查询
SEIECT name,income=parentincome*status+selfincome*(1-status)
FROM students; (10)
从语义上与查询(9)完全等价。但查询(10)不但消耗的存储少而且处理上要有效得多,因为它只遍历一次表students而且避免了可怕的排序操作。这个例子说明,对同一个查询结果,不同的查询表达在处理效率和资源消耗上可能会相去甚远。因此,寻求有效的查询表达方式,不但是必要的而且是可行的。
查询表达(10)与像(9)那样的常规表达不同之处在于,后者的查询条件由两个WHERE子句和算子UNION显式给出,首者将查询条件间接地隐藏在SELECT子句的算术表达式中。无论查询表达采用什么形式,本例都属于“条件检索”的查询类型。如果对照一下查询要求和查询表达(10),不难发现只所以能给出如此简洁而正确的回签,实在是有点“事有凑巧”。如果问题稍作些许改变(例如,属性student取0和1以外的值,或者student取两个以上的标法值,如此等等)问题就不会这么简单了。因此,是否有一种很一般、很系统的解决方案,能让我们对任何显式表达在WHERE子句和相关算子中的选择条件找到与之语义等价的算术表达式?答案是肯定的,这就是我们在下面要介绍的“特征函数法”。
4. 几个典型查询的特征函数解
正如上面所讲,特征函数能够实现我们的愿望,即将显式的布尔条件转化为标量表达式。因此,特征函数最直接和简便的运用是针对条件检索型的查询,但它的作用并不仅仅止于此。为了较全面地了解特征函数在解决复杂查询中的作用,本节将由易到难介绍和分析若干典型实例。对某些实例,我们还将说明它的应用领域。为了表达上更紧凑,所有出现在标量表达式中的特征函数都没有用元函数展开。因此,如果要通过实际运行验证这里的实例,必须先借助于(5)、(6)替换这里的特征函数。
4.1 条件检索
由(10)给出的查询可借助于特征函数识为
SELECT name,
income=parentincome*d [status=1]+selfincome*d [status=0]
FROM student (11)
如果检索条件仅止于此,用(11)代替(10)并没有什么本质上的意义。但实际问题中的检索条件远比此处复杂而多样。例如,若将上例的要求稍作修改,即在保留status原有语义而外,加上按学生的年龄分段的要求,即以19岁和23岁为年龄的分界点,凡年龄不超过19岁而且依靠父母的学生为一组,凡年龄超过23岁者完全自食其力的学生为第二组,所有其他学生为第三组。在查询结果中,收入(income)一栏有不同的含义:对前两组学生,分别对应于他们父母的收入和学生自己的收入,对第三组学生,则对应于前两组学生收入的算术平均值。在习惯于用常规方法处理查询的人看来,这样的条件就显得大复杂了。实际上,这是很自然的要求,相对于原问题而言,要求的扩展是很轻微的。对照查询表达式(11),不难验证
SELECT name,
income=parentincome*d [atatus=1]*d [age<=19]+selfincome*sign (d [status=O]+d [age>23])+( (parentinceome+selfinco me)/2.0*(1一d [status=1])*d [age<=19]-sign(d [sign=0] +d [age>23])
FROM students; (12)
正是上述查询所要求的有效表达方式。从income的表达形式看,与查询条件的要求完全是一一对应,都具有很典型的级联式IF�THEN�ELSE结构。一般而言,在特征函数的参与下,无论查询条件多复杂(例如有更多的属性出现在条件中,同一属性值被划分为更多的区段,等等),条件检索型的查询都具有如(11),(12)那样的典型结构。不同之处仅仅在于,条件越多则级联数越多,但正则算术表达式的逻辑结构都相同。所有这类查询表达,在执行中都只对表student遍历一次。相反,若按常规方法求解,原则上每一个分类条件需要一个子查询来回答,最终的结果是所有子查询结果往UNION运算所得。两种表达两种效果,孰优孰劣一目了然。
4. 2 直方图问题
求直方图是统计应用中经常要解决的问题白如果统计数据来自数据库而且数据量很大的话,用常规方法求解并不是一个很轻松的任务。但是,借用特征函数可以顺利地解决问题,不但处理的过程很高效而且很直观。为了说明这一点,让我们看一个具体的例子。假定统计数据存在表employee(name,age dept,kids)中,其中kid也表示每个雇员的子女数。要求给出形如(nokids,onekids,fewkids,manykids)的统计结果,即分别计算出所有雇员中没有孩子、有一个孩子的、有两个或三个孩子的以及有三个以上孩子的雇员总数。
如果用常规方法,需要查历表employee四次,分别计算出nokids,onekids,fewkids和manykids的值,然后经3个UNION运算才能得出最终的结果。如果原问题不是将雇员的子女数划分为4个区段而是8个甚至更多个区段,常规方法的低效就更明显不过了。运用特征函数,上述问题的解显然是
SELECT nokids=SIJM(d [kids=0]),
Onekids=SUM(d [kids=1])
fewkids=SUM(d [2<=kids<=3])
manykids=SUM[kids>=4])
FROM employee; (13)
这个查询结果的正确性很容易验证:对于表中任意一行,如果kids=0,则d [kids=0]=1而且d [kids=l]=d [kids>=4]=d [2<=kids <=3]=0,所以该行在区段nokids中求和而不在任何其他三个区段中求和,对于kids的其他取值依此类推,这表明(13)的结果正是原问题所需要的结果。重要的是这个结果不但正确,得到这个结果的途境非常有效,因为处理中只遍历表一次。如果将雇员所拥有的子女数区分为更多的值段,运用特征函数的查询处理仍然只遍历表一次而不是更多,不同之处仅在于选择表中的计算多几项而已,查询表达在逻辑上的复杂性一点也没有增加。
同一个基本问题也可以引导出不同的变异。若没上述的基本解作基础,直接解决这些变异问题往往很困难。
变异问题之一:对同一个表employee,按雇员所在的不同部门分别计算子女数的分布,即要求得到如(dept,nokids,onekid,fewkids,manykids)的结果。
这个问题的解显然是以下的查询表达
SELECT dept.
nokids=SUM(d [kids=0]),
onekid=SUM(d [kids=1])
fewkids=SUM(d [2<=kids <=3]
manykids=SUM(d [kids>=4])
FROM employee;GROUP BY dept; (14)
变异问题之二:按照年龄区段求雇员子女分布的直方图。为确定起见,雇员的年龄分为三个区段,即:小于25岁的、大于45岁的以及年龄在25岁到45岁之间的,分别称为第1、第3和第2年龄段。这个问题事实上是要求对表employ回给出形如(ageCategoy,nokids,onekids,fewkids,manykids)的结果,其中
1 若age<25
d (a)= 2 若25<=age<=45 3 若age>45
(15)
这个问题虽有相当的难度,但对于允许表达式出现在GROUP BY子句中的系统(例如,Sybase的Transact SQL就是如此),答案也是直接了当的。不难验证以下查询表达正是我们所需要的解答
SELECT ageCategory=1xd [age<25]+2×d [25<=age<=45] +3×d [age>45],
nokids =SUM(d [kids=0]),
onekids =SUM(d [kids=1]),
onekid =SUM( d [kids<=3 AND Kids>=2])
manykids =SUM(d [kids>=4])
FROM employee
GROUP BY 1′ d [age<25]+2×d [25<=age<=45]+ 3×d [age>45]; (16)
这个问题与上一个问题的区别仅仅在选择表和GROUP BY子句中使用3年龄段表达式ageCaegory,按照(15)式的定义,很容易验证(16)确是我们所需要的查询。
沿着这个思路走下去,还可以处理更复杂的问题。当直方图越来越“宽”时,特征函数的有效性也越能显现出来。
4.3 表转置
表转置是一个变换过程,它将一个窄而长的表转化成一种宽而短的表,这是在数据库应用设计中经常遇到的问题。C.j Date很早就注意到这一点并给出了处理这一问题的一般原则。Date将中文中前一种形式的表称之为表的“列式”表示,而将后一种形式的表称为“行式”表示。鉴于SQL的集函数本质上是面向列式表示而不是面向行式表示的,所以列式表示有便于运用集函数的给应用处理带来灵活性的优点。因此,基表的设计多考虑采用列式表示一般是个好主意。
针对列式表示的基表作查询时,特征函数是实现表转置的有力主具。例如,考察一个记录雇员月奖金的表bonus(name,month,amount)。这个表显然是列式表示,相对于它的行式表示,例如就可以写成bonus’(name, janAmount,…,decAmount)。如果想得到每个雇员各月奖金一览表,从bonus''表查询最简便。但这种行式表示的表本质上只对这一种查询有效,对其他的查询要求很难有适应性。相反,形如bonus的列式表示不仅可以回答上述查询而且还可以回签其他处理要求。例如,针对上述要求的特征函数表示就是
SELECT name,
janAmount=SUM(amount×d [month=1]),
febAmount=SUM(amount×d [month=2]),
. . . .
decAmount=SUM(amount×d [month=12])
FROM bonus
GROUP BY name; (17)
读者不妨思考一下,针对bonus表的同一查询要求若不采用特征函数该怎么满足。
4.4 求中位数
在实验数据处理中,经常有求一组数据的“中位数”(median的要求。众所周知,关于中位数,存在两种定义,即统计学定义和所谓的“财务”定义。按照统计学的定义,中位数必须是这组数中的某一个。因此,当有偶数个数时,必须从两个数中作出选择,或选大者或选小者,要视具体应用背景而定。中位数的后一种定义取两数(在有偶数个数时)的算术平均值。如果数值的个数为奇数(设为n),则中位数就是数组中第(n+1)/2个数。不论采用什么样的定义,用一个语句求一组数的中位数始终是一个难题。既使是训练有素的SQL程序员,要想用一般方法来处理这个问题,也得写一个很复杂的过程。但是,借用于特征函数,很容易得到这个问题的一个很简洁的解。为确定起见,考虑这样的一组实验数据data (value)。这表明这组数据中不存在重复的值。除此而外,我们还要假定所有的数据都是非空的数据。在这样的假定下,数据集data的中位数正是下述查询语句的结果:
SELECT x.value FROM data x, data y
GROUP BY x.value
HAVING SUM(d [y.value<=x.value])=(COUNT(×) +1)/2 (18)
因为,对于每个x.value,表达式SUM(d [y.value<=x.value])的结果是数据集中小于或等于该值数据的个数,所以由这个HAVING子句选择的正是所要求的中位数(细心的读者不难发现,我们这里利用了Sybase两个整数相除的结果是实际相除后再取截断而得到的值。另外,当数据集包含偶数个元素时,取的数是两数中较小的一个,这正符合中位数的统计学定义。)
上面求中位数的方法很容易延拓到数据集往某些属性而分割为若干个子集的情形。例如,考察数据集data 2(partition,value),其中value仍为非空数值量,但属性partition则可以是任意数据类型,整个数据集data2经此属性而分割为若干个子集。下述查询语句的结果恰是各个子集的中位数:
SELECT x.partition,x.value
FROM data2 x,data2 y
WHERE x. partition=y. Partition
GROUP BY x.Partition , x.value
HAVING SUM (d [y.Value<=x.value])=(COUNT(x)+1) /2 (19)
上式中的自连结是表分割的需要,除此而外上两种方法没有本质上的区别。
4.5 求端值
在某些实际问题中,所设计的表的行数据包含了若干个可以彼此比较分析的数据项。我们将求这些数据项中取值最大或最小的称为“端值问题”。例如,考察表scores(name,sat1,sat2),其中sat 1和sat 2代表学生两次考试的成绩。假定需要得到每个人两次考试中的成绩优异的一览表,即求形如(name,bestSat)的结果,其中bestSat表示每个学生两次成绩中的成绩优异。
某些数据库系统(例如Oracle)有内部函数greatest(value 1,value 2…),可供直接解决问题。在不具备这种函数的系统(例如Sybase等)中,一般解决方法是,第一次遍历全表得以满足条件,sat 1>=sat 2的sat 1,第二次遍历全表得到满足条件sat 2>sat l的sat 2,再将中间结果经UNION运算才能获得最终结果。
借助于特征函数只须扫描表一遍而且查询表达非常简单,即:
SELECT name,
bestSat=sat 1% d [sat 1>=sat 2]+sat 2×d [sat 2>sat1]
FROM score; (20)
假定我们不只要得到每个学生两次考试中的成绩优异,而且还想知道这个成绩是哪次考试所得,只须在(20)的选择表中补加一项,即:
SELECT name
bestSat=sat l % d [sat1>=sat 2]+sat 2% d [sat 2>sat 1] whichSat=1% d [sat 1>=sat 2]+2% d [sat 2>sat l]
FROM score; (21)
这个结果只在sat 1=sat2 时有点歧义但并不错(在这种情况下,(21)认为是第一次考试所得)。除此而外似乎不必再做任何解释了。以上只考虑了最大值,求最小值可仿此办理。有兴趣的读者不妨考虑一下这个问题的种种变异情况及其解答。
5 几个值得进一步思考的问题
由(5)式给出的特征函数表示,并不是唯一的形式,还可有其他的表现形式。不同的元函数,在计算的复杂性上会有差异。因此,选择具有更低计算复杂性的函数,有可能更进一步改善特征函数的效率。其次,本文开头关于出现在特征函数中的属性必须是非空数值量的假定,是为了保证元函数abs()和sign()有定义和整个(5)式正确的充分性条件。所以,存在着降低这一条件强度的可能性。考虑到几乎所有的主流数据库系统都支持三值逻辑(3VL),去掉非空假定就显得特别必要了。
尽管本文只考虑了特征函数在查询中的作用,我们认为同样的思想也应当针对数据库更新作较系统的考察。另外,在E.birger等人原始工作的框架内,也存在着种种明显的可扩充的方面(例如,出现在特征函数中的属性,既可以是普通的表属性,也应当允许是某种受限的抽象数据类型等等)。所有这些,都有待于我们更进一步的工作去挖掘或作出分析判断。