牛!単一テーブル数千万行のデータベース:LIKE検索最適化ノート

推奨読書:

データベースでLIKE演算子を使用してデータのファジー検索を完了することがよくあります。LIKE演算子は、WHERE句の列で指定されたパターンを検索するために使用されます。

姓が「Zhang」であるすべてのデータを顧客テーブルで検索する必要がある場合は、次のSQLステートメントを使用できます。

SELECT * FROM Customer WHERE Name LIKE '张%'

携帯電話の終了番号が「1234」である顧客テーブルのすべてのデータを検索する必要がある場合は、次のSQLステートメントを使用できます。

SELECT * FROM Customer WHERE Phone LIKE '%123456'

名前に「show」を含む顧客テーブル内のすべてのデータを検索する必要がある場合は、次のSQLステートメントを使用できます。

SELECT * FROM Customer WHERE Name LIKE '%秀%'

上記の3つは、左プレフィックスマッチング、右サフィックスマッチング、ファジークエリに対応し、さまざまなクエリ最適化方法に対応しています。

データの概要

これで、tbl_likeという名前のデータテーブルが作成されました。これには、4つのクラシックのすべての文が含まれ、数千万のデータが含まれています。

左プレフィックス一致クエリの最適化

「Monkey King」で始まるすべての文をクエリする場合は、次のSQLステートメントを使用できます。

SELECT * FROM tbl_like WHERE txt LIKE '孙悟空%'

SQL Serverデータベースは比較的強力で、800ミリ秒以上かかりますが、高速ではありません。

txt列にインデックスを作成して、クエリを最適化できます。

CREATE INDEX tbl_like_txt_idx ON [tbl_like] ( [txt] )

インデックスを適用した後、クエリ速度は大幅に加速され、わずか5ミリ秒です。

このことから、左側のプレフィックスマッチングでは、インデックスを増やすことでクエリを高速化できることがわかります。

正しいサフィックス一致クエリの最適化

正しい接尾辞の照合クエリでは、上記のインデックスは右接尾辞の照合には有効ではありません。次のSQLステートメントを使用して、「Monkey King」で終わるすべてのデータをクエリします。

SELECT * FROM tbl_like WHERE txt LIKE '%孙悟空'

効率は非常に低く、2.5秒かかります。

「時間のスペース」アプローチを使用して、正しいサフィックスマッチングクエリの効率が低いという問題を解決できます。

簡単に言えば、文字列を逆にして、右側のサフィックスの一致を左側のプレフィックスの一致に変えることができます。例として「グ・ハイを取り戻し、モンキー・キングを捕まえる」を例にとると、反転させた文字列は「コン・ウー・サンが海を行き来して守っている」。「Monkey King」で終わるデータを見つける必要がある場合は、「Kong Wu Sun」で始まるデータを検索してください。

具体的な方法は、「txt_back」列をテーブルに追加し、「txt」列の値を反転し、「txt_back」列に入力し、最後に「txt_back」列のインデックスを追加します。

ALTER TABLE tbl_like ADD txt_back nvarchar(1000);-- 增加数据列
UPDATE tbl_like SET txt_back = reverse(txt); -- 填充 txt_back 的值
CREATE INDEX tbl_like_txt_back_idx ON [tbl_like] ( [txt_back] );-- 为 txt_back 列增加索引

データテーブルを調整した後、SQLステートメントも調整する必要があります。

SELECT * FROM tbl_like WHERE txt_back LIKE '空悟孙%'

この操作の後、実行速度は非常に速くなります。

このことからわかるように、右のサフィックスの照合では、逆の順序フィールドを作成して、右のサフィックスの照合を左のプレフィックスの照合に変更して、クエリを高速化できます。

あいまいなクエリの最適化

「Wukong」を含むすべてのステートメントを照会する場合、次のSQLステートメントを使用します。

SELECT * FROM tbl_like WHERE txt LIKE '%悟空%'

ステートメントはインデックスを使用できないため、クエリは非常に遅く、2.7秒必要です。

残念ながら、このクエリを最適化する簡単な方法はありません。しかし、簡単な方法はありません。だからといって、方法がないという意味ではありません。解決策の1つは、単語のセグメンテーション+逆索引です。

単語のセグメンテーションは、特定の仕様に従って、連続する単語シーケンスを単語シーケンスに再結合するプロセスです。英語の文章では、単語間のスペースは自然な区切り文字として使用されますが、中国語のみの単語、文、段落は、明白な区切り文字で簡単に区切ることができますが、単語には正式な区切り文字はありません。英語には句の分割の問題もありますが、単語レベルでは、中国語は英語よりもはるかに複雑で困難です。

転置インデックスは、実際のアプリケーションで属性の値に基づいてレコードを検索する必要性から生じます。このインデックステーブルの各項目には、属性値と、属性値を持つ各レコードのアドレスが含まれています。属性値はレコードによって決定されるのではなく、レコードの位置は属性値によって決定されるため、転置インデックスと呼ばれます。転置インデックスを含むファイルは、転置インデックスファイル、または略して転置ファイルと呼ばれます。

上記の2つの不可解なテキストはBaidu Baikeからのものです。私と同じように無視することもできます。

中国語の特性上、「バイナリ」の単語セグメンテーションのみが必要なため、優れた単語セグメンテーションスキルは必要ありません。

いわゆるバイナリワードセグメンテーションとは、段落内のテキストの2文字ごとがワードとして使用され、ワー​​ドをセグメント化することを意味します。例として「Gu Haiを警戒してモンキーキングを捕まえる」という例を考えてみましょう。バイナリセグメンテーションの後、結果は次のようになります。古代の海を警戒し、古代の海に戻り、海に戻り、再び戻ってきて、再び捕まえ、孫を捕まえます。モンキーキング、ウコン。C#を使用して簡単に実装します。

