看来从版本3.3.1基本上已经支持线程句柄的传递功能。具体限制我标记了一下。
(6) Is SQLite threadsafe?
SQLite is threadsafe. We make this concession since many users choose to ignore the advice given in the previous paragraph. But in order to be thread-safe, SQLite must be compiled with the SQLITE_THREADSAFE preprocessor macro set to 1. Both the Windows and linux precompiled binaries in the distribution are compiled this way. If you are unsure if the SQLite library you are linking against is compiled to be threadsafe you can call the sqlite3_threadsafe() interface to find out.
Prior to version 3.3.1, an sqlite3 structure could only be used in the same thread that called sqlite3_open() to create it. You could not open a database in one thread then pass the handle off to another thread for it to use. This was due to limitations (bugs?) in many common threading implementations such as on RedHat9. Specifically, an fcntl() lock created by one thread cannot be removed or modified by a different thread on the troublesome systems. And since SQLite uses fcntl() locks heavily for concurrency control, serious problems arose if you start moving database connections across threads.
The restriction on moving database connections across threads was relaxed somewhat in version 3.3.1. With that and subsequent versions, it is safe to move a connection handle across threads as long as the connection is not holding any fcntl() locks.
You can safely assume that no locks are being held if no transaction is pending and all statements have been finalized.
只要不在事务过程当中,并且所有的statements已经被finalized,那么不同线程间共享句柄就是安全的.
Under UNIX, you should not carry an open SQLite database across a fork() system call into the child process. Problems will result if you do.
(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函数。
如果两个或更多进程同时打开同一个数据库,其中一个进程创建了新的表或索引,则其它进程可能不能立即看见新的表。其它进程可能需要关闭并重新连结数据库。
这几天研究了一下SQLite这个嵌入式数据库在多线程环境下的应用,感觉里面的学问还挺多,于是就在此分享一下。
先说下初衷吧,实际上我经常看到有人抱怨SQLite不支持多线程。而在iOS开发时,为了不阻塞主线程,数据库访问必须移到子线程中。为了解决这个矛盾,很有必要对此一探究竟。
关于这个问题,最权威的解答当然是SQLite官网上的“Is SQLite threadsafe?”这个问答。
简单来说,从3.3.1版本开始,它就是线程安全的了。而iOS的SQLite版本没有低于这个版本的:
3.4.0 - iPhone OS 2.2.1
3.6.12 - iPhone OS 3.0 / 3.1
3.6.22 - iPhone OS 4.0
3.6.23.2 - iPhone OS 4.1 / 4.2
3.7.2 - iPhone OS 4.3
3.7.7 - iPhone OS 5.0
当然,你也可以自己编译最新版本。只是我发现自己编译出来的3.7.8居然比iOS 4.3.3内置的3.7.2慢了一半,不知道苹果做了什么优化。发现是我编译成了debug版本,改成release后性能比内置版本高5%左右,不过构建出来的app会大420k左右。
不过这个线程安全仍然是有限制的,在这篇《Is SQLite thread-safe?》里有详细的解释。
另一篇重要的文档就是《SQLite And Multiple Threads》。它指出SQLite支持3种线程模式:
- 单线程:禁用所有的mutex锁,并发使用时会出错。当SQLite编译时加了SQLITE_THREADSAFE=0参数,或者在初始化SQLite前调用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)时启用。
- 多线程:只要一个数据库连接不被多个线程同时使用就是安全的。源码中是启用bCoreMutex,禁用bFullMutex。实际上就是禁用数据库连接和prepared statement(准备好的语句)上的锁,因此不能在多个线程中并发使用同一个数据库连接或prepared statement。当SQLite编译时加了SQLITE_THREADSAFE=2参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)启用;或者在创建数据库连接时,设置SQLITE_OPEN_NOMUTEX flag。
- 串行:启用所有的锁,包括bCoreMutex和bFullMutex。因为数据库连接和prepared statement都已加锁,所以多线程使用这些对象时没法并发,也就变成串行了。当SQLite编译时加了SQLITE_THREADSAFE=1参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_SERIALIZED)启用;或者在创建数据库连接时,设置SQLITE_OPEN_FULLMUTEX flag。
而这里所说的初始化是指调用sqlite3_initialize()函数,这个函数在调用sqlite3_open()时会自动调用,且只有第一次调用是有效的。
另一个要说明的是prepared statement,它是由数据库连接(的pager)来管理的,使用它也可看成使用这个数据库连接。因此在多线程模式下,并发对同一个数据库连接调用sqlite3_prepare_v2()来创建prepared statement,或者对同一个数据库连接的任何prepared statement并发调用sqlite3_bind_*()和sqlite3_step()等函数都会出错(在iOS上,该线程会出现EXC_BAD_ACCESS而中止)。这种错误无关读写,就是只读也会出错。文档中给出的安全使用规则是:没有事务正在等待执行,所有prepared statement都被finalized。
顺带一提,调用sqlite3_threadsafe()可以获得编译期的SQLITE_THREADSAFE参数。标准发行版是1,也就是串行模式;而iOS上是2,也就是多线程模式;Python的sqlite3模块也默认使用串行模式,可以用sqlite3.threadsafety来配置。但是默认情况下,一个线程只能使用当前线程打开的数据库连接,除非在连接时设置了check_same_thread=False参数。
现在3种模式都有所了解了,清楚SQLite并不是对多线程无能为力后,接下来就了解下事务吧。
数据库只有在事务中才能被更改。所有更改数据库的命令(除SELECT以外的所有SQL命令)都会自动开启一个新事务,并且当最后一个查询完成时自动提交。
而BEGIN命令可以手动开始事务,并关闭自动提交。当下一条COMMIT命令执行时,自动提交再次打开,事务中所做的更改也被写入数据库。当COMMIT失败时,自动提交仍然关闭,以便让用户尝试再次提交。若执行的是ROLLBACK命令,则也打开自动提交,但不保存事务中的更改。关闭数据库或遇到错误时,也会自动回滚事务。
经常有人抱怨SQLite的插入太慢,实际上它可以做到每秒插入几万次,但是每秒只能提交几十次事务。因此在插入大批数据时,可以通过禁用自动提交来提速。
事务在改写数据库文件时,会先生成一个rollback journal(回滚日志),记录初始状态(其实就是备份),所有改动都是在数据库文件上进行的。当事务需要回滚时,可以将备份文件的内容还原到数据库文件;提交成功时,默认的delete模式下会直接删除这个日志。这个日志也可以帮助解决事务执行过程中断电,导致数据库文件损坏的问题。但如果操作系统或文件系统有bug,或是磁盘损坏,则仍有可能无法恢复。
而从3.7.0版本(对应iOS 4.3)开始,SQLite还提供了Write-Ahead Logging模式。与delete模式相比,WAL模式在大部分情况下更快,并发性更好,读和写之间互不阻塞;而其缺点对于iPhone这种嵌入式设备来说可以忽略,只需注意不要以只读方式打开WAL模式的数据库即可。
使用WAL模式时,改写操是附加(append)到WAL文件,而不改动数据库文件,因此数据库文件可以被同时读取。当执行checkpoint操作时,WAL文件的内容会被写回数据库文件。当WAL文件达到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默认值是1000)页(默认大小是1KB)时,会自动使用当前COMMIT的线程来执行checkpoint操作。也可以关闭自动checkpoint,改为手动定期checkpoint。
为了避免读取的数据不一致,查询时也需要读取WAL文件,并记录一个结尾标记(end mark)。这样的代价就是读取会变得稍慢,但是写入会变快很多。要提高查询性能的话,可以减小WAL文件的大小,但写入性能也会降低。
需要注意的是,低版本的SQLite不能读取高版本的SQLite生成的WAL文件,但是数据库文件是通用的。这种情况在用户进行iOS降级时可能会出现,可以把模式改成delete,再改回WAL来修复。
要对一个数据库连接启用WAL模式,需要执行“PRAGMA journal_mode=WAL;”这条命令,它的默认值是“journal_mode=DELETE”。执行后会返回新的journal_mode字符串值,即成功时为"wal",失败时为之前的模式(例如"delete")。一旦启用WAL模式后,数据库会保持这个模式,这样下次打开数据库时仍然是WAL模式。
要停止自动checkpoint,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函数。手动执行checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函数。
还有一个很重要的知识点需要强调:事务是和数据库连接相关的,每个数据库连接(使用pager来)维护自己的事务,且同时只能有一个事务(但是可以用SAVEPOINT来实现内嵌事务)。
也就是说,事务与线程无关,一个线程里可以同时用多个数据库连接来完成多个事务,而多个线程也可以同时(非并发)使用一个数据库连接来共同完成一个事务。
下面用Python来演示一下:
# -*- coding: utf-8 -*-
import sqlite3
import threading
def f():
con.rollback()
con = sqlite3.connect(‘test.db‘, check_same_thread=False) # 允许在其他线程中使用这个连接
cu = con.cursor()
cu.execute(‘CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)‘)
print cu.execute(‘SELECT count(*) FROM test‘).fetchone()[0] # 0
cu.execute(‘INSERT INTO test VALUES (NULL)‘)
print cu.execute(‘SELECT count(*) FROM test‘).fetchone()[0] # 1
thread = threading.Thread(target=f)
thread.start()
thread.join()
print cu.execute(‘SELECT count(*) FROM test‘).fetchone()[0] # 0
cu.close()
con.close()
在这个例子中,虽然是在子线程中执行rollback,但由于和主线程用的是同一个数据库连接,所以主线程所做的更改也被回滚了。
而如果是用不同的数据库连接,每个连接都不能读取其他连接中未提交的数据,除非使用read-uncommitted模式。
而要实现事务,就不得不用到锁。
一个SQLite数据库文件有5种锁的状态:
- UNLOCKED:表示数据库此时并未被读写。
- SHARED:表示数据库可以被读取。SHARED锁可以同时被多个线程拥有。一旦某个线程持有SHARED锁,就没有任何线程可以进行写操作。
- RESERVED:表示准备写入数据库。RESERVED锁最多只能被一个线程拥有,此后它可以进入PENDING状态。
- PENDING:表示即将写入数据库,正在等待其他读线程释放SHARED锁。一旦某个线程持有PENDING锁,其他线程就不能获取SHARED锁。这样一来,只要等所有读线程完成,释放SHARED锁后,它就可以进入EXCLUSIVE状态了。
- EXCLUSIVE:表示它可以写入数据库了。进入这个状态后,其他任何线程都不能访问数据库文件。因此为了并发性,它的持有时间越短越好。
一个线程只有在拥有低级别的锁的时候,才能获取更高一级的锁。SQLite就是靠这5种类型的锁,巧妙地实现了读写线程的互斥。同时也可看出,写操作必须进入EXCLUSIVE状态,此时并发数被降到1,这也是SQLite被认为并发插入性能不好的原因。
另外,read-uncommitted和WAL模式会影响这个锁的机制。在这2种模式下,读线程不会被写线程阻塞,即使写线程持有PENDING或EXCLUSIVE锁。
提到锁就不得不说到死锁的问题,而SQLite也可能出现死锁。
下面举个例子:
连接1:BEGIN (UNLOCKED)
连接1:SELECT ... (SHARED)
连接1:INSERT ... (RESERVED)
连接2:BEGIN (UNLOCKED)
连接2:SELECT ... (SHARED)
连接1:COMMIT (PENDING,尝试获取EXCLUSIVE锁,但还有SHARED锁未释放,返回SQLITE_BUSY)
连接2:INSERT ... (尝试获取RESERVED锁,但已有PENDING锁未释放,返回SQLITE_BUSY)
现在2个连接都在等待对方释放锁,于是就死锁了。当然,实际情况并没那么糟糕,任何一方选择不继续等待,回滚事务就行了。
不过要更好地解决这个问题,就必须更深入地了解事务了。
实际上BEGIN语句可以有3种起始状态:
- DEFERRED:默认值,开始事务时不获取任何锁。进行第一次读操作时获取SHARED锁,进行第一次写操作时获取RESERVED锁。
- IMMEDIATE:开始事务时获取RESERVED锁。
- EXCLUSIVE:开始事务时获取EXCLUSIVE锁。
现在考虑2个事务在开始时都使用IMMEDIATE方式:
连接1:BEGIN IMMEDIATE (RESERVED)
连接1:SELECT ... (RESERVED)
连接1:INSERT ... (RESERVED)
连接2:BEGIN IMMEDIATE (尝试获取RESERVED锁,但已有RESERVED锁未释放,因此事务开始失败,返回SQLITE_BUSY,等待用户重试)
连接1:COMMIT (EXCLUSIVE,写入完成后释放)
连接2:BEGIN IMMEDIATE (RESERVED)
连接2:SELECT ... (RESERVED)
连接2:INSERT ... (RESERVED)
连接2:COMMIT (EXCLUSIVE,写入完成后释放)
这样死锁就被避免了。
而EXCLUSIVE方式则更为严苛,即使其他连接以DEFERRED方式开启事务也不会死锁:
连接1:BEGIN EXCLUSIVE (EXCLUSIVE)
连接1:SELECT ... (EXCLUSIVE)
连接1:INSERT ... (EXCLUSIVE)
连接2:BEGIN (UNLOCKED)
连接2:SELECT ... (尝试获取SHARED锁,但已有EXCLUSIVE锁未释放,返回SQLITE_BUSY,等待用户重试)
连接1:COMMIT (EXCLUSIVE,写入完成后释放)
连接2:SELECT ... (SHARED)
连接2:INSERT ... (RESERVED)
连接2:COMMIT (EXCLUSIVE,写入完成后释放)
不过在并非很高的情况下,直接获取EXCLUSIVE锁的难度比较大;而且为了避免EXCLUSIVE状态长期阻塞其他请求,最好的方式还是让所有写事务都以IMMEDIATE方式开始。
顺带一提,要实现重试的话,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函数。
由此可见,要想保证线程安全的话,可以有这4种方式:
- SQLite使用单线程模式,用一个专门的线程访问数据库。
- SQLite使用单线程模式,用一个线程队列来访问数据库,队列一次只允许一个线程执行,队列里的线程共用一个数据库连接。
- SQLite使用多线程模式,每个线程创建自己的数据库连接。
- SQLite使用串行模式,所有线程共用全局的数据库连接。
接下来就一一测试这几种方式在iPhone 4(iOS 4.3.3,SQLite 3.7.2)上的性能表现。
第一种方式太过麻烦,需要线程间通信,这里我就忽略了。
第二种方式可以用dispatch_queue_create()来创建一个serial queue,或者用一个maxConcurrentOperationCount为1的NSOperationQueue来实现。
这种方式的缺点就是事务必须在一个block或operation里完成,否则会乱序;而耗时较长的事务会阻塞队列。另外,没法利用多核CPU的优势。
先初始化数据库:
#import <sqlite3.h>
static char dbPath[200];
static sqlite3 *database;
static sqlite3 *openDb() {
if (sqlite3_open(dbPath, &database) != SQLITE_OK) {
sqlite3_close(database);
NSLog(@"Failed to open database: %s", sqlite3_errmsg(database));
}
return database;
}
- (void)viewDidLoad {
[super viewDidLoad];
sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);
NSLog(@"%d", sqlite3_threadsafe());
NSLog(@"%s", sqlite3_libversion());
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
strcpy(dbPath, [[documentsDirectory stringByAppendingPathComponent:@"data.sqlite3"] UTF8String]);
database = openDb();
char *errorMsg;
if (sqlite3_exec(database, "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER);", NULL, NULL, &errorMsg) != SQLITE_OK) {
NSLog(@"Failed to create table: %s", errorMsg);
}
}
再插入1000条测试数据:
static void insertData() {
char *errorMsg;
if (sqlite3_exec(database, "BEGIN TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) {
NSLog(@"Failed to begin transaction: %s", errorMsg);
}
static const char *insert = "INSERT INTO test VALUES (NULL, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, insert, -1, &stmt, NULL) == SQLITE_OK) {
for (int i = 0; i < 1000; ++i) {
sqlite3_bind_int(stmt, 1, arc4random());
if (sqlite3_step(stmt) != SQLITE_DONE) {
--i;
NSLog(@"Error inserting table: %s", sqlite3_errmsg(database));
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
}
if (sqlite3_exec(database, "COMMIT TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) {
NSLog(@"Failed to commit transaction: %s", errorMsg);
}
static const char *query = "SELECT count(*) FROM test;";
if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
NSLog(@"Table size: %d", sqlite3_column_int(stmt, 0));
} else {
NSLog(@"Failed to read table: %s", sqlite3_errmsg(database));
}
sqlite3_finalize(stmt);
}
}
然后创建一个串行队列:
static dispatch_queue_t queue;
- (void)viewDidLoad {
// ...
queue = dispatch_queue_create("net.keakon.db", NULL);
}
再设置一个计数器,每秒执行一次:
static int lastReadCount = 0;
static int readCount = 0;
static int lastWriteCount = 0;
static int writeCount = 0;
- (void)count {
int lastRead = lastReadCount;
int lastWrite = lastWriteCount;
lastReadCount = readCount;
lastWriteCount = writeCount;
NSLog(@"%d, %d", lastReadCount - lastRead, lastWriteCount - lastWrite);
}
- (void)viewDidLoad {
// ...
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(count) userInfo:nil repeats:YES];
}
这样就可以开始测试select和update了:
static void readData() {
static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;";
void (^ __block readBlock)() = Block_copy(^{
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, arc4random());
int returnCode = sqlite3_step(stmt);
if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) {
++readCount;
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
dispatch_async(queue, readBlock);
});
dispatch_async(queue, readBlock);
}
static void writeData() {
static const char *update = "UPDATE test SET value = ? WHERE id = ?;";
void (^ __block writeBlock)() = Block_copy(^{
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, arc4random());
sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1);
if (sqlite3_step(stmt) == SQLITE_DONE) {
++writeCount;
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
dispatch_async(queue, writeBlock);
});
dispatch_async(queue, writeBlock);
}
这里是用dispatch_async()来异步地递归调用block。
因为block是在栈里生成的,异步执行时已经被销毁,所以需要copy到堆。因为需要一直执行,所以我就没release了。
此外,光copy的话还是无法正常执行,但是把block本身的存储类型设为__block后就正常了,原因我也不清楚。
测试结果为只读时平均每秒165次,只写时每秒68次,同时读写时每秒各47次。换成多线程或串行模式时,效率也差不多。
接着试试WAL模式:
if (sqlite3_exec(database, "PRAGMA journal_mode=WAL;", NULL, NULL, &errorMsg) != SQLITE_OK) {
NSLog(@"Failed to set WAL mode: %s", errorMsg);
}
sqlite3_wal_checkpoint(database, NULL); // 每次测试前先checkpoint,避免WAL文件过大而影响性能
测试结果为只读时平均每秒166次,只写时每秒244次,同时读写时每秒各97次。并发性增加了1倍有木有!更夸张的是写入比读取还快了。
在自编译的3.7.8版中,同时读写为每秒各102次,加上SQLITE_THREADSAFE=0参数后为每秒各104次,性能稍有提升。
第三种方式需要打开和关闭数据库连接,所以会额外消耗一些时间。此外还要维持各个连接间的互斥,事务也比较容易冲突,但能确保事务正确执行。
首先需要移除全局的database变量,并修改openDb()函数:
static sqlite3 *openDb() {
sqlite3 *database = NULL;
if (sqlite3_open(dbPath, &database) != SQLITE_OK) {
sqlite3_close(database);
NSLog(@"Failed to open database: %s", sqlite3_errmsg(database));
}
return database;
}
再配置成多线程模式:
sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
队列改成可以乱序执行的:
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
然后是访问数据库:
static void readData() {
static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;";
dispatch_async(queue, ^{
sqlite3 *database = openDb();
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) {
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
int returnCode = sqlite3_step(stmt);
if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) {
++readCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
sqlite3_close(database);
});
}
static void writeData() {
static const char *update = "UPDATE test SET value = ? WHERE id = ?;";
dispatch_async(queue, ^{
sqlite3 *database = openDb();
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) {
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1);
if (sqlite3_step(stmt) == SQLITE_DONE) {
++writeCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
sqlite3_close(database);
});
}
这里就无需递归调用了,直接在子线程中循环即可。
测试结果为只读时平均每秒164次,只写时每秒68次,同时读写时分别为每秒14和30次(波动很大)。此外,这种方式因为最初启动的几个线程持续访问数据库,后加入的线程会滞后几秒才启动,且很难打开数据库连接或创建prepare statement。调试时发现只会启用2个线程,但是随队列中block数目的增加,读性能增高,写性能降低。读写各3个block时分别为每秒35和14次。
WAL模式下甚至连初始时启动2个线程都会被lock,因此只能改成不断重试:
static void readData() {
static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;";
dispatch_async(queue, ^{
sqlite3 *database = openDb();
sqlite3_stmt *stmt;
while (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) != SQLITE_OK);
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
int returnCode = sqlite3_step(stmt);
if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) {
++readCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
sqlite3_close(database);
});
}
static void writeData() {
static const char *update = "UPDATE test SET value = ? WHERE id = ?;";
dispatch_async(queue, ^{
sqlite3 *database = openDb();
sqlite3_stmt *stmt;
while (sqlite3_prepare_v2(database, update, -1, &stmt, nil) != SQLITE_OK);
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1);
if (sqlite3_step(stmt) == SQLITE_DONE) {
++writeCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
sqlite3_close(database);
});
}
结果为只读时平均每秒169次,只写时每秒246次,同时读写时每秒分别为90和57次(波动较大)。并发效率有了显著提升,但仍不及第二种方式。
第四种方式相当于让SQLite来维护队列,只不过SQL的执行是乱序的,因此无法保证事务性。
先恢复全局的database变量,然后配置成串行模式:
sqlite3_config(SQLITE_CONFIG_SERIALIZED);
再是访问数据库:
static void readData() {
static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;";
dispatch_async(queue, ^{
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) {
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
int returnCode = sqlite3_step(stmt);
if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) {
++readCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
});
}
static void writeData() {
static const char *update = "UPDATE test SET value = ? WHERE id = ?;";
dispatch_async(queue, ^{
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) {
while (YES) {
sqlite3_bind_int(stmt, 1, arc4random());
sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1);
if (sqlite3_step(stmt) == SQLITE_DONE) {
++writeCount;
}
sqlite3_reset(stmt);
}
sqlite3_finalize(stmt);
} else {
NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database));
}
});
}
测试结果为只读时平均每秒164次,只写时每秒68次,同时读写时每秒分别为57和43次。读线程比写线程的速率更高,而且新线程的加入不需要等待。
WAL模式下,只读时平均每秒176次,只写时每秒254次,同时读写时每秒分别为109和85次。
由此可见,要获得最好的性能的话,WAL模式是必须启用的,为此也有必要自己编译SQLite 3.7.0以上的版本(除非不支持iOS 4.2及以下版本)。
而在测试过的后3种方式中:第3种是效率最低的,不建议使用;第4种读取性能更高,适合无需使用事务的场合;第2种适用范围更广,效率也足够优秀,一般应采用这种方式。
不过要注意的是,第2种方式在测试时的逻辑是完全与数据库相关的。实际中可能要做计算或IO访问等工作,在此期间其他线程都是被阻塞的,这样就会大大降低效率了。因此只建议把访问数据库的逻辑放入队列,其余工作在其他线程里完成。
刚才洗澡时我又想到一点,既然第2种方式不能并行,第4种方式不能保证事务性,那么能否将各自的优点结合起来呢?
于是一个新的实现方案又浮出水面了:使用2个串行队列,分别负责读和写,每个队列各使用一个数据库连接,线程模式可以采用多线程或串行模式。
代码拿方式2稍做修改就行了,这里就不列出了。测试结果波动比较大(估计是checkpoint的影响),多线程模式下平均约为89和73次,串行模式下为91和86次。
但在iPad 2这种双核的机型上,多线程明显要比单队列更具优势:方式2的成绩是每秒各85次,方式3是94和124次(写波动较大),方式4是95和72次,而新方案在多线程模式下是104和168次(写波动很大,40~280之间),串行模式下为108和177次(写波动很大)。
因此极端的优化情况下,可以根据CPU核心数来创建队列数,然后把数据库访问线程随机分配到某个队列中。不过考虑到iOS设备这种嵌入式平台并不需要密集地访问数据库,而且除数据库线程以外还有其他事要做,如果没遇到瓶颈的话,简单的方案2其实也够用了。
1)如果数据库配置成为串行模式:sqlite3_config(SQLITE_CONFIG_SERIALIZED)
此模式下可以多个线程使用一个数据库连接,但显然对数据库的操作很慢;
所以建议:
数据库配置成多线程模式:sqlite3_config(SQLITE_CONFIG_MULTITHREAD)
此模式下多个线程必须各自使用自己的数据库连接,多个连接对SQLite的操作不用担心,因为SQLite是线程安全的。
但是这样仍然不好,线程数过多(上百左右个线程发起数据操作)会导致数据库压力大,尤其是有事务操作时会发起很多事务请求,这时所有的带事务请求都会卡在beginTransaction,严重影响性能,使得整个App中所有对数据库数据有请求的页面一直loading,虽然界面不会卡死,但是数据一直出不来;
所以,采用FMDB queue(其实就是GCD串行队列),因为FMDB queue串行队列,所以所有的数据操作包括事务都是串行执行的,所有多线程发起的数据库操作都被异步放到了这个队列里,不会卡住UI,也不会有数据库事务并发问题;
2)事务中有大数据量(几千条)条件查询(注意是条件查询),然后再数据更新和插入
按1)优化后也有可能性能还是慢的情况,也就是如2)题,这种查询特别慢,严重影响了事务的执行;所以尽量避免在事务中有大数据量条件查询;
3)最后,把数据库的WAL模式开启,这样数据库的写操作很快,而且可以对数据并发写和读取;
WAL is significantly faster in most scenarios.
WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.
Disk I/O operations tends to be more sequential using WAL.
WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems where the fsync() system call is broken.
综上,总结如下:
1)数据库多线程模下,有并发事务或并发事务多会超成数据库操作拥堵,这个与数据库模式无关(FMDB queue解决)
2)事务中有大数据(几千条)的先条件查询后更新或插入操作时会很慢(避免事务中大数据条件查询)
2)开启WAL模式;
3)区分对待数据库串行模式和串行数据库操作队列;
目前,我采用的是:a)多线程数据库操作+FMDB Queue(W/R两队表两数据库连接)+数据库SQLITE_CONFIG_MULTITHREAD模式+WAL+事务中避免大数据条件查询
但是,我觉得另一种方式的性能也应该不错:b)多线程数据库操作+数据库SQLITE_CONFIG_SERIALIZED模式+(WAL)+一个数据库连接+事务中避免大数据条件查询
a)中数据库模式虽然是多线程(SQLITE_CONFIG_MULTITHREAD)模式,但所有程序级多线程数据库操作都被FMDB queue串行化,最多也就两个数据库并发(W/R),各自一个数据库连接; SQLITE_CONFIG_MULTITHREAD模式是好处在于读和写可分成各一个FMDB queue以及开启WAL模式支持可并发写和读
b)中数据库模式是串行(SQLITE_CONFIG_SERIALIZED)模式且一个数据库连接,所以不需要队列来串行化程序级的多线程数据库操作;同时WAL模式是可选开启,如果开启则写数据库会更快,但没有并发的读和写(因为当前数据库是SQLITE_CONFIG_SERIALIZED模式)
sqlite3中的常见问题
sqlite常见问题
(1) 如何建立自动增长字段?
简短回答:声明为 INTEGER PRIMARY KEY 的列将会自动增长。
长一点的答案: 如果你声明表的一列为 INTEGER PRIMARY KEY,那么, 每当你在该列上插入一NULL值时, NULL自动被转换为一个比该列中最大值大1的一个整数,如果表是空的, 将会是1。 (如果是最大可能的主键 9223372036854775807,那个,将键值将是随机未使用的数。) 如,有下列表:
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)SQLite3支持何种数据类型?
NULL
INTEGER
REAL
TEXT
BLOB
但实际上,sqlite3也接受如下的数据类型:
smallint 16 位元的整数。
interger 32 位元的整数。
decimal(p,s) p 精确值和 s 大小的十进位整数,精确值p是指全部有几个数(digits)大小值 ,s是指小数点後有几位数。如果没有特别指定,则系统会设为 p=5; s=0 。
float 32位元的实数。
double 64位元的实数。
char(n) n 长度的字串,n不能超过 254。
varchar(n) 长度不固定且其最大长度为 n 的字串,n不能超过 4000。
graphic(n) 和 char(n) 一样,不过其单位是两个字元 double-bytes, n不能超过127。 这个形态是为了支援两个字元长度的字体,例如中文字。
vargraphic(n) 可变长度且其最大长度为 n 的双字元字串,n不能超过 2000。
date 包含了 年份、月份、日期。
time 包含了 小时、分钟、秒。
timestamp 包含了 年、月、日、时、分、秒、千分之一秒。
参见 http://www.sqlite.org/datatype3.html.
--------------------------------------------------------------------------------
(3)SQLite允许向一个integer型字段中插入字符串!
这是一个特性,而不是一个bug。SQLite不强制数据类型约束。 任何数据都可以插入任何列。你可以向一个整型列中插入任意长度的字符串, 向布尔型列中插入浮点数,或者向字符型列中插入日期型值。 在 CREATE TABLE 中所指定的数据类型不会限制在该列中插入任何数据。 任何列均可接受任意长度的字符串(只有一种情况除外: 标志为INTEGER PRIMARY KEY的列只能存储64位整数, 当向这种列中插数据除整数以外的数据时,将会产生错误。
但SQLite确实使用声明的列类型来指示你所期望的格式。所以, 例如你向一个整型列中插入字符串时,SQLite会试图将该字符串转换成一个整数。 如果可以转换,它将插入该整数;否则,将插入字符串。 这种特性有时被称为 类型或列亲和性(type or column affinity).
--------------------------------------------------------------------------------
(4)为什么SQLite不允许在同一个表不同的两行上使用0和0.0作主键?
主键必须是数值类型,将主键改为TEXT型将不起作用。
每一行必须有一个唯一的主键。对于一个数值型列, SQLite认为 ‘0‘ 和 ‘0.0‘ 是相同的, 因为他们在作为整数比较时是相等的(参见上一问题)。 所以,这样值就不唯一了。
--------------------------------------------------------------------------------
(5)多个应用程序或一个应用程序的多个实例可以同时访问同一个数据库文件吗?
多个进程可同时打开同一个数据库。多个进程可以同时进行SELECT 操作,但在任一时刻,只能有一个进程对数据库进行更改。
SQLite使用读、写锁控制对数据库的访问。(在Win95/98/ME等不支持读、 写锁的系统下,使用一个概率性的模拟来代替。)但使用时要注意: 如果数据库文件存放于一个NFS文件系统上,这种锁机制可能不能正常工作。 这是因为 fcntl() 文件锁在很多NFS上没有正确的实现。 在可能有多个进程同时访问数据库的时候,应该避免将数据库文件放到NFS上。 在Windows上,Microsoft的文档中说:如果使用 FAT 文件系统而没有运行 share.exe 守护进程,那么锁可能是不能正常使用的。那些在Windows上有很多经验的人告诉我: 对于网络文件,文件锁的实现有好多Bug,是靠不住的。如果他们说的是对的, 那么在两台或多台Windows机器间共享数据库可能会引起不期望的问题。
我们意识到,没有其它嵌入式的 SQL 数据库引擎能象 SQLite 这样处理如此多的并发。SQLite允许多个进程同时打开一个数据库, 同时读一个数据库。当有任何进程想要写时,它必须在更新过程中锁住数据库文件。 但那通常只是几毫秒的时间。其它进程只需等待写进程干完活结束。 典型地,其它嵌入式的SQL数据库引擎同时只允许一个进程连接到数据库。
但是,Client/Server数据库引擎(如 PostgreSQL, MySQL, 或 Oracle) 通常支持更高级别的并发,并且允许多个进程同时写同一个数据库。 这种机制在Client/Server结构的数据库上是可能的, 因为总是有一个单一的服务器进程很好地控制、协调对数据库的访问。 如果你的应用程序需要很多的并发,那么你应该考虑使用一个Client/Server 结构的数据库。但经验表明,很多应用程序需要的并发,往往比其设计者所想象的少得多。
当SQLite试图访问一个被其它进程锁住的文件时,缺省的行为是返回 SQLITE_BUSY。 可以在C代码中使用 sqlite3_busy_handler() 或 sqlite3_busy_timeout() API 函数调整这一行为。
---------------------------------------------------------------------------
(6)SQLite线程安全吗?
线程是魔鬼(Threads are evil)。 避免使用它们。
SQLite 是线程安全的。由于很多用户会忽略我们在上一段中给出的建议, 我们做出了这种让步。但是,为了达到线程安全,SQLite在编译时必须将 SQLITE_THREADSAFE 预处理宏置为1。在Windows和Linux上, 已编译的好的二进制发行版中都是这样设置的。 如果不确定你所使用的库是否是线程安全的,可以调用 sqlite3_threadsafe() 接口找出。
在 3.3.1 版本之前,一个 sqlite3 结构只能被用于调用 sqlite3_open 创建的同一线程。你不能在一个线程中打开数据库, 然后将数据库句柄传递给另外一个进程使用。 这主要是由于在好多通用的线程实现(如RedHat9)中的限制引起的(是Bug吗?)。 特别的,在有问题的系统上,一个进程创建的 fcntl() 锁无法被其它线程清除或修改。 所以,由于 SQLite 大量使用 fcntl() 锁做并发控制, 如果你在不同的线程间移动数据库连接,就可能会出现严重的问题。
在3.3.1版本上,关于在线程间移动数据库连接的限制变得宽松了。 在它及以后的版本中,只要连接没有持有 fcntl() 锁,在线程间移动句柄是安全的。 如果没有未决的事务,并且所有的语句都已执行完毕, 你就可以安全的假定不再持有任何锁。
在UNIX中,在执行 fork() 系统调用时不应携带已打开的数据库进入子进程。 那样做将会有问题。
--------------------------------------------------------------------------------
(7)在SQLite数据库中如何列出所有的表和索引?
如果你运行 sqlite3 命令行来访问你的数据库,可以键入 “.tables”来获得所有表的列表。或者,你可以输入 “.schema” 来看整个数据库模式,包括所有的表的索引。 输入这些命令,后面跟一个LIKE模式匹配可以限制显示的表。
在一个 C/C++ 程序中(或者脚本语言使用 Tcl/Ruby/Perl/Python 等) 你可以在一个特殊的名叫 SQLITE_MASTER 上执行一个SELECT查询以获得所有 表的索引。每一个 SQLite 数据库都有一个叫 SQLITE_MASTER 的表, 它定义数据库的模式。 SQLITE_MASTER 表看起来如下:
CREATE TABLE sqlite_master (
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);
对于表来说,type 字段永远是 ‘table‘,name 字段永远是表的名字。所以,要获得数据库中所有表的列表, 使用下列SELECT语句:
SELECT name FROM sqlite_master
WHERE type=‘table‘
ORDER BY name;
对于索引,type 等于 ‘index‘, name 则是索引的名字,tbl_name 是该索引所属的表的名字。 不管是表还是索引,sql 字段是原先用 CREATE TABLE 或 CREATE INDEX 语句创建它们时的命令文本。对于自动创建的索引(用来实现 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
--------------------------------------------------------------------------------
(8) SQLite数据库有已知的大小限制吗?
在 Windows 和 Unix 下,版本 2.7.4的 SQLite 可以达到 2的41次方字节 (2T 字节)。老版本的为 2的31 次方字节(2G 字节)。
SQLite 版本 2.8 限制一个记录的容量为 1M。SQLite 版本 3.0 则对单个记录容量没有限制。
表名、索引表名、视图名、触发器名和字段名没有长度限制。但 SQL 函数的名称 (由 sqlite3_create_function() API 函数创建) 不得超过 255 个字符。
对有关SQLite限制的详细讨论,见 limits.html 。
--------------------------------------------------------------------------------
(9) 在SQLite中,VARCHAR字段最长是多少?
SQLite 不强制 VARCHAR 的长度。 你可以在 SQLITE 中声明一个 VARCHAR(10),SQLite还是可以很高兴地允许你放入500个字符。 并且这500个字符是原封不动的,它永远不会被截断。
--------------------------------------------------------------------------------
(10) SQLite支持二进制大对象吗?
SQLite 3.0 及以后版本允许你在任何列中存储 BLOB 数据。 即使该列被声明为其它类型也可以。
--------------------------------------------------------------------------------
(11) 在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;
--------------------------------------------------------------------------------
(12) 我在数据库中删除了很多数据,但数据库文件没有变小,是Bug吗?
不是。当你从SQLite数据库中删除数据时, 未用的磁盘空间将会加入一个内部的“自由列表”中。 当你下次插入数据时,这部分空间可以重用。磁盘空间不会丢失, 但也不会返还给操作系统。
如果删除了大量数据,而又想缩小数据库文件占用的空间,执行 VACUUM 命令。 VACUUM 将会从头重新组织数据库。这将会使用数据库有一个空的“自由链表”, 数据库文件也会最小。但要注意的是,VACUUM 的执行会需要一些时间 (在SQLite开发时,在Linux上,大约每M字节需要半秒种),并且, 执行过程中需要原数据库文件至多两倍的临时磁盘空间。
对于 SQLite 3.1版本,一个 auto-vacumm 模式可以替代 VACUUM 命令。 可以使用 auto_vacuum pragma 打开。
--------------------------------------------------------------------------------
(13) 我可以在商业产品中使用SQLite而不需支付许可费用吗?
是的。SQLite 在 public domain。 对代码的任何部分没有任何所有权声明。你可以使用它做任何事。
--------------------------------------------------------------------------------
(14) 如何在字符串中使用单引号(‘)?
SQL 标准规定,在字符串中,单引号需要使用逃逸字符,即在一行中使用两个单引号。在这方面 SQL 用起来类似 Pascal 语言。 SQLite 尊循标准。如:
INSERT INTO xyz VALUES(‘5 O‘‘clock‘);
--------------------------------------------------------------------------------
(15) SQLITE_SCHEMA error是什么错误?为什么会出现该错误?
当一个准备好的(prepared)SQL语句不再有效或者无法执行时, 将返回一个 SQLITE_SCHEMA 错误。发生该错误时,SQL语句必须使用 sqlite3_prepare() API来重新编译. 在 SQLite 3 中, 一个 SQLITE_SCHEMA 错误只会发生在用 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API 执行 SQL 时。而不会发生在使用 sqlite3_exec()时。 在版本2中不是这样。
准备好的语句失效的最通常原因是:在语句准备好后, 数据库的模式又被修改了。另外的原因会发生在:
数据库离线:DETACHed.
数据库被 VACUUMed
一个用户存储过程定义被删除或改变。
一个 collation 序列定义被删除或改变。
认证函数被改变。
在所有情况下,解决方法是重新编译并执行该SQL语句。 因为一个已准备好的语句可以由于其它进程改变数据库模式而失效, 所有使用 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API 的代码都应准备处理 SQLITE_SCHEMA 错误。下面给出一个例子:
int rc;
sqlite3_stmt *pStmt;
char zSql[] = "SELECT .....";
do {
sqlite3_prepare(pDb, zSql, -1, &pStmt, 0);
while( SQLITE_ROW==sqlite3_step(pStmt) ){
}
rc = sqlite3_finalize(pStmt);
} while( rc==SQLITE_SCHEMA );
--------------------------------------------------------------------------------
(16) 为什么 ROUND(9.95,1) 返回 9.9 而不是 10.0? 9.95不应该圆整 (四舍五入)吗?
SQLite 使用二进制算术,在二进制中, 无法用有限的二进制位数表示 9.95 。使用 64-bit IEEE 浮点 (SQLite就是使用这个)最接近 9.95 的二进制表示是 9.949999999999999289457264239899814128875732421875。 所在,当你输入 9.95 时,SQLite实际上以为是上面的数字, 在四舍五入时会舍去。
这种问题在使用二进制浮点数的任何时候都会出现。 通常的规则是记住很多有限的十进制小数都没有一个对应的二进制表示。 所以,它们只能使用最接近的二进制数。它们通常非常接近, 但也会有些微小的不同,有些时候也会导致你所期望的不同的结果。