Sqlite 技术内幕(译注版)(一) 目录和概述

来源:互联网 发布:python和java和php 编辑:程序博客网 时间:2024/05/15 07:24

目录

目录...2

1.     概述...3

1.1 应用示例...5

1.1.1sqlite3_open.7

1.1.2sqlite3_prepare.7

1.1.3sqlite3_step.8

1.1.4sqlite3_column_int.8

1.1.5sqlite3_finalize.8

1.1.6sqlite3_close.8

1.1.7 其他有用的函数...8

1.1.8 返回值...9

1.2 Sqlite架构...13

1.3 Sqlite的局限...15

2.     数据库文件格式...17

2.1 数据库命名惯例...17

2.2 数据库文件结构...18

3.     页缓存管理...21

3.1 页缓存管理器的职责...22

3.2 页缓存管理器的接口结构...23

3.3 缓存管理...23

4.     事务管理...26

4.1 事务类型...26

4.2 锁管理...27

4.3 日志管理...30

4.4 事务操作...30

5. 表和索引管理...30

5.1 B+树结构...30

5.2 Sqlite里的B+树...30

5.3 页结构...30

5.4 模块功能...30

6. Sqlite引擎...30

6.1 字节码编程语言...30

6.2 记录格式...30

6.3 数据类型管理...30

7. 更多信息...30

 

1.    概述

过去几十年,出现了很多数据库管理系统(DBMSs)。而有一小部分是商业上成功的:DB-2,Informix,Ingress,MySQL,Oracle,SQL Server,Sybase。SQLite最近才加入DBMSs家族,但它在商业上也相当成功。从它2000年3月29日初次登台到如今,确实已走过一个漫长的旅途。它是一个基于SQL的关系型数据库管理系统(RDBMS),有以下特点:

[注]:SQLite2005年荣获OSCON Google和O’Reilly 集成类奖

零配置:

在使用它之前,无需分步安装或者设置初始化过程。没有配置文件。数据库只需要最小化或者几乎不需要管理机制。你可以从SQLite官网http://www.sqlite.org上下载它的源代码,用你最喜欢的C编译器编译,然后就可以开始用编译好的库了。

可嵌入的:

无需特别为SQLite单独维护一个服务进程。它的库可嵌入你自己写的应用里。

应用接口:

SQLite为C语言编写的应用来操纵数据库提供一个SQL环境。它为动态SQL提供一个调用级接口库;用户可以即时编写SQL语句并传给该接口执行(除了SQL,也没有其他方式来操纵数据库)。不需为应用做任何的预处理和编译限制;一个普通的C编译器就够了。

事务支持:

SQLite支持核心事务属性,即原子性atomicity、一致性consistency、独立性isolation和持续性durability,简称ACID属性。系统宕机或者断电情况下,不需要数据库管理员做任何操作,来恢复数据库。当SQLite读数据库时,它自动会执行必要的恢复操作,对用户是透明的。

线程安全:

SQLite是一个线程安全库,一个应用进程内的可以并发访问同一个或者不同的数据库。SQLite实现了数据库级别的线程并发。

轻量级:

SQLite的空数据库大小大约是250KB,并且可以通过在编译时禁用某些高级属性,使之更小。SQLite可以在Linux、Windows、Mac OS X、OpenBSD和一小部分其他操作系统上运行。

可定制:

SQLite提供一个良好的框架,你可以定义和使用定制的SQL函数、聚合函数和核对序列。它同时为unicode文本提供UTF-8和UTF-16标准的编码。

跨平台:

SQLite可以让你进行跨平台数据库迁移。比如,你在一台linux-x86的机器上创建数据库,并使用同样的数据库(简单复制一下)在ARM平台上使用,无需任何修改。其他同等支持的所有平台下也一样。

