SQLite का अनुकूलन मुश्किल है। C अनुप्रयोग का बल्क-इंसर्ट प्रदर्शन 85 आवेषण प्रति सेकंड से बढ़कर 96,000 आवेषण प्रति सेकंड तक हो सकता है!
पृष्ठभूमि: हम एक डेस्कटॉप अनुप्रयोग के हिस्से के रूप में SQLite का उपयोग कर रहे हैं। हमारे पास XML फ़ाइलों में संग्रहित बड़ी मात्रा में कॉन्फ़िगरेशन डेटा है जो अनुप्रयोग प्रारंभ होने पर आगे की प्रक्रिया के लिए SQLite डेटाबेस में पार्स और लोड किए जाते हैं। SQLite इस स्थिति के लिए आदर्श है क्योंकि यह तेज़ है, इसे किसी विशेष कॉन्फ़िगरेशन की आवश्यकता नहीं है, और डेटाबेस को डिस्क पर एक फ़ाइल के रूप में संग्रहीत किया जाता है।
Rationale: शुरू में मैं जो प्रदर्शन देख रहा था उससे निराश था। यह पता चलता है कि SQLite का प्रदर्शन काफी भिन्न हो सकता है (दोनों थोक-आवेषण और चयन के लिए) इस बात पर निर्भर करता है कि डेटाबेस कैसे कॉन्फ़िगर किया गया है और आप एपीआई का उपयोग कैसे कर रहे हैं। सभी विकल्पों और तकनीकों के बारे में जानने के लिए यह एक तुच्छ मामला नहीं था, इसलिए मैंने स्टैक ओवरफ्लो पाठकों के साथ परिणामों को साझा करने के लिए इस समुदाय विकी प्रविष्टि को बनाने के लिए विवेकपूर्ण विचार किया ताकि दूसरों को समान जांच की परेशानी से बचाया जा सके।
प्रयोग: केवल सामान्य अर्थों में प्रदर्शन युक्तियों के बारे में बात करने के बजाय (यानी "लेन-देन का उपयोग करें!" ), मैंने कुछ सी कोड लिखना और वास्तव में विभिन्न विकल्पों के प्रभाव को मापना सबसे अच्छा समझा । हम कुछ सरल डेटा के साथ शुरुआत करने जा रहे हैं:
- टोरंटो शहर के लिए पूर्ण पारगमन अनुसूची का 28 एमबी TAB-सीमांकित पाठ फ़ाइल (लगभग 865,000 रिकॉर्ड)
- मेरी परीक्षण मशीन 3.60 गीगाहर्ट्ज़ पी 4 है जो विंडोज़ एक्सपी चल रही है।
- कोड को "पूर्ण अनुकूलन" (/ ऑक्स) और फेवरेट फास्ट कोड (/ ओटी) के साथ विजुअल C ++ 2005 के साथ "रिलीज़" के रूप में संकलित किया गया है ।
- मैं अपने परीक्षण आवेदन में सीधे संकलित SQLite "समामेलन" का उपयोग कर रहा हूं। मेरे द्वारा किया जाने वाला SQLite संस्करण थोड़ा पुराना है (3.6.7), लेकिन मुझे संदेह है कि ये परिणाम नवीनतम रिलीज़ के लिए तुलनीय होंगे (यदि आप अन्यथा सोचते हैं तो एक टिप्पणी छोड़ दें)।
कुछ कोड लिखते हैं!
कोड: एक साधारण सी प्रोग्राम जो पाठ फ़ाइल लाइन-बाय-लाइन पढ़ता है, स्ट्रिंग को मूल्यों में विभाजित करता है और फिर डेटा को SQLite डेटाबेस में सम्मिलित करता है। कोड के इस "बेसलाइन" संस्करण में, डेटाबेस बनाया जाता है, लेकिन हम वास्तव में डेटा नहीं डालेंगे:
/*************************************************************
Baseline code to experiment with SQLite performance.
Input data is a 28 MB TAB-delimited text file of the
complete Toronto Transit System schedule/route info
from http://www.toronto.ca/open/datasets/ttc-routes/
**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"
#define INPUTDATA "C:\\TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:\\TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256
int main(int argc, char **argv) {
sqlite3 * db;
sqlite3_stmt * stmt;
char * sErrMsg = 0;
char * tail = 0;
int nRetCode;
int n = 0;
clock_t cStartClock;
FILE * pFile;
char sInputBuf [BUFFER_SIZE] = "\0";
char * sRT = 0; /* Route */
char * sBR = 0; /* Branch */
char * sVR = 0; /* Version */
char * sST = 0; /* Stop Number */
char * sVI = 0; /* Vehicle */
char * sDT = 0; /* Date */
char * sTM = 0; /* Time */
char sSQL [BUFFER_SIZE] = "\0";
/*********************************************/
/* Open the Database and create the Schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
/*********************************************/
/* Open input file and import into Database*/
cStartClock = clock();
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
/* ACTUAL INSERT WILL GO HERE */
n++;
}
fclose (pFile);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_close(db);
return 0;
}
नियंत्रण"
कोड को चलाने के रूप में-वास्तव में किसी भी डेटाबेस संचालन नहीं करता है, लेकिन यह हमें एक विचार देगा कि कच्ची सी फ़ाइल I / O और स्ट्रिंग प्रोसेसिंग ऑपरेशन कितनी तेजी से होते हैं।
0.94 सेकंड में 864913 रिकॉर्ड आयात किया
महान! हम प्रति सेकंड 920,000 आवेषण कर सकते हैं, बशर्ते हम वास्तव में कोई आवेषण नहीं करते हैं :-)
"सबसे खराब मामला-परिदृश्य"
हम फ़ाइल से पढ़े गए मानों का उपयोग करके SQL स्ट्रिंग उत्पन्न करने जा रहे हैं और sqlite3_exec का उपयोग करते हुए उस SQL कार्रवाई को लागू करते हैं:
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);
यह धीमा होने जा रहा है क्योंकि एसक्यूएल प्रत्येक डालने के लिए वीडीबीई कोड में संकलित किया जाएगा और प्रत्येक प्रविष्टि अपने लेनदेन में होगी। कितना धीमा?
9933.61 सेकंड में 864913 रिकॉर्ड आयात किया
ओह! 2 घंटे और 45 मिनट! यह केवल 85 आवेषण प्रति सेकंड है।
लेन-देन का उपयोग करना
डिफ़ॉल्ट रूप से, SQLite प्रत्येक INSERT / UPDATE स्टेटमेंट का मूल्यांकन एक अद्वितीय लेनदेन के भीतर करेगा। यदि बड़ी संख्या में आवेषण करते हैं, तो अपने ऑपरेशन को लेनदेन में लपेटना उचित है:
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
...
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
38.03 सेकंड में 864913 रिकॉर्ड आयात किया
वह बेहतर है। बस एक ही लेन-देन में हमारे सभी आवेषण को लपेटने से हमारे प्रदर्शन में 23,000 आवेषण प्रति सेकंड में सुधार हुआ ।
एक तैयार कथन का उपयोग करना
लेन-देन का उपयोग करना एक बहुत बड़ा सुधार था, लेकिन अगर हम एक ही एसक्यूएल का उपयोग कर रहे हैं, तो हर प्रविष्टि के लिए एसक्यूएल स्टेटमेंट को फिर से जमा करने का कोई मतलब नहीं है। आइए sqlite3_prepare_v2
एक बार हमारे एसक्यूएल स्टेटमेंट को संकलित करने के लिए उपयोग करें और फिर हमारे मापदंडों को उस स्टेटमेंट में बाँधें sqlite3_bind_text
:
/* Open input file and import into the database */
cStartClock = clock();
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db, sSQL, BUFFER_SIZE, &stmt, &tail);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
n++;
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
16.27 सेकंड में 864913 रिकॉर्ड आयात किया
अच्छा! थोड़ा अधिक कोड है (कॉल करना न भूलें sqlite3_clear_bindings
और sqlite3_reset
), लेकिन हमने अपने प्रदर्शन को दोगुना कर 53,000 आवेषण प्रति सेकंड कर दिया है।
PRAGMA तुल्यकालिक = बंद
डिफ़ॉल्ट रूप से, SQLite OS-level लिखने की कमांड जारी करने के बाद विराम देगा। यह गारंटी देता है कि डेटा डिस्क पर लिखा गया है। सेटिंग करके synchronous = OFF
, हम SQLite को निर्देश दे रहे हैं कि वह डेटा को OS के लिए लिखने के लिए बस हाथ से बंद कर दे और फिर जारी रखें। एक मौका है कि डेटाबेस फ़ाइल दूषित हो सकती है यदि डेटा को प्लाटर को लिखे जाने से पहले कंप्यूटर एक भयावह दुर्घटना (या बिजली की विफलता) से ग्रस्त है:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
12.41 सेकंड में 864913 रिकॉर्ड आयात किया
सुधार अब छोटे हैं, लेकिन हम प्रति सेकंड 69,600 आवेषण तक हैं।
PRAGMA journal_mode = मेमोरी
मूल्यांकन करके मेमोरी में रोलबैक जर्नल को संग्रहीत करने पर विचार करें PRAGMA journal_mode = MEMORY
। आपका लेन-देन तेज़ होगा, लेकिन यदि आप किसी लेनदेन के दौरान बिजली खो देते हैं या आपका प्रोग्राम क्रैश हो जाता है, तो आप डेटाबेस को एक आंशिक रूप से पूर्ण किए गए लेनदेन के साथ भ्रष्ट स्थिति में छोड़ सकते हैं:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
13.50 सेकंड में 864913 रिकॉर्ड आयात किया गया
64,000 आवेषण प्रति सेकंड पिछले अनुकूलन की तुलना में थोड़ा धीमा ।
PRAGMA सिंक्रोनस = ऑफ़ और PRAGMA journal_mode = मेमोरी
चलो पिछले दो अनुकूलन को मिलाते हैं। यह थोड़ा अधिक जोखिम भरा है (दुर्घटना के मामले में), लेकिन हम सिर्फ डेटा आयात कर रहे हैं (बैंक नहीं चला रहे हैं):
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
12.00 सेकंड में 864913 रिकॉर्ड आयात किया गया
बहुत खुबस! हम प्रति सेकंड 72,000 आवेषण करने में सक्षम हैं ।
इन-मेमोरी डेटाबेस का उपयोग करना
बस किक के लिए, चलो पिछली सभी ऑप्टिमाइज़ेशन पर निर्माण करते हैं और डेटाबेस फ़ाइलनाम को फिर से परिभाषित करते हैं ताकि हम पूरी तरह से रैम में काम कर सकें:
#define DATABASE ":memory:"
10.94 सेकंड में 864913 रिकॉर्ड आयात किया
यह हमारे डेटाबेस को रैम में संग्रहीत करने के लिए सुपर-व्यावहारिक नहीं है, लेकिन यह प्रभावशाली है कि हम प्रति सेकंड 79,000 आवेषण प्रदर्शन कर सकते हैं ।
री कोडिंग सी कोड
हालांकि विशेष रूप से एक SQLite सुधार नहीं है, मुझे लूप char*
में अतिरिक्त असाइनमेंट संचालन पसंद नहीं है while
। आइए उस कोड को तुरंत रिफलेक्टर strtok()
करते हैं sqlite3_bind_text()
और सीधे आउटपुट को पास करते हैं , और कंपाइलर हमारे लिए चीजों को गति देने की कोशिश करते हैं:
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "\t"), -1, SQLITE_TRANSIENT); /* Get Route */
sqlite3_bind_text(stmt, 2, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Branch */
sqlite3_bind_text(stmt, 3, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Version */
sqlite3_bind_text(stmt, 4, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Stop Number */
sqlite3_bind_text(stmt, 5, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Vehicle */
sqlite3_bind_text(stmt, 6, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Date */
sqlite3_bind_text(stmt, 7, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Time */
sqlite3_step(stmt); /* Execute the SQL Statement */
sqlite3_clear_bindings(stmt); /* Clear bindings */
sqlite3_reset(stmt); /* Reset VDBE */
n++;
}
fclose (pFile);
नोट: हम एक वास्तविक डेटाबेस फ़ाइल का उपयोग करने के लिए वापस आ गए हैं। इन-मेमोरी डेटाबेस तेज़ हैं, लेकिन जरूरी नहीं कि व्यावहारिक हो
8.94 सेकंड में 864913 रिकॉर्ड आयात किया
हमारे पैरामीटर बाइंडिंग में उपयोग किए जाने वाले स्ट्रिंग प्रोसेसिंग कोड के लिए थोड़ी सी भी रिफैक्टिंग ने हमें प्रति सेकंड 96,700 आवेषण प्रदर्शन करने की अनुमति दी है । मुझे लगता है कि यह कहना काफी सुरक्षित है कि यह काफी तेज है । जैसा कि हम अन्य चर (यानी पृष्ठ आकार, सूचकांक निर्माण, आदि) को ट्विस्ट करना शुरू करते हैं, यह हमारा बेंचमार्क होगा।
सारांश (अब तक)
मुझे आशा है कि आप अभी भी मेरे साथ हैं! हमने इस सड़क को शुरू करने का कारण यह बताया कि बल्क-इंसर्ट का प्रदर्शन SQLite के साथ बहुत बेतहाशा भिन्न होता है, और यह हमेशा स्पष्ट नहीं होता है कि हमारे ऑपरेशन को गति देने के लिए किन बदलावों की आवश्यकता है। एक ही संकलक (और संकलक विकल्प) का उपयोग करते हुए, SQLite का एक ही संस्करण और उसी डेटा को हमने अपने कोड और SQLite के उपयोग को 85 आवेषण प्रति सेकंड 85,000 से अधिक आवेषण के सबसे खराब स्थिति वाले परिदृश्य से जाने के लिए अनुकूलित किया है !
फिर INDEX बनाएँ, INSERT बनाम INSERT फिर CREATE INDEX
इससे पहले कि हम SELECT
प्रदर्शन को मापना शुरू करें , हम जानते हैं कि हम सूचकांक बना रहे हैं। यह नीचे दिए गए उत्तरों में से एक में सुझाव दिया गया है कि जब थोक आवेषण करते हैं, तो डेटा डालने के बाद सूचकांक बनाने के लिए तेज़ होता है (जैसा कि पहले सूचकांक बनाने के लिए विरोध किया जाता है फिर डेटा सम्मिलित किया जाता है)। कोशिश करते हैं:
इंडेक्स बनाएं फिर डेटा डालें
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
18.13 सेकंड में 864913 रिकॉर्ड आयात किया
डेटा डालें फिर इंडेक्स बनाएं
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
13.66 सेकंड में 864913 रिकॉर्ड आयात किया
जैसा कि अपेक्षित है, बल्क-इंसर्ट्स अगर एक कॉलम इंडेक्स किए जाते हैं, तो धीमे होते हैं, लेकिन डेटा डालने के बाद इंडेक्स बनाया जाता है, तो इससे फर्क पड़ता है। हमारी नो-इंडेक्स बेसलाइन 96,000 इन्सर्ट प्रति सेकंड है। पहले इंडेक्स बनाना फिर डेटा डालना हमें प्रति सेकंड 47,700 इंसर्ट देता है, जबकि पहले डेटा डालना फिर इंडेक्स बनाने से हमें प्रति सेकंड 63,300 इंसर्ट मिलते हैं।
मैं ख़ुशी से अन्य परिदृश्यों के लिए सुझाव लेने की कोशिश करूँगा ... और जल्द ही चयनित प्रश्नों के लिए समान डेटा संकलित किया जाएगा।
sqlite3_clear_bindings(stmt);
? आप हर बार बाइंडिंग सेट करते हैं जिसके माध्यम से पर्याप्त होना चाहिए: sqlite3_step () sqlite3_reset () के बाद पहली बार कॉल करने से पहले, एप्लिकेशन पैरामीटर में मान संलग्न करने के लिए sqlite3_stind () इंटरफेस में से किसी एक को आमंत्रित कर सकता है। Sqlite3_bind () पर प्रत्येक कॉल एक ही पैरामीटर पर पूर्व बाइंडिंग को ओवरराइड करता है (देखें: sqlite.org/cintro.html )। उस फ़ंक्शन के लिए डॉक्स में ऐसा कुछ भी नहीं है जो कह रहा हो कि आपको इसे कॉल करना होगा।
feof()
अपने इनपुट लूप की समाप्ति को नियंत्रित करने के लिए उपयोग न करें । द्वारा लौटाए गए परिणाम का उपयोग करें fgets()
। stackoverflow.com/a/15485689/827263