GroupJoin は、PolarDB for MySQL のクエリオペレーターであり、HASH JOIN と HASH GROUP BY をデータに対する単一パスで統合します。順次実行に必要な 2 番目のハッシュテーブルを排除することで、中間結果のサイズを削減し、インメモリ列指向インデックス (IMCI) 有効化操作における分析クエリのパフォーマンスを向上させます。
本トピックを読む前に、HASH JOIN および HASH GROUP BY の概念を理解していることを確認してください。
GroupJoin の適用条件
GroupJoin を適用するには、以下のすべての条件が満たされている必要があります:
クエリが EQUAL JOIN および GROUP BY を実行すること。
GROUP BY キーが JOIN の片側の結合キーと一致すること(または、結合キーに対して関数従属性を有すること)。
各集計関数が JOIN の片側のテーブルのみを参照すること(両方のテーブルを参照してはいけません)。たとえば、
SUM(t1.a + t2.a)はサポートされていません。
PolarDB for MySQL の IMCI 有効化操作では、GroupJoin は、RIGHT OUTER JOIN と右側での GROUP BY を組み合わせた場合を除き、常に HASH JOIN + HASH GROUP BY の順次実行よりも優れたパフォーマンスを発揮します。
背景情報
次のクエリを例に考えます:
SELECT
key1,
SUM(sales) AS total_sales
FROM
fact_table LEFT JOIN dimension_table ON fact_table.key1 = dimension_table.key1
GROUP BY
fact_table.key1
ORDER BY
total_sales
LIMIT 100;GroupJoin を使用しない場合、このクエリは 2 つの独立した操作を必要とします:
HASH JOIN —
dimension_table.key1からハッシュテーブルを構築し、その後fact_table.key1でそのハッシュテーブルを検索します。HASH GROUP BY — 結果を集約するために、
fact_table.key1から 2 番目のハッシュテーブルを構築します。
両操作ともハッシュテーブル構築に key1 を使用しており、これは冗長です。M 行のファクトテーブルと N 行のディメンションテーブル(key1 が一意キーである場合)において、HASH JOIN は M 行を出力し、HASH GROUP BY はさらに M 行のハッシュテーブルを構築します。
GroupJoin は、結合と集約を単一パスで統合することで 2 番目のハッシュテーブルを排除します。すなわち、ディメンションテーブルから N 行のハッシュテーブルを構築し、その中に直接集約処理を行うため、メモリ消費量が低減され、中間行数も削減されます。
仕組み
GroupJoin は 2 つのフェーズで実行されます:
ビルドフェーズ — 小さな左側テーブルがハッシュテーブルを構築します。このフェーズでは、左側データに対して集計関数が実行されます(
HashGroupBy left_tableと同等)。プローブフェーズ — 大きな右側テーブルがハッシュテーブルを検索します。マッチした行は既存のハッシュテーブルエントリに集約され、マッチしなかった行は除外されます。
制限事項
GROUP BY キーは、JOIN の片側の結合キーと一致する必要があります。一部のケースでは、結合キーの一部に対する関数従属性で十分です。
RIGHT OUTER JOIN と右側での GROUP BY を組み合わせる場合、右側の結合キーは一意である必要があります。一意性が保証できない場合は、JOIN を LEFT OUTER JOIN に変更するか、GROUP BY を左側で実行してください。どちらも不可能な場合は、GroupJoin を使用できません。
各集計関数は、一度に 1 つのテーブルのみを参照できます。たとえば
SUM(t1.a + t2.a)のように、両テーブルをまたぐ集計関数は GroupJoin の対象外です。
JOIN タイプ別の動作
INNER JOIN、左側での GROUP BY
l_table INNER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY l_table.key1以下の説明では、SQL ステートメントが示された順序で実行され、結合対象が動的に切り替わらないものと仮定しています。
左側テーブルからハッシュテーブルを構築し、左側データに対して集計関数を実行します。各ハッシュテーブルエントリに対して、右側テーブルのマッチ行数をカウントする「繰り返し回数」を記録します。
右側テーブルでハッシュテーブルを検索します。マッチした各行について、繰り返し回数を 1 増加させ、右側データに対して集計関数を実行します。マッチしなかった行は破棄されます。
結合完了後、マッチしたハッシュテーブルエントリのみを出力します。マッチしなかったエントリは破棄されます。
最終的な集約結果は
SUM(expr) × 繰り返し回数と等しくなります。たとえば、SUM(expr)が 200 で繰り返し回数が 5 の場合、結果は 1,000 になります。
INNER JOIN、右側での GROUP BY
l_table INNER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY r_table.key1INNER JOIN では l_table.key1 と r_table.key1 が等しいため、このシナリオは左側での GROUP BY を伴う INNER JOIN と等価です。
LEFT OUTER JOIN、左側での GROUP BY
l_table LEFT OUTER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY l_table.key1左側テーブルからハッシュテーブルを構築し、左側データに対して集計関数を実行します。右側集約用に繰り返し回数を使用します。
右側テーブルでハッシュテーブルを検索します。マッチしたエントリについては繰り返し回数を増加させ、右側の集計関数を実行します。マッチしなかった右側の行は破棄されます。
結合完了後、マッチしたエントリを出力します。マッチしなかったハッシュテーブルエントリは、右側の集計関数の入力として NULL を指定した別グループに収集されます。
LEFT OUTER JOIN、右側での GROUP BY
l_table LEFT OUTER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY r_table.key1左側テーブルからハッシュテーブルを構築し、左側データに対して集計関数を実行します。右側集約用に繰り返し回数を使用します。
右側テーブルでハッシュテーブルを検索します。マッチしたエントリについては繰り返し回数を増加させ、右側の集計関数を実行します。マッチしなかった右側の行は破棄されます。
結合完了後、マッチしたエントリを出力します。マッチしなかったエントリは、右側の集計関数の入力として NULL を指定した別グループに収集されます。
RIGHT OUTER JOIN、左側での GROUP BY
l_table RIGHT OUTER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY l_table.key1左側テーブルからハッシュテーブルを構築し、左側データに対して集計関数を実行します。右側集約用に繰り返し回数を使用します。
右側テーブルでハッシュテーブルを検索します。マッチしたエントリについては繰り返し回数を増加させ、右側の集計関数を実行します。マッチしなかった右側の全行は、左側の集計関数の結果として NULL を指定した別グループに収集されます。
結合完了後、マッチしたハッシュテーブルエントリを出力します。マッチしなかったエントリは破棄されます。
RIGHT OUTER JOIN、右側での GROUP BY
l_table RIGHT OUTER JOIN r_table
ON l_table.key1 = r_table.key1
GROUP BY r_table.key1r_table.key1 は一意である必要があります。一意性が保証できない場合は、オプティマイザーを使用して JOIN を LEFT OUTER JOIN に変換するか、GROUP BY を左側に変更してください。左側テーブルからハッシュテーブルを構築し、左側データに対して集計関数を実行します。右側集約用に繰り返し回数を使用します。
右側テーブルでハッシュテーブルを検索します。マッチしたエントリについては、左側および右側の集計結果の両方を出力します。マッチしなかった右側の行については、左側の集計関数の結果として NULL 値を出力します。
結合完了後、GroupJoin の処理は終了します。これ以降、ハッシュテーブルエントリの管理は不要です。
データのスピル
GroupJoin は、パーティションベースの HASH JOIN および HASH GROUP BY 実行と同様の方法でデータのスピルを処理します:
GroupJoin はパーティションベースのアプローチを使用します。
ビルドフェーズでは、一部のパーティションがメモリ内に保持され、他のパーティションはディスク上の一時ファイルにスピルされます。これらのパーティションへの増分書き込みも、対応する一時ファイルにスピルされます。プローブフェーズで非マッチの右側行を効率的にフィルタリングするために、各ディスク上パーティションごとにブロームフィルターが作成されます。
プローブフェーズでは、まずメモリ内のパーティションを上記のロジックで検索します。ディスク上のパーティションについては、ブロームフィルターで照会データが該当パーティションに属するかをチェックします。属する場合は、データが対応する一時ファイルにスピルされます。属さない場合は、データは除外または出力されます。
メモリ内のパーティションの処理が完了した後、システムはディスク上のパーティションを順次処理します。少なくとも 1 つのパーティションは、サブパーティションに分割せずにディスク上に収容可能であり、同じ結合および集約ロジックが適用されます。
関連論文
GroupJoin の PolarDB における理論的・実践的基盤を提供する学術論文が 2 編あります。
論文 1(2011 年): *Accelerating Queries with Group-By and Join by Groupjoin*
この論文は、さまざまなクエリタイプにわたる GroupJoin の理論的実現可能性を確立しています。適用可能なシナリオおよび集計関数の制約について記述されていますが、内容は抽象的であり、実装に関する詳細はほとんどありません。
論文 2(2021 年): *A Practical Approach to Groupjoin and Nested Aggregates*
ミュンヘン大学ルートヴィヒ・マクシミリアン大学の HyPer データベースチームによる論文で、インメモリデータベースにおける効率的な GroupJoin 実装について述べています。主な貢献点は以下のとおりです:
1. 相関サブクエリの非相関化における GroupJoin

