テーブル結合は JOIN と FROM どちらで行うべきか。両者の違いと注意点

この記事は個人ブログと同じ内容です

www.ritolab.com


JOIN での結合式と、FROM での結合式

テーブル結合といえば JOIN 句が一般的ですが、FROM 句に複数テーブルを指定することでもテーブルを結合できます。例えば、以下 2 つのクエリは全く同じ結果を返します。

SELECT
  *
FROM books
INNER JOIN author_book ON books.id = author_book.book_id
INNER JOIN authors ON author_book.author_id = authors.id
;
SELECT
  *
FROM
  books,
  author_book,
  authors
WHERE books.id = author_book.book_id
  AND author_book.author_id = authors.id
;

リレーショナルデータベースにおいて上記のようなテーブル結合式は、製品に関わらず広く実装されていますが、例えばこれらは、MySQLオープンソース化された ver 3.23(2001年)の時点で、両者とも存在する伝統的な結合式です。

では、両者の違いはどこにあるのでしょうか?

(以下、クエリの実行は MySQL 8.0 です)

FROM 句での結合は用途が限定的

FROM 句に複数テーブルを指定して結合する場合、前提はデカルト積になります。つまり、クロス結合です。

SELECT id, name FROM tbl_a;
+----+--------------+
| id | name         |
+----+--------------+
|  1 | tbl a name 1 |
|  2 | tbl a name 2 |
|  3 | tbl a name 3 |
|  4 | tbl a name 4 |
|  5 | tbl a name 5 |
+----+--------------+

SELECT id, tbl_a_id, name FROM tbl_c;
+----+----------+--------------+
| id | tbl_a_id | name         |
+----+----------+--------------+
|  1 |        1 | tbl c name 1 |
|  2 |        1 | tbl c name 2 |
|  3 |        1 | tbl c name 3 |
|  4 |        1 | tbl c name 4 |
|  5 |        1 | tbl c name 5 |
+----+----------+--------------+

-- FROM 句でのテーブル結合
SELECT 
   tbl_a.id, 
   tbl_a.name,
   tbl_c.tbl_a_id,
   tbl_c.id, 
   tbl_c.name 
FROM 
   tbl_a, 
   tbl_c 
ORDER BY tbl_a.id, tbl_c.id;
+----+--------------+----------+----+--------------+
| id | name         | tbl_a_id | id | name         |
+----+--------------+----------+----+--------------+
|  1 | tbl a name 1 |        1 |  1 | tbl c name 1 |
|  1 | tbl a name 1 |        1 |  2 | tbl c name 2 |
|  1 | tbl a name 1 |        1 |  3 | tbl c name 3 |
|  1 | tbl a name 1 |        1 |  4 | tbl c name 4 |
|  1 | tbl a name 1 |        1 |  5 | tbl c name 5 |
|  2 | tbl a name 2 |        1 |  1 | tbl c name 1 |
|  2 | tbl a name 2 |        1 |  2 | tbl c name 2 |
|  2 | tbl a name 2 |        1 |  3 | tbl c name 3 |
|  2 | tbl a name 2 |        1 |  4 | tbl c name 4 |
|  2 | tbl a name 2 |        1 |  5 | tbl c name 5 |
|  3 | tbl a name 3 |        1 |  1 | tbl c name 1 |
|  3 | tbl a name 3 |        1 |  2 | tbl c name 2 |
|  3 | tbl a name 3 |        1 |  3 | tbl c name 3 |
|  3 | tbl a name 3 |        1 |  4 | tbl c name 4 |
|  3 | tbl a name 3 |        1 |  5 | tbl c name 5 |
|  4 | tbl a name 4 |        1 |  1 | tbl c name 1 |
|  4 | tbl a name 4 |        1 |  2 | tbl c name 2 |
|  4 | tbl a name 4 |        1 |  3 | tbl c name 3 |
|  4 | tbl a name 4 |        1 |  4 | tbl c name 4 |
|  4 | tbl a name 4 |        1 |  5 | tbl c name 5 |
|  5 | tbl a name 5 |        1 |  1 | tbl c name 1 |
|  5 | tbl a name 5 |        1 |  2 | tbl c name 2 |
|  5 | tbl a name 5 |        1 |  3 | tbl c name 3 |
|  5 | tbl a name 5 |        1 |  4 | tbl c name 4 |
|  5 | tbl a name 5 |        1 |  5 | tbl c name 5 |
+----+--------------+----------+----+--------------+

ここから更に、WHERE 句を指定することで、内部結合の状態を作り出します。