public static List<String> Cut(String str)
{
       var list = new List<String>();
       var buffer = new Char[2];
       for (int i = 0; i < str.Length - 1; i++)
       {
             buffer[0] = str[i];
             buffer[1] = str[i + 1];
             list.Add(new String(buffer));
       }
       return list;
}

結果をテストします。

セグメント化されたエントリを元のデータと照合するためのデータテーブルが必要です。効率を高めるために、カバリングインデックスも使用します。

CREATE TABLE tbl_like_word (
  [id] int identity,
  [rid] int NOT NULL,
  [word] nchar(2) NOT NULL,
  PRIMARY KEY CLUSTERED ([id])
);
CREATE INDEX tbl_like_word_word_idx ON tbl_like_word(word,rid);-- 覆盖索引(Covering index)

上記のSQLステートメントは、「tbl_like_word」という名前のデータテーブルを作成し、その「word」列と「rid」列に結合インデックスを追加します。これは私たちの逆さの表であり、次のステップはデータでそれを埋めることです。

LINQPadの組み込みデータベースリンク関数を使用してデータベースにリンクする必要があるので、LINQPadでデータベースを操作できます。最初に、tbl_likeテーブルのデータをIdの順序で3000エントリのバッチで読み取り、txtフィールドの値をセグメント化して、tbl_like_wordに必要なデータを生成してから、データをバッチで格納します。完全なLINQPadコードは次のとおりです。

void Main()
{
       var maxId = 0;
       const int limit = 3000;
       var wordList = new List<Tbl_like_word>();
       while (true)
       {
             $"开始处理:{maxId} 之后 {limit} 条".Dump("Log");
             //分批次读取
             var items = Tbl_likes
             .Where(i => i.Id > maxId)
             .OrderBy(i => i.Id)
             .Select(i => new { i.Id, i.Txt })
             .Take(limit)
             .ToList();
             if (items.Count == 0)
             {
                    break;
             }
             //逐条生产
             foreach (var item in items)
             {
                    maxId = item.Id;
                    //单个字的数据跳过
                    if (item.Txt.Length < 2)
                    {
                           continue;
                    }
                    var words = Cut(item.Txt);
                    wordList.AddRange(words.Select(str => new Tbl_like_word {  Rid = item.Id, Word = str }));
             }
       }
       "处理完毕,开始入库。".Dump("Log");
       this.BulkInsert(wordList);
       SaveChanges();
       "入库完成".Dump("Log");
}
// Define other methods, classes and namespaces here
public static List<String> Cut(String str)
{
       var list = new List<String>();
       var buffer = new Char[2];
       for (int i = 0; i < str.Length - 1; i++)
       {
             buffer[0] = str[i];
             buffer[1] = str[i + 1];
             list.Add(new String(buffer));
       }
       return list;
}

上記のLINQPadスクリプトは、Entity Framework Coreを使用してデータベースに接続し、NuGetパッケージ「EFCore.BulkExtensions」を参照してデータの一括挿入を実行します。

その後、クエリを調整し、最初に逆インデックスをクエリしてから、それをメインテーブルに関連付けることができます。

SELECT TOP 10 * FROM tbl_like WHERE id IN (
SELECT rid FROM tbl_like_word WHERE word IN ('悟空'))

クエリ速度は非常に速く、わずか数十ミリ秒です。

すべての文を2文字のフレーズに分割したため、単一の文字のあいまいクエリが必要な場合は、LIKEを直接使用する方が経済的です。クエリする文字が3つ以上ある場合は、クエリワードをセグメント化する必要があります。「East Tu Datang」という用語をクエリする必要がある場合、構成されたクエリ文は次のようになります。

SELECT TOP 10*FROM tbl_like WHERE id IN (
SELECT rid FROM tbl_like_word WHERE word IN ('东土','土大','大唐'))

ただし、クエリは「大きな土壌」のみを含む文も除外するため、期待どおりにはなりません。

最初にGROUPなど、いくつかのトリックを使用してこの問題を解決できます。

SELECT TOP
    10 *
FROM
    tbl_like
WHERE
    id IN (
    SELECT
        rid
    FROM
        tbl_like_word
    WHERE
        word IN ( '东土', '土大', '大唐' )
    GROUP BY
        rid
    HAVING
    COUNT ( DISTINCT ( word ) ) = 3
    )

上記のSQLステートメントでは、ridをグループ化し、一意のフレーズの数が3(つまり、クエリワードの数)であることを除外しています。したがって、正しい結果を得ることができます。

これからわか​​ること:あいまいなクエリの場合、単語のセグメンテーション+逆索引によってクエリ速度を最適化できます。

追記

プレゼンテーションではSQL Serverデータベースが使用されていますが、上記の最適化エクスペリエンスは、MySQLやOracleなどのほとんどのリレーショナルデータベースに共通しています。

著者のような実際の作業でPostgreSQLデータベースを使用する場合は、配列タイプを直接使用し、逆インデックスを作成するときにGiNインデックスを構成して、より良い開発と使用体験を得ることができます。PostgreSQLは関数型インデックスをサポートしていますが、関数の結果がLIKEフィルタリングされている場合、インデックスはヒットしないことに注意してください。

SQLiteなどの小さなデータベースの場合、あいまい検索ではインデックスを使用できないため、左プレフィックス検索と右サフィックス検索の最適化方法は有効になりません。ただし、通常はSQLiteを使用して大量のデータを格納しませんが、単語分割+逆索引の最適化方法はSQLiteにも実装できます。

おすすめ

転載: blog.csdn.net/weixin_45784983/article/details/108411887