本文為轉載文章,轉載地址:
http://blog.csdn.net/majiakun1/article/details/46607163(1-5,8小結)
http://blog.csdn.net/lijinqi1987/article/details/51852721(6-7小結)
前言
SQLite數據庫由於其簡單、靈活、輕量、開源,已經被越來越多的被應用到中小型應用中。甚至有人說,SQLite完全可以用來取代c語言中的文件讀寫操作。因此我最近編寫有關遙感數據處理的程序的時候,也將SQLite引入進來,以提高數據的結構化程度,並且提高大數據的處理能力(SQLite最高支持2PB大小的數據)。但是最開始,我發現,直接使用SQL語句的插入效率簡直低的令人發指的。后來不斷查文檔、查資料,才發現了一條快速的“數據插入”之路。本文就以插入數據為例,整合網上和資料書中的各種提高SQLite效率的方法,給出提高SQLite數據插入效率的完整方法。
1 數據
我使用的電腦是Win7 64位系統,使用VC2010編譯,SQLIte版本為3.7.15.2 ,電腦CPU為二代i3處理器,內存6G。
實驗之前,先建立要插入數據的表:
View Code1 create table t1 (id integer , x integer , y integer, weight real)
2 慢速——最粗暴的方法
SQLite的API中直接執行SQL的函數是:
View Code1 int sqlite3_exec( sqlite3*, const char *sql, int (*callback)(void*,int,char**,char**), void *, char **errmsg)直接使用INSERT語句的字符串進行插入,程序部分代碼(完整代碼見后文),如下:
View Code1 for(int i=0;i<nCount;++i) 2 { 3 std::stringstream ssm; 4 ssm<<"insert into t1 values("<<i<<","<<i*2<<","<<i/2<<","<<i*i<<")"; 5 sqlite3_exec(db,ssm.str().c_str(),0,0,0); 6 }這個程序運行的太慢了,我已經沒時間等待了,估算了一下,基本上是 7.826 條/s
3 中速——顯式開啟事務
所謂”事務“就是指一組SQL命令,這些命令要么一起執行,要么都不被執行。在SQLite中,每調用一次sqlite3_exec()函數,就會隱式地開啟了一個事務,如果插入一條數據,就調用該函數一次,事務就會被反復地開啟、關閉,會增大IO量。如果在插入數據前顯式開啟事務,插入后再一起提交,則會大大提高IO效率,進而加數據快插入速度。
開啟事務只需在上述代碼的前后各加一句開啟與提交事務的命令即可:
View Code1 sqlite3_exec(db,"begin;",0,0,0); 2 for(int i=0;i<nCount;++i) 3 { 4 std::stringstream ssm; 5 ssm<<"insert into t1 values("<<i<<","<<i*2<<","<<i/2<<","<<i*i<<")"; 6 sqlite3_exec(db,ssm.str().c_str(),0,0,0); 7 } 8 sqlite3_exec(db,"commit;",0,0,0);顯式開啟事務后,這個程序運行起來明顯快很多,估算效率達到了34095條/s,較原始方法提升約5000倍。
4 高速——寫同步(synchronous)
我要使用一個遙感處理算法處理10000*10000的影像,中間有一步需要插入100000000條數據到數據庫中,如果按照開啟事務后的速度34095條/s,則需要100000000÷34095 = 2932秒 = 48.9分,仍然不能夠接受,所以我接着找提升速度的方法。終於,在有關講解SQLite配置的資料中,看到了“寫同步”選項。
在SQLite中,數據庫配置的參數都由編譯指示(pragma)來實現的,而其中synchronous選項有三種可選狀態,分別是full、normal、off。這篇博客以及官方文檔里面有詳細講到這三種參數的設置。簡要說來,full寫入速度最慢,但保證數據是安全的,不受斷電、系統崩潰等影響,而off可以加速數據庫的一些操作,但如果系統崩潰或斷電,則數據庫可能會損毀。
SQLite3中,該選項的默認值就是full,如果我們再插入數據前將其改為off,則會提高效率。如果僅僅將SQLite當做一種臨時數據庫的話,完全沒必要設置為full。在代碼中,設置方法就是在打開數據庫之后,直接插入以下語句:
View Code1 sqlite3_exec(db,"PRAGMA synchronous = OFF; ",0,0,0);此時,經過測試,插入速度已經變成了41851條/s,也就是說,插入100000000條數據,需要2389秒 = 39.8分
5 極速——執行准備
雖然寫同步設為off后,速度又有小幅提升,但是仍然較慢。我又一次踏上了尋找提高SQLite插入效率方法的道路上。終於,我發現,SQLite執行SQL語句的時候,有兩種方式:一種是使用前文提到的函數sqlite3_exec(),該函數直接調用包含SQL語句的字符串;另一種方法就是“執行准備”(類似於存儲過程)操作,即先將SQL語句編譯好,然后再一步一步(或一行一行)地執行。如果采用前者的話,就算開起了事務,SQLite仍然要對循環中每一句SQL語句進行“詞法分析”和“語法分析”,這對於同時插入大量數據的操作來說,簡直就是浪費時間。因此,要進一步提高插入效率的話,就應該使用后者。
“執行准備”主要分為三大步驟:
1.調用函數:
View Code1 int sqlite3_prepare_v2( sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail);並且聲明一個指向sqlite3_stmt對象的指針,該函數對參數化的SQL語句zSql進行編譯,將編譯后的狀態存入ppStmt中。
2.調用函數 sqlite3_step() ,這個函數就是執行一步(本例中就是插入一行),如果函數返回的是SQLite_ROW則說明仍在繼續執行,否則則說明已經執行完所有操作;
3.調用函數 sqlite3_finalize(),關閉語句。
關於執行准備的API的具體語法,詳見官方文檔。本文中執行准備的c++代碼如下:
View Code1 sqlite3_exec(db,"begin;",0,0,0); 2 sqlite3_stmt *stmt; 3 const char* sql = "insert into t1 values(?,?,?,?)"; 4 sqlite3_prepare_v2(db,sql,strlen(sql),&stmt,0); 5 6 for(int i=0;i<nCount;++i) 7 { 8 sqlite3_reset(stmt); 9 sqlite3_bind_int(stmt,1,i); 10 sqlite3_bind_int(stmt,1,i*2); 11 sqlite3_bind_int(stmt,1,i/2); 12 sqlite3_bind_double(stmt,1,i*i); 13 } 14 sqlite3_finalize(stmt); 15 sqlite3_exec(db,"commit;",0,0,0);此時測試數據插入效率為:265816條/s,也就是說,插入100000000條數據,需要376秒 = 6.27分。這個速度已經很滿意了。
綜上所述啊,SQLite插入數據效率最快的方式就是:事務+關閉寫同步+執行准備(存儲過程),如果對數據庫安全性有要求的話,就開啟寫同步。
6 使用WAL模式
(1).WAL:Write Ahead Logging,他是數據庫中用於實現原子事務的一種機制,從3.7.0版本后引入
(2).WAL模式主要有兩個優點:
a.讀寫可以完全並發進行,不會互相阻塞(但是寫之間仍然不能並發)
b.WAL在大多情況下,擁有更好的性能(因為無需每次寫入時都要寫兩個文件)
(3).Rollback journal機制原理:在修改數據庫中的數據前,先將修改所在分頁中的數據備份在另一個地方,然后再將修改寫入到數據中;如果事務失敗,則將備份數據拷貝回來,撤銷修改;如果事務成功,則刪除備份,提交修改。
(4).WAL機制原理:修改並不直接寫入到數據庫文件中,而是寫入到另外一個稱為WAL的文件中,如果事務失敗,wal中的文件會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。
性能差異主要源於每次事務提交,wal只需要將更新的日志寫入磁盤,而delete模式首先要將原始數據拷貝到日志文件中,並進行fsync,然后將修改頁寫入磁盤,同時也需要fsync,確保數據落盤,並且還要清除日志文件。因此寫事務在WAL模式下,只需要一次 fsync,並且是順序寫,而在delete模式下需要至少兩次fsync(日志,數據),並且更新的數據離散分布在多個page中,因此可能需要多個fsync。
WAL使用共享內存技術,因此所有讀寫進程必須在同一個機器上
開啟WAL模式的方法:

1 sqlite3_exec(db, "PRAGMA journal_mode=WAL; ", 0,0,0);
1 sqlite3_exec(db, "PRAGMA journal_mode=WAL; ", 7 總
在前面的基礎上,使用WAL模式后執行5次操作平均耗時4.324秒
7.內存數據庫
另外,如果數據無需長時間保存,可以使用sqlite的內存數據庫替代文件數據庫
開啟sqlite內存數據庫的方式:

1 sqlite3* db = NULL; 2 rc = sqlite3_open(":memory", &db);</span>
執行5次平均耗時:4.052秒
但是內存數據庫存在如下缺點:
(1).斷電或者程序崩潰后數據庫就會消失
(2).在內存中的數據庫不能被別的進程訪問
(3).不支持像在硬盤上的讀寫互斥處理,需要自己加鎖
8 總結
參考資料:
1. SQLite官方文檔: http://www.sqlite.org/docs.html2.《解決sqlite3插入數據很慢的問題》: http://blog.csdn.net/victoryknight/article/details/74617033.《The Definitive Guide to SQLite》Apress出版: http://www.apress.com/9781430232254 (這是本好書)
附最終完整代碼:

1 #include <iostream> 2 #include <string> 3 #include <sstream> 4 #include <time.h> 5 #include "sqlite3.h" 6 7 const int nCount = 500000; 8 9 int main (int argc,char** argv) 10 { 11 sqlite3* db; 12 sqlite3_open("testdb.db" ,&db); 13 sqlite3_exec(db,"PRAGMA synchronous = OFF; ",0,0,0); 14 sqlite3_exec(db,"drop table if exists t1",0,0,0); 15 sqlite3_exec(db,"create table t1(id integer,x integer,y integer ,weight real)",0,0,0); 16 clock_t t1 = clock(); 17 18 sqlite3_exec(db,"begin;",0,0,0); 19 sqlite3_stmt *stmt; 20 const char* sql = "insert into t1 values(?,?,?,?)"; 21 sqlite3_prepare_v2(db,sql,strlen(sql),&stmt,0); 22 23 for(int i=0;i<nCount;++i) 24 { 25 // std::stringstream ssm; 26 // ssm<<"insert into t1 values("<<i<<","<<i*2<<","<<i/2<<","<<i*i<<")"; 27 // sqlite3_exec(db,ssm.str().c_str(),0,0,0); 28 sqlite3_reset(stmt); 29 sqlite3_bind_int(stmt,1,i); 30 sqlite3_bind_int(stmt,2,i*2); 31 sqlite3_bind_int(stmt,3,i/2); 32 sqlite3_bind_double(stmt,4,i*i); 33 sqlite3_step(stmt); 34 } 35 sqlite3_finalize(stmt); 36 sqlite3_exec(db,"commit;",0,0,0); 37 clock_t t2 = clock(); 38 39 sqlite3_close(db); 40 41 std::cout<<"cost tima: "<<(t2-t1)/1000.<<"s"<<std::endl; 42 43 return 0; 44 }