fastdb
fastdb 简介查询语言一、介绍
FastDb 是高效的内存数据库系统,具备实时能力及便利的C++接口。FastDB 不支持
client-server 架构因而所有使用FastDB的应用程序必须运行在同一主机上。FastDB针对应
用程序通过控制读访问模式作了优化。通过降低数据传输的开销和非常有效的锁机制提供了
高速的查询。对每一个使用数据库的应用数据库文件被影射到虚拟内存空间中。因此查询在
应用的上下文中执行而不需要切换上下文以及数据传输。fastdb中并发访问数据库的同步机
制通过原子指令实现,几乎不增加查询的开销。fastdb假定整个数据库存在于RAM中,并且
依据这个假定优化了查询算法和接口。此外,fastdb没有数据库缓冲管理开销,不需要在数
据库文件和缓冲池之间传输数据。这就是fastdb运行速度明显快于把数据放在缓冲池中的传
统数据库的原因。
fastdb 支持事务、在线备份以及系统崩溃后的自动恢复。事务提交协议依据一个影子根页面
算法来自动更新数据库。恢复可以执行得非常快,为临界应用提供了高可用性。此外,取消
事务日志改进了整个系统的性能,并且使得可以更有效的利用系统资源。
fastdb 是一个面向应用的数据库,数据库表通过应用程序的类信息来构造。fastdb支持自动
的模式评估,使你可以只需要在一个地方更改-你的应用程序的类。fastdb 提供一个灵活方
便的接口来从数据库中获取数据。使用一个类sql 的查询语言进行指定的查询。通过一些后
关系特性如非原子字段,嵌套数组,用户定义类型和方法,对象间直接引用简化了数据库应
用程序的设计并使之更有效率。
尽管fastdb的优化是立足于假定整个数据库配置在计算机的物理内存中,但是也有可能出现
使用的数据库的大小超过了系统物理内存的大小的情况,在这种情况下标准的操作系统交换
机制就会工作。但是整个fastdb 的搜索算法和结构是建立在假定所有的数据都存在于内存中
的,因此数据换出的效率不会很高。
查询语言
fastdb 支持一个类sql 句法的查询语言。fastdb使用更流行于面向对象程序设计的表达式而
不是关系数据库的表达式。表中的行被认为是对象实例,表是这些对象的一个类。与sql 不
同,fastdb 面向的是对对象的操作而不是对sql 元组。所以每一次查询的结果就是来自一个
类的对象的集合。fastdb 查询语言与标准sql 的主要差别在于:
1.不支持多个表之间的连接(join)操作,不支持嵌套子查询。查询总是返回来自一个表的
对象的集合。
2.原子表列使用标准的c 数据类型
3.没有NULL 值,只有null 引用。我完全同意C.J.Date对3 值逻辑的批评以及他使用缺省
值来代替NULL 的意图
4.结构和数组可以作为记录元素。一个特别的exists算子(quantor)用来定位数组中的元素
5.可以为表记录(对象)也可以为记录元素定义无参用户自定义方法,
6.应用程序可以定义只有一个串或者数值类型参数的用户自定义函数
7.支持对象间的引用,包括自动支持逆引用
8.通过使用引用,start from follow by 执行递归的记录遍历。
9.因为查询语言深度继承在了c++类中,语言标识符和关键字是大小写敏感的
10. 不进行整形和浮点型到串的隐含转换,如果需要这样的转换,必须显式进行
下面类 BNF表达式的规则指定了Fastdb查询语言搜索断言的语法:
Grammar conventions
example Meaning
expression non-terminals
not terminals
| disjoint alternatives
(not) optional part
{1..9} repeat zero or more times
select-condition ::= ( expression ) ( traverse ) ( order )
expression ::= disjunction
disjunction ::= conjunction
| conjunction or disjunction
conjunction ::= comparison
| comparison and conjunction
comparison ::= operand = operand
| operand != operand
| operand <> operand
| operand < operand
| operand <= operand
| operand > operand
| operand >= operand
| operand (not) like operand
| operand (not) like operand escape string
| operand (not) in operand
| operand (not) in expressions-list
| operand (not) between operand and operand
| operand is (not) null
operand ::= addition
additions ::= multiplication
| addition + multiplication
| addition || multiplication
| addition - multiplication
multiplication ::= power
| multiplication * power
| multiplication / power
power ::= term
| term ^ power
term ::= identifier | number | string
| true | false | null
| current | first | last
| ( expression )
| not comparison
| - term
| term [ expression ]
| identifier . term
| function term
| exists identifier : term
function ::= abs | length | lower | upper
| integer | real | string | user-function
string ::= ' { { any-character-except-quote } ('') } '
expressions-list ::= ( expression { , expression } )
order ::= order by sort-list
sort-list ::= field-order { , field-order }
field-order ::= [length] field (asc | desc)
field ::= identifier { . identifier }
traverse ::= start from field ( follow by fields-list )
fields-list ::= field { , field }
user-function ::= identifier
标识符大小写敏感,必须以一个a-z,A-Z ,_ 或者$字符开头,只包含a-z, A-Z, 0-9,_或
者$字符,不能使用SQL 保留字。
保留字列表
abs and asc between by
current desc escape exists false
first follow from in integer
is length like last lower
not null or real start
string true upper
可以使用ANSI标准注释,所有位于双连字符后直到行末的字符都将被忽略掉。
fastdb 扩展了ansi标准sql操作符,支持位运算。and/of操作符不仅可以运用到布尔操作
数也可以操作整形操作书。and/or运用到整形操作数返回的结果是一个整形值,这个值是对
操作数进行按位and 或者按位or 得到的结果。对于小的集合位运算是高效的。fastdb 也支
持对整形和浮点型的升幂运算(x^y)
structures
fastdb 接受结构体作为记录的元组。结构的字段可以通过标准的点表达式访问:
company.address.city 结构体的字段可以索引,从而可以按照指定的序列使用。结构体可以
包含其他的结构体作为其元组,嵌套深度没有限制。程序员可以为结构体定义方法,这些方
法可以用在查询中,与对普通结构元组的句法是一样的。这些方法除了一个指向其隶属的对
象的指针外(C++中的this 指针)不能有参数,并且返回原子类型(bool 型,数值、字符串
或者引用类型)。这些方法也不应该改变对象实例(immutable method).如果方法返回字符
串,该字串必须用new 字符操作符分配,因为该字串值拷贝之后就会被删掉。因此用户自定
义方法可以用来创建虚元组-不是保存在数据库中而是使用其他元组计算出来的元组。例如:
fastdb 的dbDateTime 类型只包含整形时间戳元组和象dbDateTime::year(),
dbDateTime::month()...这样的方法。因此可以在应用中指定象"delivery.year = 1999"这
样的查询,其中delivery 记录字段拥有dbDateTime类型。方法在应用的上下文中执行,在
其中定义,对其他应用和交互SQL 是无效的
Arrays
fastdb 接受动态数组作为记录元组,不支持多维数组,但可以定义数组的数组,可以按照结
果集中数组字段的长度对记录排序。fastdb提供了一个特别的构造集来处理数组:
1.可以用length()函数来取得数组中元素的数目。
2.数组元素可以用[]操作符来获取。如果索引表达式超出了数组范围,将产生异常
3.in 操作符可以用来检查一个数组是否包含有一个由左操作书指定的值。该操作只能用于
原子类型的数组:boolean , 数值,引用和字符串。
4.数组可以用update方法更新,该方法复制数组然后返回一个非常量的引用。
5.使用exists 运算符迭代数组元素。exists 关键字后指定的变量可以作为在exists 算子
后面的表达式中的数组的索引。该索引变量将迭代所有可能的数组索引值,直到表达式的值
为true 或者索引越界,下面的情况:
exists i: (contract[i].company.location = 'US')
将选择由位于‘US'的公司载运的合同的所有细节,而下面的查询:
not exists i: (contract[i].company.location = 'US')将选择由'us'之外的公司载运的合
同的所有细节可以由嵌套的exists 子句。使用嵌套exist算子等同于使用相应的索引变量的
嵌套循环。例如查询 exists column: (exists row: (matrix[column][row] = 0))将选择
matrix 字段的元素为0 的所有记录,该字段拥有整形数组的数组数据类型。这个构造等同于
下面的两层嵌套循环:
bool result = false;
for (int column = 0; column < matrix.length(); column++)
{
for (int row = 0; row < matrix[column].length(); row++)
{
if (matrix[column][row] == 0)
{
result = true;
break;
}
}
}
Strings
fastdb 中的所有字符串都是变长的因此程序员无需费心去指定字符字段的最大长度,所有对
数组适用的操作也适用于字符串。此外字符串也有属于自己的操作集。首先,字符串可用标
准关系运算符相互比较。目前,fastdb 只支持ascii 字符集(对应于c 的char 类型)以及
对字符串逐字节的比较而忽略本地设置。like 运算符可以通过一个包含通配符'%'和'_'的模
式来匹配字符串, 。'_'字符匹配任意的单个字符,'%'匹配0 个或多个字符。like 运算符
的一个扩展形式是与escape 关键字一起用来处理模式中的'%'和'_'字符,如果他们出现在一
个特定的逃逸字符之后(指escape 关键字)就被当作普通字符处理而不是通配符。可以用
in 操作符在字符串中查找子串。表达式('blue' in color)对于所有包含color字段包含
'blue'的记录都为真。如果被查找的字符串的长度大于某个门槛值(当前为512),则使用
boyer-moore子串查找算法而不是直接查找方式。字符串可以用+或者||运算符进行连接,后
者是为了与ansi sql标准的兼容性而加入的。由于fastdb不支持在表达式中隐含的字符串
转换,因此+运算符的语义可以为字符串重新定义
引用可以用与访问结构元组同样的点表达式来解析,例如下面的查询:
company.address.city = 'Chicago'
将访问的Contract 记录的company 元组引用的supplier 表中的记录并展开其中的address
字段的city元组。引用可以用is null 或is not null 断言来检查。引用也可以互相比较是
否相等以及与null 关键字比较。解析null 引用时fastdb 将抛出异常。一个特别的关键字
current 可以用来在表查找时指向当前记录。通常current 关键字用来当前记录标志符与其
它引用的比较或者在引用数组中定位当前记录。
例如,下面的查询将在Contract 表中查找所有活动的合同
(假定cancelContracts 的数据类型为dbArray<dbReference<Contract>>)
current not in supplier.canceledContractsfastdb 提供特别的运算符以通过引用来递归
遍历记录
start from root-references
( follow by list-of-reference-fields )
这个结构的第一部分用来指定根对象,无结束符的root-references应该是一个引用变量或
者一个引用数组类型的变量。这里可以使用两个特别的关键字first和last,分别用来定位
表中第一个或最后一个记录。如果要要检查一个引用数组或者某些情况下一个引用字段引用
的所有记录,这个结构可以无需follow 部分。如果指定了follow by 部分,fastdb 将递归
遍历表中的记录,从根引用开始,使用list-of-reference-fields 在记录间转换。
list-of-reference-fields 应当由引用字段或者引用数组。遍历是按照顶-左-右顺序的层次
遍历(首先访问父结点然后是从左到右顺序的兄弟结点)。当遇到null 引用或者一个已经被
访问过的纪录的引用时递归终止。例如下面的查询将按照TLR 顺序在一棵记录树中查找
weight 大于1 的记录:
"weight > 1 start from first follow by left, right"
对于下面的树:
A:1.1
B:2.0 C:1.5
D:1.3 E:1.8 F:1.2 G:0.8
查询结果将是:('A', 1.1), ('B', 2.0), ('D', 1.3), ('E', 1.8), ('C', 1.5), ('F', 1.2)
正如已经提到过的fastdb 总是处理对象并且不支持连接。连接可以用引用来实现。考虑经典
的Supplier-Shipment-Detail例子:
struct Detail
{
char const* name;
double weight;
TYPE_DESCRIPTOR((KEY(name, INDEXED),
FIELD(weight)));
};
struct Supplier
{
char const* company;
char const* address;
TYPE_DESCRIPTOR((KEY(company, INDEXED), FIELD(address)));
};
struct Shipment {
dbReference<Detail> detail;
dbReference<Supplier> supplier;
int4 price;
int4 quantity;
dbDateTime delivery;
TYPE_DESCRIPTOR((KEY(detail, HASHED), KEY(supplier, HASHED),FIELD(price),
FIELD(quantity), FIELD(delivery)));
};
我们打算获得某些特定供应商的供应的某些特定细节。在关系数据库中这种查询将会写成这
样:
select from Supplier,Shipment,Detail where Supplier.SID = Shipment.SID and
Shipment.DID = Detail.DID and Supplier.company like ? and Supplier.address like ?
and Detail.name like ?
fastdb 中将会写成这样:
dbQuery q = "detail.name like",name,"and supplier.company like",company,"and
supplier.address like",address,"order by price";
fastdb 将首先在表Detail 中进行索引查找获得匹配查找条件的记录。然后在所选的记录中
再进行一次索引查找以定位装载记录。然后对剩余的选择断言进行顺序查找。
Functions
Predefined functions
Name Argument type Return type Description
abs integer integer absolute value of the argument
abs real real absolute value of the argument
integer real integer conversion of real to integer
length array integer number of elements in array
lower string string lowercase string
real integer real conversion of integer to real
string integer string conversion of integer to string
string real string conversion of real to string
upper string string uppercase string
fastdb 允许用户自定义函数和运算符。函数应当至少有一个但不超过3 个参数,参数类型可
以是字符串、整形、布尔型、引用或者用户定义(源二进制)类型。返回值应当是整形、实
数、字符串或者布尔型。用户定义函数应当用USER_FUNC(f)宏来注册,该宏创建一个
dbUserFunction 类的静态对象,把函数指针和函数名绑定。在应用中有两种方式来实现这些
函数。第一个只能用于只有一个参数的函数,这个参数必须是int8、real8 或者char * 类
型。函数返回值应当是int8、real8、char* 或者bool。如果函数有不止一个参数或者接受
不同类型的参数(多形)则参数应当以引用的方式传送给dbUserFunctionArgument 结构。这个
结构包含一个type 字段,其值可以用在函数实现中检测传入的参数类型并且与参数值结合。
下表映像了参数类型以及参数值的取值。
Argument type Argument value Argument value type
dbUserFunctionArgument::atInteger u.intValue int8
dbUserFunctionArgument::atBoolean u.boolValue bool
dbUserFunctionArgument::atString u.strValue char const*
dbUserFunctionArgument::atReal u.realValue real8
dbUserFunctionArgument::atReference u.oidValue oid_t
dbUserFunctionArgument::atRawbinary u.rawValue void*
例如下面语句使得可以在sql语句中使用sin 函数。
#include <math.h>
...
USER_FUNC(sin);
函数只能在定义的应用中使用。函数对于其他应用和交互sql 是不可访问的。在返回字符串
的函数中,返回的字符串必须用new 运算符复制,因为fastdb 在拷贝完返回值后将调用析构
函数。在fastdb,函数的参数可以(当不是必须)用圆括号括起来,因此下面的表达式都是
合法的:
'$' + string(abs(x))
length string y
带两个参数的函数也可以当作运算符。考虑下面的例子,其中定义了函数contains进行大小
写敏感的字串查找。
bool contains(dbUserFunctionArgument& arg1, dbUserFunctionArgument& arg2)
{
assert(arg1.type == dbUserFunctionArgument::atString && arg2.type ==
dbUserFunctionArgument::atString);
return stristr(arg1.u.strValue, arg2.u.strValue) != NULL;
}
USER_FUNC(contains);
dbQuery q1, q2;
q1 = "select * from TestTable where name contains 'xyz'";
q2 = "select * from TestTable where contains(name, 'xyz')";
在这个例子中,查询q1 和q2是等价的。
c++ 接口
fastdb 主要的目标之一就是提供一个灵活并且方便的应用语言接口。任何使用过odbc 或者
类似的sql接口的人会明白我说的是什么。在fastdb中,一个查询可以用c++写成下面的样
子:
dbQuery q;
dbCursor<Contract> contracts;
dbCursor<Supplier> suppliers;
int price, quantity;
q = "(price >=",price,"or quantity >=",quantity,
") and delivery.year=1999";
// input price and quantity values
if (contracts.select(q) != 0) {
do {
printf("%s\n", suppliers.at(contracts->supplier)->company);
} while (contracts.next());
}
Table
fastdb 中的数据保存在表中,这些表对应于c++类,其中表记录对应于类实例。下面的c++
数据类型可以作为fastdb 记录的原子组件:
Type Description
bool boolean type (true,false)
int1 one byte signed integer (-128..127)
int2 two bytes signed integer (-32768..32767)
int4 four bytes signed integer (-2147483648..2147483647)
int8 eight bytes signed integer (-2**63..2**63-1)
real4 four bytes ANSI floating point type
real8 eight bytes ANSI double precision floating point type
char const* zero terminated string
dbReference<T> reference to class T
dbArray<T> dynamic array of elements of type T
除了上表定义的数据类型外,fastdb 记录还可以包括这些元组的嵌套结构。fastdb不支持无
符号数据类型以简化查询语言,清除由于符号数/无符号数比较产生的错误,减少数据库引擎
的大小。不幸的是c++没有提供在运行时获得一个类的元信息(metainformation)的方法
(RTTI 并不被所有编译器支持,并且也不能提供足够的信息)。因此程序员必须明确枚举包
含在数据库表中的类字段(这也使得在类和表之间的映像更为灵活)。fastdb 提供了一个宏
和相关的类的集合来使得这个映像尽可能的灵活。每一个要在数据库中使用的c++类或者结
构,,都包含一个特别的方法来描述其字段。宏TYPE_DESCRIPTOR(field_list)构成了这个方
法。这个宏的用括号括起来的单一参数是一个类字段描述符的列表。如果要为这个类定义一
些方法并使之可以用于对应的数据库,则用宏CLASS_DESCRIPTOR(name, field_list)来代替
TYPE_DESCRIPTOR。需要类名来取得成员函数的引用。
下面的宏可以用来构造字段描述符。FIELD(name)指定名字的非索引字段KEY(name,
index_type)索引字段。index_type 必须是HASHED 和INDEXED 标志的组合。当指定HASHED
标志的时候,fastdb 将为是用这个字段作为关键字的表创建一个hash 表。当指定INDEXED
标志时,fastdb将创建为使用这个字段作为关键字的表创建一个T_tree(一种特殊的索引).
UDT(name, index_type, comparator)用户自定义原始二进制类型。数据库把这种类型作为指
定大小的字节序列处理。这个字段可以用来查询(比较下同一类型的查询参数),可以通过
order 子句来索引和使用。通过程序员提供的comparator 函数来进行比较操作。比较函数接
受3 个参数:两个指向待比较的原始二进制对象的指针及其大小。index_type 的语义与KEY
宏中的一致。RAWKEY(name, index)带有预定义比较算子的原始二进制类型。这个宏只是一个
把memcmp 作为比较算子的UDT宏的特例。RAWFIELD(name)另一个UDT宏的特例,使用memcmp
作为预定义比较算子,并且没有索引。SUPERCLASS(name)指定当前类的基类(父亲)的信息。
RELATION(reference, inverse_reference) 指定类(表)之间的一对一、一对多或者多对多
的关系。reference 和inverse_reference 字段都必须是引用或者引用数组类型。
inverse_reference字段是一个包含了指向当前表的逆引用的引用表。逆引用自动由fastdb
更新并用于查询优化。OWNER(reference, inverse_reference) 指定类之间的一对一、一对
多或者多对多的owner-member关系。当owner记录被删除时所有引用的member 记录也会被
删除(层叠式删除)。如果member记录要引用owner就必须通过RELATION宏声明。METHOD(name)
为类指定一个方法。该方法必须是无参的实例成员函数,返回bool 值、数值、引用或者字符
串类型。方法必须在类的所有属性之后指定。尽管只有原子字段可以被索引,但可以为一个
结构指定一个索引类型。只有当该索引类型在该结构的索引mask中指定时才会为该结构的成
员创建。这就允许程序员可以根据该结构在记录中的角色来设置或者取消索引。
下面的例子说明了头文件中类型描述符的创建过程:
class dbDateTime {
int4 stamp;
public:
int year() {
return localtime((time_t*)&stamp)->tm_year + 1900;
}
...
CLASS_DESCRIPTOR(dbDateTime,
(KEY(stamp,INDEXED|HASHED),
METHOD(year), METHOD(month), METHOD(day),
METHOD(dayOfYear), METHOD(dayOfWeek),
METHOD(hour), METHOD(minute), METHOD(second)));
};
class Detail {
public:
char const* name;
char const* material;
char const* color;
real4 weight;
dbArray< dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(name, INDEXED|HASHED),
KEY(material, HASHED),
KEY(color, HASHED),
KEY(weight, INDEXED),
RELATION(contracts, detail)));
};
class Contract {
public:
dbDateTime delivery;
int4 quantity;
int8 price;
dbReference<Detail> detail;
dbReference<Supplier> supplier;
TYPE_DESCRIPTOR((KEY(delivery, HASHED|INDEXED),
KEY(quantity, INDEXED),
KEY(price, INDEXED),
RELATION(detail, contracts),
RELATION(supplier, contracts)));
};
所有数据库中使用的类都要定义类型描述符。除了定义类型描述符外,还必须在C++类和数
据库表之间建立一个映像。宏REGISTER(name)就做这个工作。与TYPE_DESCRIPTOR 宏不同的
是,REGISTER 宏应该在实现文件中使用而不是在头文件中。该宏构造一个与类相连的表的描
述符。如果你要在一个应用中使用多个数据库,那么就可能使用REGISTER_IN(name,database)
宏在一个具体数据库中注册一个表。该宏的database 参数应该是一个指向dbDatabase 对象
的指针。
你可以像下面这样注册数据库的表:
REGISTER(Detail);
REGISTER(Supplier);
REGISTER(Contract);
表(以及对应的类)在每一时刻只能对应于一个数据库。当你打开一个数据库,fastdb向数
据库中导入所有在应用中定义的类。如果一个同名的类在数据库中已经存在,则会比较描述
符在数据库中的类与描述符在应用中的类,如果两个类的定义不同,则fastdb试图将该表转
换成新的格式。数值类型之间的任何转换(整形到实型,实型到整形,扩展或者截断)都是允
许的。增加字段也很容易,但是只有对空表才可以删除字段(以避免偶然的数据崩溃).装载所
有的类描述符后,fastdb 就检查在应用程序的类描述符中指定的索引是否存在于数据库中、
创建新的索引并且删除不再使用的索引。只有在不超过一个应用程序访问数据库是才可以进
行表的重构以及增加/删除索引。所以只有第一个与数据库关联的应用程序可以进行表的转
换,所有其余的应用只能向数据库中增加新类。有一个特殊的内部表Metatable,该表包含
了数据库中所有其他表的信息。C++程序员不需要访问这个表,因为数据库表的结构是由C++
类指定的。但在交互SQL 程序中,还是有必要检查这个表来获取记录字段的信息。从版本2.30
开始,fastdb支持自增字段(有数据自动赋值的值唯一的字段).要使用自增字段必须:
1.带上-DAUTOINCREMENT_SUPPROT 标志重新编译fastdb 和你的应用程序。(在fastdb
makefile 中的DEFS变量中增加这个标志)
注意:不带该标记编译的fastdb 创建的数据库文件与带标记编译的fastdb 创建的数据库文
件不兼容。
2.如果你要使用初始值非0的计数器,则必须给dbTableDescriptor::initialAutoincrementCount
赋个值。该变量由所有的表共享,因此所有的表都有一个共同初始值的自增计数器。
3.自增字段必须是int4 类型,并且必须用AUTOINCREMENT标志声明
class Record
{
int4 rid;
char const* name;
...
TYPE_DESCRIPTOR((KEY(rid,AUTOINCREMENT|INDEXED), FIELD(name), ...));
}
4.当要在数据库中插入带有自增字段的记录是不需要为自增字段赋值(将会被忽略)。当插入
成功后,该字段将被赋给一个唯一的值(这样确保在数据库中从未使用过).
Record rec;
// no rec.rid should be specified
rec.name = "John Smith";
insert(rec);
// rec.rid now assigned unique value
int newRecordId = rec.rid; // and can be used to reference this record
5.当记录被删除该值将不会再使用,当事务中止时,表的自增计数器也将回滚。
Query
query 类用于两个目的:
1.构造一个查询并绑定查询参数
2.作为已编译的查询的缓存
fastdb 提供重载的c++运算符'='和','来构造带参数的查询语句。参数可以在被使用的地方
直接指定,消除了在参数占位符和c 变量之间的任何映像,在下面的查询示例中,参数price
和quantity的指针保存在查询中,因此该查询可以用不同的参数执行多次。c++函数重载使
之可以自动的确定参数的类型,不需要程序员提供额外信息(从而减少了bug 的可能性).
dbQuery q;
int price, quantity;
q = "price >=",price,"or quantity >=",quantity;
由于char *可以用来指定一个查询的分片(fraction)(例如"price >=")和一个字符串类型的
参数,fastdb 使用了一个特别的规则来解决这个模糊性。该规则基于这样一个假定即没有理
由把一个查询文本分解成两个字符串如("price",">=")或者指定多于一个的参数序列
("color=",color,color).因此fastdb假定第一个字符串是该查询文本的一个分片并且随之
转换到操作数模式。在操作数模式中,fastdb 认为char * 参数是一个查询参数然后切换回
到查询文本模式,依此类推。也可以不用这个“句法糖”(syntax sugar)而是显示的通过
dbQuery::APPend(dbQueryElement::ElementType type, void const* ptr)方法来构造查询
元素。在向查询添加元素之前,必须通过dbQuery::reset()方法来重置查询('operator='
自动作了这个事情)。不能使用c++数值常量来作为查询参数,因为参数是通过引用来访问的。
但可以使用字符串常量,因为字符串时传值的。有两种方法在一个查询中指定字符串参数:
使用一个字符串缓冲或一个指向字符串指针的指针
dbQuery q;
char* type;
char name[256];
q = "name=",name,"and type=",&type;
scanf("%s", name);
type = "A";
cursor.select(q);
...
scanf("%s", name);
type = "B";
cursor.select(q);
...查询变量既不能作为一个参数传给一个函数也不能赋给另一个变量。当fastdb 编译
查询时,会把编译树存到该对象中。下一次使用该查询时,不需要再次编译并且可以使用已
编译好的树。这样节省了一些编译查询的时间。
fastdb 提供了两个方法来集成数据库中的用户自定义类型。第一种方法-定义类方法-已经讨
论过,另一个方法只处理查询构造。程序员要定义方法,该方法并不作确实的运算,而是返
回一个表达式(根据预先定义的数据库类型),该方法来执行必要的查询。最好通过例子来说
明这点。fastdb 没有内置的日期时间类型,而是使用一个普通的c++类dbDateTime。该类定
义了方法用来在有序列表中指定日期时间字段和使用通常的运算符来比较两个日期。
class dbDateTime
{
int4 stamp;
public: ...
dbQueryExpression operator == (char const* field)
{
dbQueryExpression expr;
expr = dbcomponent(field,"stamp"),"=",stamp;
return expr;
}
dbQueryExpression operator != (char const* field)
{
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<>",stamp;
return expr;
}
dbQueryExpression operator < (char const* field)
{
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),">",stamp;
return expr;
}
dbQueryExpression operator <= (char const* field)
{
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),">=",stamp;
return expr;
}
dbQueryExpression operator > (char const* field)
{
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<",stamp;
return expr;
}
dbQueryExpression operator >= (char const* field)
{
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<=",stamp;
return expr;
}
friend dbQueryExpression between(char const* field, dbDateTime&
from,dbDateTime& till)
{
dbQueryExpression expr;
expr=dbComponent(field,"stamp"),"between",from.stamp,"and",till.stamp;
return expr;
}
friend dbQueryExpression ascent(char const* field)
{
dbQueryExpression expr;
expr=dbComponent(field,"stamp");
return expr;
}
friend dbQueryExpression descent(char const* field)
{
dbQueryExpression expr;
expr=dbComponent(field,"stamp"),"desc";
return expr;
}
};
所有这些方法接受参数作为一个记录的字段的名字,该名字用来构造一个记录组件的全名。
使用类dbComponent来作这个事情,该类把结构字段的名字和结构组件的名字组合成一个用
'.'符号分隔的复合名字。类dbQueryExpression用来收集表达式项,表达式自动的用圆括号
括起来,消除了运算符优先级引起的冲突。
假定一个记录包含了一个字段dbDateTime 类型的字段delivery,可以如下构造查询:
dbDateTime from, till;
q1 = between("delivery", from, till),"order by",ascent("delivery");
q2 = till >= "delivery";
除了这些方法外,一些类指定方法也可以用这种方法定义,擂如一个区域类型的overlaps
方法。这种方法的好处是数据库引擎可以使用预定义的类型并且可以使用索引和其他的一些
优化方法来执行查询。另一方面,这些类的实现的封装已保留,因此程序员在一个类的表示
改变时不应该重写所有的查询。下面这些c++类型可以用作查询参数:
int1 bool
int2 char const*
int4 char **
int8 char const**
real4 dbReference<T>
real8 dbArray< dbReference<T> >
Cursor
游标用来访问选择语句返回的记录。fastdb提供了有类型的游标,也就是说,与具体表相关
的游标。fastdb 有两种游标:只读游标和用于更新的游标。fastdb 中的游标用c++模板类
dbCursor<T>来表示,其中T 为与数据库表相关的C++类的的名字。游标类型必须在构造游标
的时候指定。缺省创建一个只读游标。要创建一个用于更新的游标,必须给构造函数传递一
个dbCursorForUpdate 参数。
执行一个查询要么通过游标select(dbQuery &q)方法,要么通过select()方法,后者可以
迭代表中的所有记录。两个方法都返回中选的记录的数量,并且把当前位置置于第一个记录
(如果有的话)。游标可以前滚或者后滚。next(),prev(),first(),last()方法可以用来改变
游标的当前位置。如果由于没有(更多)的记录存在而使得操作无法进行,这些方法将返回
NULL,并且游标的位置不改变。
一个类T 的游标包含一个类T的实例,用来获得当前的记录。这就是为什么表类必须要有一
个缺省构造函数(无参数的构造函数)而没有副作用。fastdb 优化了从数据库中获取记录,只
从对象的固定部分复制记录。字符串本身并不复制,而是使相应的字段直接指向数据库中。
数组也是如此:他们的组件在数据库中的表示根在应用程序中的表示是一样的(标量类型的
数组或者标量组件的嵌套结构的数组).
应用程序不能直接改变数据库中的字符串和数据元素。当一个数组方法需要更新一个数组体
时,先在内存中创建一个副本然后更新这个副本。如果程序员要更新字符串字段,他应该给
这个指针赋一个新值,而不是直接修改数据库里的字符串。对于字符串元素建议使用char
const * 而不是char *,以使编译器可以检查对字符串的非法使用。
游标类提供了get()方法来获得指向当前记录(保存在游标中)的指针。重载的运算符' ->'
也可以用来访问当前记录的元素。如果一个游标一更新方式打开,就可以改变当前的记录并
且用update()方法保存到数据库中或者被删除掉。如果当前记录被删除,下一个记录变为当
前记录。如果没有下一个记录,则前一个记录(如果有的话)变为当前。removeAll()方法删
除表中的所有记录。而removeAllSelected方法只删除游标选择的所有记录。
当更新记录后,数据库的大小可能会增加。从而虚拟存储器的数据库区域需要进行扩展。该
重映射的后果之一就是,该区域的基地址可能发生变化,然后应用程序中保存的所有数据库
指针都变得无效。当数据库区域重映象时fastdb自动更新所有打开的游标中的当前记录。因
此,当一个数据库更新时,程序员应该通过游标的->方法来访问记录字段,而不应该使用指
针变量。当前选择使用的内存可以通过reset()方法来释放。该方法自动的由select()、
dbDatabase::commit()、dbDatabase::rollback()方法以及游标的销毁(destructor)函数
调用,因此大多数情况下不需要显式调用reset()方法。游标也可以通过引用来访问记录。
at(dbReference<T> const& ref)方法把游标指向引用所指的记录。在这种情况下,选择将只
包含一个记录,而next(),prev()方法将总是返回NULL。由于游标和引用在fastdb重视严格
类型化的,所有必须的检查可以有编译器静态的进行而不需要动态类型检查。运行时唯一要
进行的检查是对空引用的检查。游标中当前记录的对象标识符可以用currentId()方法获得。
可以限制select 语句返回的记录的数目。游标类有两个方法setSelectionlimit(size_t lim)
和unsetSelectionLimit()用来设置/取消查询返回的记录数的限制。在某些情况下,程序员
可能只需要几个记录或者头几个记录,从而查询的执行时间和消耗的内存大小可以通过限制
选择的大小来降低。但如果你指定了被选记录的排序方式,只选择k 个记录的查询并不返回
关键字最小的头k 个记录,而是返回任意k 个记录然后排序。于是所有数据库数据的操作都
可以通过游标来进行,唯一的例外是插入操作,fastDB提供了一个重载的插入函数:
template<class T>
dbReference<T> insert(T const& record);
该函数将在表的末尾插入一个记录然后返回该对象的引用。fastdb 中插入的顺序是严格指定
的因而应用程序可以使用表中记录排序方式的假定。因为应用程序大量使用引用在对象之间
导航,有必要提供一个根对象,从这个对象开始进行引用遍历。这样一个根对象的一个可取
候选者就是表中的第一个记录(也是表中最老的记录).该记录可以通过不带参数执行select
()方法来访问。游标中的当前记录就是表中的第一条记录
fastdb 的c++API 为引用类型定义了一个特殊的null 变量,可以用null 变量与引用比较或
者赋给一个引用:
void update(dbReference<Contract> c)
{
if (c != null)
{
dbCursor<Contract> contract(dbCursorForUpdate);
contract.at(c);
contract->supplier = null;
}
}
查询参数通常跟c++变量绑定。大多数情况下这是方便而且灵活的机制。但在多线程应用中,
无法保证同一查询会在同一时刻不被另一线程以不同的参数执行。一个解决的方法是使用同
步原语(临界区或者mutex)来排除查询的并发执行。但这种方法会导致性能退化。fastdb
可以并行操作读操作而提高了整体系统吞吐量。另一个解决方法是使用延迟参数绑定。如下
所示:
dbQuery q;
struct Queryparams
{
int salary;
int age;
int rank;
};
void open()
{
QueryParams* params = (QueryParams*)NULL;
q = "salary > ", params->salary, "and age < ", params->age, "and rank =",
params->rank;
}
void find(int salary, int age, int rank)
{
QueryParams params;
params.salary = salary;
params.age = age;
params.rank = rank;
dbCursor<Person> cusor;
if (cursor.select(q, ¶ms) > 0)
{
do
{
cout << cursor->name << NL;
}while (cursor.next());
}
}
在这个例子中open 函数只为结构中的字段偏移绑定查询变量。然后再find 函数中,指
向带有参数的结构的真实的指针传递给select 结构。find 函数可以被多个线程并发执行而
只有一个编译好的查询被所有这些线程使用。这种机制从版本2.25 开始使用。
Database
dbDatabase类控制应用与数据库的交互,进行数据库并发访问的同步,事务处理,内存分配,
出错处理...
dbDatabase对象的构造函数允许程序员制定一些数据库参数。
dbDatabase(dbAccessType type = dbAllAccess,
size_t dbInitSize = dbDefaultInitDatabaseSize,
size_t dbExtensionQuantum = dbDefaultExtensionQuantum,
size_t dbInitIndexSize = dbDefaultInitIndexSize,
int nThreads = 1);
支持下面的数据库访问类型:
Access type Description
dbDatabase::dbReadOnly Read only mode
dbDatabase::dbAllAccess Normal mode
dbDatabase::dbConcurrentRead Read only mode in which application can access the
database concurrently with application updating the same database in
dbConcurrentUpdate mode
dbDatabase::dbConcurrentUpdate Mode to be used in conjunction with dbConcurrentRead
to perform updates in the database without blocking read applications for a long time
当数据库已只读方式打开时,不能向数据库添加新的类定义,不能改变已有的类的定义和索
引。在数据库主要用只读模式访问而更新不应该长时间堵塞读操作的情况下应该同时使用
dbConcurrentUpdate 和dbConcurrentRead 模式。在这种模式下更新数据库可以与读访问并
发执行(读将不会看到改变的数据直到事务提交)。只有在更新事务提交时,才会设置排他锁
然后在当前对象索引自增改变(incremental change)之后马上释放掉。于是你可以使用
dbConcurrentRead 模式启动一个或多个应用而其读事务将并发执行。也可以使用
dbConcurrentUpdate 模式启动一个或多个应用。所有这些应用的事务将通过一个全局的
mutex 来同步。因此这些事务(甚至是只读)将排他性的执行。但是dbConcurrentUpdate模式
的应用的事务可以与dbConcurrentRead模式的应用的事务并发运行。
请参阅testconc.cpp例子,里边说明了这些模式的使用方法。
注意!不要把dbConcurrentUpdate 和dbConcurrentRead模式与其他模式混合使用,也不要
在一个进程中同时使用他们(因此不能启动两个线程其中一个用dbConcurrentUpdate模式打
开数据库另一个用dbConcurrentRead 模式)。在dbConcurrentUpdate 模式下不要使用
dbDatabase::precommit 方法。
dbInitSize参数指定了数据库文件的初始大小。数据库文件按照需要增长;设置初始大小只
是为了减少重新分配空间(会占用很多时间)的次数。在当前fastdb 数据库的实现中该大小
在每次扩展时至少加倍。该参数的缺省值为4 兆字节。dbExtensionQuantum 指定了内存分配
位图的扩展量子。简单的说,这个参数的值指定了再不需要试图重用被释放的对象的空间时
分配多少连续内存空间。缺省值为4MB.详细情况参见Memory allocation 小节。
dbInitIndexSize 参数指定了初始的索引大小。fastdb 中的所有对象都通过一个对象索引访
问。该对象索引有两个副本:当前的和已提交的。对象索引按照需要重新分配,设置一个初
始值只是为了减少(或者增加)重新分配的次数。该参数的缺省值是64K个对象标识符。
最后一个参数nThreads 控制并行查询的层次。如果大于1,则fastdb 启动一些查询的并行
执行(包括对结果排序).在这种情况下,fastdb 引擎将派生指定数目的并行线程。通常为该
参数指定超过系统中在线cpu数目的值是没有意义的。也可以为该参数传递0 值。
在这种情况下,fastdb 将自动侦测系统中在线cpu 的数目。在任何时候都可以用
dbDatabase::setConcurrency来指定线程数。
dbDatabase类包含一个静态字段dbparallelScanthreshold,该字段指定了在使用并行查询
后表中记录数的一个阈值,缺省为1000。
可以用open(char const* databaseName, char const* fileName = NULL, unsigned
waitLockTimeout = INFINITE)方法来打开数据库。如果文件名参数省略,则通过数据库名家
一个后缀“.fdb"来创建一个文件。数据库名应该是由除了‘\’之外的任意符号组成的标识
符。最后一个参数waitLockTimeout 可以设置用来防止当工作于该数据库的所有活动进程中
的某些进程崩溃时把所有的进程锁住。如果崩溃的进程锁住了数据库,则其他的进程都将无
法继续执行。为了防止这种情况,可以指定一个等待该锁的最大延迟,当该延迟过期后,系
统将试图进行恢复并继续执行活动的进程。如果数据库成功打开open方法返回true,否则返
回false。在后面这种情况,数据库的handleERROR 方法将带上DatabaseOpenError 错误码
被调用。一个数据库会话可以用close方法中止,该方法隐含的提交当前事务。在一个多线
程的应用中,每一个要访问数据库的线程都应该首先与数据库粘附(attach).
dbDatabase::attach()方法分配线程指定数据然后把线程与数据库粘附起来。该方法自动由
open()方法调用。因此没有理由为打开数据的线程调用attach()方法。当该线程工作完毕,
应当调用dbDatabase::detach() 方法。close 方法自动调用detach()方法。detach()方法
隐含提交当前事务。一个已经分离的线程试图访问数据库时将产生断言错误(assertion
failure)。fastdb 可以并行的编译和执行查询,在多处理器系统中显著的提高了性能。但不
能并发更新数据库(这是为了尽可能少的日志事务(log-less transaction)机制和0 等待恢
复的代价).当一个应用程序试图改变数据库(打开一个更新游标或者在表中插入新记录)时,
首先就以排他方式锁住数据库,禁止其他应用程序访问数据库,即使是只读的查询。这样来
避免锁住数据库应用程序过长的时间,改变事务应当尽可能的短。在该事务中不能进行堵塞
操作(如等待用户的输入).
在数据库层只使用共享锁和排它锁使得fastdb 几乎清除锁开销从而优化无冲突操作的执行
速度。但是如果多个应用同时更新数据库的不同部分,则fastdb使用的方法将非常低效。这
就是为什么fastdb 主要适用于单应用程序数据局访问模式或者多应用读优势
(read-dominated)访问模式模型。
在多线程应用中游标对象应该只由一个线程使用。如果在你的应用中有超过一个的线程,则
在每个线程中使用局部游标变量。在线程间有可能共享查询变量,但要注意查询参数。查询
要么没有参数,要么使用相关的参数绑定形式。
数据库对象由所有的线程共享,使用线程专有数据来进行查询的同步代价最小的并行编译和
执行。需要同步的全局的东西很少:符号表,树结点池…..。但是扫描、解析和执行查询可
以不需要任何的同步来进行,如果有多处理器系统的高层并发机制。
一个数据库事务由第一个选择或者插入操作开始。如果使用用于更新的游标,则数据库以排
他方式锁住,禁止其他应用和线程访问数据库。如果使用只读游标,这数据库以共享模式锁
住,防止其他的应用或者线程改变数据库,但允许并发读请求的执行。一个事务必须显示终
止,要么通过dbDatabase::commit()方法提交该事务对数据库的所有更改,或者通过
dbDatabase::rollback()方法来取消事务所作的所有更改。dbDatabase::close()方法自动提
交当前事务。
如果你使用只读游标来执行选择从而启动一个事务然后又适用更新游标来对数据库作某些改
变,则数据库将首先以共享模式锁住,然后锁变成排他模式。如果该数据被多个应用访问这
种情况可能会造成死锁。想象一下应用A 启动了一个读事务而应用B 也启动了一个读事务。
二者都拥有数据库的共享锁。
如果二者都试图把它们的锁改变为排他模式,他们将永远被互相堵塞(另外一个进程的共享
锁存在时不能授予排它锁)。为了避免这种情况,在事务开始就试着使用更新游标,或者显示
的使用dbDatabase::lock()方法。关于fastdb 中事务实现的信息可以参见《事务》这一
节。
可以使用lock()方法来显示的锁住数据库。锁通常是自动进行的。只有很少的情况下你才需
要使用这个方法。它将以排他方式锁住数据库知道当前事务结束。
可以用dbDatabase::backup(char const* file)方法来备份数据库。备份操作将以共享模式
锁住数据然后从内存向指定的文件刷新数据库的映像。因为使用了影子对象索引,数据库文
件总是处于一致状态,因此从备份恢复至需要把备份文件改一下名字(如果备份被放到磁带,
则首先要把文件恢复到磁盘).
dbDatabase类也负责处理一些应用的错误,如编译查询时的句法错误,执行查询时的索引越
界或者空引用访问。由一个虚方法dbDatabase::handleError来处理这些错误。
virtual void handleError(dbErrorClass error,char const* msg = NULL,int arg = 0);
程序员可以从dbDatabase 类来派生出自定义的子类,并重定义缺省的错误处理。
Error classes and default handling
Class Description Argument Default reaction
QueryError query compilation error position in query string abort compilation
ArithmeticError arithmetic error during pision or power operations - terminate
application
IndexOutOfRangeError index is out if array bounds value of index terminate
application
DatabaseOpenError error while database opening - open method will return false
FileError failure of file IO operation error code terminate application
outofmemoryError not enough memory for object allocation requested allocation size
terminate application
Deadlock upgrading lock causes deadlock - terminate application
NullReferenceError null reference is accessed during query execution - terminate
application
Query optimization
查询优化
与传统RDBMS 的查询相比,因为所有的数据在内存中所以查询的执行是很快的。但fastdb
通过应用许多优化措施更加提高了查询执行的速度:使用索引,逆引用和查询并行化。下面
几节提供这些优化的详细信息。
Using indices in queries
查询中使用索引
索引是提升RDBMS 性能的传统方法。Fastdb使用两种类型的索引:extensible hash table 和
T-tree。第一种对指定了关键字的值的记录的访问速度最快(一般来是常量时间)。而T-tree,
是AVL-tree和数组的混合体,在MMRDBMS的角色与B-tree在传统的RDBMS角色是一样的。
提供了对数算法复杂度的搜索、插入和删除操作(也就是说,对一个有N 个记录的表的搜索/
插入/删除的操作的时间是C*log2(N),其中C 是某一常量)。T-tree 比B-tree 更适用于
MMDBMS,因为B-tree试图最小化需要装载的页面数目(对于基于磁盘的数据库来说页面装载
代价是昂贵的),而T-tree 则试图优化比较/移动操作的次数。T-tree 最适合于范围操作或
者记录有显著的顺序。
fastdb 使用简单的规则来应用索引,让程序员来预言什么时候以及哪一个索引将被使用。索
引的适用性检查在每一次查询执行期间进行,因此该决策可以依赖于操作数的值来决定。下
面的规则说明了fastdb 应用索引的算法:
编译好的条件表达式总是从左到右检查
如果最终(topmost)表达式是AND,则尝试在表达式的左半部分使用索引,右半部分作为过
滤(filter) 如果最终表达式是OR,则如果左半可以使用索引则使用,然后测试右半使用
索引的可能性此外,当下列条件满足时,则索引适用于表达式
最终表达式是关系操作 (= < > <= >= between like)
操作数的类型是布尔型,数值型,字符串和引用
表达式的右操作数是文本常量或者C++变量,或者
左操作数是记录的索引字段
索引与关系操作兼容
现在我们应当确认“索引与操作兼容”的意思以及在没种情况中使用什么类型的索引,一个
哈希表在下列情况下可以使用:
相等=比较;
Between 操作并且两个端点操作数的值相等
Like 操作并且模式串不包含特别字符(’%’或者’_’)并且没有转义字符(在escape 部
分指定)
当 hash 表不适合并且如下条件满足时,可以使用T-tree:
比较运算( = < > <= >= between)
Like 运算并且模式串包含非空前缀(也就是说模式的第一个字符不是’%’或者’_’)
如果用索引来搜索like 表达式的前缀,并且其后缀不只是’%’字符,则这个索引搜索操作
能够返回的记录比真正匹配模式的记录要多。在这种情况下,我们应当过滤模式匹配的索引
搜索的结果。。
如果搜索条件是一些子表达式的析取(用or 操作符连接的许多可选项的表达式),则查询的
执行可以使用多个索引。为了避免此时的记录重复,在游标中使用位图来标记记录已经选中
了。
如果搜索条件需要扫描线型表,在order by子句中包含了定义T-tree索引的单一记录字段,
则可以使用T-tree 索引。只要排序是一个非常昂贵的操作,使用索引来代替排序显著的减少
了查询执行的时间。
使用参数-DDEBUG=DEBUG_TRACE编译fastdb,可以检查查询执行中使用了哪些索引,以及在索
引搜索期间所作的许多探测。在这种情况下,fastdb 将dump 数据库操作性能包括索引的追
踪信息。
逆引用
逆引用提供了在表之间建立关系的高效并且可靠的方法。Fastdb在插入/更新/删除记录时以
及查询优化时使用逆引用的信息。记录之间的关系可以是这些类型:一对一,一对多以及多
对多。
.一对一的关系用自身以及目标记录的一个引用字段表示。
. 一对多用自身的一个引用字段及目标表中的一个引用数组字段表示。
多对一用自身的一个引用数组字段以及所引用的表中的记录的一个引用字段表示
多对多用自身及目标记录中的引用数组字段表示。
当一个声明了关系的记录被插入表中,所有表中的与该记录关联的逆引用,都被更新至指向
这个记录。当更新了一个记录并且一个指明了该记录的关系的字段发生变化,则该逆引用自
动重构,删除那些不再与该被更新的记录关联的记录对该记录的引用,并且设置包含在该关
系中的新记录的的逆引用至该更新的记录。当一个记录被删除,所有逆引用字段中指向其的
引用都被删除。
出于效率的原因,fastdb 并不保证所有引用的一致性。如果你从表中删除一个记录,在数据
库中仍然可能会有指向该记录的引用。访问这些引用将会造成应用程序不可预料的结果甚至
数据库崩溃。使用逆引用可以清除这个问题,因为所有的引用都会自动更新从而引用的一致
性得以保留。
使用下面的表作为例子:
class Contract;
class Detail {
public:
char const* name;
char const* material;
char const* color;
real4 weight;
dbArray< dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(name, INDEXED|HASHED),
KEY(material, HASHED),
KEY(color, HASHED),
KEY(weight, INDEXED),
RELATION(contracts, detail)));
};
class Supplier {
public:
char const* company;
char const* location;
bool foreign;
dbArray< dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(company, INDEXED|HASHED),
KEY(location, HASHED),
FIELD(foreign),
RELATION(contracts, supplier)));
};
class Contract {
public:
dbDateTime delivery;
int4 quantity;
int8 price;
dbReference<Detail> detail;
dbReference<Supplier> supplier;
TYPE_DESCRIPTOR((KEY(delivery, HASHED|INDEXED),
KEY(quantity, INDEXED),
KEY(price, INDEXED),
RELATION(detail, contracts),
RELATION(supplier, contracts)));
};
这个例子中,在表Detail-Contract 和Supplier-Contract之间存在一对多的关系。当一个
Contract记录插入数据库中,仅仅只要把引用detail和supplier设置到Detail和Supplier
表的相应记录上。这些记录的逆引用contracts将自动更新。当一个Contract 记录被删除时
同样:从被引用的Detail 和Supplier的记录的contracts 字段中自动排除被删除的记录的
引用。
此外,使用逆引用可以在查询执行时选择更有效的规划。考虑下面的查询,选择某公司装船
的细节:
q = "exists i:(contracts[i].supplier.company=",company,")";
这个查询执行的最直接地方法是扫描Detail 表,并用这个条件测试每一条记录。但使用逆引
用我们可以选择另一种方法:用指定的公司名在Supplier 表中进行记录的索引搜索,然后从
表Detail 中使用逆引用定位记录,Detail表与所选的supplier记录有传递关系。当然我们
要清除重复的记录,这是有可能的因为一个公司可能运送许多不同的货物。这个通过游标对
象的位图来实现。由于索引查找明显的快于顺序查找并且通过引用访问是非常快的操作,这
个查询的总执行时间比直接方法要短得多。
从 1.20 版本开始,fastdb 支持串联(cascade)删除。如果使用OWNER 宏声明一个字段,该
记录就被当作是这个层次关系的所有者(owner)。当所有者记录被删除,该关系的所有的成
员(从所有者引用的记录)将被自动删除。如果该关系的成员记录要保持对所有者记录的引
用,该字段应当用RELATION 宏声明。
fastdb 延迟事务和在线备份调度
Delayed transactions and online backup scheduler
Fastdb 支持ACID 事务。也就是说当数据库得到事务已经提交的报告后,可以保证该数据库
在系统出错时(除了硬盘上的数据库镜像损坏外)能够恢复。在标准配置(例如没有非易变
RAM)和通用操作系统中(windows,unix….)提供这种特性的唯一方法是对硬盘进行同步写。
在这里“同步”意味着操作系统直到数据被真正写到硬盘上之后才会把控制权交回应用程序。
不幸的是同步写是非常耗时的操作—平均磁盘访问时间是10ms,因此每秒很难达到处理100
个事务的性能。
但是在很多情况下,丢失最后几秒的变化是可以接受的(但是要与数据库保持一致性)。依照
这个假定,数据库性能可以显著得到提高,fastdb 为这样的应用程序提供了“延迟事务提交
模式”。当提交事务延迟非零时,数据库并不马上执行提交操作,而是根据一个指定的超时时
间延迟操作。当超时时间过期,事务正常提交,这保证了在系统崩溃时只有在指定的超时时
间内的变化才被丢失。
如果以延迟事务初始化的线程在被延迟的事务提交之前启动了新的事务,则延迟提交操作被
忽略。因此fastdb 可以把一个客户端执行的许多继起(subsequent)的事务组成一个单一的大
事务。这样就极大地提高了性能,因为其减少了同步写的次数和创建的映像页的个数。(参看
事务一节)。
如果其他客户端试图在延迟提交超时时间过期前启动事务,则fastdb 强制进行延迟提交然后
释放资源。因此同步不受延迟提交的影响。
延迟提交缺省是关闭的(超时时间为0)。你可以指定提交延迟参数作为dbDatabase::open
方法的第二个可选参数。在SubSQL 工具中也可以通过设置FASTDB_COMMIT_DELAY 环境变量
(秒)来指定事务提交延迟的值。
fastdb 使用的事务提交模式保证了在软硬件出现故障时只要磁盘上的数据库没有损坏(写到
盘上的数据可以正确的读出来)的恢复。如果由于某些原因数据库文件损坏了,则恢复的唯
一途径是使用备份(但愿在不久之前做过这样的操作)。
当数据库离线是可以通过拷贝数据库文件来备份。dbDatabase 类提供了backup 方法来进行
在线备份而不需要停止数据库。程序员在任何时候都可以调用这个方法。不过更进一步,
fastdb 提供了备份调度可以自动进行备份。唯一需要的是—备份文件名和备份之间的时间间
隔。
dbDatabase::scheduleBackup(char const* fileName, time_t period)方法派生出单独的线
程在指定的时间内(秒)向指定的位置进行备份。如果filename以"?"字符结尾,则备份初
始化的时间被附加到文件名的末尾来产生唯一的文件名。在这种情况下所有的备份文件保存
在磁盘上(把太老的备份文件移除或者把它们移到别的介质上是管理员的责任)。否则备份被
写入到以fileName+".new"命名的文件中,备份完成后旧备份文件被删除新文件被重命名为
fileName.在后一种情况下,fastdb 也将检查旧备份文件(如果有的话)的创建日期然后按
照这样的方式来调整等待时间,就是备份之间的时间差要等于指定的间隔(因此如果数据库
服务器每天只启动8个小时,而备份间隔为24 小时,则备份将每天都进行,这与唯一文件名
模式不同)。
可以通过设置FASTDB_BACKUP_NAME 环境变量在SubSQL 工具中进行备份调度。如果指定了
FASTDB_BACKUP_NAME则间隔值依此取定,否则设置为每天。从备份中恢复只需要用一些备份
文件替代损坏的数据库文件。
fastdb 容错支持
Fault tolerant support
从2.49 版本开始fastdb 提供了可选的容错支持。可以启动一个主要的(活动的)和几个备
用的结点,所有在主要结点发生的变化同时被复制到备用结点上。如果主结点崩溃,其中一
个备用结点将变为活动的并成为主结点。一旦一个崩溃的结点重新启动,它要进行恢复,与
主结点的状态同步,然后作为备用结点投入使用。结点通过套接字连接并规定放置在不同的
计算机上。通信被假定为时可靠的。
要使用容错支持,应该使用REPLICATION_SUPPORT 可选项来重新编译fastdb.在makefile开
始把FAULT_TOLERANT 变量设置为1 来把它打开。应该使用dbReplicatedDatabase 来代替
dbDatabase。在open 方法的参数中,除了数据库名和文件名之外,应当指定这个结点的标志
符(从0 到N-1 的整数),包含所有结点地址(主机:端口)的数组以及结点数(N).然后就可以
在N 个结点的每一个启动程序。一旦所有的实例都启动,ID=0 的结点成为活动的(主结点)。
在这个实例中open 方法返回true.其他结点在open 方法堵塞。如果主结点崩溃,其中一个
备用结点被激活(open 方法返回true),然后这个实例继续执行。如果崩溃的实例重新启动,
它将尝试连接所有服务器,恢复其状态然后作为备用结点,等待其代替崩溃的主结点的机会。
如果主结点正常终止,所有备用结点的close 方法返回false.
在容错模式下fastdb保留两个文件:一个包含数据库本身,另一个则是页更新计数器。带有
页更新计数器的文件用于增量恢复。当崩溃结点重启动时,它将向主结点发送页计数器,并
只接受这段时间在主结点发生变化的页(其时间戳大于被恢复的结点所发送的页)。
在复制模式中(在主结点)应用程序在事务提交期间并不阻塞知道所有的变化被刷新到硬盘。
以改变的页由独立的线程异步的刷新到磁盘上。这样带来了显著的性能提升。但如果所有的
结点都崩溃了,数据库就可能处于不一致状态。也可以指定向硬盘刷新数据的时间延迟:延
迟越大,磁盘IO 开销越小。但在崩溃的情况下,需要从主结点发送更多的数据以进行恢复。
可以在无盘模式中使用容错模式(DISKLESS_configuration 构建选项)。在这种情况下,没
有数据保存在磁盘上(没有数据库文件,也没有页更新计数器).假定至少有一个结点总是活
动的。只要有一个在线结点数据就不会丢失。当崩溃结点恢复时,主结点向其发送完整的数
据库快照(增量恢复是无法实现的因为崩溃结点的状态已经丢失)。由于这种模式没有磁盘操
作,操作性能是非常高的并且只受限于网络吞吐量。
当复制结点启动后就开始在指定的时限内尝试连接所有其他的结点。如果在这个时间内无法
建立连接,则该结点被假定为自主启动的并作为普通(非复制)数据库开始工作。如果结点
与其它结点建立了连接,则具有最小ID的节点被选为复制主结点。所有的其他结点被切换到
旁置模式并等待来自主结点的复制请求。如果主结点和从结点的数据库的内容不一致(使用
页计数器数组来决定),则主结点进行旁置结点的恢复,向其发送最近的页面。如果主结点崩
溃,则旁置结点选择一个新的主结点(最小ID 的节点)。所有的旁置结点都在open方法堵塞
直到下面的情况之一发生:
1. 主结点崩溃并且该节点被选为新的主结点。在这种情况下open方法返回true。
2. 主结点正常关闭数据库。在这种情况下所有复制结点的open方法返回false。
可以从其他应用程序中对复制数据库进行只读的访问。在这种情况,复制的结点必须通过
dbReplicatedDatabase(dbDatabase::dbConcurrentUpdate)构造掉用来创建。其他应用程序
可以用dbDatabase(dbDatabase::dbConcurrentReadMode)实例来访问同一数据库。
并非所有应用都需要容错。许多应用使用复制只是为了提高可测量性,在许多结点间分担负
载。对于这些应用,fastdb提供了简化复制模型。在这种情况下,有两种结点:读者和写者。
任何一个写者结点都可以作为复制主结点。而读者结点只能从主结点接收复制的数据而不能
自己更新数据库。与上面所述的复制模型的最主要区别是读者永远不能变成主结点并且这个
结点的open方法一旦与主结点建立了连接就马上归还控制权。来自主结点的更新通过单独的
县城接收。读者结点要用dbReplicatedDatabase(dbDatabase::dbConcurrentRead)构造器来
创建。必须使用预主结点同样的数据库模式(类)。当主结点关闭连接时来自读者结点的数据
库连接并不自动关闭,其仍然保持打开并且应用仍然可以以只读模式访问数据库。一旦主结
点重启,就会与所有的旁置结点建立连接并继续向它们发送更新。如果没有读者结点,则复
制模型就等同于前面所述的容错模型,如果只有一个写者结点和一个或多个读者结点,这就
是经典的主从复制。
可以使用Guess 例子来测试容错模式。这个例子用-DREPLICATION_SUPPORT 编译展示了3 个
结点的簇(所有地址指向localhost,但你当然也可以用你的网络中真实的主机来代替他们)。
必须用参数0..2 来启动guess应用的3个实例。当所有的实例启动后,用参数0 启动的应用
开始正常的用户对话(这是游戏:“guess an animal”).如果你用Crtl-c来模拟该应用程序
的崩溃,则其中的一个备用结点继续执行。
testconc 示例演示了更复杂的复制模型。有3 个复制结点,通过使用testconc update N 命
令来启动,其中N是这些结点的标志符:0,1,2。启动了这3 个结点后,它们就会互相连接,
结点0 成为主结点并开始更新数据库,把改变复制到结点1 和2。可以启动一个或多个检查
者,即用只读模式(使用dbConcurrentRead 访问类型)来连接到复制的数据库的应用程序。
检查者可以用testconc inspect N T 来启动,其中N 是检查者要连接的复制结点的标志符,
T 是检查线程的编号。
同一个 testconc 示例可以用来演示简化的复制模型。启动一个主结点:testconc update 0,
然后启动两个只读复制结点:testconc coinspect 1 和 testconc coinspect 2。请注意与
前面所述的场景的区别:在容错模式下,普通的复制结点使用testconc update N 命令启动,
而连接到同一数据库的只读结点(不包括复制进程)通过testconc inspect N 命令启动。在
简化的主-从复制模型中,有只读的复制结点,其不能变成主结点(因此如果最初的主结点崩
溃,没有人能够扮演这个角色),但运行在这个结点上的应用可以同通常的只读应用一样访问
复制结点。
相关阅读
因为项目中使用的fastdb,前2天的面试也有所提到,就想着要仔细研究一下。在网上看到了一下主存数据库的性能测试,相对于BerkeleyDB和S
就个人理解而言,fastdb client-server模式,只是在client和server之间添加了一个socket通信,其实操作都是在server端完成的。但是clie