SQLite不同于其他大多数现代SQL数据库,它的基本设计原则就是简单。SQLite为追其简单,甚至牺牲了某些情况下某些属性的高效率实现。它不论是维护、定制、操作还是管理、嵌入在C应用里,都是非常简单的。它用简单的技术来实现ACID属性。

SQLite支持一个SQL-92标准下的数据定义和操作属性的大子集,还有一些SQLite专属命令。你可以用标准的数据定义SQL指令,来创建表、索引、触发器和视图。你可以用INSERT、DELETE、UPDATE和SELECT SQL指令来操纵存储的信息。下面是SQLite3.3.6 release版本支持的附加属性清单(可支持的最新属性集可从官网上知悉):

l  部分支持ALTER TABLE(重命名表、增加列)

l  UNIQUE,NOT NULL,CHECK约束

l  子查询,包括关联子查询、INNER JOIN、LEFT OUTER JOIN、NATURAL JOIN、UNION、UNION ALL、INTERSPECT、EXCEPT

l  事务命令:BEGIN、COMMIT、ROLLBACK

l  SQLite命令:reindex、attach、detach、pragma

下列SQL-92特性目前还未在SQLite 3.3.6 发布版本中支持:

l  外键约束(解析了,但是未实现强制约束)

l  很多ALTER TABLE的特性

l  一些触发器相关的特性

l  RIGHT 和 FULL OUTER JOIN

l  更新视图

l  GRANT 和 REVOKE

SQLite将整个数据库存储在一个普通的、可以存放在任意本地文件夹中的文件里。任何有读取文件权限的用户都可以读取数据库中的任何信息。任何有写文件权限的用户都可以修改数据库中的任何信息。SQLite使用一个单独的日志文件存储事务恢复信息以防止事务中止或者系统崩溃。SQLite的attach命令支持一个事务在多个数据库上同时运行。这些事务也是ACID协同的。Pragma命令使你可以改变SQLite库的行为。SQLite允许多应用程序并发访问同一个数据库。然而,它只支持很有限的事务并发机制。它允许在同一数据库上的任意数目的读事务并发,但是允许仅仅一个排他性的写事务。

SQLite是个成功的关系型数据库管理系统。它被广泛应用于中低端数据库应用,例如网络站点服务(SQLite能正常响应一天10万以上的分散点击;SQLite开发团队证明了SQLite一天能承受100万次点击),移动电话,掌上电脑,set-top boxesstandalone 家电中。你甚至可以用它,在数据库初学者课堂上,教授学生SQL。代码本身就有非常完善的文档和注释,你可用它在学习高级数据库管理课程上,或者在项目开发中作为参考技术。你可以免费获取它,并且没有复杂的证书限制,完全对公众开放。

1.1应用示例

在这一节,我将列举一些简单的应用,表明SQLite的多样性。应用列举在如下子小节中。

让我们以学习一个非常简单的应用来探索SQLite神秘大陆吧。下列示例呈现了一个非常典型的SQLite应用。它是个C程序,调用SQLite API来操纵SQLite数据库文件:

#include <stdio.h>

#include "sqlite3.h"

intmain(void)

{

sqlite3* db = 0;

sqlite3_stmt* stmt = 0;

int retcode;

retcode = sqlite3_open("MyDB", &db); /* 打开名为MyDB的数据库 */

if (retcode != SQLITE_OK){

sqlite3_close(db);

fprintf(stderr, "Couldnot open MyDB\n");

return retcode;

}

retcode = sqlite3_prepare(db, "select SID fromStudents order by SID",

-1, &stmt, 0);

if (retcode != SQLITE_OK){

sqlite3_close(db);

fprintf(stderr, "Couldnot execute SELECT\n");

return retcode;

}

while (sqlite3_step(stmt) == SQLITE_ROW){

int i =sqlite3_column_int(stmt, 0);

printf("SID = %d\n",i);

}

sqlite3_finalize(stmt);

sqlite3_close(db);

return SQLITE_OK;

}

