2026年5月26日

2026年5月26日

MySQLのJOINの種類と使い分け|INNER・LEFT・RIGHT・CROSSの違いをWordPressのテーブルで理解する

MySQL の JOIN は、実務ではほぼ「INNER JOIN(両方にあるものだけ)」と「LEFT JOIN(左側は全部残す)」の二つで足ります。使い分けの基準は、「相手側に対応する行が無いとき、その行を結果に残したいかどうか」です。この記事では、WordPress のテーブルを例に各 JOIN の違いと書き方を示し、行が増える・LEFT JOIN が INNER JOIN のようになるといった、よくあるつまずきと確認方法を説明します。

例に使う WordPress のテーブル

WordPress のデータベースは、JOIN を練習するのにちょうどよい構造をしています。この記事では次のテーブルを使います。テーブル名の接頭辞は標準の wp_ として書いていますが、wp-config.php の $table_prefix で変えている場合は読み替えてください。

  • wp_posts:投稿・固定ページなど。主キーは ID、投稿者は post_author。
  • wp_postmeta:投稿に付くカスタムフィールド。post_id で wp_posts.ID と結び付きます。1件の投稿に多数の行があります。
  • wp_users:ユーザー。ID が wp_posts.post_author に対応します。
  • wp_term_relationships・wp_term_taxonomy・wp_terms:カテゴリーやタグと投稿の関係。

ここで紹介する SQL は SELECT で中身を確認するためのものです。本番のデータベースで試す場合も、読み取りだけなら内容は変わりません。後半で触れる削除系の SQL は、バックアップを取ってステージング環境で試してから使ってください。

INNER JOIN:両方に対応する行があるものだけ

INNER JOIN は、結合条件に一致する行が両方のテーブルにあるものだけを返します。投稿と投稿者名を並べる例です。

SELECT p.ID, p.post_title, u.display_name
FROM wp_posts AS p
INNER JOIN wp_users AS u ON u.ID = p.post_author
WHERE p.post_type = 'post'
  AND p.post_status = 'publish';

投稿者のユーザーが削除されて wp_users に存在しない投稿は、この結果に出てきません。「対応する相手がいないものは要らない」場合に使います。MySQL では JOIN とだけ書いても INNER JOIN と同じ意味になります。

テーブルを三つ以上つなぐ場合も、INNER JOIN を続けて書きます。カテゴリーのスラッグが news の公開済み投稿を探す例です。

SELECT p.ID, p.post_title
FROM wp_posts AS p
INNER JOIN wp_term_relationships AS tr ON tr.object_id = p.ID
INNER JOIN wp_term_taxonomy AS tt
        ON tt.term_taxonomy_id = tr.term_taxonomy_id
       AND tt.taxonomy = 'category'
INNER JOIN wp_terms AS t ON t.term_id = tt.term_id
WHERE t.slug = 'news'
  AND p.post_type = 'post'
  AND p.post_status = 'publish';

wp_term_relationships は投稿と分類を結ぶ中間テーブルで、term_id ではなく term_taxonomy_id を持っている点がつまずきやすいところです。wp_terms の term_id と直接結び付けると、結果が合わなくなることがあります。

LEFT JOIN:左側のテーブルは全部残す

LEFT JOIN(LEFT OUTER JOIN)は、左側(FROM に書いたテーブル)の行をすべて残し、右側に対応する行が無ければその列を NULL にします。

アイキャッチ画像の有無を一覧にする例です。アイキャッチ画像は wp_postmeta の _thumbnail_id というキーに保存されています。

SELECT p.ID, p.post_title, pm.meta_value AS thumbnail_id
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS pm
       ON pm.post_id = p.ID
      AND pm.meta_key = '_thumbnail_id'
WHERE p.post_type = 'post'
  AND p.post_status = 'publish';

アイキャッチ画像が無い投稿も一覧に残り、thumbnail_id が NULL になります。ここで INNER JOIN を使うと、画像の無い投稿は結果から消えてしまいます。

「対応する行が無いもの」を探す

LEFT JOIN と IS NULL を組み合わせると、「相手が存在しない行」を探せます。上の例を変えて、アイキャッチ画像が設定されていない投稿だけを抜き出します。

SELECT p.ID, p.post_title
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS pm
       ON pm.post_id = p.ID
      AND pm.meta_key = '_thumbnail_id'
WHERE p.post_type = 'post'
  AND p.post_status = 'publish'
  AND pm.meta_id IS NULL;

同じ考え方で、削除された投稿のカスタムフィールドが残っている「孤立したデータ」も見つけられます。

SELECT COUNT(*)
FROM wp_postmeta AS pm
LEFT JOIN wp_posts AS p ON p.ID = pm.post_id
WHERE p.ID IS NULL;

同じ結果は NOT EXISTS でも書けます。どちらが読みやすいかで選んでよく、MySQL のバージョンによって実行計画が変わることもあるため、気になる場合は後述の EXPLAIN で比べます。

RIGHT JOIN と FULL OUTER JOIN

RIGHT JOIN は LEFT JOIN の左右を逆にしたもので、右側のテーブルをすべて残します。テーブルを書く順番を入れ替えれば LEFT JOIN で同じことができるため、実務では LEFT JOIN にそろえて書くことが多いです。一つの SQL に LEFT と RIGHT が混ざると、どちらのテーブルを基準にしているのか読み取りにくくなります。

両方のテーブルの行をすべて残す FULL OUTER JOIN は、MySQL には用意されていません(2026年10月現在)。必要な場合は、LEFT JOIN と RIGHT JOIN の結果を UNION でまとめて代わりにします。

