ラベル SQLite の投稿を表示しています。 すべての投稿を表示
ラベル SQLite の投稿を表示しています。 すべての投稿を表示

2007年4月9日月曜日

Sqlite中文排序研究

Sqlite是一个用C语言实现的小型SQL数据库引擎。它体积小巧但功能强大,对硬件资源要求很低而且性能表现卓越,非常适合于嵌入式应用环境。最近发现sqlite并不支持中文(拼音/笔画)排序,而这个功能又是我们必需的,所以花了些时间去研究。我对Sqlite的了解只能算是业余级,在研究的过程或许走了些弯路,或许已经有现存的算法可利用,不管怎么样,在研究过程中还是有不少收获,写出来和大家探讨一下。

我 们知道,计算机中的每一个字符都有一个内码。在默认情况下,计算机排序时,比较两个字符的大小就是比较字符内码的大小,这对于英文来说没有问题,因为英文 字母的内码是按字母顺序递增的。对于中文来说,就比较麻烦了:首先,中文的排序方式有多种,比如按内码排序、按拼音排序和按笔画排序,要通过参数指定排序 的方式,否则计算机就按内码排序了。其次,汉字的内码顺序即不同于拼音顺序,也不同于按笔画顺序。在GB2312编码中,汉字基本上按拼音排序(据说有例外,不太清楚)。在GBK中,它在GB2312基础上进行了扩充,兼容GB2312中的所有字符,所以不是按拼音排序了。在Unicode中,汉字的排列似乎更没有什么规律可言了。

为了解决内码顺序与用户习惯顺序(如拼音顺序)的冲突,在glibclocale数据里,要求提供排序方式(collate)的描述。我看了一下glibc-2.3.5提供的locale数据,在简体中文(zh_CN)locale数据描述里,关于排序方式的描述如下:

% ISO 14651 collation sequence

LC_COLLATE

copy "iso14651_t1"

END LC_COLLATE

也就是说,照抄iso14651_t1的排序方式。打开iso14651_t1文件看了一下,也没有发现关于中文的特殊处理,可以推断glibc默认的排序方式就是按unicode排序。

所以不能指望glibc提供中文排序功能,如果SQLite支持了中文排序只能是做了特殊处理。浏览了一下SQLite的代码,这种希望似乎也不大。在网上也没有查到相关的资料和补丁,看来只能靠自己了。

不过,在浏览SQLite代码时还是有些收获,至少知道了它比较数据记录的过程:

1. sqlite3VdbeExec调用sqlite3BtreeInsert把记录插入到适当的位置。

2. sqlite3BtreeInsert调用sqlite3BtreeMoveto找到要插入的位置。

3. sqlite3BtreeMoveto调用sqlite3VdbeRecordCompare比较两条记录的大小。

4. sqlite3VdbeRecordCompare调用sqlite3MemCompare比较字段的大小。

5. sqlite3MemCompare调用binCollFunc去做真正的比较。

6. binCollFunc是一个回调函数,由外层设置的。

进一步研究,知道了binCollFunc的来源:

1. struct CollSeq是一个用来比较的对象,它带有一个比较函数和相关上下文。

2. 通过multiSelectCollSeq找到合适的CollSeq对象。

3. multiSelectCollSeq调用sqlite3ExprCollSeq查找。

4. multiSelectCollSeq调用sqlite3CheckCollSeq查找。

5. 查找标准是SelectCreate TABLE所带的COLLATE子句。

6. 也就是说可以通过SelectCreate TABLE的参数来决定选择哪个比较函数。

基于上面这些认识,我们知道比较函数是可以指定的了。接下来的问题是,我们能否自定义比较函数,如何自定义,以及如何安装到SQLite里。很快发现SQLite已经提供了安装比较函数的接口:

int sqlite3_create_collation16(

sqlite3* db,

const char *zName,

int enc,

void* pCtx,

int(*xCompare)(void*,int,const void*,int,const void*)

)

int sqlite3_create_collation(

sqlite3* db,

const char *zName,

int enc,

void* pCtx,

int(*xCompare)(void*,int,const void*,int,const void*)

)

前者用来安装UTF-16的比较函数,后者用来安装UTF-8的比较函数。我们发现,在main.c里已经安装了一些内置的比较函数:

sqlite3_create_collation(db, "BINARY", SQLITE_UTF8, 0,binCollFunc);

sqlite3_create_collation(db, "BINARY", SQLITE_UTF16, 0,binCollFunc);

sqlite3_create_collation(db, "NOCASE", SQLITE_UTF8, 0, nocaseCollatingFunc);

