web-dev-qa-db-ja.com

SQLiteの1秒あたりのINSERTパフォーマンスを改善しますか?

SQLiteの最適化には注意が必要です。 Cアプリケーションの一括挿入のパフォーマンスは、1秒あたり85個の挿入から1秒あたり96,000個を超える挿入までさまざまです。

Background:デスクトップアプリケーションの一部としてSQLiteを使用しています。 XMLファイルに保存された大量の構成データが解析され、アプリケーションの初期化時にさらに処理するためにSQLiteデータベースにロードされます。 SQLiteは高速で、特別な構成を必要とせず、データベースが単一のファイルとしてディスクに保存されるため、この状況に最適です。

理論的根拠:最初、私が見ていたパフォーマンスに失望しました。 -inserts and select)データベースの設定方法とAPIの使用方法に応じて。すべてのオプションとテクニックが何であるかを理解することは簡単なことではありませんでしたので、同じ調査の問題を他の人に保存するために、このコミュニティwikiエントリを作成してStack Overflowリーダーと結果を共有するのが賢明だと思いました。

実験:一般的な意味でのパフォーマンスのヒント(つまり "Use a transaction!" )について単に話すのではなく、いくつかのCコードを記述し、実際に測定さまざまなオプションの影響。いくつかの簡単なデータから始めます。

  • 28 MBのTAB区切りテキストファイル(約865,000レコード)の トロント市の完全な乗り継ぎスケジュール
  • 私のテストマシンは、Windows XPを実行している3.60 GHz P4です。
  • コードは、 Visual C++ 2005で、 "完全最適化"(/ Ox)およびFavor Fast Code(/ Ot)で "リリース"としてコンパイルされます。
  • 私はテストアプリケーションに直接コンパイルされたSQLite "Amalgamation"を使用しています。私がたまたま持っているSQLiteバージョンは少し古い(3.6.7)が、これらの結果は最新リリースに匹敵すると思われる(そうでないと思われる場合はコメントを残してください)。

コードを書きましょう!

The Code:テキストファイルを1行ずつ読み取り、文字列を値に分割してから、データをSQLiteデータベースに挿入する単純なCプログラム。この「ベースライン」バージョンのコードでは、データベースが作成されますが、実際にはデータを挿入しません。

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

制御"

コードをそのまま実行しても、実際にはデータベース操作は実行されませんが、生のCファイル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);

SQLは挿入ごとにVDBEコードにコンパイルされ、すべての挿入は独自のトランザクションで行われるため、これは遅くなります。 どのくらい遅い?

9933.61秒で864913レコードをインポートしました

いいね! 2時間45分! 1秒あたりの挿入数は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レコードをインポートしました

それは良いです。単一のトランザクションですべての挿入をラップするだけで、パフォーマンスが1秒あたり23,000挿入に改善されました。

準備済みステートメントの使用

トランザクションの使用は大幅に改善されましたが、同じSQLを繰り返し使用する場合、挿入ごとにSQLステートメントを再コンパイルすることは意味がありません。 sqlite3_prepare_v2を使用してSQLステートメントを一度コンパイルし、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を呼び出すことを忘れないでください)、パフォーマンスは2倍以上になり、1秒あたり53,000の挿入になりました。

PRAGMA同期=オフ

デフォルトでは、SQLiteはOSレベルの書き込みコマンドを発行した後に一時停止します。これにより、データがディスクに書き込まれることが保証されます。 synchronous = OFFを設定することにより、書き込みのためにOSにデータをハンドオフしてから続行するようにSQLiteに指示しています。データがプラッターに書き込まれる前にコンピューターに壊滅的なクラッシュ(または電源障害)が発生すると、データベースファイルが破損する可能性があります。