SELECT a.id, b.id
FROM table_a AS a LEFT JOIN table_b AS b ON b.a_id = a.id
UNION
SELECT a.id, b.id
FROM table_a AS a RIGHT JOIN table_b AS b ON b.a_id = a.id;

CROSS JOIN と自己結合

CROSS JOIN は、結合条件なしで「左の全行 × 右の全行」の組み合わせを作ります。100行と100行なら1万行になるため、日付の一覧と店舗の一覧から全組み合わせの表を作る、といった目的がはっきりした場面以外では使いません。結合条件の書き忘れでも同じことが起きるので、思ったより結果が多いときはまず ON を確認してください。

同じテーブルを別名を付けて二回使う「自己結合」もよく使われます。固定ページとその親ページを並べる例です。

SELECT child.post_title AS page, parent.post_title AS parent_page
FROM wp_posts AS child
LEFT JOIN wp_posts AS parent ON parent.ID = child.post_parent
WHERE child.post_type = 'page'
  AND child.post_status = 'publish';

親を持たないページも残すために、ここでは LEFT JOIN を使っています。

よくあるつまずき

LEFT JOIN なのに行が減る

LEFT JOIN の右側のテーブルに対する条件を WHERE に書くと、右側が NULL の行が条件で落とされ、結果として INNER JOIN と同じになります。

-- 意図どおりにならない例:画像の無い投稿が消える
SELECT p.ID, pm.meta_value
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS pm ON pm.post_id = p.ID
WHERE pm.meta_key = '_thumbnail_id';

右側のテーブルを絞り込む条件は ON に書きます。前述のアイキャッチ画像の例で pm.meta_key = '_thumbnail_id' を ON に入れていたのは、この理由です。「結合の相手を選ぶ条件は ON、結果全体を絞る条件は WHERE」と覚えておくと迷いにくくなります。

行が増えて件数が合わない

wp_postmeta のように、一つの投稿に多数の行があるテーブルを条件なしで結合すると、投稿1件がメタデータの行数分だけ繰り返されます。件数を数えると実際より多くなるのはこのためです。

  • 結合する相手を ON の条件で1行に絞る(meta_key を指定するなど)。
  • 「存在するかどうか」だけを知りたいなら、JOIN ではなく EXISTS を使う。
  • 集計するなら COUNT(DISTINCT p.ID) のように、重複を除いて数える。

DISTINCT で見かけの重複を消す方法もありますが、原因を隠してしまうことがあるため、まずなぜ行が増えたのかを確認してから使ってください。

JOIN が遅いときの確認

JOIN の速さは、結合に使う列にインデックスがあるかどうかで大きく変わります。WordPress の標準のテーブルでは、wp_postmeta.post_id や wp_term_relationships.term_taxonomy_id などの主要な結合列にはインデックスがあります。遅い場合は、SQL の先頭に EXPLAIN を付けて実行計画を確認します。

EXPLAIN SELECT p.ID
FROM wp_posts AS p
INNER JOIN wp_postmeta AS pm ON pm.post_id = p.ID
WHERE pm.meta_key = '_price';

type 列が ALL(全件走査)になっているテーブルや、rows 列の見積もりが極端に大きいテーブルがあれば、そこが遅さの原因になっている可能性があります。wp_postmeta の meta_value は長い文字列型のため、値で絞り込む条件は速くなりにくい点にも注意が必要です。インデックスの追加はテーブルの構造を変える作業なので、バックアップを取り、ステージングで効果を確かめてから行います。

WordPress のコードで JOIN を書くとき

テーマやプラグインの中で投稿を取り出すだけなら、SQL を直接書かずに WP_Query の tax_query や meta_query を使う方が安全です。WordPress 側で JOIN を組み立て、キャッシュも効きます。

どうしても SQL を書く場合は、$wpdb->posts のようにテーブル名をプロパティから取り、利用者の入力が入る値は $wpdb->prepare() で埋め込みます。

global $wpdb;

$slug = sanitize_title( wp_unslash( $_GET['cat'] ?? '' ) );

$sql = $wpdb->prepare(
	"SELECT p.ID, p.post_title
	   FROM {$wpdb->posts} AS p
	   INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID
	   INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'category'
	   INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id
	  WHERE t.slug = %s AND p.post_status = 'publish'",
	$slug
);
$rows = $wpdb->get_results( $sql );

取得した値を画面に出すときは、esc_html() などでエスケープします。

孤立したデータを JOIN で探して削除する、といったメンテナンスでは、DELETE pm FROM wp_postmeta AS pm LEFT JOIN ... WHERE p.ID IS NULL のように複数テーブルを使った DELETE も書けます。ただし条件を一つ誤ると必要なデータまで消えるため、先に同じ条件の SELECT で件数と中身を確認し、データベースのバックアップを取ってから実行してください。

まとめ

  • 使い分けの基準は「相手が無い行を残すか」。残さないなら INNER JOIN、残すなら LEFT JOIN。
  • RIGHT JOIN は左右を入れ替えた LEFT JOIN で書ける。FULL OUTER JOIN は MySQL に無いため、UNION で代わりにする。
  • LEFT JOIN で右側を絞る条件は ON に書く。WHERE に書くと INNER JOIN と同じ結果になる。
  • 一対多のテーブルを結合すると行が増える。条件で1行に絞るか、EXISTS や COUNT(DISTINCT ...) を使う。
  • 遅いときは EXPLAIN で確認し、WordPress のコードでは WP_Query か $wpdb->prepare() を使う。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