好了,原理清楚了,我们要做的只是提供一个比较函数,并且安装进去就OK了。为了测试,我写一个按拼音排序的比较函数(按笔画排序类似):

int pinyin_cmp(

void *NotUsed,

int nKey1, const void *pKey1,

int nKey2, const void *pKey2)

{

int n = nKey1 < nKey1 ? nKey1 : nKey2;

return pinyin_strncmp(pKey1, pKey2, n + 1);

}

安装比较函数时要注意,因为我们实现的比较函数是针对UTF-16的,所以名字要用UTF-16编码。但是由于linux下默认的wchar_t32位的,不能直接用L”pinyin”的方式把ANSI字符串转换成UTF-16字符串,只能按下列方式。

unsigned short zName[] = {'p', 'i', 'n', 'y', 'i', 'n', 0};

sqlite3_create_collation16(db, zName, SQLITE_UTF16, 16, pinyin_cmp);

测试结果正常(红色部分为按拼音排序,蓝色部分为默认排序):

sqlite> create table person(name text, age int);

sqlite> insert into person values("张三", 23);

sqlite> insert into person values("张三丰", 23);

sqlite> insert into person values("李四", 24);

sqlite> insert into person values("李四叔", 24);

sqlite> insert into person values("王五", 25);

sqlite> insert into person values("王五妹", 25);

sqlite> insert into person values("赵七", 26);

sqlite> insert into person values("赵七姑", 26);

sqlite>

sqlite> select * from person order by name collate pinyin;

李四|24

李四叔|24

王五|25

王五妹|25

张三|23

张三丰|23

赵七|26

赵七姑|26

sqlite> select * from person order by name;

张三|23

张三丰|23

李四|24

李四叔|24

王五|25

王五妹|25

赵七|26

赵七姑|26

总结:SQLite的架构设计非常优秀,接口定义得也比较合理,支持中文排序变得非常简单。

2007年3月13日火曜日

SQLite的体系结构简介

简介

Block Diagram Of SQLite

这篇文档主要描述了SQLite类库的结构。这篇文档的内容对于那些想了解和修改SQLite内部结构的人将会非常有用。

右侧是一个结构图,它显示了SQLite的主要成分及各成分之间是如何相互关联的。接下来的文本将简要的介绍每个单一的成分。

这篇文档描述SQLite第三版,它和 2.8版以及早期的版本基本相似,但在一些细节上是有区别的。

接口程序

SQLite类库大部分的公共接口程序是由main.c, legacy.c, 和 vdbeapi.c源文件中的功能执行的。 但有些程序是分散在其他文件夹的,因为在其他文件夹里他们可以访问有文件作用域的数据结构。 sqlite3_get_table() 这个程序是在table.c中执行的。 sqlite3_mprintf()printf.c中执行。 sqlite3_complete()tokenize.c中执行。 Tcl 接口程序用tclsqlite.c来执行。

为了避免和其它软件在名字上有冲突,SQLite类库中所有的外部符号都是以sqlite3为前缀来命名的。这些被用来做外部使用的符号(换句话说,这些符号用来形成SQLite的API)是以sqlite3_.

来命名的。

Tokenizer

当执行一个包含SQL语句的字符串时,接口程序要把这个字符串传递给tokenizer。Tokenizer的任务是把原有字符串分成一个个标示符,并把这些标示符传递给剖析器。Tokenizer是在C文件夹tokenize.c中用手编译的。

在这个设计中需要注意的一点是,tokenizer调用parser。熟悉YACC和BISON的人们也许会习惯于用parser调用tokenizer。 The author of SQLite的作者已经尝试了这两种方法,并发现用tokenizer调用parser会使程序运行的更顺利。YACC使程序更滞后一些。

Parser

The parser是一个部分,它基于文件场景赋予tokens意思。SQLite的parser是由 Lemon LALR(1) parser generator产生的。Lemon和YACC/BISON一样做同样的工作,但是它使用不同的输入语句,这个输入语句是不易出错的。 Lemon也产生一个parser,这个parser是可重入的并且是线程安全的。 Lemon 定义了无终端解除程序的概念,所以当遇到语法错误的时候,它不会泄露内存。驱动Lemon的原文件在parse.y.

因为lemon是一个在发展机械上不常见的程序,所以lemon的源代码(只是一个C文件)是在SQLite分布区的"tool"子目录下的。 lemon的文档是在分布区的 "doc"子目录下的。

代码发生器