你可以编译该示例并执行。贯穿本书的输出示例都是在linux机器上生成的,但是这些示例都可以在其他SQLite支持的平台正常运行。

这些示例也假定你已经准备了sqlite3的执行文件和libsqlite3.so(在windows平台的sqlite3.dll或者Mac OS X的libsqlite3.dylib)共享库,和sqlite3.h的接口定义头文件。你可以从http://www.sqlite.org上获取源代码自己编译或者直接下载二进制形式的支持文件。建议把sqlite3执行文件、共享库、sqlite3.h像这些示例一样,放在同一个目录中,编译将很方便。

例如,假设你在linux系统环境运行,而且你已经保存好app1.c示例程序,并将sqlite3执行文件、共享库、sqlite3.h和app1.c放在同一个文件夹中。你可以执行以下命令编译:gcc app1.c -o ./app1 -lsqlite3 -L.

然后会在同一文件夹中生成一个名为app1的二进制文件。这样就可以执行该文件查看输出了。

 

SQLite的源文件和应用文件必须用同一编译器编译。

如果你安装包形式安装,或者你的操作系统本身预装了SQLite,你可能需要用一套不同的编译器参数。例如,在Ubuntu上,你可以通过sudo aptitude install sqlite3 libsqlite3-dev命令安装sqlite,然后你可以通过cc app1.c -o ./app1 -lsqlite3命令编译上述示例应用。

因为最近版本的Mac OS X已经包含了sqlite,所以同样的编译指令在这也可以用。

这个示例应用打开当前目录中的MyDB数据库文件。这个数据库至少需要有一个Student表;表必须至少有一个integer类型的列,名叫SID。在下面的应用中,你讲学习如何创建新表、在表中插入行(也成为元组或者记录)。现在,你可以用一下命令创建表和插入记录:

sqlite3 MyDB "create table students (SIDinteger)"

sqlite3MyDB "insert into students values (200)"

sqlite3MyDB "insert into students values (100)"

sqlite3 MyDB"insert into students values (300)"

如果你现在运行app1(为了链接Sqlite库,在linux系统环境,你必须设置当前sqlite库文件的目录到LD_LIBRARY_PATH环境变量中),然后可以看到下列输出:

SID =100

SID =200

SID = 300

 

在linux、unix和Mac OS X下,命令行运行app1时,你可能需要在app1加前缀./运行:./app1。这个示例先执行预备操作,然后执行SQL语句的操作:select SID from Students order by SID。然后一行行单步读取结果集,获取SID,并打印出来。最后,关闭数据库。

SQLite是个嵌入到应用中的调用级接口库。该库以C函数的方式实现了所有Sqlite的API。所有的API函数名都冠以sqlite3_前缀,并在sqlite3.h中声明。上述示例中只用了很少一部分API,即sqlite3_open,sqlite3_prepare,sqlite3_step,sqlite3_column_int,sqlite3_finalizesqlite3_close该示例还用了预编译常量SQLITE_OKSQLITE_ROW来检验API返回的结果。这些宏也在sqlite3.h中定义。

下面的小节讨论一些关键的 SQLite API函数。

1.1.1sqlite3_open

执行sqlite3_open函数,通过sqlite库与数据库文件建立一个新的连接。(同一个应用可能还有其他指向同一个或者不同数据库的打开的连接。SQLite的角度看,这些连接是独立于其他连接的。)如果指向的数据库文件不存在,SQLite自动创建该文件。

当打开或者创建一个文件时,SQLite遵循延迟操作的原则:实际的打开或者创建操作,在文件被访问以读取信息之后。

Sqlite3_open函数返回一个连接句柄(一个指向sqlite3对象的指针),传递给对应类型的变量(在前一个例子中,该变量名是db),然后这个指针被用于在该连接上的后续操作。这个句柄描述了该连接的完整状态信息。

1.1.2sqlite3_prepare

