JOINクエリと複数クエリの使い分けるポイントを探ってみた

JOINクエリと複数クエリの使い分けるポイントを探ってみた

データベース設計や運用において、テーブル同士を結合させるJOINクエリは欠かせません。特に、複数のテーブルから情報をまとめて取得できる利便性は大きなメリットといえます。

一方で、「別テーブルのデータが必要になるたびに安易に LEFT JOIN を増やしていたら、明らかにパフォーマンスが悪化してきた」というケースに直面し、複数のクエリに分割して取得する方法が有効であるという情報を得たため、実際に検証してみました!

背景と課題

LEFT JOINを多用したクエリを運用していました。しかし課題をいくつか浮き彫りになってきました。

  • テーブル結合が増えるほどクエリが複雑化し、取得結果も膨大になりがち
  • 実行速度が明らかに低下しており、アプリケーションのレスポンスにも影響していた

そこで、何か手軽にできる改善策はないか模索していたところ、「複数のクエリを組み合わせる方が速いケースもある」という情報を得たので検証してみることにしました。

検証のアプローチ

検証対象

ECサイトの受注管理(顧客・注文・配送・支払・割引コードを扱う)を想定したテーブルを用意しました。

データは下記のように用意しました。

  • customers: Alice(ID=1), Bob(ID=2)
  • orders: 5件 (Alice=3件, Bob=2件)
  • shipping_info: 3件 (注文ID=1,2,5)
  • payments: 4件 (注文ID=1,2,3,4)
  • discount_codes: 2件 (注文ID=2,4)

検証すること

下記のアプローチで「ある顧客(customer_id=1)の注文 + 配送・支払・割引情報」を取得する場合のそれぞれのパフォーマンス計測を行います。

  • LEFT JOINを多用した単一クエリ
    SELECT
        c.customer_name,
        o.order_id,
        o.order_date,
        o.total_amount,
        s.shipping_address,
        s.shipping_date,
        p.payment_method,
        p.payment_date,
        p.status AS payment_status,
        d.code AS discount_code,
        d.discount_amount
    FROM orders o
    JOIN customers c
        ON c.customer_id = o.customer_id
    LEFT JOIN shipping_info s
        ON s.order_id = o.order_id
    LEFT JOIN payments p
        ON p.order_id = o.order_id
    LEFT JOIN discount_codes d
        ON d.order_id = o.order_id
    WHERE c.customer_id = 1
    ORDER BY o.order_id;
    
  • 複数クエリに分けて必要な情報を段階的に取得
    -- 顧客ID=1の顧客情報
    SELECT * FROM customers WHERE customer_id = 1;
    
    -- 顧客ID=1の注文情報
    SELECT * FROM orders WHERE customer_id = 1;
    
    -- 取得した orders の order_id (1,2,3) に対して
    SELECT * FROM shipping_info   WHERE order_id IN (1,2,3);
    SELECT * FROM payments        WHERE order_id IN (1,2,3);
    SELECT * FROM discount_codes  WHERE order_id IN (1,2,3);
    

テスト環境と計測方法

  • テスト環境: PostgreSQL
  • クエリの実行時間: EXPLAIN ANALYZE などで計測
  • 比較指標:
    • Planning Time(DBがクエリの実行計画を立てる時間)
    • Execution Time(実際のクエリ実行時間)

実測データ

検証の一例を紹介します。以下は同じ結果を返す2パターンのクエリをそれぞれ実行した際の計測値です。

パターン

Planning Time

Execution Time

単一クエリ

1.252 ms

0.439 ms

複数クエリ

0.732 ms

0.093 ms

  • 単一クエリ(LEFT JOINを含む複雑なクエリ)では、Planning Time/Execution Time ともに大きめ。
  • 複数クエリの場合は、複数回クエリを発行するにもかかわらず、合計の実行時間が短い結果となった。

もちろん、これはクエリの内容やデータ構造によって左右されるため、すべてのケースで「複数クエリが速い」とは限りません。しかし、LEFT JOINでテーブルを多重に結合していた部分を複数クエリに分解してみるだけで、一定のパフォーマンス向上が得られる可能性が示唆されます。

さらに、JOINの仕方によっては結果セットが爆発的に増え、パフォーマンスが大きく変化するケースも報告されています。以下は、Frank氏が実施したベンチマークの抜粋です。

For inner joins, a single query makes sense, since you only get matching rows. For left joins, multiple queries is much better... look at the following benchmark I did:

Single query with 5 Joins

  • query: 8.074508 seconds
  • result size: 2268000

5 queries in a row

  • combined query time: 0.00262 seconds
  • result size: 165 (6 + 50 + 7 + 12 + 90)