在剖析器收集完符号并把之转换成完全的SQL语句时,它调用代码产生器来产生虚拟的机器代码,这些机器代码将按照SQL语句的要求来工作。在代码产生器中有许多文件; attach.c, auth.c, build.c, delete.c, expr.c, insert.c, pragma.c, select.c, trigger.c, update.c, vacuum.c and where.c. 正是在这些文件中,最具有重要意义的事情发生了。 expr.c 处理表达式代码的生成。 where.c 处理SELECT, UPDATE and DELETE语句中WHERE子句的代码的生成。文件 attach.c, delete.c, insert.c, select.c, trigger.c update.c,和vacuum.c处理SQL语句中具有同样名字的语句的代码的生成。(每个文件调用expr.c and where.c中的程序) All other 所有SQL的其它语句的代码是由build.c生成的。文件auth.c 执行sqlite3_set_authorizer()的功能.

虚拟机器

由代码生成器产生的程序由虚拟机器来运行。总而言之,虚拟机器主要用来执行一个为操作数据库而设计的抽象的计算引擎。机器有一个用来存储中间数据的存储栈。每个指令包含一个操作代码和三个额外的操作数。

虚拟机器本身是被包含在一个单独的文件vdbe.c中的。虚拟机器也有它自己的标题文件:vdbe.h它在虚拟机器和剩下的SQLite类库之间定义了一个接口程序,vdbeInt.h 它定义了虚拟机器的结构。文件vdbeaux.c包含了虚拟机器所使用的实用程序和一些被其它类库用来建立VM程序的接口程序模块。文件vdbeapi.c 包含虚拟机器的外部接口,比如 sqlite3_bind_... 类的函数。单独的值(字符串,整数,浮动点数值,BLOBS)被存储在一个叫 "Mem"的内部目标程序里,"Mem"是由vdbemem.c执行的。

SQLite使用C语言程序来来执行SQL函数。即使内置的SQL函数也是用这种方法来执行的。大部分的SQL内置函数(ex: coalesce(), count(),substr(), and so forth)可以在func.c里发现。日期和时间转换函数在date.c.

B-树

SQLite数据库在磁盘里维护,使用源文件btree.c中的B-树执行。数据库中的每个表格和目录使用一个单独的B-tree。所有的 B-trees 被存储在同样的磁盘文件里。文件格式的细节被记录在btree.c.

开头的备注里。

B-tree子系统的接口程序被标题文件btree.h所定义。.

页面高速缓存

B-tree模块要求信息来源于磁盘上固定规模的程序块。默认程序块的大小是1024个字节,但是可以在512和65536个字节间变化。 页面高速缓存负责读,写和高速缓存这些程序块。页面高速缓存还提供重新运算和提交抽象命令,它还管理关闭数据库文件夹。 B-tree驱动器要求页面高速缓存器中的特别的页,当它想修改页或重新运行改变的时候,它会通报页面高速缓存。为了保证所有的需求被快速,安全和有效的 处理,页面高速缓存处理所有的微小的细节。

运行页面高速缓存的代码在专门的C源文件pager.c中。页面高速缓存的子系统的接口程序被目标文件pager.h所定义。

OS 接口程序

为了在POSIX和Win32 之间提供一些可移植性,SQLite操作系统的接口程序使用一个提取层。 OS提取层的接口程序被定义在os.h. 每个支持的操作系统有它自己的执行文件: Unix使用os_unix.c,windows使用os_win.c。每个具体的操作器具有它自己的标题文件: os_unix.h, os_win.h, etc.

Utilities

内存分配和字符串比较程序位于util.c。剖析器使用的表格符号被hash.c中的无用信息表格维护。源文件utf.c包含UNICODE转换子程序。SQLite有它自己的执行文件printf() (有一些扩展)在printf.c中,还有它自己随机数量产生器在random.c.

测试代码

如果你计算回归测试脚本,多于一半的SQLite代码数据库的代码将被测试。 在主要代码文件中有许多assert()语句。另外,源文件test1.c通过test5.cmd5.c 执行只为测试用的扩展名。os_test.c向后的接口程序用来模拟断电,来验证页面调度程序中的系统性事故恢复机制。

Sqlite.net 2.0使用笔记

以下内容使用Visual C# 2005 和 Sqlite.net 2.0 1.0.35.1版
(2.0指支持的.net协议,1.0.35.1指该软件)

下载安装好Sqlite.net 2.0 之后(安装的时候会选择支持的IDE,如下图,默认没有选中,记得选上),在visual stdio 2005里面就可以使用他了,

在Server Explorer里面右击Data Connections,

即出现下面的对话框:

注意Data Source里面的内容,如果不是Sqlite相关,则点选change,选中SQLite Database File

点 击OK回到创建数据库对话框,其他需要更改的选项是选择New创建一个数据库和更改Encoding,我选中了Unicode 16以便更好的支持中文(我并不确定Unicode 8对中文的支持情况,我只确定Unicode 16对中文支持良好,如果你了解相关内容,请留言)。
点击OK之后就可以使用SQLite作为你的后台数据库了。

连接好之后,我们就可以用SQL编辑我们的数据库了,如果我们的数据库没有连接好(表现为有一个小红叉),右击之后点Refresh就可以了,这个过程可能有点漫长,反正在我的256内存的机器上有半分钟的时间。


再次右击Sqlite,选择New Query,我们就可以以Sql的方式编辑数据库,点击之后出来一个对话框叫选择数据表,因为我们还没有一个具体的表,所以这个就直接点击Close就可以了,然后就出来如下面第二幅图所示的Sql界面,可以输入Sql语句了。

2007年3月2日金曜日

FAQ中文版

(1) 如何创建自增字段?

简单的回答:一个声明为 INTEGER PRIMARY KEY 的字段将自动增加。

这里是详细的答案: 从 SQLite 的 2.3.4 版本开始,如果你将一个表中的一个字段声明为 INTEGER PRIMARY KEY,那么无论你何时向该表的该字段插入一个 NULL 值,这个 NULL 值将自动被更换为比表中该字段所有行的最大值大 1 的整数;如果表为空,那么将被更换为 1。比如,假设你有这样的一张数据表:

CREATE TABLE t1(
a INTEGER PRIMARY KEY,
b INTEGER
);

在这张数据表里,声明

INSERT INTO t1 VALUES(NULL,123);

在逻辑意义上等价于:

INSERT INTO t1 VALUES((SELECT max(a) FROM t1)+1,123);

一个新的API函数 sqlite3_last_insert_rowid() 返回最近的插入操作的整形键

注意这个整型键始终比之前插入表中的最后一个键大1。新键相对于表中的已有键来说是唯一的, 但它可能与之前从表中删除的键值重叠。要始终得到在整个表中唯一的键,在INTEGER PRIMARY KEY的声明之前加关键词AUTOINCREMENT.这样被选的键将总是比表中已存在的最大键大1。若可能的 最大键已存在于表中,INSERT操作将失败并返回一个SQLITE_FULL错误码.


(2) SQLite 支持哪些数据类型?

参见 http://www.sqlite.org/datatype3.html.


(3) 为什么能向 SQLite 数据库的整型字段中插入字符串?

这是一个功能,不是一个 bug。你可以在任何字段中放任何信息,而不用管字段声明为什么类型。 你可以往整型字段中插入任意长度的字符串,或者往布尔字段中插入浮点数,或者往字符字段中 插入日期。在 CREATE TABLE 命令中你指定给这个字段的数据类型不会限制插入这个字段的数据。 所有的字段可以插入任意长度的字符串。但对于 INTEGER PRIMARY KEY 字段例外。这种字段只能 存放一个64位的整数,否则会出错。

但SQLite会默认你希望使用声明的字段类型。所以,比如你希望在一个声明为INTEGER的字段 中插入一个字符串,SQLite会试图将其转换为一个整数。如果转换成功,那么整数将被插入,否 则插入字符串,这种特性有时被称作type or column affinity.


(4) 为什么 SQLite 认为表达式 '0'=='00' 为真?

在 2.7.0 之后,表达式不成立。参见文档 datatypes in SQLite version 3


(5) 为什么 SQLite 不允许在同一张表里使用 '0' 和 '0.0' 作为两个不同的行的主键?

你的主键一定是数值类型的,把类型改为 TEXT 就可以了。

每一行必须有一个唯一的主键。作为一个数字类型的字段,SQLite 认为 '0''0.0'的值是相同的, 因为他们在数字上的比较是相等的(看前面的问题)因此值不是唯一的。


(6) 为什么不能在 Linux box 中读取在 SparcStation 中创建的 SQLite 数据库?

你需要升级你的 SQLite 库到 2.6.3 或更新版本。

x86 处理器是 little-endian 型的而 Sparc 是 big-endian 型的。新版本的 SQLite 解决了这个问题。

注:   big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。


(7) 多个应用程序或者同一个应用程序的多个例程能同时存取同一个数据库文件吗?

多进程可以同时打开同一个数据库,也可以同时 SELECT 。但只有一个进程可以立即改数据库。