SELECT 
   tbl_a.id, 
   tbl_a.name,
   tbl_c.tbl_a_id,
   tbl_c.id, 
   tbl_c.name 
FROM 
   tbl_a, 
   tbl_c 
WHERE tbl_a.id = tbl_c.tbl_a_id
ORDER BY tbl_a.id, tbl_c.id;
+----+--------------+----------+----+--------------+
| id | name         | tbl_a_id | id | name         |
+----+--------------+----------+----+--------------+
|  1 | tbl a name 1 |        1 |  1 | tbl c name 1 |
|  1 | tbl a name 1 |        1 |  2 | tbl c name 2 |
|  1 | tbl a name 1 |        1 |  3 | tbl c name 3 |
|  1 | tbl a name 1 |        1 |  4 | tbl c name 4 |
|  1 | tbl a name 1 |        1 |  5 | tbl c name 5 |
+----+--------------+----------+----+--------------+

このように、FROM 句に複数テーブルを指定してテーブルを結合させるのは、クロス結合、ないし内部結合が前提となります。外部結合はできません。

JOIN 句は結合職人

JOIN 句を用いたテーブル結合は、内部結合、外部結合、そしてクロス結合と柔軟に結合式を組み立てられます。まさにテーブル結合のためのセンテンスです。

INNER JOIN tbl_b ON ... 
                     
LEFT JOIN tbl_b ON ...
                    
RIGHT JOIN tbl_b ON ...
                     
CROSS JOIN tbl_b

パフォーマンス比較

では、どちらの方がパフォーマンスが良いのでしょうか。

テーブルとデータを用意しました。本と出版の世界の一部を表現しています。

books テーブルには 100 万件のデータを収録し、あとはそれぞれ、1 対 1 または多対多のリレーションです。

books_count authors_count publishers_count managers_count author_book_count book_manager_count
1000000 100000 500 5000 1000000 1000000

実行クエリはそれぞれ以下になります。

-- JOIN 句で結合
SELECT
*
FROM books b
INNER JOIN author_book ab ON b.id=ab.book_id
INNER JOIN authors a ON ab.author_id=a.id
INNER JOIN publishers p on b.publisher_id=p.id
INNER JOIN book_manager bm ON b.id=bm.manager_id
INNER JOIN managers m ON bm.manager_id=m.id
;

-- FROM 句で結合
SELECT
  *
FROM
  books b,
  author_book ab,
  authors a,
  publishers p,
  book_manager bm,
  managers m
WHERE b.id=ab.book_id
  AND ab.author_id=a.id
  AND b.publisher_id=p.id
  AND b.id=bm.manager_id
  AND bm.manager_id=m.id
;

Explain

双方の実行計画を確認しましたが、両方とも全く同じ結果になりました。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE managers NULL ALL PRIMARY NULL NULL NULL 4845 100.00 NULL
1 SIMPLE books NULL eq_ref PRIMARY,books_publisher_id_foreign PRIMARY 8 sample.managers.id 1 100.00 NULL
1 SIMPLE publishers NULL eq_ref PRIMARY PRIMARY 8 sample.books.publisher_id 1 100.00 NULL
1 SIMPLE author_book NULL ref author_book_author_id_foreign,author_book_book_id_foreign author_book_book_id_foreign 8 sample.managers.id 1 100.00 NULL
1 SIMPLE authors NULL eq_ref PRIMARY PRIMARY 8 sample.author_book.author_id 1 100.00 NULL
1 SIMPLE book_manager NULL ref book_manager_manager_id_foreign book_manager_manager_id_foreign 8 sample.managers.id 190 100.00 NULL

JOIN 句は結合として明示的に意図を示せる句ですが、FROM 句に複数テーブルを指定した場合でも、結合として JOIN 句と同等に処理されることが確認できます。

実行時間

実行計画が同じではあったものの、念のため両者の実行時間を見てみます。

それぞれのクエリを 10 回実行し、その実行時間を確認します。

試行回数 JOIN: Execution time JOIN: Table lock wait time FROM: Execution time FROM: Table lock wait time
1 3.84146363 0.000132 3.70469371 0.000019
2 3.86136084 0.00015 3.85744995 0.000007
3 3.78419258 0.000018 3.77823229 0.000034
4 3.87785846 0.000184 3.80834454 0.00011
5 3.78526071 0.000016 3.81698154 0.000121
6 3.83468558 0.000057 3.79614567 0.000031
7 4.27876295 0.000013 3.76076388 0.000011
8 4.43237696 0.000016 3.82177008 0.000158
9 3.82524521 0.000123 3.98029196 0.000102
10 3.76645421 0.000033 3.88980984 0.000116
---------- ---------- ---------- ---------- ----------
AVERAGE 3.928766113 0.0000742 3.821448346 0.0000709