Note that we get the same results in both cases (6 x 50 x 7 x 12 x 90 = 2268000)

left joins use exponentially more memory with redundant data.

The memory limit might not be as bad if you only do a join of two tables, but generally three or more and it becomes worth different queries.

As a side note, my MySQL server is right beside my application server... so connection time is negligible. If your connection time is in the seconds, then maybe there is a benefit

— Frank

参考:JOIN queries vs multiple queries

このように、多段のLEFT JOIN を一度に実行すると、行の組み合わせ(カルテシアン積)が膨大になり、結果セットが「2,268,000行」など指数的に膨らむ場合があります。対して、複数クエリで各テーブルの必要行を分割して取得すれば、実際の件数(合計165行)にとどまり、処理時間も大幅に短くなるという例です。極端な例ではありますが、この例からも

JOINクエリ vs. 複数クエリ ─ メリットとデメリット

以上を経て僕なりにそれぞれのメリット、デメリットをまとめてみました。

JOINクエリを使う場合

  • メリット
    • 一度のクエリ発行で関連データをすべて取得できる
    • 通信回数が少ない(アプリケーションとDB間の往復が1回で済む)
    • 集約(GROUP BY)やフィルタリング(WHERE)を一括で行えるため、ロジックをDB側に集約しやすい
  • デメリット
    • 多段のLEFT JOINで不要な行・重複行が大量に返る場合がある
    • クエリが複雑化するほど実行計画が複雑になり、最適化が難しくなる
    • 必要以上に多くの列をJOINで取ってしまい、転送データ量が増える可能性がある

複数クエリを使う場合

  • メリット
    • シンプルなクエリを複数回発行するため、個々のクエリは早く処理されることが多い
    • アプリ側でデータを組み立てるフローが、ステップバイステップで理解しやすい
    • 無駄な結合を省きやすく、本当に必要なデータだけをピンポイントで取得できる
  • デメリット
    • クエリ回数が増えるため、ネットワーク経由の場合は遅延が蓄積する可能性
    • アプリ側でデータを突合・加工するコードを書く必要があり、実装コストが高まる
    • 多数のレコードに対して IN 句を多用する場合など、状況によっては逆に遅くなることもある

使い分けのポイント

データ量・結合関係

テーブル間に多対多の関係があり、LEFT JOIN を重ねると爆発的に行数が増える場合には、複数クエリの方が有利だと思います。

逆に、単純な1対Nの関係で、JOIN条件が明確にあり、かつ全ての結合結果が必要な場合はJOINクエリ一発の方が早いことが多いように思えます。

可読性・保守性

JOINクエリでロジックを集約すると、SQLの可読性が落ちる場合があるが、1箇所で完結しやすい。

複数クエリの場合、処理の流れは理解しやすい一方で、コードが分散しやすく、将来的な修正が煩雑になる可能性もあるように思えます。

AnycloudではloadRelations 関数という複数テーブルに分散している関連データ(リレーション)を、1回の呼び出しで一括ロードしてアプリ側のオブジェクトに紐づけるためのユーティリティを用意しており、複数クエリの煩雑さを軽減する工夫をしています。

まとめ

LEFT JOIN をむやみに増やしていたケースでは、複数クエリに分割するだけで大幅なパフォーマンス改善が期待できる場合があります。今回のテスト結果では、単一クエリより複数クエリの方が「Planning Time」「Execution Time」ともに短縮されました

ただし、これはあくまで具体的なデータ量や設計によるものであり、すべての状況で複数クエリが優位とは限りません。多テーブルJOINが合理的な場合などは、JOINでまとめた方が効率的なケースも多々あります。

重要なのは、各テーブル同士の関係や、実際に取得すべきデータの量・範囲をしっかり見極めること。 LEFT JOIN が増えているのに不要なカラムや重複行を大量に返していないか、複数クエリへの分割がアプリ側に過度な負担をかけていないかを定期的にチェックし、適切な改善を行うことが大事だと思います。

最後に

今回の記事で取り上げたように

  1. JOINクエリの多様によるパフォーマンス低下
  2. 複数クエリへの分割

という二つのアプローチの間には、明確なトレードオフがあります。今後のシステム開発やリファクタリングにおいては「どこまでJOINを使うか」「どこから複数クエリを活用するか」を判断し、ベストなパフォーマンスを引き出せるよう設計・実装を進めていきたいです。

この記事が、パフォーマンスチューニングに悩む皆さんの一助となれば幸いです!

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

やました

PdM

やました

Twitter

株式会社AnycloudでPdMをしています