Cải thiện hiệu suất INSERT mỗi giây của SQLite


2975

Tối ưu hóa SQLite là khó khăn. Hiệu suất chèn hàng loạt của ứng dụng C có thể thay đổi từ 85 lần chèn mỗi giây đến hơn 96.000 lần chèn mỗi giây!

Bối cảnh: Chúng tôi đang sử dụng SQLite như một phần của ứng dụng máy tính để bàn. Chúng tôi có một lượng lớn dữ liệu cấu hình được lưu trữ trong các tệp XML được phân tích cú pháp và được tải vào cơ sở dữ liệu SQLite để xử lý thêm khi ứng dụng được khởi tạo. SQLite là lý tưởng cho tình huống này vì nó nhanh, không yêu cầu cấu hình chuyên biệt và cơ sở dữ liệu được lưu trữ trên đĩa dưới dạng một tệp.

Đặt vấn đề: Ban đầu tôi thất vọng với màn trình diễn mà tôi đang xem. Hóa ra, hiệu suất của SQLite có thể thay đổi đáng kể (cả cho chèn và chọn hàng loạt) tùy thuộc vào cách cơ sở dữ liệu được định cấu hình và cách bạn sử dụng API. Việc tìm ra tất cả các tùy chọn và kỹ thuật là gì không phải là vấn đề nhỏ, vì vậy tôi nghĩ nên thận trọng khi tạo mục nhập wiki cộng đồng này để chia sẻ kết quả với độc giả Stack Overflow để cứu những người khác gặp rắc rối trong cùng một cuộc điều tra.

Thử nghiệm: Thay vì chỉ nói về các mẹo hiệu suất theo nghĩa chung (nghĩa là "Sử dụng giao dịch!" ), Tôi nghĩ tốt nhất nên viết một số mã C và thực sự đo lường tác động của các tùy chọn khác nhau. Chúng ta sẽ bắt đầu với một số dữ liệu đơn giản:

  • Tệp văn bản được phân định bằng TAB 28 MB (khoảng 865.000 bản ghi) về lịch trình quá cảnh hoàn chỉnh cho thành phố Toronto
  • Máy thử nghiệm của tôi là P4 3,60 GHz chạy Windows XP.
  • Mã được biên dịch với Visual C ++ 2005 dưới dạng "Phát hành" với "Tối ưu hóa hoàn toàn" (/ Ox) và Mã nhanh ưu tiên (/ Ot).
  • Tôi đang sử dụng "Hợp nhất" SQLite, được biên dịch trực tiếp vào ứng dụng thử nghiệm của tôi. Phiên bản SQLite mà tôi tình cờ có một chút cũ hơn (3.6.7), nhưng tôi nghi ngờ những kết quả này sẽ tương đương với phiên bản mới nhất (vui lòng để lại nhận xét nếu bạn nghĩ khác).

Hãy viết một số mã!

Mã: Một chương trình C đơn giản đọc từng dòng tệp văn bản, chia chuỗi thành các giá trị và sau đó chèn dữ liệu vào cơ sở dữ liệu SQLite. Trong phiên bản "cơ sở" của mã này, cơ sở dữ liệu được tạo, nhưng chúng tôi sẽ không thực sự chèn dữ liệu:

/*************************************************************
    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ều khiển"

Chạy mã như không thực sự thực hiện bất kỳ hoạt động cơ sở dữ liệu nào, nhưng nó sẽ cho chúng ta ý tưởng về tốc độ I / O của tệp C thô và các hoạt động xử lý chuỗi.

Nhập 864913 hồ sơ trong 0,94 giây

Tuyệt quá! Chúng tôi có thể thực hiện 920.000 lần chèn mỗi giây, miễn là chúng tôi thực sự không thực hiện bất kỳ thao tác chèn nào :-)


"Kịch bản tồi tệ nhất"

Chúng tôi sẽ tạo chuỗi SQL bằng cách sử dụng các giá trị được đọc từ tệp và gọi hoạt động SQL đó bằng sqlite3_exec:

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);

Điều này sẽ chậm vì SQL sẽ được biên dịch thành mã VDBE cho mỗi lần chèn và mỗi lần chèn sẽ xảy ra trong giao dịch của chính nó. Làm thế nào chậm?

Nhập 864913 hồ sơ trong 9933,61 giây

Rất tiếc! 2 giờ 45 phút! Đó chỉ là 85 lần chèn mỗi giây.

Sử dụng giao dịch

Theo mặc định, SQLite sẽ đánh giá mọi câu lệnh INSERT / UPDATE trong một giao dịch duy nhất. Nếu thực hiện một số lượng lớn các phần chèn, bạn nên bọc hoạt động của mình trong một giao dịch:

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);

Nhập 864913 hồ sơ trong 38,03 giây

Cái đó tốt hơn. Chỉ cần gói tất cả các chèn của chúng tôi trong một giao dịch đã cải thiện hiệu suất của chúng tôi lên 23.000 chèn mỗi giây.

Sử dụng một tuyên bố chuẩn bị

Sử dụng một giao dịch là một cải tiến lớn, nhưng biên dịch lại câu lệnh SQL cho mỗi lần chèn không có ý nghĩa gì nếu chúng ta sử dụng cùng một SQL. Chúng ta hãy sử dụng sqlite3_prepare_v2để biên dịch câu lệnh SQL của chúng ta một lần và sau đó liên kết các tham số của chúng ta với câu lệnh đó bằng cách sử dụng 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;

Nhập 864913 hồ sơ trong 16,27 giây

Đẹp! Có thêm một chút mã (đừng quên gọi sqlite3_clear_bindingssqlite3_reset), nhưng chúng tôi đã tăng gấp đôi hiệu suất của chúng tôi lên 53.000 lần chèn mỗi giây.

PRAGMA đồng bộ = TẮT

Theo mặc định, SQLite sẽ tạm dừng sau khi ban hành lệnh ghi mức hệ điều hành. Điều này đảm bảo rằng dữ liệu được ghi vào đĩa. Bằng cách cài đặt synchronous = OFF, chúng tôi đang hướng dẫn SQLite chỉ cần đưa dữ liệu lên HĐH để viết và sau đó tiếp tục. Có khả năng tệp cơ sở dữ liệu có thể bị hỏng nếu máy tính gặp sự cố thảm khốc (hoặc mất điện) trước khi dữ liệu được ghi vào đĩa:

/* 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);

Nhập 864913 hồ sơ trong 12,41 giây

Các cải tiến bây giờ nhỏ hơn, nhưng chúng tôi có tới 69.600 lần chèn mỗi giây.

Tạp chí PRAGMA_mode = BỘ NHỚ

Xem xét việc lưu trữ tạp chí rollback trong bộ nhớ bằng cách đánh giá PRAGMA journal_mode = MEMORY. Giao dịch của bạn sẽ nhanh hơn, nhưng nếu bạn mất điện hoặc chương trình của bạn gặp sự cố trong khi giao dịch, cơ sở dữ liệu của bạn có thể bị chuyển sang trạng thái bị hỏng với giao dịch đã hoàn thành một phần:

/* 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);

Nhập 864913 hồ sơ trong 13,50 giây

Chậm hơn một chút so với tối ưu hóa trước đó với 64.000 lần chèn mỗi giây.

PRAGMA đồng bộ = TẮT tạp chí PRAGMA_mode = MEMOR

Hãy kết hợp hai tối ưu hóa trước. Sẽ rủi ro hơn một chút (trong trường hợp xảy ra sự cố), nhưng chúng tôi chỉ nhập dữ liệu (không chạy ngân hàng):

/* 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);

Nhập 864913 hồ sơ trong 12 giây

Tuyệt diệu! Chúng tôi có thể thực hiện 72.000 lần chèn mỗi giây.

Sử dụng cơ sở dữ liệu trong bộ nhớ

Chỉ với các cú đá, hãy xây dựng dựa trên tất cả các tối ưu hóa trước đó và xác định lại tên tệp cơ sở dữ liệu để chúng tôi làm việc hoàn toàn trong RAM:

#define DATABASE ":memory:"

Nhập 864913 hồ sơ trong 10,94 giây

Việc lưu trữ cơ sở dữ liệu của chúng tôi trong RAM không thực tế lắm, nhưng thật ấn tượng khi chúng tôi có thể thực hiện 79.000 lần chèn mỗi giây.

Tái cấu trúc mã C

Mặc dù không đặc biệt là một cải tiến SQLite, tôi không thích các char*hoạt động gán thêm trong whilevòng lặp. Chúng ta hãy nhanh chóng cấu trúc lại mã đó để chuyển đầu ra strtok()trực tiếp vào sqlite3_bind_text()và để trình biên dịch cố gắng tăng tốc mọi thứ cho chúng ta:

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);

Lưu ý: Chúng tôi quay lại sử dụng tệp cơ sở dữ liệu thực. Cơ sở dữ liệu trong bộ nhớ nhanh, nhưng không nhất thiết phải thực tế

Nhập 864913 hồ sơ trong 8,94 giây

Một cấu trúc lại một chút cho mã xử lý chuỗi được sử dụng trong liên kết tham số của chúng tôi đã cho phép chúng tôi thực hiện 96.700 lần chèn mỗi giây. Tôi nghĩ thật an toàn khi nói rằng điều này rất nhanh . Khi chúng tôi bắt đầu điều chỉnh các biến khác (ví dụ kích thước trang, tạo chỉ mục, v.v.), đây sẽ là điểm chuẩn của chúng tôi.


Tóm tắt (cho đến nay)

Tôi hy vọng bạn vẫn còn với tôi! Lý do chúng tôi bắt đầu trên con đường này là vì hiệu suất chèn số lượng lớn thay đổi rất lớn với SQLite và không phải lúc nào cũng rõ ràng những thay đổi cần được thực hiện để tăng tốc hoạt động của chúng tôi. Sử dụng cùng một trình biên dịch (và các tùy chọn trình biên dịch), cùng một phiên bản SQLite và cùng một dữ liệu chúng tôi đã tối ưu hóa mã của chúng tôi và việc sử dụng SQLite của chúng tôi để đi từ một trường hợp xấu nhất là 85 lần chèn mỗi giây lên hơn 96.000 lần chèn mỗi giây!


TẠO INDEX rồi INSERT so với INSERT rồi CREATE INDEX

Trước khi bắt đầu đo SELECThiệu suất, chúng tôi biết rằng chúng tôi sẽ tạo các chỉ số. Một trong những câu trả lời dưới đây được đề xuất là khi thực hiện chèn số lượng lớn, việc tạo chỉ mục sẽ nhanh hơn sau khi dữ liệu được chèn vào (trái ngược với việc tạo chỉ mục trước sau đó chèn dữ liệu). Hãy thử:

Tạo chỉ mục sau đó chèn dữ liệu

sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...

Nhập 864913 hồ sơ trong 18,13 giây

Chèn dữ liệu rồi tạo chỉ mục

...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);

Nhập 864913 hồ sơ trong 13,66 giây

Như mong đợi, chèn hàng loạt sẽ chậm hơn nếu một cột được lập chỉ mục, nhưng nó sẽ tạo ra sự khác biệt nếu chỉ mục được tạo sau khi dữ liệu được chèn. Đường cơ sở không có chỉ số của chúng tôi là 96.000 chèn mỗi giây. Tạo chỉ mục trước sau đó chèn dữ liệu cho chúng tôi 47.700 lần chèn mỗi giây, trong khi chèn dữ liệu trước sau đó tạo chỉ mục cho chúng tôi 63.300 lần chèn mỗi giây.


Tôi sẵn sàng nhận đề xuất cho các kịch bản khác để thử ... Và sẽ sớm biên dịch dữ liệu tương tự cho các truy vấn CHỌN.


8
Điểm tốt! Trong trường hợp của chúng tôi, chúng tôi đang xử lý khoảng 1,5 triệu cặp khóa / giá trị được đọc từ các tệp văn bản XML và CSV thành các bản ghi 200k. Nhỏ so với các cơ sở dữ liệu chạy các trang web như SO - nhưng đủ lớn để điều chỉnh hiệu suất SQLite trở nên quan trọng.
Mike Willekes

51
"Chúng tôi có một lượng lớn dữ liệu cấu hình được lưu trữ trong các tệp XML được phân tích cú pháp và được tải vào cơ sở dữ liệu SQLite để xử lý thêm khi ứng dụng được khởi tạo." Tại sao bạn không giữ mọi thứ trong cơ sở dữ liệu sqlite ở nơi đầu tiên, thay vì lưu trữ trong XML và sau đó tải mọi thứ tại thời điểm khởi tạo?
CAFxX

14
Bạn đã thử không gọi sqlite3_clear_bindings(stmt);? Bạn đặt các liên kết mỗi lần là đủ: Trước khi gọi sqlite3_step () lần đầu tiên hoặc ngay sau sqlite3_reset (), ứng dụng có thể gọi một trong các giao diện sqlite3_bind () để gắn các giá trị vào các tham số. Mỗi lệnh gọi sqlite3_bind () ghi đè các ràng buộc trước đó trên cùng một tham số (xem: sqlite.org/cintro.html ). Không có gì trong các tài liệu cho chức năng đó nói rằng bạn phải gọi nó.
ahcox

21
Bạn đã làm các phép đo lặp đi lặp lại? 4 "chiến thắng" để tránh 7 con trỏ cục bộ là lạ, thậm chí giả sử một trình tối ưu hóa nhầm lẫn.
peterchen

5
Đừng sử dụng feof()để kiểm soát việc chấm dứt vòng lặp đầu vào của bạn. Sử dụng kết quả trả về bởi fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Câu trả lời:


785

Một số lời khuyên:

  1. Đặt chèn / cập nhật trong một giao dịch.
  2. Đối với các phiên bản cũ hơn của SQLite - Hãy xem xét chế độ nhật ký ít hoang tưởng ( pragma journal_mode). Có NORMAL, và sau đó OFF, có thể tăng đáng kể tốc độ chèn nếu bạn không quá lo lắng về việc cơ sở dữ liệu có thể bị hỏng nếu hệ điều hành gặp sự cố. Nếu ứng dụng của bạn gặp sự cố, dữ liệu sẽ ổn. Lưu ý rằng trong các phiên bản mới hơn, các OFF/MEMORYcài đặt không an toàn cho các sự cố ở cấp ứng dụng.
  3. Chơi với kích thước trang cũng tạo ra sự khác biệt ( PRAGMA page_size). Có kích thước trang lớn hơn có thể làm cho việc đọc và ghi nhanh hơn một chút vì các trang lớn hơn được giữ trong bộ nhớ. Lưu ý rằng nhiều bộ nhớ hơn sẽ được sử dụng cho cơ sở dữ liệu của bạn.
  4. Nếu bạn có chỉ số, hãy cân nhắc việc gọi CREATE INDEXsau khi thực hiện tất cả các thao tác chèn của bạn. Điều này nhanh hơn đáng kể so với việc tạo chỉ mục và sau đó thực hiện chèn của bạn.
  5. Bạn phải khá cẩn thận nếu bạn có quyền truy cập đồng thời vào SQLite, vì toàn bộ cơ sở dữ liệu bị khóa khi viết xong và mặc dù có thể có nhiều trình đọc, ghi sẽ bị khóa. Điều này đã được cải thiện phần nào với việc bổ sung WAL trong các phiên bản SQLite mới hơn.
  6. Tận dụng tiết kiệm không gian ... cơ sở dữ liệu nhỏ hơn đi nhanh hơn. Ví dụ: nếu bạn có các cặp giá trị khóa, hãy thử tạo khóa INTEGER PRIMARY KEYnếu có thể, điều này sẽ thay thế cột số hàng duy nhất được ngụ ý trong bảng.
  7. Nếu bạn đang sử dụng nhiều luồng, bạn có thể thử sử dụng bộ đệm trang chia sẻ , điều này sẽ cho phép các trang được tải được chia sẻ giữa các luồng, điều này có thể tránh các cuộc gọi I / O đắt tiền.
  8. Đừng sử dụng !feof(file)!

Tôi cũng đã hỏi những câu hỏi tương tự ở đâyđây .


9
Tài liệu không biết một tạp chí PRAGMA_mode BÌNH THƯỜNG sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
Đã được một thời gian, các đề xuất của tôi áp dụng cho các phiên bản cũ hơn trước khi WAL được giới thiệu. Có vẻ như XÓA là cài đặt bình thường mới, và bây giờ cũng có cài đặt TẮT và NHỚ. Tôi cho rằng TẮT / NHỚ sẽ cải thiện hiệu suất ghi với chi phí toàn vẹn cơ sở dữ liệu và TẮT hoàn toàn vô hiệu hóa các rollback.
Snazzer

4
đối với # 7, bạn có một ví dụ về cách bật bộ đệm trang chia sẻ bằng cách sử dụng trình bao bọc c # system.data.sqlite không?
Aaron Hudon

4
# 4 mang lại những ký ức cũ từ thời xa xưa - Có ít nhất một trường hợp trở lại thời trước khi bỏ chỉ số trước khi một nhóm thêm và tạo lại nó sau đó tăng tốc đáng kể. Có thể vẫn hoạt động nhanh hơn trên các hệ thống hiện đại đối với một số bổ sung mà bạn biết bạn có quyền truy cập duy nhất vào bảng trong giai đoạn này.
Bill K

Đồng ý với mục tiêu số 1: Bản thân tôi đã rất may mắn với các giao dịch.
Enno

146

Hãy thử sử dụng SQLITE_STATICthay vì SQLITE_TRANSIENTcho những chèn.

SQLITE_TRANSIENT sẽ khiến SQLite sao chép dữ liệu chuỗi trước khi quay trở lại.

SQLITE_STATICnói với nó rằng địa chỉ bộ nhớ bạn đã cung cấp sẽ hợp lệ cho đến khi truy vấn được thực hiện (trong vòng lặp này luôn luôn như vậy). Điều này sẽ giúp bạn tiết kiệm một số hoạt động phân bổ, sao chép và phân bổ cho mỗi vòng lặp. Có thể là một cải tiến lớn.


109

Tránh sqlite3_clear_bindings(stmt).

Mã trong thử nghiệm đặt các ràng buộc mỗi lần thông qua đó là đủ.

Các giới thiệu C API từ các tài liệu SQLite nói:

Trước khi gọi sqlite3_step () lần đầu tiên hoặc ngay sau sqlite3_reset () , ứng dụng có thể gọi các giao diện sqlite3_bind () để gắn các giá trị vào các tham số. Mỗi cuộc gọi đến sqlite3_bind () ghi đè các ràng buộc trước đó trên cùng một tham số

Không có gì trong tài liệu cho sqlite3_clear_bindings nói rằng bạn phải gọi nó ngoài việc đơn giản là thiết lập các ràng buộc.

Chi tiết hơn: Tránh_sqlite3_clear_bindings ()


5
Tuyệt vời đúng: "Trái ngược với trực giác của nhiều người, sqlite3_reset () không đặt lại các ràng buộc trên một câu lệnh đã chuẩn bị. Sử dụng thói quen này để đặt lại tất cả các tham số máy chủ thành NULL." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia

63

Trên chèn số lượng lớn

Lấy cảm hứng từ bài đăng này và câu hỏi Stack Overflow dẫn tôi đến đây - Có thể chèn nhiều hàng cùng một lúc trong cơ sở dữ liệu SQLite không? - Tôi đã đăng kho Git đầu tiên của mình :

https://github.com/rdpoor/CreateOrUpdate

khối lượng lớn tải một mảng ActiveRecords vào cơ sở dữ liệu MySQL , SQLite hoặc PostgreQuery . Nó bao gồm một tùy chọn để bỏ qua các bản ghi hiện có, ghi đè lên chúng hoặc đưa ra lỗi. Điểm chuẩn thô sơ của tôi cho thấy sự cải thiện tốc độ gấp 10 lần so với ghi tuần tự - YMMV.

Tôi đang sử dụng nó trong mã sản xuất nơi tôi thường xuyên cần nhập bộ dữ liệu lớn và tôi khá hài lòng với nó.


4
@Jess: Nếu bạn theo liên kết, bạn sẽ thấy rằng anh ấy có nghĩa là cú pháp chèn hàng loạt.
Alix Axel

48

Nhập hàng loạt dường như hoạt động tốt nhất nếu bạn có thể kiểm tra các câu lệnh INSERT / UPDATE của mình. Giá trị 10.000 hoặc hơn đã hoạt động tốt đối với tôi trên một bảng chỉ có một vài hàng, YMMV ...


22
Bạn muốn điều chỉnh x = 10.000 sao cho x = cache [= cache_size * page_size] / kích thước trung bình của phần chèn của bạn.
Alix Axel

43

Nếu bạn chỉ quan tâm đến việc đọc, phiên bản nhanh hơn (nhưng có thể đọc dữ liệu cũ) là đọc từ nhiều kết nối từ nhiều luồng (kết nối trên mỗi luồng).

Đầu tiên tìm các mục, trong bảng:

SELECT COUNT(*) FROM table

sau đó đọc trong các trang (GIỚI HẠN / OFFSET):

SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>

ở đâu và được tính trên mỗi luồng, như thế này:

int limit = (count + n_threads - 1)/n_threads;

cho mỗi chủ đề:

int offset = thread_index * limit

Đối với db nhỏ (200mb) của chúng tôi, điều này đã tăng tốc 50-75% (3.8.0.2 64 bit trên Windows 7). Các bảng của chúng tôi không được chuẩn hóa nhiều (1000-1500 cột, khoảng 100.000 hàng trở lên).

Quá nhiều hoặc quá ít chủ đề sẽ không làm điều đó, bạn cần phải tự chuẩn và hồ sơ.

Ngoài ra, đối với chúng tôi, SHAREDCACHE làm cho hiệu suất chậm hơn, vì vậy tôi đặt thủ công PRIVATECACHE (vì nó được kích hoạt trên toàn cầu cho chúng tôi)


29

Tôi không nhận được bất kỳ lợi ích nào từ các giao dịch cho đến khi tôi nâng cache_size lên giá trị cao hơn, tức là PRAGMA cache_size=10000;


Lưu ý rằng sử dụng giá trị dương để cache_sizeđặt số lượng trang vào bộ đệm , không phải tổng kích thước RAM. Với kích thước trang mặc định là 4kB, cài đặt này sẽ chứa tới 40 MB dữ liệu cho mỗi tệp đang mở (hoặc trên mỗi quy trình, nếu chạy với bộ đệm chung ).
Groo

21

Sau khi đọc hướng dẫn này, tôi đã cố gắng thực hiện nó cho chương trình của mình.

Tôi có 4-5 tệp có chứa địa chỉ. Mỗi tệp có khoảng 30 triệu hồ sơ. Tôi đang sử dụng cùng một cấu hình mà bạn đang đề xuất nhưng số lượng INSERT của tôi mỗi giây ở mức thấp (~ 10.000 bản ghi mỗi giây).

Đây là nơi đề nghị của bạn thất bại. Bạn sử dụng một giao dịch duy nhất cho tất cả các bản ghi và một lần chèn không có lỗi / không thành công. Giả sử bạn đang chia từng bản ghi thành nhiều phần chèn trên các bảng khác nhau. Điều gì xảy ra nếu hồ sơ bị phá vỡ?

Lệnh ON CONFLICT không áp dụng, vì nếu bạn có 10 phần tử trong một bản ghi và bạn cần mỗi phần tử được chèn vào một bảng khác nhau, nếu phần tử 5 bị lỗi CONSTRAINT, thì tất cả 4 phần chèn trước đó cũng cần phải đi.

Vì vậy, đây là nơi rollback đến. Vấn đề duy nhất với rollback là bạn mất tất cả các phần chèn của mình và bắt đầu từ đầu. Làm thế nào bạn có thể giải quyết điều này?

Giải pháp của tôi là sử dụng nhiều giao dịch. Tôi bắt đầu và kết thúc một giao dịch cứ sau 10.000 hồ sơ (Đừng hỏi tại sao con số đó, đó là lần nhanh nhất tôi đã kiểm tra). Tôi đã tạo ra một mảng có kích thước 10.000 và chèn các hồ sơ thành công ở đó. Khi xảy ra lỗi, tôi thực hiện khôi phục, bắt đầu giao dịch, chèn các bản ghi từ mảng của mình, cam kết và sau đó bắt đầu một giao dịch mới sau khi bản ghi bị hỏng.

Giải pháp này đã giúp tôi bỏ qua các vấn đề tôi gặp phải khi xử lý các tệp chứa hồ sơ xấu / trùng lặp (tôi có gần 4% hồ sơ xấu).

Thuật toán tôi tạo giúp tôi giảm quá trình 2 giờ. Quá trình tải cuối cùng của tệp 1 giờ 30m vẫn còn chậm nhưng không so với 4 giờ ban đầu. Tôi đã quản lý để tăng tốc độ chèn từ 10.000 / s lên ~ 14.000 / s

Nếu bất cứ ai có bất kỳ ý tưởng nào khác về cách tăng tốc nó, tôi sẽ mở để đề xuất.

CẬP NHẬT :

Ngoài câu trả lời của tôi ở trên, bạn nên nhớ rằng việc chèn mỗi giây tùy thuộc vào ổ cứng bạn đang sử dụng. Tôi đã thử nghiệm nó trên 3 PC khác nhau với các ổ cứng khác nhau và có sự khác biệt lớn về thời gian. PC1 (1hr 30m), PC2 (6hrs) PC3 (14hrs), vì vậy tôi bắt đầu tự hỏi tại sao lại như vậy.

Sau hai tuần nghiên cứu và kiểm tra nhiều tài nguyên: Ổ cứng, Ram, Cache, tôi phát hiện ra rằng một số cài đặt trên ổ cứng của bạn có thể ảnh hưởng đến tốc độ I / O. Bằng cách nhấp vào các thuộc tính trên ổ đĩa đầu ra mong muốn của bạn, bạn có thể thấy hai tùy chọn trong tab chung. Opt1: Nén ổ đĩa này, Opt2: Cho phép các tệp của ổ đĩa này được lập chỉ mục nội dung.

Bằng cách vô hiệu hóa hai tùy chọn này, cả 3 PC hiện mất khoảng thời gian tương tự để hoàn thành (1 giờ và 20 đến 40 phút). Nếu bạn gặp phải chèn chậm, hãy kiểm tra xem ổ cứng của bạn có được cấu hình với các tùy chọn này không. Nó sẽ giúp bạn tiết kiệm rất nhiều thời gian và đau đầu khi cố gắng tìm giải pháp


Tôi sẽ đề nghị như sau. * Sử dụng SQLITE_STATIC so với SQLITE_TRANSIENT để tránh sao chép chuỗi, bạn phải đảm bảo chuỗi sẽ không bị thay đổi trước khi giao dịch được thực hiện * Sử dụng chèn số lượng lớn INSERT INTO stop_times VALUES (NULL,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,? ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?, tòa nhà cao tầng.
rouzier

Làm điều đó tôi có thể nhập 5.582.642 bản ghi trong 11,51 giây
rouzier

11

Câu trả lời cho câu hỏi của bạn là SQLite 3 mới hơn đã cải thiện hiệu suất, hãy sử dụng nó.

Câu trả lời này Tại sao SQLAlchemy chèn với sqlite chậm hơn 25 lần so với sử dụng sqlite3 trực tiếp? bởi SqlAlchemy Orm Tác giả có 100k chèn trong 0,5 giây và tôi đã thấy kết quả tương tự với python-sqlite và SqlAlchemy. Điều đó khiến tôi tin rằng hiệu suất đã được cải thiện với SQLite 3.


-1

Sử dụng ContentProvider để chèn dữ liệu hàng loạt trong db. Phương pháp dưới đây được sử dụng để chèn dữ liệu số lượng lớn vào cơ sở dữ liệu. Điều này sẽ cải thiện hiệu suất INSERT-mỗi giây của SQLite.

private SQLiteDatabase database;
database = dbHelper.getWritableDatabase();

public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {

database.beginTransaction();

for (ContentValues value : values)
 db.insert("TABLE_NAME", null, value);

database.setTransactionSuccessful();
database.endTransaction();

}

Gọi phương thức BulkInsert:

App.getAppContext().getContentResolver().bulkInsert(contentUriTable,
            contentValuesArray);

Liên kết: https://www.vogella.com/tutorials/AndroidSQLite/article.html kiểm tra Sử dụng Phần ContentProvider để biết thêm chi tiết

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.