SQLite使用读/写锁定来控制数据库访问。(Win95/98/ME 操作系统缺乏读/写锁定支持,在低于 2.7.0 的版本中,这意味着在 windows 下在同一时间内只能有一个进程读数据库。在版本 2.7.0 中 这个问题通过在 windows 接口代码中执行一个用户间隔几率读写锁定策略解决了。) 但如果数据库文件在一个 NFS 文件系统中,控制并发读书的锁定机制可以会出错。因为 NFS 的fcntl() 文件锁定有时会出问题。如果有多进程可能并发读数据库则因当避免把数据库文件放在 NFS 文件系统中。 根据微软的文档,如果不运行 Share.exe 后台程序则 FAT 文件系统中的锁定可能不工作。对 Windows 非常有经验的人告诉我网络文件的锁定有许多问题并且不可靠。如果是这样,在2个或以上 Windows 系统中共享一个 SQLite 数据库文件会导致不可预知的问题。

我们知道没有其他的嵌入式 SQL数据库引擎比SQLite支持更多的并发性。 SQLite允许多进程 同时打开和读取数据库。任何一个进程需要写入时,整个数据库将在这一过程中被锁定。但这一般仅耗时 几毫秒。其他进程只需等待然后继续其他事务。其他嵌入式SQL数据库引擎往往只允许单进程访问数据库。

但是,client/server型的数据库引擎 (如 PostgreSQL, MySQL, 以及 Oracle) 通常支持更高的并发度, 并支持多进程同时写入同一个数据库。由于总有一个控制良好的服务器协调数据库的访问,这才保证了以上 特性的实现。如果你的应用需要很高的并发度,你应该考虑使用client/server数据库。事实上,经验告诉 我们大多数应用所需要的并发度比他们的设计者们想象的要少得多。

当 SQLite 尝试操作一个被另一个进程锁定的文件时,缺省的行为是返回 SQLITE_BUSY。你可以用 C代码更改这一行为。 使用 sqlite3_busy_handler()sqlite3_busy_timeout() API函数。

如果两个或更多进程同时打开同一个数据库,其中一个进程创建了新的表或索引,则其它进程可能不能立即看见新的表。其它进程可能需要关闭并重新连结数据库。


(8) SQLite是线程安全的吗?

有时候是的。为了线程安全,SQLite 必须在编译时把 THREADSAFE 预处理宏设为1。在缺省的发行的已编译版本中 Windows 版的是线程安全的,而 Linux 版的不是。如果要求线程安全,Linux 版的要重新编译。

“线程安全”是指二个或三个线程可以同时调用独立的不同的sqlite3_open() 返回的"sqlite3"结构。而不是在多线程中同时使用同一个 sqlite3 结构指针。

一个sqlite3结构只能在调用 sqlite3_open创建它的那个进程中使用。你不能在一个线程中打开一个数据库然后把指针传递给另一个线程使用。这是因为大多数多线程系统的限制(或 Bugs?)例如RedHat9上。在这些有问题的系统上,一个 线程创建的fcntl()锁不能由另一个线程删除或修改。由于SQLite依赖fcntl()锁来进行并发控制,当在线程间传递数据库连接时会出现严重的问题。

也许在Linux下有办法解决fcntl()锁的问题,但那十分复杂并且对于正确性的测试将是极度困难的。因此,SQLite目前不允许在线程间共享句柄。

在UNIX下,你不能通过一个 fork() 系统调用把一个打开的 SQLite 数据库放入子过程中,否则会出错。


(9) 如何列出一个 SQLite 数据库中的所有的表/索引?

sqlite3 命令行程序中你可以用命令 ".tables" 来显示所有的表或者用 ".schema"来显示所有的表结构和索引。但命令后不要跟 LIKE 语句,否则会限制表的显示。

在 C/C++ 程序中 (或使用 Tcl/Ruby/Perl/Python绑定的脚本中)你可以通过访问名为"SQLITE_MASTER的表来实现。每个 SQLite 数据库有一个 SQLITE_MASTER 表,表内有数据库的结构。SQLITE_MASTER表是这样的:

CREATE TABLE sqlite_master (
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);

对于表来说,type字段的值为'table'name 字段是表的名称。使用以下语句可以等到所有表的列表:

SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name;

对于索引来说, type = 'index', name 是索引的名称, tbl_name 是索引所属的表的名称。对于表和索引,sql 字段是创建表或索引的原始语句文本。对于自动创建的索引(一般是使用 PRIMARY KEY 或 UNIQUE 创建的),sql字段为 NULL.