sqlite3_prepare函数编译SQL语句,并生成一个对等的内部对象。该对象在数据库语义上就是指该SQL语句,不过在SQLite中是用一个字节码程序实现的。字节码程序运行在虚拟机或者解释器上,是一种SQL语句的抽象描述。更多的有关信息,请参看后续章节:字节码编程语言。这本书中我将交替使用术:字节码程序和预处理语句。

sqlite3_prepare函数返回一个语句句柄(一个指向sqlite3_stmt对象的指针[blade注:其实这个sqlite3_stmt并没有实际定义,是SQLite内部结构体vdbe的外部接口描述,从外部来看,仅仅用来传递该结构体的句柄而已]),传递给对应类型的变量(在前一例子中,该变量名是stmt),然后这个指针被应用于操作该预处理语句的后续操作上。在示例中,‘我’将SQL语句select SID from Students order by SID预处理转化成stmt句柄。这个句柄像一个光标一样,获取SELECT语句的结果集,每次一行。

1.1.3sqlite3_step

sqlite3_step函数一直执行字节码程序,直到因计算出新的一行而碰到断点,或者因没有新的记录而停止。前者返回SQLITE_ROW,后者返回SQLITE_DONE。对于没有返回记录的SQL语句(例如UPDATE,INSERT,DELETE和CREATE),因为没有记录需要处理计算,总是返回SQLITE_DONE。对于SELECT语句,sqlite3_step函数操作对应结果集的光标指针每次下移一行。该光标的起始位置在结果集第一行的上方,并且每次移动都只能向下。

1.1.4sqlite3_column_int

如果sqlite3_step函数返回SQLITE_ROW,你可以通过执行sqlite3_column_*函数,获取每一列(也叫属性或者域)的值。SQL/SQLite和C语言之间的Inpedence(数据类型)不匹配是自动处理的:API自动转换两种语言中的数据类型,从存储类型到需要的类型。这个示例中,每一行的输出都是整型值,我们就通过执行sqlite3_column_int函数返回要读取的SID整型值。

1.1.5sqlite3_finalize

sqlite3_finalize函数销毁预处理语句。即擦除字节码程序,释放所有分配给该语句句柄的资源。至此语句句柄将无效。

1.1.6sqlite3_close

sqlite3_close函数关闭数据库连接,释放所有分配给连接句柄的资源。至此连接句柄将无效。

1.1.7其他有用的函数

未提及的被广泛应用的API是sqlite3_bind_*和sqlite3_reset。在一个SQL语句字符串(sqlite3_prepare的输入参数)中,一个或者多个语义值可以被SQL参数符?替换(或者?NNN,:AAA,@AAA 和$AAA,其中NNN是数字,AAA是标识符)。他们是预处理语句的输入参数。这些参数的值可以通过sqlite3_bind* API来设置。如果参数符没有通过sqlite3_bind* 绑定,sqlite会取默认值,或者如果没在schema中设置默认值,会取SQL的 NULL。Sqlite3_reset API重设语句句柄(即,预处理语句)回到初始状态,只有一个例外:所有有绑定值的参数符保持他们的值。回到初始状态后,语句将准备重新执行,并在重新执行中,重新使用这些绑定值。也可以在重新执行之前重新使用sqlite3_bind_* APIs,从而利用新的绑定值。

1.1.8返回值

所有的函数返回0或者正整型值。SQLite推荐使用预编译宏作为返回值,方便返回值检测。返回SQLITE_OK表示执行成功;SQLITE_ROW表示sqlite3_step函数从SELECT语句的结果集中找到新的一行;SQLITE_DONE表示语句执行完毕。