相関サブクエリに GROUP BY が含まれる場合、MagicSet によりテーブルの重複排除が可能となり、サブクエリの非相関化のために JOIN + GROUP BY の組み合わせを導入できます。これにより GroupJoin の適用が可能となります。PolarDB は、IMCI 有効化操作において同様の非相関化をサポートしていますが、共有子オブジェクトを含む実行計画はまだ生成されていません。
2. eager aggregation
ハッシュテーブル構築時に左側データを集約し、後続の集約のために各エントリにペイロードを保持するのではなく、即時集約を行います。PolarDB は、IMCI 有効化操作において eager aggregation をサポートしています。
3. 同時ハッシュテーブル競合へのメモ化
極端なケースでは、プローブ時にすべての右側行が同一のハッシュテーブルエントリにマッチすることがあります。SUM() のような集計関数を同一エントリに対して繰り返し実行すると、競合(パフォーマンスボトルネック)が発生します。これは、アトミック加算操作であっても発生します。
論文の解決策:各エントリに対して CAS(Compare-And-Swap)コマンドを実行し、所有者スレッドを割り当てます。所有権の取得に失敗したスレッドは、自身の計算用にローカルハッシュテーブルを作成し、その後グローバルハッシュテーブルに結果をマージします。
4. GroupJoin が最適でない場合
左側の選択性が低い場合(プローブ後にほとんどの左側行がフィルターで除外される場合)、以下のようなトレードオフが生じます:
eager aggregation 使用時: 保持するペイロードがないためメモリ使用量が節約されますが、選択されない行に対する事前集計作業の多くが無駄になります。
eager aggregation 非使用時: メモリ使用量が増加し、eager aggregation の利点が相殺されます。
論文では、選択性が低い場合、集約を延期することを推奨しています。すなわち、結合完了後に、小さな結果グループに対して HASH GROUP BY を使用してローカル集約を行い、最適な戦略を選択するために、オプティマイザーが選択性およびカーディナリティを推定する必要があります。
PolarDB の立場(このトレードオフについて)
PolarDB の専門家は、論文の結論のうち 2 つの点に関して異議を唱えています:
スループット測定: 論文ではタプル/秒を用いて測定しています。PolarDB の実験では、同じ 1 GB のデータセットを用いて異なる結果が得られており、これは実装の違いによるものです。PolarDB のデータによると、RIGHT OUTER JOIN + GROUP BY RIGHT の組み合わせを除き、GroupJoin は一貫してスループットを向上させています:
IMCI 有効化シナリオでは、Q17 に対して GroupJoin は適用されません。
クエリ HASH JOIN + HASH GROUP BY GroupJoin Q3 130 MB 152 MB Q13 11 MB 33 MB Q18 315 MB 1 GB メモ化の有効性: PolarDB の実験では、TPC-H クエリにおいてハッシュテーブルエントリの競合は最小限であることが示されています。メモ化によって作成されるローカルハッシュテーブルは実際にはほとんど必要とされず、そのためメモ化のパフォーマンスは PolarDB の環境において HASH JOIN + HASH GROUP BY とほぼ同等です。論文の比較では、メモ化に明確な優位性は示されていません。
PolarDB の IMCI 有効化操作では、この選択性のトレードオフはそれほど重要ではありません。PolarDB は通常、小さいテーブルを用いてハッシュテーブルを構築します。選択性が低く eager aggregation を使用する場合、コストは小さいテーブルでの無駄な処理時間であり、軽微なペナルティにすぎません。そのため、PolarDB の IMCI シナリオでは、GroupJoin はほぼ常に HASH JOIN + HASH GROUP BY よりも優先されます。
TPC-H クエリにおける GroupJoin
TPC-H は、分析処理(AP)能力を評価する標準ベンチマークです。GroupJoin はその 22 クエリの多くに適用可能ですが、ほとんどのクエリでは GroupJoin を有効化するためにクエリの修正が必要です。
Q13
Q13 は修正なしで GroupJoin を直接適用できます:
SELECT
c_count,
count(*) AS custdist
FROM
(
SELECT
c_custkey,
count(o_orderkey) AS c_count
FROM
customer
LEFT OUTER JOIN orders ON c_custkey = o_custkey
AND o_comment NOT LIKE '%pending%deposits%'
GROUP BY
c_custkey
) c_orders
GROUP BY
c_count
ORDER BY
custdist DESC,
c_count DESC;GroupJoin を使用しない実行計画:

GroupJoin を使用した実行計画:

Q3
Q3 は GroupJoin を使用する前に GROUP BY の書き換えが必要です:
SELECT
l_orderkey,
sum(l_extendedprice * (1 - l_discount)) AS revenue,
o_orderdate,
o_shippriority
FROM
customer,
orders,
lineitem
WHERE
c_mktsegment = 'BUILDING'
AND c_custkey = o_custkey
AND l_orderkey = o_orderkey
AND o_orderdate < DATE '1995-03-15'
AND l_shipdate > DATE '1995-03-15'
GROUP BY
l_orderkey,
o_orderdate,
o_shippriority
ORDER BY
revenue DESC,
o_orderdate
LIMIT 10;IMCI における元の実行計画:

GROUP BY キー(l_orderkey、o_orderdate、o_shippriority)は、すべての結合キーと異なり、GroupJoin は直接適用できません。以下の関数従属性の連鎖により、書き換えが可能になります:
結合述語
l_orderkey = o_orderkeyにより、任意の結果行においてl_orderkeyとo_orderkeyは等しくなります。したがって、
GROUP BY l_orderkey, o_orderdate, o_shippriorityはGROUP BY o_orderkey, o_orderdate, o_shippriorityと等価です。o_orderkeyはordersテーブルのプライマリキーであるため、o_orderdateおよびo_shippriorityはo_orderkeyに対して関数従属性を有します。したがって、
GROUP BY o_orderkey, o_orderdate, o_shippriorityはGROUP BY o_orderkeyに簡約されます。
GROUP BY 句を GROUP BY o_orderkey に置き換えることで、GroupJoin を有効化できます:

MySQL オプティマイザーは関数従属性を部分的に導出できますが、GROUP BY o_orderkey への完全な簡約は自動的には行えません。SQL Server では理論上完全な導出が可能ですが、IMCI 有効化環境における実証はまだ行われていません。この関数従属性の導出は、Q3、Q4、Q10、Q13、Q18、Q20、Q21 にも同様に適用されます。オプティマイザーが完全な導出を行える場合、GROUP BY キーが短縮され、集約処理が高速化されます。
Q10
Q10 は GroupJoin を使用する前に 2 つの修正が必要です:
SELECT
c_custkey,
c_name,
sum(l_extendedprice * (1 - l_discount)) AS revenue,
c_acctbal,
n_name,
c_address,
c_phone,
c_comment
FROM
customer,
orders,
lineitem,
nation
WHERE
c_custkey = o_custkey
AND l_orderkey = o_orderkey
AND o_orderdate >= DATE '1993-10-01'
AND o_orderdate < DATE '1993-10-01' + INTERVAL '3' MONTH
AND l_returnflag = 'R'
AND c_nationkey = n_nationkey
GROUP BY
c_custkey,
c_name,
c_acctbal,
c_phone,
n_name,
c_address,
c_comment
ORDER BY
revenue DESC
LIMIT 20;Q3 と同じ関数従属性の推論を用いて、集約キーを
c_custkey(customerテーブルのプライマリキー)に置き換えます。結合順序を調整し、
customerテーブルの結合が最も外側になるようにします。
最初の修正は常にパフォーマンスを向上させます。2 番目の修正は、場合によって副作用を引き起こすことがあります。
Q17
Q17 には相関サブクエリが含まれています:
SELECT
sum(l_extendedprice) / 7.0 AS avg_yearly
FROM
lineitem,
part
WHERE
p_partkey = l_partkey
AND p_brand = 'Brand#44'
AND p_container = 'WRAP PKG'
AND l_quantity < (
SELECT
0.2 * avg(l_quantity)
FROM
lineitem
WHERE
l_partkey = p_partkey
);2 つの非相関化アルゴリズムにより、GroupJoin を使用しない実行計画が生成されます:


非相関化に MagicSet を使用すると、GroupJoin と互換性のある中間状態が導入されます:

このアプローチは、論文 2 の図 3 にも示されています:

PolarDB は、IMCI 有効化シナリオにおいて MagicSet を非相関化手法として部分的にサポートしていますが、共有子オブジェクトを含む実行計画はまだ生成されていません。その結果、IMCI 有効化シナリオでは Q17 に対して GroupJoin を使用できません。
Q18
Q18 は GROUP BY の簡略化後に GroupJoin を適用できます。以下の例では、IN サブクエリおよび ORDER BY 句を明確化のため省略しています:
SELECT
c_name,
c_custkey,
o_orderkey,
o_orderdate,
o_totalprice,
sum(l_quantity)
FROM
customer,
orders,
lineitem
WHERE
c_custkey = o_custkey
AND o_orderkey = l_orderkey
GROUP BY
c_name,
c_custkey,
o_orderkey,
o_orderdate,
o_totalprice以下の簡約を適用します:
c_custkeyはcustomerのプライマリキーであるため、c_nameはそれに関数従属性を有します。o_orderkeyはordersのプライマリキーであるため、o_orderdateおよびo_totalpriceはそれに関数従属性を有します。したがって、GROUP BY c_name, c_custkey, o_orderkey, o_orderdate, o_totalpriceはGROUP BY c_custkey, o_orderkeyに簡約されます。結合述語
c_custkey = o_custkeyにより、結果にはc_custkey = o_custkeyとなる行のみが含まれます。したがって、GROUP BY c_custkey, o_orderkeyはGROUP BY o_custkey, o_orderkeyに簡約されます。o_orderkeyがo_custkeyを一意に決定するため、GROUP BY o_custkey, o_orderkeyはGROUP BY o_orderkeyに簡約されます。
書き換えられたクエリ:
SELECT
ANY_VALUE(c_name),
ANY_VALUE(c_custkey),
o_orderkey,
ANY_VALUE(o_orderdate),
ANY_VALUE(o_totalprice),
sum(l_quantity)
FROM
customer,
orders,
lineitem
WHERE
c_custkey = o_custkey
AND o_orderkey = l_orderkey
GROUP BY
o_orderkeyGroupJoin を使用しない実行計画:

GroupJoin を使用した実行計画:

GROUP BY キーの短縮は、GroupJoin を使用しない通常の実行計画に対してもメリットがあります。
Q20
Q20 には Q17 と同様の相関サブクエリが含まれています。MagicSet 非相関化は、MagicSet が除去される前に GroupJoin と互換性のある中間状態を導入します:
...
AND ps_availqty > (
SELECT
0.5 * sum(l_quantity) /* スカラー集約 */
FROM
lineitem
WHERE
l_partkey = ps_partkey /* 相関述語 1 */
AND l_suppkey = ps_suppkey /* 相関述語 2 */
AND l_shipdate >= '1993-01-01'
AND l_shipdate < DATE_ADD('1993-01-01', INTERVAL '1' YEAR)
)その他のクエリ
論文 1 および論文 2 によると、Q5、Q9、Q16、Q21 についても修正後に GroupJoin が適用可能ですが、これらの論文では具体的な修正手順は記述されていません。これらのクエリに対する GroupJoin の実行計画は、HyPer WebInterface(https://hyper-db.de/interface.html#)でも利用できません。
クエリパフォーマンス
多くの TPC-H クエリは JOIN と GROUP BY を組み合わせており、GroupJoin の最適化対象となります。
論文 1 では、1 GB のデータを用いて Q3、Q5、Q9、Q10、Q13、Q16、Q17、Q20、Q21 のクエリパフォーマンスを比較しました。GroupJoin を使用することで、これらのクエリの合計遅延が 1,932 ms から 1,295 ms に短縮されました。

論文 2 では、1 GB のデータを用いて Q3、Q13、Q17、Q18 のパフォーマンスを 3 つの戦略で比較しました:
Separate(分離) — 標準の JOIN の後に GROUP BY を実行
Eager(即時) — eager aggregation を使用した GroupJoin
Memoizing(メモ化) — メモ化最適化を適用した GroupJoin

論文の折れ線グラフでは、Q3、Q13、Q17、Q18 のいずれにおいても以下の 2 つのパターンが見られます:
メモ化のパフォーマンスは、HASH JOIN + HASH GROUP BY とほぼ同等です。
eager aggregation は、Q13 のみで他の手法より優れたパフォーマンスを発揮します。
これらの結果は、最適な GroupJoin 戦略がクエリの特性に依存し、オプティマイザーが選択性およびカーディナリティの推定に基づいて実行計画を選択すべきであるという論文の結論を裏付けています。
PolarDB の実験では、異なる結論が得られています。同じ 1 GB のデータセットを用いた実験において、PolarDB では GroupJoin が一貫してスループットを向上させていること(関連論文 の表を参照)、および TPC-H クエリにおける競合が最小限であることが示されています。メモ化によって作成されるローカルハッシュテーブルはほとんど必要とされず、そのためメモ化のパフォーマンスは PolarDB の実装において HASH JOIN + HASH GROUP BY と同等です。
まとめ
GroupJoin は、HASH JOIN および HASH GROUP BY を単一のオペレーターに統合することで、冗長な処理を排除します。PolarDB for MySQL の IMCI 有効化操作では、RIGHT OUTER JOIN と右側での GROUP BY を組み合わせた場合を除き、一貫してクエリパフォーマンスを向上させます。
GroupJoin は普遍的に適用できるわけではありません。EQUAL JOIN + GROUP BY(片側のキーが一致)であること、集計関数に対する制約、および特定の実装条件を満たす必要があります。これらの要件により、GroupJoin の実行計画は適用範囲が狭く、保守が複雑になります。
GroupJoin の恩恵を最大化するには、TPC-H の例で示された GROUP BY キーの関数従属性による簡略化など、一般的なクエリパターンをカバーするよう、実行計画を一般化する必要があります。