SQLITE_MASTER表是只读的。你不能对该表使用 UPDATE, INSERT, 或 DELETE。该表自动由 CREATE TABLE, CREATE INDEX, DROP TABLE 和 DROP INDEX 命令更新。

临时表及其索引不在 SQLITE_MASTER 表中而在 SQLITE_TEMP_MASTER 中出现。SQLITE_TEMP_MASTER 与 SQLITE_MASTER 表一样工作,但只对于创建临时表的程序可见。要得到所在表包括临时表可以使用如下命令:

SELECT name FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
WHERE type='table'
ORDER BY name

(10) SQLite数据库是否有已知的大小限制?

数据库大小被限制在 2TB(241 bytes). 这是理论限制。事实上,你应该把 SQLite数据库的大小限制在100GB以下,以免出现运行性能上的问题。如果你需要储存100GB或更多数据在一个数据库中, 考虑使用为此而设计的企业版数据库吧。

一个数据库的理论行数限制是 264-1,显然你会在达到行数限制之前先超过文件大小的限制。目前一行可以存放 230 bytes 数据。而基本的文件格式可以支持行大小到约 262 bytes.

可能还会有对于表、索引的数目或表和索引中的字段数的限制,但没人知道是多少。事实上,每当新数据库打开时,SQLite需要读取和 分析所有表和索引声明的初始SQL,所以,为了调用 sqlite3_open() 时获得最佳性能,最好减少声明的表的数目。同样的,即使 对于表中字段数没有限制,多于100个也显得太多了。 只有表开头的31个字段会得到优化。你可以在一个索引中放入任意多的字段但超过30字段的索引将不用于优化查询。

表,索引,视图,触发器和字段名称可以任意长,但SQL 函数名 (由 sqlite3_create_function() API创建的)不得超过255个字符。


(11) 在 SQLite 中 VARCHAR 的最大长度是多少?

SQLite不强制VARCHAR的长度。你可以声明一个VARCHAR(10),SQLite一样可以让你存放500个字符在里面。 并且它们会始终完整无缺——决不会被截断。


(12) SQLite 是否支持 BLOB 类型?

SQLite 3.0 版支持在任何字段存放 BLOB 数据,不管字段声明为什么类型。


(13) 如何从一个已存在的 SQLite 数据表中添加/删除字段?

SQLite有有限的ALTER TABLE支持,可以用于添加字段到表的末尾 或更改表名。如果你要对表的结构作更复杂的修改,你需要重新创建表。你可以在一个临时表中备份数据,撤销旧表,重建新表后再恢复数据。

例如,假设你有一个名为 "t1" 的表,有名为 "a", "b", 和 "c" 三个字段,你要删除字段 "c" 。可按如下步骤操作:

BEGIN TRANSACTION;
CREATE TEMPORARY TABLE t1_backup(a,b);
INSERT INTO t1_backup SELECT a,b FROM t1;
DROP TABLE t1;
CREATE TABLE t1(a,b);
INSERT INTO t1 SELECT a,b FROM t1_backup;
DROP TABLE t1_backup;
COMMIT;

(14) 我删除了很多数据但是数据库文件并没有减小,是不是 Bug?

不是的。当你从 SQLite 删除数据之后,未使用的磁盘空间被添加到一个内在的“空闲列表”中用于存储你下次插入的数据。磁盘空间并没有丢失,但是也不向操作系统返回磁盘空间。

如果你删除了大量的数据且想要减小数据库文件,执行 VACUUM命令。VACUUM 命令会清空“空闲列表”,把数据库尺寸缩到最小。注意, VACUUM 会耗费一些时间(在 Linux 系统下大约0.5秒/兆)并且要使用两倍于数据库文件大小的磁盘空间。

对于SQLite version 3.1, 替代VACUUM命令的一个方法是auto-vacuum模式,用 auto_vacuum pragma语法开启该模式。


(15) 是否能将 SQLite 用于商业用途而不用交版权费用?

可以。SQLite 是公开的。代码的任何部分都没有声明所有权。你可以用它来做你想要的任何事情。