下面是另一个SQLite应用的例子,也可以从命令行运行,并与数据库交互。它有两个参数:第一个是数据库文件名,第二个是SQL语句。它首先打开数据库文件,然后通过执行sqlite3_exec函数将SQL语句应用于数据库,最后关闭数据库文件。Sqlite3_exec函数直接执行SQL语句,无需像上一例中经由sqlite3_prepare和sqlite3_step API函数处理。如果SQL语句有输出,就每次输出时调用回调函数。用户必须有数据库文件的读权限,可能在某些语句类型下,还需要该文件和包含该文件的文件夹的写权限。以下是基于命令行的该应用代码:

#include <stdio.h>

#include "sqlite3.h"

static int callback(void *NotUsed, int argc, char**argv, char **colName)

{

// Loopover each column in the current row

int i;

for (i= 0; i < argc; i++){

printf("%s= %s\n", colName[i], argv[i] ? argv[i] : "NULL");

}

return0;

}

intmain(int argc, char **argv)

{

sqlite3*db = 0;

char*zErrMsg = 0;

int rc;

if(argc != 3){

fprintf(stderr,"Usage: %s DATABASE-FILE-NAME SQL-STATEMENT\n", argv[0]);

return-1;

}

rc =sqlite3_open(argv[1], &db);

if (rc!= SQLITE_OK){

fprintf(stderr,"Can't open database: %s\n", sqlite3_errmsg(db));

sqlite3_close(db);

return-2;

}

rc =sqlite3_exec(db, argv[2], callback, 0, &zErrMsg);

if (rc!= SQLITE_OK){

fprintf(stderr,"SQL error: %s\n", zErrMsg);

}

sqlite3_close(db);

returnrc;

}

编译上述代码生成执行文件,假设名为app2。然后你可以发布SQL语句操作数据库了。假设操作的同样是当前目录中的MyDB数据库。通过执行下列命令,你可以向Student表里面插入新的行:

./app2 MyDB "insert into Studentsvalues(100)"

./app2MyDB "insert into Students values(10)"

./app2 MyDB "insert into Students values(1000)"

如果你重新运行之前的应用app1,你将看到以下输出:

SID =10

SID =100

SID =100

SID =200

SID =300

SID = 1000

你也可以创建新的表,例如./app2 MyDBExtn"create table Courses(name varchar, SID integer)" 在当前目录的新数据库MyDBExtn中创建表Courses。

SQLite有一个交互命令行程序(sqlite3),之前提到过。通过这个程序,也可以发布SQL命令。你可以从SQLite的官网上下载一个预编译版本的二进制文件或者自己从源代码编译生成。App2正是sqlite3交互命令程序的实现的基本骨架。

SQLite是个线程安全的库。因此,一个应用中的多线程并发地可以同时访问同一个或者不同的数据库。

为了编译生成线程安全的库,需要在SQLite的源代码中,设置线程安全预编译的宏THREADSAFE的值为1(在准备编译SQLite时,可以通过传入--enable-threadsafe启用该设置).在默认设置中,多线程属性在Linux系统上是关闭的,而在windows系统上是打开的。SQLite没有为该属性定义接口([blade注:可能用处不是很大,不过实现起来也比较容易。你要是想实现,可以在shell.c中试试^_^])。

