この記事は、Huawei Cloud Community「MySQL フルテキスト インデックス ソース コード分析: Insert ステートメント実行プロセス」 (著者: GaussDB データベース) から共有されたものです。
1. 背景の紹介
全文インデックスは、情報検索の分野で一般的に使用される技術手段であり、たとえば、キーワードを入力した場合に、その単語を含むドキュメントを検索する場合に使用されます。ブラウザや検索エンジンは、関連するすべてのドキュメントを検索し、関連性によって並べ替える必要があります。
フルテキスト インデックスの基礎となる実装は、転置インデックスに基づいています。いわゆる逆インデックスは、単語とドキュメントの間のマッピング関係を記述し、(単語、(単語が配置されているドキュメント、ドキュメント内の単語のオフセット)) の形式で表されます。次の例は、その方法を示します。全文インデックスは次のように構成されています。
mysql> CREATE TABLE starting_lines ( id INT UNSIGNED AUTO_INCREMENT NOT NULL 主キー、 開始行 TEXT(500), 著者 VARCHAR(200)、 タイトル VARCHAR(200)、 全文idx (開始行) ) ENGINE=InnoDB; mysql> INSERT INTO starting_lines(opening_line,author,title) VALUES (「イシュマエルと呼んでください。」、「ハーマン・メルヴィル」、「白鯨」)、 (「空に叫び声が聞こえる。」、「トーマス・ピンチョン」、「重力の虹」)、 (「私は透明人間です。」、「ラルフ・エリソン」、「透明人間」)、 (「今どこ? 今誰? いつ?」、「サミュエル・ベケット」、「名状しがたいもの」); mysql> SET GLOBAL innodb_ft_aux_table='test/opening_lines'; mysql> select * from information_schema.INNODB_FT_INDEX_TABLE; +-----------+--------------+---------------+-------- ---+--------+----------+ |単語 | FIRST_DOC_ID | LAST_DOC_ID | DOC_COUNT |文書ID |位置 | +-----------+--------------+---------------+-------- ---+--------+----------+ |横に | 4 | 4 | 1 | 4 | 18 | |電話 | 3 | 3 | 1 | 3 | 0 | |来る | 4 | 4 | 1 | 4 | 12 | |目に見えない | 5 | 5 | 1 | 5 | 8 | |イシュマエル | 3 | 3 | 1 | 3 | 8 | |男 | 5 | 5 | 1 | 5 | 18 | |今 | 6 | 6 | 1 | 6 | 6 | |今 | 6 | 6 | 1 | 6 | 9 | |今 | 6 | 6 | 1 | 6 | 10 | |叫ぶ | 4 | 4 | 1 | 4 | 2 | |空 | 4 | 4 | 1 | 4 | 29 | +-----------+--------------+---------------+-------- ---+--------+----------+
上記のように、テーブルが作成され、opening_line 列にフルテキスト インデックスが確立されます。例として「Call me Ishmael.」を挿入すると、ID が 3 のドキュメントになります。全文インデックスを作成すると、ドキュメントは「call」、「me」の 3 つの単語に分割されます。 、「ishmael 」。「me」は ft_min_word_len(4) で設定された最小単語長より小さいため、最終的には「call」と「ishmael」のみがフルテキスト インデックスに記録されます。 'call' の開始位置はドキュメント内の 0 番目の文字、オフセットは 0、'ishmael' の開始位置はドキュメント内の 12 番目の文字、オフセットは 12 です。
フルテキスト インデックスの機能の詳細については、『MySQL 8.0 リファレンス マニュアル』を参照してください。この記事では、Insert ステートメントの実行プロセスをソース コード レベルから簡単に分析します。
2. 全文インデックスキャッシュ
全文索引テーブルに記録されるのは、{単語,{文書ID,出現位置}}です。つまり、文書を挿入するには、文書を複数の{単語,{文書ID,出現位置}}に分割する必要があります。毎回の構造 単語の分割直後にディスクがフラッシュされると、パフォーマンスが非常に低下します。
この問題を軽減するために、Innodb は変更バッファと同様に機能するフルテキスト インデックス キャッシュを導入しました。文書が挿入されるたびに、単語の分割結果がまずキャッシュにキャッシュされ、キャッシュがいっぱいになるとバッチでディスクにフラッシュされるため、頻繁なディスクのフラッシュが回避されます。次の図に示すように、Innodb はキャッシュを管理するための fts_cache_t 構造体を定義します。
各テーブルはキャッシュを維持し、フルテキスト インデックスが作成されるテーブルごとに、メモリ内に fts_cache_t オブジェクトが作成されます。 fts_cache_t はテーブル レベルのキャッシュであることに注意してください。テーブルに対して複数のフルテキスト インデックスが作成された場合でも、対応する fts_cache_t オブジェクトがメモリ内に存在します。 fts_cache_t の重要なメンバーの一部は次のとおりです。
-
optimize_lock、deleted_lock、doc_id_lock: 同時操作に関連するミューテックス ロック。
-
delete_doc_ids: ベクトル型、削除された doc_id を格納します。
-
インデックス: ベクトル型。各要素はフルテキスト インデックスを表します。フルテキスト インデックスが作成されるたびに、要素が配列に追加され、各インデックスの単語分割結果が赤黒ツリー構造に格納されます。キーは word で、値は doc_id と単語のオフセットです。
-
total_size: キャッシュによって割り当てられたすべてのメモリ (そのサブ構造によって使用されるメモリを含む)。
3. Insert文実行処理
MySQL 8.0.22 ソース コードを例にとると、Insert ステートメントの実行は主に 3 つのステージ、つまり行レコードの書き込みステージ、トランザクションの送信ステージ、ダーティ クリーニング ステージに分かれています。
3.1 行レコードの書き込みフェーズ
行レコードを書き込むための主なワークフローを次の図に示します。
上の図に示すように、この段階で最も重要なことは、doc_id を生成し、それを Innodb 行レコードに書き込み、トランザクション送信フェーズ中に doc_id に基づいてテキスト コンテンツを取得できるように doc_id をキャッシュすることです。関数呼び出しスタックは次のとおりです。
ha_innobase::write_row ->mysql の行挿入 ->mysqlのrow_insert_using_ins_graph ->row_mysql_convert_row_to_innobase ->fts_create_doc_id ->fts_get_next_doc_id ->fts_trx_add_op ->fts_trx_table_add_op
fts_get_next_doc_id と fts_trx_table_add_op は、Innodb 行レコードに row_id や trx_id などの隠し列が含まれる 2 つの重要な関数です。フルテキスト インデックスが作成されると、隠しフィールド FTS_DOC_ID も行に追加されます。レコードの場合、この値は次のように fts_get_next_doc_id で取得されます。
また、fts_trx_add_op は、trx に全文インデックス操作を追加し、トランザクションがコミットされるときにさらに処理されます。
3.2 トランザクション送信フェーズ
トランザクション送信フェーズの主なワークフローを次の図に示します。
この段階は、FTS 挿入全体の中で最も重要なステップです。文書の Word 分割、{word, {文書 ID, 出現位置}} の取得、およびキャッシュへの挿入はすべてこの段階で完了します。その関数呼び出しスタックは次のとおりです。
fts_commit_table ->fts_add ->fts_add_doc_by_id ->fts_cache_add_doc // doc_id に基づいてドキュメントを取得し、ドキュメントを単語に分割します ->fts_fetch_doc_from_rec // 単語分割結果をキャッシュに追加します ->fts_cache_add_doc ->fts_optimize_request_sync_table // ダーティ スレッドをクリーンアップするようにダーティ スレッドに通知する FTS_MSG_SYNC_TABLE メッセージを作成します ->fts_optimize_create_msg(FTS_MSG_SYNC_TABLE)
その中で、fts_add_doc_by_id は主に次のことを実現する重要な機能です。
1) doc_id に基づいて行レコードを検索し、対応するドキュメントを取得します。
3) キャッシュ -> かどうかを判断します。 total_size がしきい値に達した場合、ダーティ スレッドのメッセージ キューに FTS_MSG_SYNC_TABLE メッセージを追加して、スレッドにフラッシュするように通知します (fts_optimize_create_msg)。具体的なコードは次のとおりです。
理解を容易にするために、コードの例外処理部分とレコード検索のいくつかの共通部分を省略し、簡単なコメントを付けました。
静的 ulint fts_add_doc_by_id(fts_trx_table_t *ftt, doc_id_t doc_id) { /* 1. docid に基づいて fts_doc_id_index インデックス内のレコードを検索します*/ /* btr_cur_search_to_nth_level、btr_cur_search_to_nth_level は btr_pcur_open_with_no_init 関数で呼び出されます b+ ツリー検索レコード処理は、まずルートノードから docid レコードが存在するリーフノードを見つけ、次に二分探索によって docid レコードを見つけます。 */ btr_pcur_open_with_no_init(fts_id_index, タプル, PAGE_CUR_LE, BTR_SEARCH_LEAF、&pcur、0、&mtr); if (btr_pcur_get_low_match(&pcur) == 1) { /* docid レコードが見つかった場合 */ if (is_id_cluster) { /** 1.1 fts_doc_id_index がクラスター化インデックスの場合、行レコード データが見つかったことを意味し、行レコードは直接保存されます**/ doc_pcur = &pcur; } それ以外 { /** 1.2 fts_doc_id_index がセカンダリ インデックスの場合、1.1 で見つかった主キー ID に基づいてクラスター化インデックス上の行レコードをさらに検索し、見つけた後に行レコードを保存する必要があります **/ btr_pcur_open_with_no_init(clust_index, clust_ref, PAGE_CUR_LE、 BTR_SEARCH_LEAF、&clust_pcur、0、&mtr); doc_pcur = &clust_pcur; } // キャッシュをトラバース->get_docs for (ulint i = 0; i < num_idx; ++i) { /***** 2. ドキュメントで単語の分割を実行し、{word, (単語が存在するドキュメント、ドキュメント内の単語のオフセット)} 関連ペアを取得し、それをキャッシュに追加します** ***/ fts_doc_t ドキュメント; fts_doc_init(&doc); /** 2.1 doc_id に従って行レコードのフルテキスト インデックスの対応する列のコンテンツ ドキュメントを取得し、主に fts_doc_t 構造のトークンを構築するためにドキュメントを解析します。トークンは赤と黒のツリーです。各要素は {word, [文書内で単語が出現する位置の構造]} であり、解析結果は doc**/ に格納されます。 fts_fetch_doc_from_rec(ftt->fts_trx->trx, get_doc, clust_index,doc_pcur, offsets, &doc); /** 2.2 2.1で取得した{word, [文書内で単語が出現する位置]}をindex_cache**/に追加します。 fts_cache_add_doc(table->fts->cache, get_doc->index_cache, doc_id, doc.tokens); /***** 3.cache->total_size がしきい値に達しているかどうかを判断します。しきい値に達した場合は、ダーティ スレッドのメッセージ キューに FTS_MSG_SYNC_TABLE メッセージを追加して、スレッドにクリーンするよう通知します *****/ ブール必要_同期 = false; if ((キャッシュ -> 合計サイズ - キャッシュ -> 同期前の合計サイズ > fts_max_cache_size / 10 || fts_need_sync) &&!キャッシュ->同期->進行中) { /** 3.1 しきい値に達したかどうかを判断します**/ need_sync = true; キャッシュ->同期前の合計サイズ = キャッシュ->合計サイズ; } if (need_sync) { /** 3.2 FTS_MSG_SYNC_TABLE メッセージをパッケージ化して fts_optimize_wq キューにマウントし、ダーティをクリーンアップするように fts_optimize_thread スレッドに通知します。メッセージの内容はテーブル ID **/ fts_optimize_request_sync_table(table); } } } }
上記のプロセスを理解した後、MySQL 8.0 リファレンス マニュアルの「InnoDB フルテキスト インデックス トランザクションの処理」セクションを参照して、フルテキスト インデックス トランザクションの送信に関する特殊な現象を説明できます。フルテキスト インデックス テーブルで、現在のトランザクションがコミットされていない場合、現在のトランザクションのフルテキスト インデックスを通じて挿入された行レコードを見つけることができません。その理由は、トランザクションがコミットされるとフルテキスト インデックスの更新が完了するためです。トランザクションがコミットされていないときは、まだ fts_add_doc_by_id が実行されていないため、フルテキスト インデックスからレコードを見つけることができません。ただし、セクション 3.1 から、この時点で Innodb 行レコードが挿入されていることがわかります。フルテキスト インデックスを介してクエリを実行すると、SELECT COUNT(*) FROM starting_lines を直接実行することでレコードを見つけることができます。
クリーニング段階の主なワークフローを次の図に示します。
InnoDB が起動すると、バックグラウンド スレッドが作成されます。スレッド関数はfts_optimize_thread、ワーク キューは fts_optimize_wq です。セクション 3.2 のトランザクション送信フェーズでは、キャッシュがいっぱいになると、fts_optimize_request_sync_table 関数が FTS_MSG_SYNC_TABLE メッセージを fts_optimize_wq キューに追加し、バックグラウンド スレッドがメッセージを削除し、キャッシュをディスクにフラッシュします。その関数呼び出しスタックは次のとおりです。
fts_optimize_thread ->ib_wqueue_timedwait ->fts_optimize_sync_table ->fts_sync_table ->fts_sync ->fts_sync_commit ->fts_cache_clear
このスレッドによって実行される主な操作は次のとおりです。
-
fts_optimize_wq キューからメッセージを取得します。
-
メッセージのタイプを判別し、FTS_MSG_SYNC_TABLE の場合はフラッシュを実行します。
-
キャッシュの内容をディスク上の補助テーブルにフラッシュします。
-
キャッシュをクリアし、キャッシュを初期状態に設定します。
-
ステップ 1 に戻り、次のメッセージを取得します。
セクション 3.2 では、トランザクションが送信されるときに、fts キャッシュの total_size が設定されたメモリ サイズしきい値より大きい場合、FTS_MSG_SYNC_TABLE が書き込まれ、fts_optimize_wq キューに挿入されます。ダーティ スレッドはメッセージを処理し、データをフラッシュします。 fts キャッシュをディスクにコピーしてからキャッシュをクリアします。
fts キャッシュの total_size が設定されたメモリ サイズのしきい値よりも大きい場合、fts_optimize_wq キューに書き込まれるメッセージは 1 つだけであることに注意してください。この時点でも、fts キャッシュは、メッセージが処理される前にデータとメモリに書き込むことができます。バックグラウンド フラッシュ スレッドは増加し続けますが、これはフルテキスト インデックスの同時挿入を引き起こす OOM 問題の根本的な原因でもあります。この問題の修正は、パッチバグ #32831765 で行われます。自分で調べることができます。
OOM チェックリンク: https://bugs.mysql.com/bug.php?id=103523
ダーティ スレッドが特定のテーブルの fts キャッシュをまだダーティにしていない場合、MySQL プロセスがクラッシュし、キャッシュ内のデータが失われます。再起動後、テーブルに対して初めて挿入または選択が実行されると、クラッシュ前のキャッシュ内のデータが fts_init_index 関数で復元されます。このとき、ディスクにドロップされた synced_doc_id が構成から読み取られます。テーブル内の synced_doc_id は synced_doc_id より大きくなります。レコードは読み取られ、ワード分割されてキャッシュに復元されます。具体的な実装については、fts_doc_fetch_by_doc_id 関数と fts_init_recover_doc 関数を参照してください。
クリックしてフォローし、できるだけ早くHuawei Cloudの新しいテクノロジーについて学びましょう~
「Celebrated More Than Years 2」の海賊版リソースが npm にアップロードされたため、npmmirror は unpkg サービスを停止せざるを得なくなり、 最初の創設者の 数百人が参加して、一斉に米国に向かいました。 フロントエンド視覚化ライブラリと Baidu の有名なオープンソース プロジェクト ECharts - Fish 詐欺師をサポートするために「海へ行く」が、TeamViewer を使用して 398 万を送金しました。リモート デスクトップ ベンダーは何をすべきでしょうか? 周宏宜: Google に残された時間はあまり多くありません。すべての製品をオープンソースにすることが推奨されています。 ある有名なオープンソース企業の元従業員が、部下から異議を申し立てられた後、激怒しました。妊娠中の女性従業員を解雇しました。Google は Android 仮想マシンで ChromeOS を実行する方法を示しました。 ここで time.sleep(6) はどのような役割を果たしますか? マイクロソフト、中国のAIチームが「米国のために荷造りしている」という噂に反応 人民日報オンラインはオフィスソフトのマトリョーシカのような課金についてコメント:「セット」を積極的に解決することによってのみ、私たちは未来を手に入れることができる