(16) 如何插入有单引号(')的字符串?

使用双单引号即可,例如:

    INSERT INTO xyz VALUES('5 O''clock');

插入数据库的是:5 0'clock。


(17) SQLITE_SCHEMA 错误代表什么?

在 SQLite 版本3中,当一个预处理 SQL 语句不合法不能执行时就会返回一个 SQLITE_SCHEMA 错误。当这个错误发生时,该语句应当用 sqlite3_prepare() API函数重新编译。在 SQLite 版本3中,只有使用 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API函数执行 SQL 才会发生这个错误,而使用 sqlite3_exec(). 则不会。这与版本2不同。

大部分发生这个错误的原因是当 SQL 预处理完时数据库已经改变了(可能是被另一个进程改变的)。还可能有如下原因:

  • 对一个数据库进行DETACH操作
  • 对一个数据库进行VACUUM操作
  • 一个用户函数定义被删除或改变了。
  • 一个排序定义被删除或改变了。
  • 一个授权函数改变了。

解决的办法是重新编译并再次尝试执行。所有涉及 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API 函数的都应当重新编译。参见下例:

    int rc;
sqlite3_stmt *pStmt;
char zSql[] = "SELECT .....";

do {
/* Compile the statement from SQL. Assume success. */
sqlite3_prepare(pDb, zSql, -1, &pStmt, 0);

while( SQLITE_ROW==sqlite3_step(pStmt) ){
/* Do something with the row of available data */
}

/* Finalize the statement. If an SQLITE_SCHEMA error has
** occured, then the above call to sqlite3_step() will have
** returned SQLITE_ERROR. sqlite3_finalize() will return
** SQLITE_SCHEMA. In this case the loop will execute again.
*/
rc = sqlite3_finalize(pStmt);
} while( rc==SQLITE_SCHEMA );



(18) 为什么ROUND(9.95,1) 返回 9.9 而不是 10.0? 难道9.95 不该向上进位么?

SQLite 内部使用二进制运算,9.95用 64-bit IEEE 浮点数 ( SQLite 内部使用的) 表示为9.949999999999999289457264239899814128875732421875。所以当你输入 "9.95"时,SQLite 就理解为上述的数字,进而四舍五入得到9.9。这个问题在处理浮点二进制数总会产生。通常的规则是十进制的有限浮点数通常无法表示为二进制有限浮点数,只能由最接近的二进制数来代替。这个近似数会非常接近原数,但总一些细微的不同,所以可能无法得到你预期的结果。

2007年3月1日木曜日

SQLite调用C++封装类

如果你已经习惯了面向对象编写代码,那么对Sqlite提供的C接口可能让你使用起来比较费劲。所以你需要对sqliteC接口进行封装,用面向对象的方法使用sqlite。以下介绍一个对sqliteC++封装。

先来看看封装之后的使用方法吧:

Void main(void) {

try {

/*

打开一个数据库连接,只需创建一个sqlite3_connection对象指定要连接的数

据库,如果数据不存在创建这个数据库。

*/

sqlite3_connection con("test.db");

/*

执行一条sql语句,查询当前系统是否存在表名为’t_test’的表

直接调用sqlite3_connection的方法可以执行语句,并根据语句类型返回不同类型的值,当然需要调用不同的方法,比如返回一个string型的值可以调用con.executestring()方法。

*/

int count=con.executeint("select count(*) from sqlite_master where name='t_test';");//

if(count==0)

{

/*

创建数据表:t_test,包含两个字段。

*/

con.executenonquery("create table t_test(number,string);");

}

//为了测试使用和不使用事务的时候效率的区别

time_t begin,end;

begin = time(NULL);

/*

使用事务执行语句

*/

sqlite3_transaction trans(con);

{

/*

创建sqlite3_command对象来执行语句,对于需要给定参数的地方,可以在语句上写“?”,在用bind()方法来给定参数的实际值。

*/

sqlite3_command cmd(con, "insert into t_test values(?,?);");

cmd.bind(2, "foobar", 6);

for(int i=0; i<1000000;>

cmd.bind(1, (int)i);

cmd.executenonquery();

}

}

trans.commit();

end = time(NULL);

con.close();

cout<<"execute time"<<

}

catch(exception &ex) {

/*

捕捉异常,输出异常信息

*/

cerr << "Exception Occured: " <<>

}

}

这样使用起来很方便,相比以前纯C接口中的回调函数,这个使用方式更加符合面向对象的思想。而且加上了异常捕捉的信息之后可以很方便的对程序中出现的问题进行判断。比如一个sql语句写错了,或者是数据库读写冲突都可以在exception信息中给出。调试起来也是相当容易的。

SQLite原来自带的回调函数方法比较的复杂,而且并没有什么实质性的好处,使用起来不太方便,数据在调用函数与回调函数之间的传递也比较麻烦。而且对面向对象的思想也是一种破坏,因此推荐使用此封装类。

使用封装类对于SQLite的另外一个主要的问题也比较容易解决-----数据库的锁定问题。关于这个问题我们将在下一篇文章中介绍。

下载本文所提到的源代码

SQLite函数使用教程(2)

SQLite的使用过程中最常发生的数据库异常便是数据库被锁定了(SQLITE_BUSY或者SQLITE_LOCKED)。SQLite对于并发的处理机制是允许同一个进程的多个线程同时读取一个数据库,但是任何时刻只允许一个线程/进程写入数据库。所以必须要必须要对数据库的读写进行控制。

SQLite数据库本身提供了两个函数用来处理锁定情况:

int sqlite3_busy_handler(sqlite3*, int(*)(void*,int), void*);

int sqlite3_busy_timeout(sqlite3*, int ms);

使用这两个函数可以设定当发生数据库锁定的时候,调用什么函数来处理,以及设定锁定时的等待时间。

当然通常情况下我们还可以用另外一种方法来解决这类问题,那便是设置互斥量。一般情况下我们可以使用互斥量,临界区等来实现。在这里我使用了互斥量,主要是因为我需要在多个不同的进程中并发的访问同一个数据库。用于锁定和解锁的函数如下:

void Lock()

{

if((hCounter = OpenMutex(MUTEX_ALL_ACCESS,FALSE,"kangxiaofang")) == NULL)

{

//如果没有其他进程创建这个互斥量,则重新创建

hCounter = CreateMutex(NULL,FALSE,"kangxiaofang");

WaitForSingleObject(hCounter,INFINITE);

}

else

{

WaitForSingleObject(hCounter,INFINITE);

}

}

void UnLock()

{

//释放使用权

ReleaseMutex(hCounter);

//关闭句柄

CloseHandle(hCounter);

}

当然我们也可以把等待时间设定为60秒,以免出现死等的现象。具体使用时可以按如下调用:

//锁定数据库

Lock();

rc = sqlite3_exec(db, "BEGIN;", 0, 0, &zErrMsg);

rc = sqlite3_exec(db, exec, 0, 0, &zErrMsg);

rc = sqlite3_exec(db, "COMMIT;", 0, 0, &zErrMsg);

//数据库解锁

UnLock();

经过测试这种方法的消耗要小于循环打开数据库的操作:)接下来我们要讨论一下,如何在SQLite中存取大容量的数据(二进制)。