下面这个例子用到了pthreads库,该库在windows系统中没有默认支持。如果在windows系统,你有两种选择:用Cygwin(http://www.cygwin.com)中的工具和编译器编译,或者从http://sourceware.org/pthreads-wind32/.下载pthreads的windows版本。以下是典型的多线程应用的代码:

#include <stdio.h>

#include <pthread.h>

#include "sqlite3.h"

void* myInsert(void* arg)

{

sqlite3*db = 0;

sqlite3_stmt*stmt = 0;

int val= (int)arg;

charSQL[100];

int rc;

rc =sqlite3_open("MyDB", &db); /* Open a database named MyDB */

if (rc!= SQLITE_OK) {

fprintf(stderr,"Thread[%d] fails to open the database\n", val);

gotoerrorRet;

}

/*Create the SQL string. If you were using string values,

youwould need to use sqlite3_prepare() and sqlite3_bind_*

toavoid an SQL injection vulnerability. However %d

guaranteesan integer value, so this use of sprintf is

safe.

*/

sprintf(SQL,"insert into Students values(%d)", val);

/*Prepare the insert statement */

rc =sqlite3_prepare(db, SQL, -1, &stmt, 0);

if (rc!= SQLITE_OK) {

fprintf(stderr,"Thread[%d] fails to prepare SQL: %s ->

returncode %d\n", val, SQL, rc);

gotoerrorRet;

}

rc =sqlite3_step(stmt);

if (rc!= SQLITE_DONE) {

fprintf(stderr,

"Thread[%d]fails to execute SQL: %s -> return code %d\n", val, SQL, rc);

}

else {

printf("Thread[%d]successfully executes SQL: %s\n", val,

SQL);

}

errorRet:

sqlite3_close(db);

return(void*)rc;

}

intmain(void)

{

pthread_tt[10];

int i;

for(i=0; i < 10; i++)

pthread_create(&t[i],0, myInsert, (void*)i); /* Pass the value of i */

/* waitfor all threads to finish */

for(i=0; i<10; i++) pthread_join(t[i], 0);

return0;

}

 这个例子可能在windows或者Mac OS X玩不转。你可能需要设置多线程支持然后重编译SQLite,并且/或者获取pthreads线程库,使该示例可以在这两个系统上运行。Mac OS X本身包含pthreads,如果是在windows平台,可以从http://sourceware.org/pthreads-win32/. 获取。

该示例创建10个线程,每一个线程都尝试在同一个数据库MyDB中插入一行。SQLite实现了一种基于锁的并发模式,所以一些或者所有的插入语句可能因为锁冲突而被中止。应用本身不用担心并发控制和数据库一致性的问题。然而,你必须检测是否失败,然后在代码中做相应合适的处理(比如,你可能重试失败的指令,或者通知用户指令执行失败了,然后由用户决定下一步干什么)。

每个线程需要一个对应它的数据库的连接,并且创建自己的SQLite(连接和预处理语句)句柄。SQLite不推荐跨线程共享句柄。即时共享句柄看起来可以运行,也没有一定能得到预期结果的保证。实际上,在某些版本的linux中,SQLite库可能会崩溃。并且,在Unix/Linux系统中,你不应该尝试通过fork系统调用保存连接句柄到子进程。如果这样做,可能会出现应用崩溃或者数据库崩溃

在SQLite3.3.1和子版本中,这种线程间共享数据库连接的限制没这么严格。线程可以安全地以互斥方式共用一个数据库连接。这意味着你可以从一个线程切换某连接到另一个线程,只要前一线程没有在该连接上设置本地文件锁。如果前一线程没有挂起的事务,并且该连接上的所有语句被重置或者结束,你可以安全地假定没有锁。以下是操作两个SQLite数据库的典型应用的代码:

#include<stdio.h>

#include"sqlite3.h"

intmain(void)

{

sqlite3*db = 0;

sqlite3_open("MyDB",&db); /* Open a database named MyDB */

sqlite3_exec(db,"attach database MyDBExtn as DB1", 0, 0, 0);

sqlite3_exec(db,"begin", 0, 0, 0);

sqlite3_exec(db,"insert into Students values(2000)", 0, 0, 0);

sqlite3_exec(db,"insert into Courses values('SQLite Database', 2000)", 0, 0, 0);

sqlite3_exec(db,"commit", 0, 0, 0);

sqlite3_close(db);

return SQLITE_OK;

}

‘我’简化了代码,没有包含错误检测。这个示例打开MyDB数据库,然后附加MyDBExtn数据库到当前连接。MyDB要有一个Student(SID)表,且MyDBExtn数据库要有一个Courses(name,SID)表。操作过程是先通过begin命令打开一个事务,然后在事务中,分别往Student表和Courses表插入一行,最后通过commit命令提交事务。插入命令不需要回调函数,所以,在本示例的sqlite3_exec调用中,‘我’传了0作为回调函数指针参数的值。

SQLite允许在一次sqlite3_exec调用中包含多个SQL语句;也就允许例中的批处理命令一次调用执行,这是传入参数为:begin; insert into Students values(2000); insert into Coursesvalues('SQLite Database', 2000); commit。如果批处理命令中包含SELECT语句,那就用同一个回调函数处理结果集。

Catalog目录表是一个由SQLite自己创建并维护的系统表。用来存储有关数据库的元信息。SQLite在每个数据库中维护一个主目录表,名为sqlite_master。这个目录表存储有关表、索引、触发器和视图的结构信息。你可以查询该主目录表(例如,select *from sqlite_master),但是不能直接修改。也有其他可选的目录表。所有的目录表都以sqlite_前缀命名,这些名字都作为SQLite保留表名,供内部使用。你不能用这些名字(不管是大写、小写还是混合的)创建数据库对象(表、视图、索引和触发器)。([blade注:任意以sqlite_前缀的表名都不能被用户创建。尝试通过create table sqlite_helloworld(idint);在sqlite命令行创建表,报错提示为:SQL error: object name reserved forinternal use: sqlite_helloworld])

1.2Sqlite架构

SQLite由七个组件子系统组成,可以划分为两部分:前端解析系统和后端引擎。如下图1-1的两张图表展示了它们如何交互的:

前端预处理由应用程序输入的SQL语句和SQLite命令。过程包括解析这些语句(和命令),优化他们,生成等价的SQLite内部字节码,让后端执行。前端由三部分组成:

词法分析器

把输入的SQL语句分割成符号

语法分析器

分析由词法分析器分析SQL语句得到的符号结构,然后生成语法树。语法分析器还包含一个重构语法树的优化器,来找功能等价的语法树,以便生成更有效率的字节码程序。

代码生成器

代码生成器遍历语法树,生成一个等价的字节码程序。

前端系统的实现在sqlite3_prepare函数内。

后端是一个解释字节码程序的引擎。这个引擎才真正做数据库处理工作。后端由四部分组成:

虚拟机:

虚拟机模块是内部字节码语言的解释器。它执行字节码,实现用户输入的SQL语句的功能。它才真正操作数据库内数据。从虚拟机的角度看,数据库就是表和索引的集合,而表或者索引是一系列的元组,或者称为记录。

B/B+树:

B/B+树模块将每一个元组集装载到一个有序的树形数据结构;表和索引分别装入B+树和B树。(‘我’将会在“表和索引管理(见第7section)”讨论这些数据结构)这个模块协助虚拟机高效实现在树中的搜索、插入和删除元组、创建新树和删除老树的操作。

存储页管理器:

在操作系统原生文件的基础上,存储页管理器模块实现了一个面向页的数据库文件的抽象层。它来管理由B/B+树模块调用的内存缓冲区(数据页的一部分),此外还有通过文件锁的管理、日志管理实现事务的ACID属性。

操作系统接口:

操作系统接口提供一个针对多种不同本地操作系统平台的统一形式接口。

后端负责实现sqlite3_bind_*,sqlite3_step,sqlite3_column_*,sqlite3_reset和sqlite3_finalize API函数。

限于篇幅,这里‘我’只讨论SQLite引擎,不讨论解析系统。这里的例子也是用Linux操作系统来展示SQLite的内部工作原理,不过既然SQLite可移植到众多其他操作系统,无论你用什么操作系统,这些例子也应该能和Linux操作系统下得到相似的运行结果。

1.3Sqlite的局限

SQLite不同于众多其他现代SQL数据库,因为它的设计初衷就是追求简单。即使偶尔实现某些特性不是很高效,依然遵循这个设计原则。下面就是SQLite的缺点清单:

SQL-92特性:

之前提到过,SQLite不支持某些在其他很多企业版数据库里支持的SQL-92特性。你可以从SQLite的官网上获取最新的消息。

低并发:

SQLite只支持简单的事务;不支持嵌套事务和驻点功能。(嵌套事务意思是在一个事务中拥有子事务。驻点功能允许事务恢复到之前在该点建立的状态。)它不能保证高度的事务并发。虽然允许多个并发的读事务,但是在一个数据库文件上,只能有一个排它性的写事务。这个限制意味着,即使只有一个事务正在读数据库文件的任意一处,所有其他写数据库文件任意一处的事务将被阻塞。同样,如果有一个写数据库文件的事务,其他任意读或者写数据库文件任意一处的事务将被阻塞。

应用限制:

因为SQLite的有限事务并发性,它仅仅适用于小尺寸的事务。大多数情形下,不存在这个问题。每个应用很快处理完数据库操作然后继续运行其他流程,因此一个事务顶多占用数据库几毫秒的时间。但是有一些应用,特别是写频繁的,要求更多的事务并发(表级或者行级,而不是数据库级),这时,你应该为这些应用选择其他的数据库管理系统。SQLite本就不是企业级的数据库。相比企业及数据库提供的无数复杂属性,它非常适用于实现、维护和管理的间接性更重要的场合。

网络文件系统问题:

SQLite使用本地原生的文件锁机制控制事务并发。如果数据库文件存在于网络分区,这将引起某些问题。许多网络文件系统的实现都在它们的锁逻辑上有问题。如果锁机制的运行流程不如预期,可能导致两个或者更多应用同时修改同一数据库文件的同一部分,从而导致数据库崩溃。由于这些bug是与所使用的文件系统实现机制相关的,SQLite也无能为力。

再一个,由于大多数网络文件系统相关的延迟,性能可能不太好。在这些环境下,非得通过网络访问数据库文件的话,一个实现客户端服务器模型的数据库可能比SQLite更高效。

数据库尺寸:

由于当初开发者的工程设计考虑,对于需要承载非常大信息量的数据库来说,SQLite可能不是一个很好的选择。理论上,SQLite的数据库文件尺寸可达2TB;但是日志子系统需要预占用和数据库文件尺寸成比例的内存。对于写事务,SQLite为每一个数据库存储页在内存中维护一个信息位,无论事务是读还是写这页。(默认的页尺寸是1024字节。)因此,当数据库尺寸超过几百万存储页时,预占用内存可能成为一个严重的瓶颈。

对象的数目和类型:

表或者索引最多有2^64-1条(entry:条目)。(当然,因为数据库有2TB的尺寸限制,不可能拥有比这更多的条目)。在SQLite的当前实现中,一条就可以装载2^30字节数据。(下面的文件格式支持原始最大尺寸大约为2^62字节)在打开数据库文件的时候,SQLite读取并预处理所有的条目,并且创建许多内存目录对象。因此,为了最佳的性能,建议降低表、索引、视图和触发器的数目。同样地,虽然没有明确限制一张表中的列数,几百列基本上到极限了。某些优化中,只有表头的31列可用于优化。你可以在一个索引里面存放多列,但是超过30列的索引将不会被用来优化查询。

主机变量参考

在某些嵌入式数据库管理系统中,SQL语句可以直接参考主机变量(即,那些从应用空间得到的变量)。这在SQLite中是不可能的。SQLite的替代解法是,通过sqlite3_bind_*API函数,允许主机变量绑定到SQL语句的输入参数,但是不能绑定输出值。这种方法普遍比直接访问要好,因为后者要求转换SQL语句为特殊的API调用的预处理。

存储过程

许多数据库管理系统可以创建和存储存储过程。存储过程是一组用来完成某一任务的SQL语句组成的一个运行逻辑单元。SQL操作可以使用这些存储过程。SQLite不支持该功能。

0 0
原创粉丝点击