Magnus Pierre, Extender 开发专家, IBM
Kevin Foster, 经理,Extenders and DataBlade Development, IBM
2004 年 4 月 本文为您提供了理解和开发索引扩展所需的全部内容,包括涉及哪些部分,以及这些部分如何组合到一起。本文还谈到为了成功地构建一个索引扩展,您需要做些什么。
简介
在诸如生命科学、图像处理和空间数据管理之类领域中的应用程序,需要更复杂的数据类型,而不仅仅是简单的字母数字(alphanumeric)数据类型,比如传统数据处理应用程序中用到的那些数据类型。IBM DB2 UDB 通过其用户定义结构类型(User-Defined Structured Type)功能为这种复杂数据类型提供了支持。相应地,我们可能需要通过基于这些复杂数据类型的索引来增强应用程序性能。本文为创建用于使用用户定义结构类型的扩展索引提供了一个完整的实例。
索引,数据库性能的关键
谈到数据库,就不得不提索引(index)和建索引(indexing)。IBM 得感谢关系数据库背后的理论。这样的理论本身就很优美。通过将数据拆成小块,在合适的时候我们又可以无约束地将数据重新组合起来(而不像数据库模型那样,为保证数据的质量而定义一些约束)。 关系模型中的关键思想是,尽可能地放宽用户可以对数据集使用的查询类型,而不让数据的组织形式控制用户请求的查询类型。
随着数据集的增大,我们要想轻松一些,索引就成为一种关键的工具。不过,建索引并不能解决所有问题,实际上还有一些其他的工具可用于发现某一特定的数据集,例如 Hash Joins 的 IBM DB2 实现。索引基本上就是通往数据的快捷路径。索引的创建是这样的:为每一个要建索引的列值生成一个惟一或者半惟一的键,并将之存储起来。然后在内部将这些键组织到一棵树中,以便尽可能地减少查找某一特定键和特定行时所需的步骤。关于树和树存储的令人关心的事情是:为了能够存储大量的数据,至少需要多少层的树。
在 IBM DB2 中建索引
DB2 Universal Database 是一个非常强大的面向对象的数据库引擎,它通过 Btree 实现为索引提供了非常有效的实现。索引总是索引于一个表中的数据列。列总有一个数据类型,表明可以将什么样的数据存储到此列中,对于这种类型的操作哪些是受支持的,等等。对于数据类型的更一般的定义是:它是一个对象/变量,具有一组属性,并且有一个内部的值/状态。作为类型的一部分,还有一组定义好的针对这种类型的操作,这些操作可以修改内部的值/索引。针对某种类型的最常见的操作是赋值(assignment)、比较(comparison)、加(addition)和减(subtraction)。有时候,我们对某种类型的理解是不可能撇开针对该类型的操作的,所有基本数据类型(但不限于此),例如整数(integer)、浮点数(float)、十进制数(decimal)和字符(char)都属于这样的类型。 例如,假如我要解释整数,我可能会提到:整数就是所有的数字;它们可以相加、相减、相乘,以及比较是否相等。而对于长数据类型,Clob 和 Blob,它们具有通用的格式,用于存储任意数据,存储大量与数据类型关系不大的信息,要定义对这些类型的操作就比较困难了。对于 Clob 和 Blob 类型,也许还可以定义一个相等操作符,例如两个相互比较的 Blob 含有相同的数据,但是对于那种类型,是否可能定义一个通用的大于或等于操作符呢?从基本数据类型到通用存储数据类型,这是一个很大的逻辑上的跳跃,因为 Clob 和 Blob 都只是有一定大小的容器而已。
在 IBM DB2 中可以对哪些类型建索引?
IBM DB2 可以通过使用 Btree 索引对基本数据类型建索引。要建索引的列数据类型组合的长/宽不能大于 1024 字节。而且要建索引的列必须以基本非长数据类型定义。其原因是,在 IBM DB2 中每一种基本长数据类型都缺乏用于定义这种类型的两个变量实例如何相互比较的操作符。这一类的操作符有等于 (=)、大于(>)、小于(<)、大于或等于(>=)、小于或等于(<=)和不等于(!=)。这些都是我们熟知的比较操作符,它们为类型定义了什么是相等,什么是大于,什么是小于,等等。比较操作符都是系统定义函数,对于某种类型,只能存在一组操作符。而且,在 DB2 中不能覆盖比较操作符。这不是因为覆盖不好,而是因为比较操作符要依赖于一个内部 Boolean 类型,而在 DB2 中没有将此类型外部化。长数据类型中没有定义类型比较操作符,因为比较操作,例如比较一个 Blob 是否大于或等于另一个 Blob,是没有多大意义的。
当在 DB2 中使用一个索引进行搜索时,Btree 实现将为该类型使用类型比较操作符,以判断是否命中。
要在 IBM DB2 中创建索引:
CREATE INDEX MYIDX ON MYTABLE ( KEYCOLUMN ); 请在 SQL Reference 2 中查看关于如何通过 CREATE INDEX 语句创建索引的形式描述。
当把数据插入到具有 Btree 索引的一列/多列中时,标准 Btree 实现将为涉及索引的每一个列生成一个索引键。如果索引是以惟一关键字定义的,那么索引就只能包含惟一的键组合。UNIQUE 关键字提高了选择性,进而加快了索引的速度。索引扩展键集总应该是惟一的,所以对于一个基于索引扩展的索引,都隐含地有 UNIQUE 标志。
用户定义类型
|
索引扩展的关键特性:
- 它允许为复杂用户定义类型(结构类型)建索引。
- 创建索引扩展时,可以创建索引功能,您想在多少表中使用这种功能都行。
- 创建索引语句实际上与传统索引是一样的。
- 索引扩展是用户定义函数(User Defined Function)的索引。
- 基于搜索方法的谓词。将使用的搜索方法取决于查询中所使用的谓词。
- 索引扩展扩展了诸如键创建和可供搜索的内容之类的功能性。实际的索引功能(Btree 中的存储,对键排序,搜索,等等)是以具有良好性能、经过认真测试的标准组件实现的。这种方法的关键思想是,不要只是因为自己想使用什么就自己重新去发明它。
|
我们已经看到如何为 DB2 基本类型建索引。但是,对于不能放进典型整型的复杂数据类型或者日期类型,又该如何建索引呢?在对这一类数据建索引之前,我们需要回顾一下为这种数据进行的数据类型的选择。
在 IBM DB2 中,用户定义类型可以有两种形式,或者是用户定义独特类型(User Defined Distinct Type),或者是用户定义结构类型(User Defined Structured Type)。两者之间有很大的不同。别忘了在上一节中关于类型的讨论。用户定义独特类型是一条很好的途径,能够从已有的基本类型创建更好的类型名,从而更好地与将要为之实现特定数据库的业务域相匹配。例如,我们可以更清晰地将一列定义为:
Instead of:
DNA VARCHAR(32007) FOR BIT DATA
|
有了独特类型,业务域与数据模型之间的就有了紧密的关系,从而避免了进行比较时存在的误解,因为这种类型确保了我们实际上只能比较有可比性的数据。然而,没有不存在例外的规则,有时候,能够将一种类型转换为另一种类型确实很重要。IBM DB2 为终端用户提供了深受 C/C++ 程序员们喜爱的 nasty cast 功能。此功能的一个好处是类型覆盖不是隐式的,而是显式的,可以通过两种形式定义:
TARGET_TYPE(SOURCE_TYPE,TARGET_SIZE ) OR CAST(SOURCE AS TARGET (TARGET_SIZE))
|
创建独特类型的实例
CREATE DISTINCT TYPE DNA_TYPE AS VARCHAR(32007) FOR BIT DATA WITH COMPARISONS
WITH COMPARISONS 不是可选的,它为独特类型创建比较操作符。在定义基于长数据类型的独特类型时,不能使用 WITH COMPARISONS。假如您为之建立别名的基本数据类型受支持,并且处于 Btree 的大小限制之内,那么可以使用 用户定义独特类型。
用户定义结构类型
从某种地方来看,结构类型是介于 C 和 C++ 之间的结构概念。它就像是具有某些来自 C++ 的功能(例如方法)的 C 结构。在 IBM DB2 中,结构类型是一项非常重要的功能,因为它具有很多令开发人员和终端用户感激不已的优良性质。首先,结构类型可以包含不止一个的数据字段,并且它所包含的字段可以是 DB2 中可用的所有类型。这样,就有可能可以收集一组值作为该类型的内部状态。
结构类型的实例:(摘自实例代码)
CREATE TYPE MPIMAGE_T AS (
KEYVAL VARCHAR(256) FOR BIT DATA,
DATA BLOB(20M) COMPACT LOGGED
)
MODE DB2SQL
WITH FUNCTION ACCESS;
|
与 Blob 数据类型相比,MPIMAGE_T 是一种更丰富的类型,因为可以通过对该类型进行方法绑定,使之支持面向图像的操作,这意味着这种类型将可以更好地映射到我们的图像概念,以及可以对图像执行何种操作这上面来。还可以将结构类型实例化,并拥有一个构造函数。而且,可以为结构类型中的所有字段生成一个访问方法,以便设置和获取字段的值。
类型实例化的例子:
values( MPIMAGE_T()..KEYVAL(x'00FF')..DATA(BLOB(x'00FF'))..KEYVAL());
在这个例子中,首先通过 MPIMAGE_T() 实例化 MPIMAGE_T(),然后将 x'00FF' 赋给 KEYVAL。我们还添加了相同的字节数据到 data 字段中,最后一步是从实例化的类型检索 KEYVAL。请注意用于访问该类型的方法的双点符号。还应注意,在插入数据到一个 blob 数据类型中时,需要将 hex 字符串类型覆盖为 blob 类型。(在赋值操作符中没有隐式的覆盖)
当定义用户定义结构类型时,数据库引擎将创建一组函数,以支持新创建的类型。需创建的函数/方法是对该类型中包含的每个字段的访问方法。类型的比较操作符(系统会为每种类型生成一个惟一的 ID)。请查看一下 MPIMAGE_T 类型的定义的实例代码,以了解更多关于如何创建用户定义方法(User Defined Method)的信息。
结构类型可以继承其他的结构类型,从而创建类型层次结构。通过告诉类型它不允许被实例化,就可以创建纯抽象数据类型(Abstract Data-Type)。这些功能大多数用于处理对象关系表层次结构,而这些功能也超出了本文的范围,因为本文只是关于建索引的。
用户定义函数和用户定义方法
早先我们已看到,基本类型在 DB2 中是如何拥有它们自己的内置函数的。但用户定义数据类型又如何呢?在对这种类型建索引之前,我们需要审视一下,如何才能将函数加进 DB2 中,以支持用户定义数据类型。
用户定义函数和方法是以您自己的功能性去扩展数据库引擎的功能性的方法。函数可以是用 SQL 写成的(这是当然的),但也可以是用 C/C++、Java 等等写成的。其好处是,您可以通过一组特定于域的函数来增强数据库引擎的功能,这样您就可以得到一个这样的数据库:它可以从数据产生信息,而不只是数据存储。您所添加的函数可以直接通过 SQL 来使用,也就是说,只要应用程序可以查询数据库,在数据存储级(即数据库)实现的函数就可以在整个信息系统中使用。
下面是一个创建函数语句的例子:(摘自实例代码)
CREATE FUNCTION GetImgExtVersion()
RETURNS VARCHAR(100)
LANGUAGE C
PARAMETER STYLE SQL
NO EXTERNAL ACTION
EXTERNAL NAME 'IdxExample!GetImageExtenderVersion';
|
要了解更多关于不同标志的信息,以及这些标志如何对函数产生影响,可以参阅 IBM DB2 SQL Reference Manual 2 中的 CREATE FUNCTION 节。
有两种类型的函数,一种是标量函数(scalar function),一种是表函数(table function)。标量函数是那些只返回一个值的函数。上面 GetImgExtVersion 的定义就是标量函数的一个例子。我们可以在 SQL 语句中对列使用标量函数,作为 where 语句的一部分,等等。标量函数不能充当表,但是它可以充当表的子查询的一部分。
表函数是一种特殊的函数,其目的完全是在 SQL 语句中充当表。对于任何要求多于一行的结果集来说,这一功能十分有用,并且还可以用表函数的这种功能将外部源集成到 DB2 中。如果需要实现索引扩展,那么表函数就是基础,所以如果您想要理解索引扩展功能,本节就是关键。
表函数定义的一个实例:
CREATE FUNCTION MYSEARCH_TABLE(query varchar(1000))
RETURNS TABLE (
a MPIMAGE_T
)
LANGUAGE SQL
RETURN SELECT * FROM MYTABLE WHERE MYSEARCH(a, LoadImage(query))=1 SELECTIVITY .000000001;
|
请观察 RETURNS TABLE() 关键字,该关键字将此函数标识为一个表函数。在这个例子中,函数是用 SQL 编写的。
使用 MYSEARCH_TABLE 函数的实例:
SELECT count(*) FROM TABLE(
MYSEARCH_TABLE('c:\\Development\\IdxExample\\Images\\pic1.png')
)
as RS;
|
从终端用户的角度来看,MYSEARCH_TABLE 函数似乎可以做任何事情,因为她/他并不能洞察函数内部是怎么回事。这是一种非常有效的方式,既可以对终端用户隐藏复杂的语法,又可以让那些想要拥有完全控制的人能够维护灵活的语法。
上面的实例是用 SQL 编写的表函数的一个实例,开发表函数的最常见的方法是用 C/C++ 或 Java 编写外部函数。索引扩展功能要求函数用 C/C++ 编写。在处理外部表函数时,关于表函数功能的简洁做法是,通过 SQL 语句中函数定义的 returns 子句定义虚表的格式,然后,在实际执行函数的时候,利用一个状态来告知目前正处于过程中的哪个地方:
C/C++ 表函数的状态
- SQLUDF_FIRST:(可选)
- SQLUDF_OPEN:
- SQLUDF_FETCH:
- SQLUDF_CLOSE:
- SQLUDF_FINAL:(可选)
这个实例是基于 C/C++ 的,因为大多数用户定义表函数都是用 C/C++ 编写的。
当服务器使用一个状态标志重复调用函数时,就要用到这些状态。除了 fetch 状态,所有状态都只调用一次,只有某个标志已经在函数定义中定义了才可以调用。在返回一个状态告知已经没有数据可返回(SQLUDF_STATE = "02000")之前,将一直调用 fetch 函数。这样,根据预先定义的一组列,表函数可以返回 n 行。请查看实例表函数(index.cpp 中的 CreateKey 和 CreateRange),以了解关于其工作原理的更多信息。
用户定义方法
用户定义方法或多或少与用户定义函数有所相同,例外之处在于,用户定义方法只用于某一特定的类型,当调用这些方法时,第一个参数是一个所谓的 SELF 参数,或者是 C++ 中的一个 this 指针。如果您像上述那样声明了一个类型,那么首先所有的字段会隐式地传递给方法,然后再是开发人员给方法提供的参数。
要得到关于如何实现用户定义方法的好例子,请查看实例代码中的 GetRectangles 方法(在 IdxExample.db2 和 UDF.cpp 中实现)。
在您阅读上面一段时,我希望您能够问自己一个问题:参数(尤其是结构类型这一类的参数)是如何传入传出一个函数的呢。这个问题正是我们接下来要谈到的,即转换以及转换实际上做些什么。
结构类型的转换函数
现在我们可以开始为用户定义数据类型建索引了吗?对不起,还是不行。
在讨论结构类型的时候,我对您隐藏了一个事实。结构类型实际上没有静态交互模式!这就得开发人员您自己去定义这种类型如何与系统的其他部分交互了。IBM DB2 让某种类型的开发人员可以决定应该如何从一组基本数据类型构造出此类型,以及如何将此类型析构成基本数据类型。这里用到的技术就是所谓的转换,转换是一种非常强大的、基本的工具,通过它才可以使用用户定义结构类型。
IBM 让开发人员可以定义类型的转换组,每个转换组都有两个函数/方法。其中一个函数负责将结构拆成一个个的字段(或者拆成一个字段,这要取决于您是否愿意这么做),而另一个函数则负责获取各个字段并构造成一个结构类型。IBM DB2 让您去决定为转换组使用什么名字,但建议至少要决定实现两个转换组,这两个组分别名为 DB2_PROGRAM 和 DB2_FUNCTION。DB2_PROGRAM 和 DB2_FUNCTION 是一种类型的缺省转换组,由数据库引擎使用。如果没有为此类型设置转换组,那么所有 DB2 客户机应用程序就会使用使用 DB2_PROGRAM 这个转换组。当从一个函数那里传递或接收一种类型,并且在函数定义中并没有为此类型定义转换组,则要用到 DB2_FUNCTION 转换组。在实例代码中,我们将为转换组使用非缺省名,以便您可以看到如何使用它们。
将结构类型拆成一行各个类型的函数实例
CREATE FUNCTION IMG_FROM_SQL(A MPIMAGE_T)
RETURNS ROW(KEYVAL VARCHAR(256) FOR BIT DATA, DATA BLOB(20M))
LANGUAGE SQL
RETURN values(A..KEYVAL, A..DATA);
|
获取各个基本类型并返回一个构造好的结构类型的函数实例
CREATE FUNCTION IMG_TO_SQL(KEYVAL VARCHAR(256) FOR BIT DATA, DATA BLOB(20m))
RETURNS MPIMAGE_T
LANGUAGE SQL
RETURN MPIMAGE_T()..KEYVAL(KEYVAL)..DATA(DATA);
|
下面看看如何使用上面所声明的函数来定义此类型的转换。
CREATE TRANSFORM FOR MPIMAGE_T
DB2_FUNCTION
(
TO SQL WITH FUNCTION IMG_TO_SQL,
FROM SQL WITH FUNCTION IMG_FROM_SQL
);
|
既然在上面的例子中我们没有定义 DB2_PROGRAM 转换组,如果我在 DB2 Command Window 中编写一个下面这样的查询,结果会怎样呢?
values(MPIMAGE_T()..KEYVAL(x'00FF')..DATA(BLOB(x'00FF')));
答案:
SQL20015N A transform group "DB2_PROGRAM" is not defined for data-type "MPIMAGE_T". SQLSTATE=42741
索引扩展详解
是的,现在我们终于可以谈谈为用户定义类型建索引了。
为什么在开始谈到索引扩展,即本文主题之前,要有那些讨论呢?其原因就是,在实现索引扩展时,前面几节中提到的所有技术都要用到。索引扩展是一种非常先进而灵活的方式,使您得以为复杂/丰富数据类型建索引。通过索引扩展而可以建索引的数据类型是用户定义结构类型和用户定义独特类型。而只有用户定义结构类型才需要通过这种机制来建索引,因为独特类型可以按传统方式来建索引。
索引扩展不要求您从头开始开发自己的索引。索引扩展是一种干净利落的方式,通过它可以以用户定义的功能来扩展 DB2 Btree,这样 Btree 就可以对结构类型建索引。索引扩展使您既可以创建存储在索引中的键,又可以创建在搜索索引时用到的搜索条件,从而让您可以扩展传统的 Btree 索引。搜索条件是通过索引中每个键的最大值和最小值来定义的值的范围。范围可以应用于任何事物上,例如值测试。我想到的一个例子是,如果您有一个特征向量(feature vector):对于您不感兴趣的所有位,可以为之赋一个 x'00' 和 x'01' 的范围,而对于那些您需要设置的位,可以使用一个 x'01' 和 x'01' 的范围。我用两个值表示一位的原因是,DB2 将所有二进制信息存入字节字符串中。比较操作符对每个字节进行操作,这是一种比较容易但是也低劣的模拟位向量的方法。在索引扩展中,所有的比较都是通过范围进行的,但是在实例代码中,我们只对匹配的情况感兴趣,因而下限和上限的值是一样的。
在搜索机制之上,有一个后索引(post index )搜索过滤函数,通过定义此函数,可以使之在将从索引返回的结果集返回给调用者函数之前,进一步计算该结果集。(所有索引扩展都是通过一个用户定义函数调用的,对此后面还会有更多论述)
很多次,您知道想要检索什么,但是可能还要对键应用某些附加的规则。您可以选择在查询时非常快速地生成键,即只查找索引中一个键的前 20 个字节,因为与生成整个键相比,这样做可以有足够的选择性和足够多的速度方面的好处。首先,在过滤函数中处理存储在一个长类型(在范围生成器中生成键时不能访问该类型)中的实际数据,以便基于由大致范围的键所返回的结果集,生成和计算一个精确的键。然后,过滤函数将只返回那些匹配这个键的行。
索引扩展框架通过一个复杂的 SQL 语句结合到一起,该 SQL 语句引用了一组函数,这些函数必须在创建索引扩展之前就定义好,并且这些函数还必须遵循一些相当严格的有关参数、函数类型等等的规则,欲了解更多的限制,请参阅 IBM DB2 SQL Reference Guide 2。虽然看起来复杂,但只要您做过一两次,事实将证明这其实是相当容易的。(我敢打包票)
下面给出了索引扩展语句的概要
CREATE INDEX EXTENSION [INDEX NAME]([Index creation arguments])
FROM SOURCE KEY ([STRUCTURED TYPE TO INDEX])
GENERATE KEY USING
[CREATE KEY FUNCTION]
WITH TARGET KEY (
[INDEX KEYS CREATED BY KEY CREATOR FUNCTION]
)
SEARCH METHODS
WHEN [Search method name]
(
QUERY [structured type used for creating the ranges]
)
RANGE THROUGH
[Range producer function]
FILTER USING
[Optional filter function, or case statement];
|
要想看到对索引扩展的确切的形式描述, 请参阅 SQL Reference Volume 2。
如何定义索引扩展
正如您在前面的代码小段中看到的,索引扩展很是吓人。因此,以一种结构化的方式来处理索引扩展就显得重要起来。
- 定义 UDT 并决定想要对什么建索引。还要定义您想要使用什么算法来产生足够惟一的键,当然还要决定您想要在多少键列上建索引。索引扩展功能让您可以为一个要建索引的值定义至多 16 个键列。对于这些键中的每一个键,都需要由范围生成器(range producer)产生它们自己的范围。
- 以表函数的形式创建 create key 函数。这个函数必须是一个表函数,因为它可能返回不止一个的列,但是这也有其他含义在里面,在后面我们会讲到。
- 创建范围生成器,即将产生搜索时要用到的范围的函数。对于 creat key 函数返回的每个键,都必须有一个范围。
- 可选地,您可以添加过滤函数或者一个 case 语句,以便过滤出您想要的从索引返回的行。
- 为索引扩展的创建编写 SQL 语句,将所有小块结合起来,并尝试注册该索引扩展。
在定义索引扩展时,我总是从 SQL 定义开始,并确保语句无误。一旦完成了 SQL 定义,并且至少尝试了一次注册索引扩展,我便开始与 C/C++ 编写的实际实现打交道。这样做的原因是多方面的,但是最大的原因就是您可以使用 SQL 函数定义作为对设计以 C/C++ 编写的函数的指导。另一种方式虽然也行得通,但要花更多的工作到寻找丢失的小块上。
create key 函数
create key 函数实现的实例
CREATE FUNCTION CreateKey(keyval varchar(256) for bit data,nOptions integer)
RETURNS
TABLE(
key1 varchar(256) for bit data,
no_rectangles int
)
LANGUAGE C
DETERMINISTIC
SCRATCHPAD
DISALLOW PARALLEL
NO EXTERNAL ACTION
PARAMETER STYLE SQL
NO SQL
FENCED
CARDINALITY 1
SPECIFIC CreateKey
EXTERNAL NAME 'IdxExample!CreateKey';
|
请注意,这里 return 子句使用了 TABLE 关键字,并且用名称和类型声明了一些字段。对于一个好的表函数来说,另一块至关重要的信息是 scratchpad。scratchpad 是引擎预留的一块内存区域,在对函数的调用期间这块区域一直保留在那里,您可以用之来存储中间结果。例如,跟踪您已经从表函数返回了多少行的数据。
另一个有趣的属性是 LANGUAGE 属性。语言属性控制该函数以什么语言实现。必须指定语言关键字,以便服务器能够预测到该函数是什么样子。
对 create key 函数的限制
对于 create key 函数可以以什么数据类型作为输入参数和输出键当然是有限制的。create key 函数不能以一个长数据类型作为参数,也不能将一个长数据类型存储在索引中。对于存储长数据类型作为索引键的一部分的限制是显然的,因为长数据类型没有比较操作符。然而,之所以不能传入一个长数据类型作为 create key 函数或 generate range 函数的参数,却是因为存在一个缺陷,我希望 IBM 在将来会加以补救。
范围生成器函数
范围生成器是一个表函数,用于产生在从索引提取出数据时要用到的范围。范围是根据在查询索引时发送给该函数作为参数的查询对象来产生的。这就提供了最大的灵活性,因为在查询时,您是根据传给与索引绑定的函数的实际参数来计算自己感兴趣的范围的。当计算范围键的时候,要用到键类型的比较操作符,因此强烈建议您首先要知道比较操作符,这样才能知道范围是否计算得正确。如果类型只是基本的数字类型,那么这还不成问题,但是对于二进制字符串,很多人就要感到困惑了。二进制字符串是字节字符串,是一个字节值一个字节值地进行比较的。要了解更多关于这个和其他比较操作符的信息,请参阅 SQL Reference Volume 1。
对于每个键,范围生成器都需要产生一个范围,这意味着对于每个索引键,它需要创建两个键,一个为低键(low key),一个为高键(high key)。对于每个键,对索引的搜索可以与一个带有 between 语句的查询进行比较。这还不是全部。范围生成器可以产生多个范围行,因为范围生成器是表函数!!!这一点非常有用,因为您可以寻找一些不连续的特定的间隙。对于产生的每一行范围,您将进行一次索引扫描。扫描的结果集将通过可选的过滤函数或者 case 语句加以过滤。如果没有 case 语句或过滤函数,那么经过扫描的所有行都将返回给调用者函数。如果返回不止一个结果集,那么当计算最后的范围行时,这些结果集将作为一个结果集返回给调用者函数。
对范围生成器函数的限制
范围生成器与 create key 函数一样有着相同的限制,即,它不能以长数据类型作为参数,也不能返回长数据类型。而且,范围键的数据类型必须匹配由 create key 函数创建的索引键的宽度和类型。
过滤函数/过滤 case 语句
过滤函数或过滤 case 语句是附加的索引后(post-index)搜索步骤,用于确保从索引扫描返回的行的确是令人感兴趣。如果该行真的是感兴趣的,过滤函数或 case 语句就返回 true。记住,您只能看到索引键和查询对象,即查询索引时传递给调用者函数的参数。过滤函数可以读长数据类型,因此可以选择在过滤子句中,根据长数据类型字段的数据对一个键做更多的处理。
索引扩展 SQL 语句
如果是第一次,那么索引扩展的创建中最困难的就是要在注册索引扩展时,确保一切正确无误。当您第一次尝试开发自己的索引扩展时,有很多限制看起来是不起作用的。但一旦有了经验,您就会认识到为什么各个方面会结合起来,以及它们是如何结合的。
完整索引扩展定义的实例(如实例代码中所用到的)
CREATE INDEX EXTENSION MPIMGINDEX(OPTIONS INTEGER)
FROM SOURCE KEY(SOURCE MPIMAGE_T)
GENERATE KEY USING
CreateKey(SOURCE..KEYVAL,OPTIONS)
WITH TARGET KEY (
KEY1 VARCHAR(256) FOR BIT DATA,
LEN INTEGER
)
SEARCH METHODS
WHEN IMATCH(QUERY MPIMAGE_T)
RANGE THROUGH
CreateRange(QUERY..KEYVAL,OPTIONS)
FILTER USING
FilterFunction(key1, LEN, QUERY..KEYVAL);
|
您可以看到,我们必须为索引扩展提供一个名称,这个名称用于在 create index 语句中引用索引扩展。我们还必须为 create 语句提供至少一个参数,而且这个参数必须是 CreateKey 和 CreateRange 这两个函数各自的参数中的一个。
FROM SOURCE KEY 标识了我们为之建索引的类型。在这个例子中,我们使用类型 MPIMAGE_T。语句 GENERATE KEY USING 引用负责为索引生成键的函数。请注意,函数 CreateKey 并没有以 SOURCE 数据类型为参数,相反,它以来自 SOURCE 类型的各个字段为参数。这一点非常重要,因为这样一来,就可能可以拥有一个带长数据类型的结构类型,而仍然可以通过对其中的非常数据类型建索引来对此结构类型建索引。CreateKey 的结果是通过 WITH TARGET KEY 收集的。这是存储在索引中的实际索引键。这种索引键可以有 16 列宽,最大宽度为 1020 字节。这些限制并不是直接施加在索引扩展上的。索引扩展是在 Btree 索引之上发展而来的,因此 Btree 在列的数量和键的宽度等方面的限制对于索引扩展技术仍然有影响。
然后可以看到术语 SEARCH METHODS(搜索方法)。一个索引扩展可以有一组搜索方法。搜索方法是一个范围生成器、一个可选的过滤函数或者过滤 case 语句的集合。每个搜索方法都有一个名称和一个查询参数。搜索方法名用于在将来使用索引扩展的函数中引用特定的范围生成器和过滤函数组。
对创建索引扩展的总结
索引扩展定义封装了两个索引前(pre-index)步骤:一个是 CreateKey,一旦将数据添加到一个某列上有索引扩展索引的表中,该函数就立即执行;一个是搜索范围生成器,该函数将创建搜索前(pre-search)范围,在搜索索引时引擎要用到这些范围。当把数据插入到表中时,由 CreateKey 函数生成的键将被存储到内部 Btree 索引中。范围生成器函数(这个例子中是 CreateRange)在搜索被调用时,即查询时生成范围。一旦从范围生成器返回了一个范围,在 Btree 上就会马上执行实际的搜索。在 DB2 中没有方法可以扩展实际搜索方法。索引扩展定义还封装了一个索引后搜索步骤,即过滤函数或过滤 case 语句,过滤函数或 case 语句是在从索引扫描返回的行上执行的。之后执行结果被返回给调用者函数。
如何使用索引扩展
好了,我们已经定义了一个索引扩展,索引扩展的所有函数都有了,一切准备就绪。这只是第一步。第二步是用已经注册的索引扩展在一个实际的表上创建索引。表列必须有正确的类型,即您为之建索引的类型:
用索引扩展进行了扩展的实例 create index 语句
CREATE INDEX MYIDXEXT ON MYTABLE(A) EXTEND USING MPIMGINDEX(0);
如果表填满了数据,那么您将要很频繁地调用 CreateKey 函数(对于函数返回的每行至少要调用 4 次,因为这是表函数的最小操作:open、fetch、fetch 和 close)。
不过,要想能够使用索引,这还是不够。您可以使用自己想要的任何查询,之后将发现,如果没有使用过在函数定义中通过谓词子句绑定到索引的用户定义类型,那么您所使用的查询都不会使用新创建的索引。谓词子句需要既要提到索引扩展名,也要提到将要使用的搜索方法。谓词子句还必须指定要将搜索方法和索引扩展绑定到什么谓词上。
实例调用者函数
CREATE FUNCTION MYSEARCH(TARGET MPIMAGE_T, QUERY MPIMAGE_T)
RETURNS INTEGER
LANGUAGE C
NO EXTERNAL ACTION
PARAMETER STYLE SQL
DETERMINISTIC
CALLED ON NULL INPUT
ALLOW PARALLEL
SPECIFIC MYSEARCH
EXTERNAL NAME 'IdxExample!MySearch'
PREDICATES (
WHEN=1
SEARCH BY INDEX EXTENSION MPIMGINDEX
WHEN KEY(QUERY)
USE IMATCH(TARGET)
WHEN KEY(TARGET)
USE IMATCH(QUERY)
);
|
本节首先从 PREDICATES(展示函数和索引之间的链接)开始。这个函数有一个谓词 WHEN = 1,它表明在调用指定谓词 =1 的函数时,将用到这个索引:
使用谓词子句的实例
SELECT COUNT(*) FROM MYTABLE AS x WHERE MYSEARCH(x.a,LoadImage('c:\\Development\\IdxExample\\Images\\pic5.png'))=1;
DB2 引擎看到一个这样的查询,它将看到谓词,并且会查看 MYSEARCH 函数的定义。如果 MYSEARCH 函数有一个 PREDICATES 子句,有像我们现在所使用的那样的匹配谓词,并且像这个例子中我们所使用的表 mytable 一样,有一个类型为 MPIMAGE_T 的列 a,其索引扩展名为 MPIMGINDEX,并且在执行期间调用的所有函数都标志为 DETERMINISTIC,那么使用索引扩展,根据来自 QUERY 对象的范围来提取所有的值。在这个例子中,搜索范围将从由 LoadImage 构造函数返回的对象产生。一旦执行了索引搜索和过滤函数,结果集将返回给 MySearch,以作最后的计算。如果有一行是感兴趣的,那么返回 1,这时谓词就满足了,如果没有发现感兴趣的行,则返回 0。查询的计数计算经过索引和函数计算之后还剩下的行的数目。
如果这些测试还不够的话,您可以添加一个筛选前(pre-screening)函数到 create function 语句的谓词子句,以便在调用 MySearch 内部函数之前进一步除去候选行。在实例代码中(在 extra.db2 中)就有这样的在谓词子句中具有筛选前函数的调用者函数的例子。
对索引扩展主要特性的总结:
- 它允许对 复杂的用户定义类型(结构类型)建索引。
- 在创建一个索引扩展时,创建一个索引功能,您想要在多少表中使用它都可以。
- create index 语句实际上与传统索引是一样的。
- 索引扩展是用于用户定义函数的索引。它只能通过用户定义函数来使用。
- 它是基于搜索谓词的。要使用的搜索方法是根据在查询中所使用的谓词来决定的。
- 索引扩展扩展了诸如键创建之类的功能,以及可供搜索的内容。实际的索引功能(Btree 中的存储,对键排序,搜索,等等)是以具有良好性能、经过认真测试的标准组件实现的。这种方法的关键思想是,不要只是因为自己想使用什么就自己重新去发明它。
- 当创建索引扩展时,可选地,您可以传入一些能为范围生成器和键创建器所用的值。例如对于特定的表,这可用于为您所感兴趣的范围定义边界框。
最小索引扩展(或者接近于只做索引搜索)有一个 MySearch 函数,该函数不做任何实际的处理,而只是为从索引返回的所有对象返回 1,另外,既然过滤函数或者 case 语句是可选的,最小索引扩展还有一个不带过滤函数或 case 语句的索引扩展。
所有这些处理函数的好处当然是避免对太多的对象做过重的操作,因为那样肯定会降低性能。
保证能够使用索引扩展的关键因素是在 SQL 语句中使用的函数的确定性标志(deterministic flag)。如果有什么可以真正使您的建索引工作比较困难,并且没有表明原因,那么一定是您正在使用一个函数作为没有确定性标志的语句的一部分。在那种情况下,服务器干脆不使用索引扩展,它不关心您指定了多高的选择性,而总是选择进行一次表扫描。相信我,我们已经自己完成了。因此,永远要记得指定,如果传入了相同的信息,那么函数将返回相同的结果。
执行得如何呢?
索引性能的关键所在是索引能多大程度地避免返回太多的行。索引表扫描可能要比直接对表进行表扫描慢得多,因为后一种情况避免了必须经历所有这些麻烦的窘境。如果您有一个索引,并且即使是对于最具选择性的查询,也不得不返回大量的数据,那么您将碰到性能方面的问题,通常来讲您的键和用于计算对象的算法需要一些额外的工作。索引扩展方法好的地方就在于,范围生成器可以为您提供大致的结果集,允许您多选择一点,然后利用过滤函数发现真正感兴趣的行。这使得索引创建器的工作更加轻松,因为您所使用的键再也不必像平常那样惟一。这是因为您可以对从索引扫描返回的结果集应用附加的算法。
索引扩展在查询时的性能与以下因素有关
- 从查询对象生成范围所需的时间。
- 范围对索引有多大的选择性(实际索引搜索)。
- 过滤函数过滤行的本领如何,以及过滤函数的执行时间。
- 有多少行返回给 Search Function,以及 Search Function 的执行时间。
实例代码
现在对于索引扩展的基本情况您已经摸得比较清楚了。实例代码展示了索引扩展的一个简单的实现,但是其应用领域却是相当的复杂,即图像索引(image indexing)。为了创建一个索引机制,index.cpp 中的所有的函数都是必需的。要了解索引扩展的更高级的实例以及附加的过滤函数,请查看 extra.cpp 文件。我还提供了一组支持函数,这些函数是属于复杂“一类”的,它们用到 glue 库,后者用 OpenCV 处理一切工作,OpenCV 是由 Intel 开发的开放源代码图像库。不管 glue 库里面怎样,我们总算有了令人感兴趣的索引领域的一个完整的功能演示,而不只是与现实世界毫无瓜葛的一个演示。您应该关注 SQL 文件,创建索引扩展的格式(index.cpp),标量函数(大多数函数在 UDF.cpp 中)的形式是怎样的,它如何工作,以及方法是如何工作的,如何使用方法。(请浏览 SQL-file + 以查看 UDF.cpp 中的 GetRectangles)
构建指令以及如何获得 OpenCV,如 IdxExample 文件夹下的 readme.txt 文件中所述。
请阅读 免责声明。这决不是生产安全代码,充其量也只能认为这是一个原始的、还有很多 bug 的实例。请不要在生产环境中尝试/使用此 extender。IBM 和/或本代码和文档的作者对本代码的质量以及本代码对您的数据库引擎可能造成的影响不负任何责任。
我还提供了一个空的项目,其中具有一个索引扩展的所有部分,您可以按照自己的意愿进行定制。开发索引扩展的关键是对用户定义函数的理解以及如何用 C/C++ 开发它们。为了开发一个用户定义函数,实际上只需使用 include 文件 sqludf.h,如果需要从 UDF 访问数据库,则还需链接那些为您提供 CLI/ESQL 支持的库,这是可选的。要了解更多关于如何开发用户定义函数的信息,请参阅 IBM DB2 Server Side Programming Manual。
调试 IBM DB2 Extender
开发用户定义函数和用户定义方法并非总是轻而易举。当您修改数据库处理某一特定数据类型的方式,以及添加处理功能到数据库引擎时,是要负很大责任的。不过这样所得到的也很多,因为您扩展了自己的 SQL 能力,避免了不得不在每一个使用数据库的应用程序中都实现相同功能这样的窘境。
如果您阅读了 IBM DB2 的手册,他们都会说,如果您想要调试 extender,那么就编写一个模拟服务器的装载应用程序,然后将调试器接到那个应用程序上去。仔细一想就会清楚,对于大多数开发人员来说,工作时的情景并非如此。如果不会调试,又如何去开发应用程序呢?还好,他们说的是错的。您完全可以调试 extender,并且相当容易。我将给出的例子是基于 Windows 的,但您应该能够在 UNIX 上同样轻松地调试 extender。
- 查看服务器配置,确保 extender 设置为保留在内存中。
- 确保您想要调试的 extender 都已经定义为 fetched ,如果没有,那么就必须挂接上另一个进程,后面会有更多介绍。
- 为 extender 创建一个装载函数。最有用的装载函数是可以确保将 extender 装入内存的装载函数。我一直保留着一个版本的函数,这也正好构成了一个极好的装载函数。
- 用包括的调试信息构建 extender。
- 用装载函数装载 extender。
- 将一个调试器附到 db2fmp.exe 进程上。在 Windows 下,这个进程运行所有 fetched 用户定义函数。可能在同一时刻会有不止一个的 db2fmp.exe 正在运行,所以我建议您使用一个好的进程浏览器,例如 www.sysinternals.com Process Explorer,以便识别用于装载 extender 的 db2fmp.exe 的 PID。如果 extender 被定义为 not fenced,则服务器进程(db2syscs.exe)将装载 extender。
- 装载源文件,设置断点(breakpoint)。
- 调试...
结束语
本文对作为索引扩展一部分的所有组件,以及这些组件如何通过合作创建出一个非常灵活而有用的索引技术,给出了完整的描述。关于用户定义结构类型和索引扩展有太多要讲的东西。我们甚至还没有接触性能调优,从某种意义上讲,性能调优与常规索引(专用缓冲池,等等)是一样的。索引扩展实际上可以充当一个索引集,对此我们也没有谈什么。但事实确实如此!只需作些设计,您也可以办到,使用一对具有不同功能的索引,通过让 Create Key 创建不止一个的键行,在同一列上使用,将不同的范围应用于不同的键。当然,还有些工作需要做,不过已经有基础在这。
不要害怕对什么东西尝试其功能以外的情况。这是建立在 IBM DB2 的面向对象特性基础上的索引机制,它健壮,设计良好。通过使用更好的、更接近匹配信息系统领域的类型,使用和设计应用程序就变得更容易了,也降低了迷失于数据当中的风险。
词汇表
Btree 索引(Btree index): 非常有效的索引机制,将键值(磁盘上的位置)对存储在一个树结构中。 请参阅 IBM 文档
调用者函数(Caller Function) 在函数定义中通过谓词定义与一个索引扩展绑定的一个标量函数。
基数(Cardinality) 表函数的标志,用于指示查询优化器函数预备返回多少行。
比较操作符(Comparison Operator) 当将某类型的一个值与另一个值相比较时,系统使用一组用于该类型的已生成的函数: = 等于,!= 不等于,> 大于,< 小于,>=大于或等于,<=小于或等于。 这次操作符只能由数据库引擎定义。
确定性的(Deterministic) 函数和方法的标志,表明如果传入相同的数据,那么可期待得到相同的结果。此标志对于索引扩展功能非常重要,因为如果您没有指出正在使用的函数是确定性的,换句话说就是不是变量,那么索引扩展将不被使用。
外部函数(External Function) 用 SQL 之外的另一种语言编写的函数,通过一条函数定义语句集成到 DB2 中。外部函数可以包含 SQL,可以执行 SQL 语句,但是只能是只读的 SQL 语句。
过滤 case 语句/函数 在返回结果集到调用者函数之前,由索引扩展执行的索引后搜索步骤。这里是添加对键的更详细的分析的好地方。
索引扩展(Index Extension) 标准 Btree 索引机制下的索引,使得对用户定义类型建索引成为可能。它还让开发人员可以以自己的功能性来扩展索引功能的重要部分,例如键创建和搜索条件。索引扩展总是与绑定到索引的调用者函数一起使用。
用户定义方法(User Defined Method) 绑定到用户定义结构类型的方法。这些方法可以在 SQL 中通过 SELR 关键字访问内部类型对象。在 method 方法中,此函数的第一个参数是内部类型字段。方法可以返回自己,请查看实例代码中的 GetRectangles 方法。
External Action 控制有关在内部如何对待函数的某些方面的函数标志。要了解更多有关这个标志的信息,请参阅 IBM DB2 SQL Manual 1 和 2。
Parameter Style 函数的标志,表明数据如何在引擎与外部函数之间传递。标志风格是 SQL。
查询对象(Query object) 与索引扩展的调用方式有关。在整篇文章中,我都将从表中传入的 UDT 与用作索引扩展的参数的实例区分开来。不管调用者函数中使用何种顺序,查询实例化类型都将被传递给范围生成器,以便我搜索生成搜索范围。
范围生成器(Range Producer) 索引扩展中负责创建搜索范围的表函数的更别致的名称。
标量函数(Scalar Function) 只返回一个值的函数。
Scratchpad 由数据库引擎分配的一块内存区域,在整个查询执行期间都保留。该内存区域使开发人员能够保存调用之间的信息,是表函数的一个基本标志。当使用 SCRATCHPAD 关键字指定一个函数,而没有指定长度时,它将分配一块 100 字节的内存区域。可分配的最大宽度为 32000 字节。
SELF AS RESULT 用于方法的关键字,表明该方法将从函数返回自身。请查看实例代码中 GetRectangles 的实现,以了解更多信息。
存储过程(Stored Procedure) 一种特殊的函数,可以修改数据库中的数据。存储过程只能通过 call 语句调用。
表函数(Table Function) 可以在查询中充当一个表的函数。
目标对象(Target Object) 与索引扩展的调用方式有关。在整篇文章中我都将从表中传入的 UDT 与用作索引扩展的参数的实例区分开来。不管调用者函数中使用的顺序是什么,目标对象代表结构类型的列,经过一次搜索之后,实例将在这里从索引返回。
转换组(transform group) 一组函数或方法,用于在 SQL 与任何其他事物之间传递用户定义结构类型。DB2 使用的缺省转换组为:DB2_PROGRAM,用于在没有指定转换组的情况下给出一个 UDT,以及 DB2_FUNCTION,即在 SQL 与外部函数之间传递数据时使用的缺省转换组。
用户定义类型(User Defined Type) 不同类型的集合的名称,用户可以自己定义。
用户定义独特类型(User Defined Distinct Type) 对被视作惟一类型的一种内部类型和长度的别称。
用户定义结构类型(User Defined Structured Type) 可以包含不止一个基本类型,并且有可选的方法的一种类型。这是 IBM DB2 对象关系特性的一种基础类型,也是索引扩展的基础。 |