実行時間もほとんど差がないといえそうです。

どちらを使うべきか

パフォーマンスには差がないため、結果を抽出するということだけ考えたらどちらを使っても良いということになりますが、テーブル結合を行うならば、JOIN 句を使用しておくのが安全であると考えます。

なぜならば、FROM に複数のを指定した結合は、以下、可読性の面からヒューマンエラーが起こりやすいと感じているからです。

うっかりクロス結合

FROM 句で結合を行う際に最も注意しなければならないのは、結合条件の記述漏れです。

例えば、JOIN 句で結合を行う際は、JOIN 句に対して ON 句を記述することで、各 JOIN に対する結合条件を 1 行ずつ記述していくことができます。これは各テーブルの結合とその結合条件がセットで確認出来るため、条件やそれの指定漏れの確認も行いやすいです。

INNER JOIN author_book ab  ON b.id = author_book.book_id -- 結合テーブルと結合条件がセットで記述できる
INNER JOIN authors a       ON ab.author_id = a.id
INNER JOIN publishers p    ON b.publisher_id = p.id
INNER JOIN book_manager bm ON b.id = bm.manager_id
INNER JOIN managers m      ON bm.manager_id = m.id

対して、FROM 句で結合を行う際は、WHERE 句にて結合条件を記述していくことになります。

FROM
  -- 結合テーブル
  books b,
  author_book ab,
  authors a,
  publishers p,
  book_manager bm,
  managers m
WHERE b.id = ab.book_id     -- books, author_book の結合条件
  AND ab.author_id = a.id   -- author_book, authors の結合条件
  AND b.publisher_id = p.id -- books, publishers の結合条件
  AND b.id = bm.manager_id  -- books, book_manager の結合条件
  AND bm.manager_id = m.id  -- book_manager, managers の結合条件

この、結合したいテーブルと結合条件がセットにならない状況は、結合テーブルが増えた際に結合条件の記述漏れを引き起こす可能性が JOIN 句よりもかなり高いといえます。

前述の通り、結合条件を指定しない場合はクロス結合となるため、例えば今回用意したデータであれば、books, author_book の結合条件が未記述の場合、100 万件 × 100 万件 = 1 兆レコードの結果を返そうとすることになり、レコードの多いテーブルではなかなかリスクがあるなといえます。

(ちなみに JOIN 句であっても、結合条件を指定しなければクロス結合になるので注意)

不明瞭な境界線

JOIN 句では、結合後の絞り込みは、例えば以下のように記述するでしょう。

SELECT
    *
FROM tbl_a
INNER JOIN tbl_b ON tabl_a.id = tbl_b.tabl_a_id
WHERE tbl_b.score > 5 -- 結合後の絞り込み

結合テーブルとその条件があり、結合したデータに対して WHERE 句で絞り込みを行っています。

ON で使用される search_condition は、WHERE 句で使用できるフォームの条件式です。 通常、ON 句はテーブルの結合方法を指定する条件に使用され、WHERE 句は結果セットに含める行を制限します。

JOIN 句 - MySQL 8.0 リファレンスマニュアル

この表現を FROM 句での結合で行う場合は、以下のようなクエリになります。

SELECT
    *
FROM 
    tbl_a,
    tbl_b
WHERE tabl_a.id = tbl_b.tabl_a_id -- 結合条件
  AND tbl_b.score > 5  -- 結合後の絞り込み

結合条件と、結合後の絞り込みが同じ WHERE 句に混在することになります。シンプルなクエリなら良いかもしれませんが、結合数が多くなると可読性は一気に下がるはずです。

結合表現の一貫性

冒頭で述べた通り、FROM 句に複数テーブルを指定して結合する場合、クロス結合ないし内部結合が前提です。

外部結合のときだけ JOIN 句を用いるくらいなら、内部結合もクロス結合も JOIN を使っておけば、テーブル結合の表現は全て JOIN 句で統一されるため、メンテナンスもしやすいはずです。

SQL ステートメントは、意図の分かる作文でありたい。

どちらを使うにせよ、業務で SQL 文を作成するのであれば、欲しい結果を得る為だけでなく、意図が明確に伝わるようにクエリを組み立てていく意識は持ちたいものです。

いつかそのクエリをメンテナンスするであろう、チームの誰か、そして自分のために。

SQL 文は、パフォーマンスを大切にしながらも、生み出す自身の意図が簡単かつ適切に相手に伝わる作文でありたいですね。

参考:


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers