2026年8月21日
2026年8月21日
MySQLのSELECT文の基本と応用 WordPressのテーブルで学ぶ絞り込み・並べ替え・JOIN
SELECT 文は「どのテーブルの、どの行の、どの列を、どの順番で取り出すか」を指定するだけの文で、SELECT 列 FROM テーブル WHERE 条件 ORDER BY 並び順 LIMIT 件数 の形を覚えれば大半の用途に足ります。この記事では WordPress の wp_posts と wp_postmeta を題材に、基本の書き方、条件の書き方でつまずく点、JOIN とサブクエリ、ページ送り、PHP の $wpdb から安全に使う方法までを順に説明します。集計(GROUP BY と HAVING)は別記事で扱い、ここでは「行を探して取り出す」ことに絞ります。
作業前に決めておくこと
SELECT はデータを書き換えませんが、本番サーバーで重いクエリを流すとサイト全体が遅くなることがあります。調べ物はステージング環境か、本番データベースのコピーで行うのが無難です。本番で確かめる必要があるときは、LIMIT を付けて件数を絞り、アクセスの少ない時間帯に実行します。
接続には phpMyAdmin や Adminer などの管理ツール、mysql コマンド、WP-CLI の wp db query が使えます。テーブル名の接頭辞は wp_ とは限らないため、wp-config.php の $table_prefix か wp db prefix で確認してから書き始めてください。この記事の例は MySQL 8.0 以降と MariaDB 10.x 以降を想定しています(2026年10月現在)。
基本の形と、MySQL が処理する順番
SELECT ID, post_title, post_date
FROM wp_posts
WHERE post_type = 'post'
AND post_status = 'publish'
ORDER BY post_date DESC
LIMIT 10;
公開済みの投稿を新しい順に10件取り出すクエリです。書く順番は SELECT が先頭ですが、MySQL が論理的に処理する順番は次のとおりで、ここを押さえるとエラーの理由が読めるようになります。
FROMで対象のテーブルを決める(JOIN もここ)WHEREで行を絞り込むGROUP BYとHAVINGで集計・集計後の絞り込みをするSELECTで列を選び、別名(AS)を付けるORDER BYで並べ替えるLIMITで件数を切る
たとえば SELECT post_title AS t ... WHERE t LIKE ... と書くと Unknown column 't' in 'where clause' になります。WHERE は SELECT より先に処理されるため、別名がまだ存在しないからです。反対に ORDER BY は SELECT の後なので、別名で並べ替えられます。
列は SELECT * ではなく必要なものだけを書きます。wp_posts の post_content は本文全体を持つ大きな列なので、一覧を見るだけなら外したほうが速く、結果も読みやすくなります。
WHERE の書き方でつまずく点
文字列と数値、引用符
文字列は 'publish' のように単一引用符で囲みます。ID = 123 のような数値は引用符なしで書きます。列名やテーブル名に予約語や記号が含まれる場合だけ、バッククォート(` ``)で囲みます。
NULL は = で比較できない
WHERE post_excerpt = NULL はどの行にも一致しません。NULL は「値が無い」という状態で、比較の結果も不明扱いになるためです。IS NULL と IS NOT NULL を使います。なお、WordPress の多くの列は NULL ではなく空文字列 '' を入れているため、空かどうかは post_excerpt = '' で調べる場面のほうが多くなります。
AND と OR は括弧で意図を示す
SELECT ID, post_title, post_type
FROM wp_posts
WHERE post_status = 'publish'
AND (post_type = 'post' OR post_type = 'page');
AND は OR より先に結び付くため、括弧が無いと「公開済みの投稿、または状態を問わないすべての固定ページ」という別の意味になります。候補が複数なら post_type IN ('post', 'page') と書くと読み違いが減ります。
LIKE の部分一致
post_title LIKE '%キャッシュ%' で「キャッシュ」を含むタイトルを探せます。% は任意の文字列、_ は任意の1文字です。先頭が % の検索はインデックスが使えず、行数が多いと遅くなります。探したい文字そのものに % や _ が含まれる場合は、後述の $wpdb->esc_like() でエスケープします。
JOIN で関連する行をつなぐ
カスタムフィールドやアイキャッチ画像の情報は wp_postmeta に入っています。wp_posts.ID と wp_postmeta.post_id を結び付けるのが JOIN です。
-- アイキャッチ画像が設定されていない公開済み投稿
SELECT p.ID, p.post_title
FROM wp_posts AS p
LEFT JOIN wp_postmeta AS m
ON m.post_id = p.ID
AND m.meta_key = '_thumbnail_id'
WHERE p.post_type = 'post'
AND p.post_status = 'publish'
AND m.meta_id IS NULL;
INNER JOIN は両方に対応する行がある組み合わせだけを返し、LEFT JOIN は左側(ここでは投稿)の行をすべて残して、対応が無い右側の列を NULL で埋めます。「設定されていないもの」を探すには LEFT JOIN と IS NULL を組み合わせます。
ここでのつまずきは、m.meta_key = '_thumbnail_id' を ON に書くか WHERE に書くかです。WHERE に移すと、メタが無い行(NULL の行)が条件に合わずに消え、結果が空になります。LEFT JOIN で右側を絞る条件は ON に書く、と覚えておくと安全です。
サブクエリと EXISTS
別のクエリの結果を条件に使う書き方です。
-- 承認済みコメントが1件以上ある公開済み投稿
SELECT p.ID, p.post_title
FROM wp_posts AS p
WHERE p.post_type = 'post'
AND p.post_status = 'publish'
AND EXISTS (
SELECT 1 FROM wp_comments AS c
WHERE c.comment_post_ID = p.ID
AND c.comment_approved = '1'
);
JOIN で書くと、コメントが3件ある投稿は3行に増えてしまいます。「あるかどうか」だけを知りたいなら EXISTS のほうが意図どおりで、重複を消すための DISTINCT も要りません。逆に「無いもの」は NOT EXISTS で探せます。
ページ送りと深い OFFSET
LIMIT 20 OFFSET 40 は「41件目から20件」を返します(LIMIT 40, 20 も同じ意味です)。分かりやすい反面、OFFSET が大きいほど MySQL は読み飛ばす行を数えるために余分な処理をします。数万件以上を順に処理するバッチでは、前回の最後の ID を覚えておき、WHERE ID < 前回の最後のID ORDER BY ID DESC LIMIT 20 のように範囲で続きを取る書き方(キーセット方式)にすると、後半のページでも速度が落ちにくくなります。
ORDER BY を付けない LIMIT は、どの行が返るかが保証されません。ページ送りでは並び順を明示し、同じ値が並ぶ列(日付など)だけで並べる場合は ORDER BY post_date DESC, ID DESC のように一意な列を後ろに足します。
遅いと感じたら EXPLAIN
クエリの先頭に EXPLAIN を付けると、MySQL がどのインデックスを使い、何行くらい読む見込みかが表示されます。type が ALL(全件走査)で rows が大きい場合は、WHERE の列にインデックスが使われていません。WordPress のコアテーブルにはよく使う列のインデックスがすでにあるため、まずは条件の書き方(先頭 % の LIKE、列に関数をかけた比較など)を見直し、インデックスの追加はステージングで効果を確かめてから検討してください。
PHP から使うときは $wpdb->prepare
テーマやプラグインから SELECT を実行するときは、グローバルの $wpdb を使います。外部から受け取った値をクエリに埋め込むときは、文字列を連結せず $wpdb->prepare() のプレースホルダーを使います。
global $wpdb;
$keyword = isset( $_GET['kw'] ) ? sanitize_text_field( wp_unslash( $_GET['kw'] ) ) : '';
$like = '%' . $wpdb->esc_like( $keyword ) . '%';
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s AND post_title LIKE %s
ORDER BY post_date DESC LIMIT %d",
'post',
'publish',
$like,
10
)
);
foreach ( $rows as $row ) {
echo '<li>' . esc_html( $row->post_title ) . '</li>';
}
ポイントは3つです。テーブル名は $wpdb->posts や $wpdb->postmeta のプロパティを使い、接頭辞を直書きしないこと。LIKE の値は esc_like() でワイルドカードをエスケープしてから %s に渡すこと。出力は esc_html() などでエスケープすること。1行だけなら get_row()、1つの値だけなら get_var()、1列だけなら get_col() が使えます。
なお、投稿の一覧を表示するだけなら、多くの場合 WP_Query や get_posts() のほうが適しています。キャッシュやフィルターフックとの連携が効き、テーブル構造の変化にも追従しやすいためです。直接の SELECT は、集計や調査、WP_Query では表現しにくい条件のときに使うと考えると整理しやすくなります。
まとめ
- 基本は
SELECT 列 FROM テーブル WHERE 条件 ORDER BY 並び順 LIMIT 件数。必要な列だけを書く - 処理順は FROM → WHERE → GROUP BY/HAVING → SELECT → ORDER BY → LIMIT。WHERE で別名が使えないのはこのため
- NULL は
IS NULLで比較し、AND と OR は括弧で意図を示す - 「無いもの」を探すときは LEFT JOIN と
IS NULL、右側の絞り込み条件は ON に書く - 「あるかどうか」だけなら EXISTS。深い OFFSET はキーセット方式に切り替える
- PHP からは
$wpdb->prepare()とesc_like()を使い、出力はエスケープする。本番で重いクエリを流す前にステージングで試す