PolarDB-X source code interpretation series
前回のソースコードリーディングでは、INSERT の実行プロセスを紹介しました。INSERT とは異なり、INSERT IGNORE では挿入値と一意キーの競合があるかどうかを判定し、競合する挿入値を無視する必要があります。そのため、本記事では PolarDB-X における INSERT IGNORE の実行プロセスを紹介します。このプロセスは、挿入先のテーブルに GSI があるかどうかによって異なります。
プッシュダウン実行
挿入先のテーブルにメインテーブルのみがあり、GSI がない場合は、INSERT IGNORE を対応する物理テーブルに直接送信するだけでよく、DN が競合する値を無視します。この場合、INSERT IGNORE の実行プロセスは INSERT とほぼ同じです。読者は前回のソースコードリーディングの記事を参照してください。
論理実行
GSI がある場合、INSERT IGNORE をメインテーブルと GSI の対応する物理サブテーブルに単純に配布することはできません。そうすると、メインテーブルと GSI のデータが不整合になる可能性があります。例を挙げます。
create table t1 (a int primary key, b int, global index g1(b) dbpartition by hash(b)) dbpartition by hash(a);
insert ignore into t1 values (1,1),(1,2);
挿入される 2 つのレコードについて、メインテーブルでは同じ物理テーブルに配置されます(a が同じ)が、GSI では異なる物理テーブルに配置されます(b が異なる)。INSERT IGNORE を直接実行すると、メインテーブルでは (1,1) のみが正常に挿入されます(主キー競合)が、GSI では (1,1) と (1,2) の両方が正常に挿入されるため、GSI はメインテーブルより 1 件多いデータを持つことになります。
この場合の解決策の 1 つは、挿入値の Unique Key に基づいて競合し得るデータをデータベースから CN に SELECT で取得し、CN 側で競合値の判定と削除を行うことです。
SELECT を実行する際、最もシンプルな方法はすべての SELECT をメインテーブルに送信することです。ただし、メインテーブルに対応する Unique Key が定義されていない場合があり、その結果 SELECT 中にフルテーブルスキャンが発生し、パフォーマンスに影響します。そこで、オプティマイザステージで、Unique Key がメインテーブルと GSI のどちらに定義されているかに応じて、対応する SELECT をメインテーブルに送信するか GSI に送信するかを判定します。具体的なコードの場所は以下の通りです。
com.alibaba.polardbx.optimizer.core.planner.rule.OptimizeLogicalInsertRule#groupUkByTable
実行ステージでは、LogicalInsertIgnoreHandler で INSERT IGNORE を処理します。まず getDuplicatedValues 関数に入ります。この関数は SELECT を発行してテーブル内の Unique Key が競合するレコードを検索します。発行する SELECT ステートメントで選択するカラムを (value_index, uk_index, pk) に設定します。ここで value_index と uk_index はいずれも定数です。
たとえば、次のようなテーブルがあるとします。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE GLOBAL KEY `g_ i_ a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`)
) DBPARTITION BY HASH(`id`)
および、次の INSERT IGNORE 文があるとします。
INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);
PolarDB-X で実行される際、Unique Key に次のように番号が割り当てられるとします。
0: id
1: g_ i_ a
INSERT IGNORE 文で挿入される各値にも番号が割り当てられます。
0: (1,2,3)
1: (2,3,4)
2: (3,4,5)
すると、(2,3,4) の Unique Key に基づいて GSI に対して構築される SELECT は次のようになります。
GSI へのクエリ
SELECT 1 as `value_ index`, 1 as `uk_ index`, `id`
FROM `g_ i_ a_ xxxx`
WHERE `a` in 3;
テーブルに既に (5,3,6) が存在する場合、この SELECT の戻り結果は (1,1,5) になります。また、異なる Unique Key に対する SELECT の戻り形式は同じであるため、同一物理ライブラリ上の異なる SELECT クエリを UNION して複数の結果を一度に取得し、CN と DN 間のやり取り回数を削減します。Unique Key に重複値が存在する限り、value_index と uk_index によって、挿入される値のどの行のどの Unique Key が重複しているかを特定できます。
すべての戻り結果を取得した後、データの重複排除を行います。前のステップで取得した競合値を SET に格納し、挿入値を各行ごとに順次スキャンします。重複値が見つかった場合はその行をスキップし、見つからない場合はその行を SET に追加します(挿入値同士の競合もあり得るためです)。重複排除後、競合しないすべての値が得られます。これらの値をテーブルに挿入すると、INSERT IGNORE の実行が完了します。
RETURNING 最適化
前節で説明した INSERT IGNORE の論理実行モードはデータの正確性を保証しますが、INSERT IGNORE 文 1 つを完了するために CN と DN 間のやり取りが少なくとも 2 回必要です(1 回目は SELECT、2 回目は INSERT)。これが INSERT IGNORE の実行パフォーマンスに影響します。
現在の DN は既に AliSQL の RETURNING 最適化をサポートしており、DN 側で INSERT IGNORE を実行した後に正常に挿入された値を返すことができます。この機能を活用し、PolarDB-X は INSERT IGNORE をさらに最適化します。INSERT IGNORE を直接発行し、メインテーブルと GSI の両方で挿入がすべて正常に返された場合、挿入値に競合がないことを意味し、INSERT IGNORE は正常に実行されます。そうでない場合は、複数の挿入値のうち余分な値を削除します。
実行時、CN はまず上記の構文に従って RETURNING 句付きの物理 INSERT IGNORE ステートメントを DN に発行します。たとえば次のようになります。
call dbms_ trans. returning("a", "insert into t1_xxxx values(1,1)");
戻りカラムはプライマリキーで、バッチ挿入されたデータのうちどの行が正常に挿入されたかを識別するために使用されます。t1_xxxx は論理テーブル t1 の物理サブテーブルです。メインテーブルと GSI のすべての INSERT IGNORE が実行された後、メインテーブルと GSI で正常に挿入された値の共通部分を計算して最終結果を求め、余分な挿入値を削除します。この部分のコードは以下の場所にあります。
com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#getRowsToBeRemoved
前節の論理実行を「悲観的実行」とすると、RETURNING 最適化を適用した INSERT IGNORE は「楽観的実行」に相当します。挿入値に競合がなければ、INSERT IGNORE 文 1 つに対して CN と DN のやり取りは 1 回だけで済みます。競合がある場合は、メインテーブルまたは GSI の余分な挿入値を削除するために DELETE 文を発行する必要があるため、CN と DN のやり取りは 2 回必要です。競合時でも、CN と DN のやり取り回数は前節の論理実行を超えることはありません。したがって、直接プッシュダウンできない場合、INSERT IGNORE の実行戦略はデフォルトで RETURNING 最適化を使用します。
ただし、RETURNING 最適化の使用にもいくつかの制約があります。たとえば、挿入値にプライマリキーの重複がある場合は使用できません。この場合、どの行が正常に挿入され、どの行を削除すべきかを判別できないためです。詳細はコード内の条件判定を参照してください。RETURNING 最適化を使用できない場合、システムは自動的に前節の論理実行モードを選択して INSERT IGNORE 文を実行し、データの正確性を保証します。
RETURNING 最適化を適用した実行フローは以下の通りです。
com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute
まとめ
本記事では、PolarDB-X における INSERT IGNORE の実行プロセスを紹介しました。INSERT IGNORE のほかにも、REPLACE や INSERT ON DUPLICATE KEY UPDATE など、実行中に重複値の判定が必要な DML ステートメントがあります。これらのステートメントも GSI がある場合、論理実行方式を採用しています。すなわち、まず SELECT を実行してから重複判定や更新などの処理を行います。興味のある読者は関連コードを参照してください。
プッシュダウン実行
挿入先のテーブルにメインテーブルのみがあり、GSI がない場合は、INSERT IGNORE を対応する物理テーブルに直接送信するだけでよく、DN が競合する値を無視します。この場合、INSERT IGNORE の実行プロセスは INSERT とほぼ同じです。読者は前回のソースコードリーディングの記事を参照してください。
論理実行
GSI がある場合、INSERT IGNORE をメインテーブルと GSI の対応する物理サブテーブルに単純に配布することはできません。そうすると、メインテーブルと GSI のデータが不整合になる可能性があります。例を挙げます。
create table t1 (a int primary key, b int, global index g1(b) dbpartition by hash(b)) dbpartition by hash(a);
insert ignore into t1 values (1,1),(1,2);
挿入される 2 つのレコードについて、メインテーブルでは同じ物理テーブルに配置されます(a が同じ)が、GSI では異なる物理テーブルに配置されます(b が異なる)。INSERT IGNORE を直接実行すると、メインテーブルでは (1,1) のみが正常に挿入されます(主キー競合)が、GSI では (1,1) と (1,2) の両方が正常に挿入されるため、GSI はメインテーブルより 1 件多いデータを持つことになります。
この場合の解決策の 1 つは、挿入値の Unique Key に基づいて競合し得るデータをデータベースから CN に SELECT で取得し、CN 側で競合値の判定と削除を行うことです。
SELECT を実行する際、最もシンプルな方法はすべての SELECT をメインテーブルに送信することです。ただし、メインテーブルに対応する Unique Key が定義されていない場合があり、その結果 SELECT 中にフルテーブルスキャンが発生し、パフォーマンスに影響します。そこで、オプティマイザステージで、Unique Key がメインテーブルと GSI のどちらに定義されているかに応じて、対応する SELECT をメインテーブルに送信するか GSI に送信するかを判定します。具体的なコードの場所は以下の通りです。
com.alibaba.polardbx.optimizer.core.planner.rule.OptimizeLogicalInsertRule#groupUkByTable
実行ステージでは、LogicalInsertIgnoreHandler で INSERT IGNORE を処理します。まず getDuplicatedValues 関数に入ります。この関数は SELECT を発行してテーブル内の Unique Key が競合するレコードを検索します。発行する SELECT ステートメントで選択するカラムを (value_index, uk_index, pk) に設定します。ここで value_index と uk_index はいずれも定数です。
たとえば、次のようなテーブルがあるとします。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE GLOBAL KEY `g_ i_ a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`)
) DBPARTITION BY HASH(`id`)
および、次の INSERT IGNORE 文があるとします。
INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);
PolarDB-X で実行される際、Unique Key に次のように番号が割り当てられるとします。
0: id
1: g_ i_ a
INSERT IGNORE 文で挿入される各値にも番号が割り当てられます。
0: (1,2,3)
1: (2,3,4)
2: (3,4,5)
すると、(2,3,4) の Unique Key に基づいて GSI に対して構築される SELECT は次のようになります。
GSI へのクエリ
SELECT 1 as `value_ index`, 1 as `uk_ index`, `id`
FROM `g_ i_ a_ xxxx`
WHERE `a` in 3;
テーブルに既に (5,3,6) が存在する場合、この SELECT の戻り結果は (1,1,5) になります。また、異なる Unique Key に対する SELECT の戻り形式は同じであるため、同一物理ライブラリ上の異なる SELECT クエリを UNION して複数の結果を一度に取得し、CN と DN 間のやり取り回数を削減します。Unique Key に重複値が存在する限り、value_index と uk_index によって、挿入される値のどの行のどの Unique Key が重複しているかを特定できます。
すべての戻り結果を取得した後、データの重複排除を行います。前のステップで取得した競合値を SET に格納し、挿入値を各行ごとに順次スキャンします。重複値が見つかった場合はその行をスキップし、見つからない場合はその行を SET に追加します(挿入値同士の競合もあり得るためです)。重複排除後、競合しないすべての値が得られます。これらの値をテーブルに挿入すると、INSERT IGNORE の実行が完了します。
RETURNING 最適化
前節で説明した INSERT IGNORE の論理実行モードはデータの正確性を保証しますが、INSERT IGNORE 文 1 つを完了するために CN と DN 間のやり取りが少なくとも 2 回必要です(1 回目は SELECT、2 回目は INSERT)。これが INSERT IGNORE の実行パフォーマンスに影響します。
現在の DN は既に AliSQL の RETURNING 最適化をサポートしており、DN 側で INSERT IGNORE を実行した後に正常に挿入された値を返すことができます。この機能を活用し、PolarDB-X は INSERT IGNORE をさらに最適化します。INSERT IGNORE を直接発行し、メインテーブルと GSI の両方で挿入がすべて正常に返された場合、挿入値に競合がないことを意味し、INSERT IGNORE は正常に実行されます。そうでない場合は、複数の挿入値のうち余分な値を削除します。
実行時、CN はまず上記の構文に従って RETURNING 句付きの物理 INSERT IGNORE ステートメントを DN に発行します。たとえば次のようになります。
call dbms_ trans. returning("a", "insert into t1_xxxx values(1,1)");
戻りカラムはプライマリキーで、バッチ挿入されたデータのうちどの行が正常に挿入されたかを識別するために使用されます。t1_xxxx は論理テーブル t1 の物理サブテーブルです。メインテーブルと GSI のすべての INSERT IGNORE が実行された後、メインテーブルと GSI で正常に挿入された値の共通部分を計算して最終結果を求め、余分な挿入値を削除します。この部分のコードは以下の場所にあります。
com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#getRowsToBeRemoved
前節の論理実行を「悲観的実行」とすると、RETURNING 最適化を適用した INSERT IGNORE は「楽観的実行」に相当します。挿入値に競合がなければ、INSERT IGNORE 文 1 つに対して CN と DN のやり取りは 1 回だけで済みます。競合がある場合は、メインテーブルまたは GSI の余分な挿入値を削除するために DELETE 文を発行する必要があるため、CN と DN のやり取りは 2 回必要です。競合時でも、CN と DN のやり取り回数は前節の論理実行を超えることはありません。したがって、直接プッシュダウンできない場合、INSERT IGNORE の実行戦略はデフォルトで RETURNING 最適化を使用します。
ただし、RETURNING 最適化の使用にもいくつかの制約があります。たとえば、挿入値にプライマリキーの重複がある場合は使用できません。この場合、どの行が正常に挿入され、どの行を削除すべきかを判別できないためです。詳細はコード内の条件判定を参照してください。RETURNING 最適化を使用できない場合、システムは自動的に前節の論理実行モードを選択して INSERT IGNORE 文を実行し、データの正確性を保証します。
RETURNING 最適化を適用した実行フローは以下の通りです。
com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute
まとめ
本記事では、PolarDB-X における INSERT IGNORE の実行プロセスを紹介しました。INSERT IGNORE のほかにも、REPLACE や INSERT ON DUPLICATE KEY UPDATE など、実行中に重複値の判定が必要な DML ステートメントがあります。これらのステートメントも GSI がある場合、論理実行方式を採用しています。すなわち、まず SELECT を実行してから重複判定や更新などの処理を行います。興味のある読者は関連コードを参照してください。
Related Articles
-
A detailed explanation of Hadoop core architecture HDFS
Knowledge Base Team
-
What Does IOT Mean
Knowledge Base Team
-
6 Optional Technologies for Data Storage
Knowledge Base Team
-
What Is Blockchain Technology
Knowledge Base Team
Explore More Special Offers
-
Short Message Service(SMS) & Mail Service
50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00
