1. 程式人生 > >提升SQLite資料插入效率低、速度慢的方法

提升SQLite資料插入效率低、速度慢的方法

前言

SQLite資料庫由於其簡單、靈活、輕量、開源,已經被越來越多的被應用到中小型應用中。甚至有人說,SQLite完全可以用來取代c語言中的檔案讀寫操作。因此我最近編寫有關遙感資料處理的程式的時候,也將SQLite引入進來,以提高資料的結構化程度,並且提高大資料的處理能力(SQLite最高支援2PB大小的資料)。但是最開始,我發現,直接使用SQL語句的插入效率簡直低的令人髮指的。後來不斷查文件、查資料,才發現了一條快速的“資料插入”之路。本文就以插入資料為例,整合網上和資料書中的各種提高SQLite效率的方法,給出提高SQLite資料插入效率的完整方法。(大神們勿噴)

1 資料

我使用的電腦是Win7 64位系統,使用VC2010編譯,SQLIte版本為3.7.15.2 ,電腦CPU為二代i3處理器,記憶體6G。
實驗之前,先建立要插入資料的表:
  1. create
    table t1 (id integer , x integer , y integer, weight real)  

慢速——最粗暴的方法

SQLite的API中直接執行SQL的函式是:
  1. int sqlite3_exec(  sqlite3*,    constchar *sql,   int (*callback)(void*,int,char**,char**),   void *,   char **errmsg)  
直接使用INSERT語句的字串進行插入,程式部分程式碼(完整程式碼見後文),如下:
  1. 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
中速——顯式開啟事務
所謂”事務“就是指一組SQL命令,這些命令要麼一起執行,要麼都不被執行。在SQLite中,每呼叫一次sqlite3_exec()函式,就會隱式地開啟了一個事務,如果插入一條資料,就呼叫該函式一次,事務就會被反覆地開啟、關閉,會增大IO量。如果在插入資料前顯式開啟事務,插入後再一起提交,則會大大提高IO效率,進而加資料快插入速度。
開啟事務只需在上述程式碼的前後各加一句開啟與提交事務的命令即可:
  1. 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倍。

高速——寫同步(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。在程式碼中,設定方法就是在開啟資料庫之後,直接插入以下語句:

  1. sqlite3_exec(db,"PRAGMA synchronous = OFF; ",0,0,0);  
此時,經過測試,插入速度已經變成了41851條/s,也就是說,插入100000000條資料,需要2389秒 = 39.8分。
極速——執行準備

雖然寫同步設為off後,速度又有小幅提升,但是仍然較慢。我又一次踏上了尋找提高SQLite插入效率方法的道路上。終於,我發現,SQLite執行SQL語句的時候,有兩種方式:一種是使用前文提到的函式sqlite3_exec(),該函式直接呼叫包含SQL語句的字串;另一種方法就是“執行準備”(類似於儲存過程)操作,即先將SQL語句編譯好,然後再一步一步(或一行一行)地執行。如果採用前者的話,就算開起了事務,SQLite仍然要對迴圈中每一句SQL語句進行“詞法分析”和“語法分析”,這對於同時插入大量資料的操作來說,簡直就是浪費時間。因此,要進一步提高插入效率的話,就應該使用後者。

“執行準備”主要分為三大步驟:

1.呼叫函式

  1. int sqlite3_prepare_v2( sqlite3 *db,  constchar *zSql,  int nByte,  sqlite3_stmt **ppStmt,  constchar **pzTail);  
並且宣告一個指向sqlite3_stmt物件的指標,該函式對引數化的SQL語句zSql進行編譯,將編譯後的狀態存入ppStmt中。

2.呼叫函式 sqlite3_step() ,這個函式就是執行一步(本例中就是插入一行),如果函式返回的是SQLite_ROW則說明仍在繼續執行,否則則說明已經執行完所有操作;

3.呼叫函式 sqlite3_finalize(),關閉語句。

關於執行準備的API的具體語法,詳見官方文件。本文中執行準備的c++程式碼如下:

  1. sqlite3_exec(db,"begin;",0,0,0);  
  2.     sqlite3_stmt *stmt;  
  3.     constchar* sql = "insert into t1 values(?,?,?,?)";  
  4.     sqlite3_prepare_v2(db,sql,strlen(sql),&stmt,0);  
  5.     for(int i=0;i<nCount;++i)  
  6.     {         
  7.         sqlite3_reset(stmt);  
  8.         sqlite3_bind_int(stmt,1,i);  
  9.         sqlite3_bind_int(stmt,1,i*2);  
  10.         sqlite3_bind_int(stmt,1,i/2);  
  11.         sqlite3_bind_double(stmt,1,i*i);  
  12.     }  
  13.     sqlite3_finalize(stmt);  
  14.     sqlite3_exec(db,"commit;",0,0,0);  
此時測試資料插入效率為:265816條/s,也就是說,插入100000000條資料,需要376秒 = 6.27分。這個速度已經很滿意了。

5 總結

綜上所述啊,SQLite插入資料效率最快的方式就是:事務+關閉寫同步+執行準備(儲存過程),如果對資料庫安全性有要求的話,就開啟寫同步。

參考資料:

附最終完整程式碼:

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