SQLite函数使用教程(1)

SQLite官方的使用介绍中附带了一个C语言的实例,该实例使用了回调的方法处理由SQL语句返回的记录集,这种回调函数的方法比较复杂,而且也并没有获得什么实质性的好处。所以在这里我推荐使用另外一组函数来完成该类操作,经实践检验证明,该方法在效率方面不存在任何问题。具体代码如下:

sqlite3 *db;

char *zErrMsg = 0;

int nrow = 0, ncolumn = 0; //查询语句返回的结果集的行数,列数

char temp[256], FileRoot[256];

char ** azResult; //用于保存查询结果集数据

sprintf(temp, _T("%s"), _T("\\test.db"));

/*

SQLite3UTF-8的编码方式处理字符串,所以在使用sqlite3_open打开数据库之前需要将数据库路径进行编码转换。

*/

CCodingConv::GB2312_2_UTF8(FileRoot, 256, temp, 0);

sqlite3_open(FileRoot, &db);

if(db == NULL)

{

return -1;

}

// sqlite3_get_table函数可以返回SQL语句执行之后的记录集,以及行列数

sqlite3_get_table(db, "SELECT name, age FROM list;", &azResult, &nrow, &ncolumn, &zErrMsg);

//nrow表明一共有多少项符合该次查询条件的记录

for(int k = 1; k <= nrow; k++)

{

printf(azResult[k*ncolumn]); //对应name列的数据

printf(azResult[k*ncolumn+1]); //对应age列的数据

}

//释放为记录集分配的内存,如果返回了空记录集并不会发生错误

sqlite3_free_table(azResult);

//关闭数据库

sqlite3_close(db);

return 0;

注释:原代码中的CCodingConv类可以进行UTF-8GB2312之间的转换。

数据库的表为

CREATE TABLE list (name varchar(40) UNIQUE, age integer);

存入数据:

sqlite3_exec(db, "insert into list values ('tamsyn','24');", 0, 0, &zErrMsg);

sqlite3_exec(db, "insert into list values ('mistletoe','23');", 0, 0, &zErrMsg);

使用这种方法在C语言中可以更方便的使用SQLite3,避免了回调函数的复杂性。并且可以完成任何SQL语言的查询操作。接下来我们的文章会结合实际操作,总结一下如何更高效的使用SQLite数据库!一点愚见还望大家不吝赐教!

下载本文所附的源代码