/* 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 = MEMORY

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レコードをインポートしました

1秒あたり64,000の挿入での以前の最適化よりも少し遅い

PRAGMA同期= OFF and PRAGMA journal_mode = MEMORY

前の2つの最適化を組み合わせてみましょう。少し危険です(クラッシュの場合)が、データをインポートするだけです(銀行を経営していない):

/* 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レコードをインポートしました

素晴らしい! 1秒間に72,000の挿入を行うことができます。

インメモリデータベースの使用

キックのために、以前のすべての最適化に基づいてデータベースのファイル名を再定義し、RAMで完全に作業できるようにします。

#define DATABASE ":memory:"

10.94秒で864913レコードをインポートしました

データベースをRAMに保存するのはあまり実用的ではありませんが、1秒あたり79,000の挿入を実行できることは印象的です。

Cコードのリファクタリング

特にSQLiteの改善ではありませんが、whileループでの余分なchar*割り当て操作は好きではありません。そのコードをすばやくリファクタリングして、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レコードをインポートしました

パラメーターバインドで使用される文字列処理コードをわずかにリファクタリングすることで、1秒あたり96,700の挿入を実行できるようになりました。これはかなり高速だと言っても安全だと思います。他の変数(ページサイズ、インデックス作成など)の調整を開始すると、これがベンチマークになります。


概要(これまで)

あなたが私と一緒にいることを望みます!この道を始めたのは、SQLiteによって一括挿入のパフォーマンスが非常に大きく変化するためであり、どのような変更が必要かは必ずしも明らかではありません操作をスピードアップします。同じコンパイラー(およびコンパイラーオプション)、同じバージョンのSQLiteおよび同じデータを使用して、コードを最適化し、SQLiteの使用を1秒あたり85回の挿入という最悪のシナリオから 1秒あたり96,000の挿入


インデックスを作成してからINSERT対INSERTしてからインデックスを作成

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レコードをインポートしました

予想どおり、1つの列にインデックスが付けられている場合、一括挿入は遅くなりますが、データの挿入後にインデックスが作成されると違いが生じます。インデックスなしのベースラインは、1秒あたり96,000の挿入です。 最初にインデックスを作成してからデータを挿入すると、1秒あたり47,700回の挿入が行われますが、最初にデータを挿入してからインデックスを作成すると、1秒あたり63,300回の挿入が行われます。


私は喜んで他のシナリオを試してみてください...そして、SELECTクエリの同様のデータをまもなくコンパイルします。

2826
Mike Willekes

いくつかのヒント:

  1. 挿入/更新をトランザクションに入れます。
  2. SQLiteの古いバージョンの場合 - パラノイアの少ないジャーナルモード(pragma journal_mode)を考えてください。 NORMALがあり、それからOFFがあります。OSがクラッシュした場合にデータベースが壊れる可能性があることをあまり心配していなければ、挿入速度が大幅に向上します。アプリケーションがクラッシュした場合、データは問題ないはずです。新しいバージョンでは、OFF/MEMORY設定はアプリケーションレベルのクラッシュに対して安全ではありません。
  3. ページサイズで遊ぶことでも違いが出ます(PRAGMA page_size)。大きなページサイズを持つと、大きなページがメモリに保持されるため、読み書きが少し速くなります。データベースにより多くのメモリが使用されることに注意してください。
  4. インデックスがある場合は、すべての挿入を行った後にCREATE INDEXを呼び出すことを検討してください。これは、インデックスを作成してから挿入を行うよりもはるかに高速です。
  5. 書き込みが行われるとデータベース全体がロックされるので、SQLiteへの同時アクセスがある場合は、細心の注意を払う必要があります。複数のリーダーが存在する可能性はありますが、書き込みはロックアウトされます。これは、新しいSQLiteバージョンにWALが追加されたことで多少改善されました。
  6. スペースを節約することを利用してください...より小さなデータベースはより速く行きます。たとえば、キーと値のペアがある場合は、可能であればキーをINTEGER PRIMARY KEYにしてみてください。これによって、テーブル内の暗黙の一意の行番号列が置き換えられます。
  7. 複数のスレッドを使用している場合は、 共有ページキャッシュ を使用すると、ロードされたページをスレッド間で共有できます。/Oを呼び出します。
  8. !feof(file)!を使わないでください

私はまたここで同様の質問をした そしてここで そして ここで

725
Snazzer

これらの挿入には、SQLITE_STATICの代わりにSQLITE_TRANSIENTを使用してみてください。

SQLITE_TRANSIENTはSQLiteに戻る前に文字列データをコピーさせるでしょう。

SQLITE_STATICはあなたがそれに与えたメモリアドレスが問い合わせが実行されるまで有効であることを伝えます(このループではいつもそうです)。これにより、ループごとにいくつかの割り当て、コピー、割り当て解除の操作を省くことができます。おそらく大きな改善です。

120
Tiredofbuttons

sqlite3_clear_bindingsを避けます(stmt);

テストのコードは毎回バインディングを設定し、それを通して十分なはずです。

SQLiteのドキュメントからのC APIイントロによると

初めてsqlite3_step()を呼び出す前、またはsqlite3_reset()の直後に、アプリケーションはsqlite3_bind()インターフェースの1つを呼び出して、パラメーターに値を付加することができます。 sqlite3_bind()を呼び出すたびに、同じパラメータに対する以前のバインディングが上書きされます。

(参照: sqlite.org/cintro.html )。 その関数 についての文書には、単にバインディングを設定することに加えてそれを呼び出さなければならないということは何もありません。

より詳しく: Avoid_sqlite3_clear_bindings()

92
ahcox

一括挿入について

この投稿とここで私を導いてくれたStack Overflowの質問にインスパイアされました - SQLiteデータベースに一度に複数の行を挿入することは可能ですか? - 私は自分の投稿を投稿しましたfirst Git リポジトリ:

https://github.com/rdpoor/CreateOrUpdate

これは MySQL 、SQLite、または PostgreSQL データベースにActiveRecordの配列を一括ロードします。既存のレコードを無視する、上書きする、またはエラーを発生させるオプションがあります。私の初歩的なベンチマークは、シーケンシャル書き込み - YMMVと比較して10倍のスピード向上を示しています。

大規模なデータセットを頻繁にインポートする必要があるプロダクションコードでそれを使用しています、そしてそれには満足しています。

54
fearless_fool

一括インポートは、INSERT/UPDATE文をまとめることができれば最もパフォーマンスが良いようです。数行程度のテーブルでは、YMMVで10,000程度の値がうまく機能しました。

45
Leon

あなたが読むことだけを考えているなら、多少速い(しかし古いデータを読むかもしれない)バージョンは複数のスレッドからの複数の接続から読むことです(スレッド毎の接続)。

まず表から項目を見つけます。

SELECT COUNT(*) FROM table

それからページを読んでください(LIMIT/OFFSET):

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

ここで、とは、スレッドごとに計算されます。

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

スレッドごとに:

int offset = thread_index * limit

私達の小さい(200mb)dbのためにこれは50-75%のスピードアップ(Windows 7で3.8.0.2 64ビット)をしました。私たちのテーブルは非常に正規化されていません(1000-1500列、およそ10万行以上)。

あまりにも多くのまたは少なすぎるスレッドではうまくいかない場合は、自分でベンチマークをとり、プロファイルを作成する必要があります。

私たちにとっても、SHAREDCACHEはパフォーマンスを低下させたので、私は手動でPRIVATECACHEを設定しました。

37
malkia

Cache_sizeをもっと高い値、つまりPRAGMA cache_size=10000;に引き上げるまで、トランザクションから何の利益も得られません。

27
anefeletos

このチュートリアルを読んだ後、私は自分のプログラムにそれを実装しようとしました。

住所を含む4〜5個のファイルがあります。各ファイルには約3000万レコードがあります。私はあなたが提案しているのと同じ設定を使っていますが、私の1秒あたりのINSERT数はかなり低いです(1秒あたり〜10.000レコード)。

これはあなたの提案が失敗するところです。すべてのレコードに対して単一のトランザクションを使用し、エラーや失敗のない単一の挿入を使用します。各レコードを異なるテーブルの複数の挿入に分割しているとしましょう。レコードが壊れているとどうなりますか?

ON CONFLICTコマンドは適用されません。レコード内に10個の要素があり、各要素を異なるテーブルに挿入する必要がある場合、要素5にCONSTRAINTエラーが発生した場合は、それまでの4回の挿入もすべて実行する必要があります。

だからここでロールバックが来るのです。ロールバックの唯一の問題はあなたがすべてのあなたの挿入物を失いそして上から始めているということです。どうすればこれを解決できますか?

私の解決策は、複数のトランザクションを使用することでした。私は10.000レコードごとにトランザクションを開始し、終了します(なぜその数を尋ねないでください、それは私がテストした最も速いものでした)。私は10.000サイズの配列を作成し、そこに成功したレコードを挿入します。エラーが発生したら、ロールバックしてトランザクションを開始し、配列からレコードを挿入してコミットし、破損したレコードの後で新しいトランザクションを開始します。

この解決策は、私が悪い/重複したレコードを含むファイルを扱うときに抱えていた問題(私はほぼ4%の悪いレコードを持っていた)を回避するのに役立ちました。

私が作成したアルゴリズムは、私のプロセスを2時間短縮するのに役立ちました。ファイル1hr 30mの最終ロード処理はまだ遅いですが、最初に行った4時間とは比較になりません。私はインサートを10.000/sから〜14.000/sにスピードアップすることに成功しました

スピードを上げる方法について他のアイデアがある人がいるなら、私は提案を受け入れます。

UPDATE

上記の私の答えに加えて、あなたはあなたが使用しているハードドライブにもよりますが毎秒インサートがあることを心に留めておくべきです。私は、異なるハードドライブを備えた3つの異なるPCでそれをテストしました、そして時の大きな違いを得ました。 PC1(1時間30分)、PC2(6時間)、PC3(14時間)なので、なぜそうなるのか疑問に思い始めました。

2週間の調査と複数のリソース(ハードドライブ、RAM、キャッシュ)のチェックの後、ハードドライブの設定によってはI/Oレートに影響を与える可能性があることがわかりました。目的の出力ドライブのプロパティをクリックすると、[全般]タブに2つのオプションが表示されます。 Opt1:このドライブを圧縮し、Opt2:このドライブのファイルにインデックスを付けます。

これら2つのオプションを無効にすることで、3台のPCすべてが終了するまでにほぼ同じ時間がかかります(1時間と20〜40分)。挿入が遅い場合は、ハードドライブがこれらのオプションで設定されているかどうかを確認してください。それはあなたに多くの時間と解決策を見つけることを試みる頭痛を救うでしょう

18
Jimmy_A

あなたの質問に対する答えは、新しいsqlite3のパフォーマンスが向上したということです。それを使用してください。

この答え SqlAlchemyによるSQLAlchemyの挿入が、sqlite3を直接使用するよりも25倍遅いのはなぜですか? by SqlAlchemy Orm Authorによると、0.5秒で100kの挿入があります。 python-sqliteとSqlAlchemyによる結果。これは私にパフォーマンスがsqlite3で向上したと信じるように導きます

10
doesnt_matter