2026年8月31日
2026年8月31日
WordPressのクエリをMySQLのEXPLAINで読み解いて最適化する実践ガイド
遅いクエリを直すには、MySQL がそのクエリを「どう実行するつもりか」を EXPLAIN で読むのが近道です。EXPLAIN の結果のうち、まず見るべきなのは type・key・rows・Extra の4列で、ここから「全件を読んでいる」「並べ替えに一時テーブルを使っている」といった原因が分かります。この記事では、WordPress が発行する SQL を取り出す方法、各列の読み方、WP_Query の典型的な遅いパターンを例にした改善の進め方を説明します。
調べる SQL を取り出す
EXPLAIN にかけるには、実際に発行されている SQL が必要です。WordPress では次の方法で取り出せます。
- Query Monitor プラグイン: ページごとの全 SQL と実行時間、呼び出し元のコンポーネントが一覧で見られます。遅い順に並べ替え、上位から調べます。
- WP_Query の request プロパティ: テーマやプラグインのコードで、実行後の
$query->requestに SQL が入っています。 - スロークエリログ: 本番環境で実際に遅かったクエリを記録できます。ページ単位では見えない、管理画面や cron の重いクエリも拾えます。
// 開発環境での確認用。終わったら削除する
$q = new WP_Query( $args );
error_log( $q->request );
取り出した SQL の先頭に EXPLAIN を付けて、phpMyAdmin の SQL タブや mysql コマンド、wp db query で実行します。SELECT 文の EXPLAIN はクエリ自体を実行しないため、本番でも比較的安全に使えます。
EXPLAIN の結果の読み方
EXPLAIN は、クエリが触るテーブルごとに1行を返します。JOIN があれば複数行になり、上から順に処理されるのが基本です。
type:どうやって行を探すか
アクセス方法を表し、おおむね次の順に効率がよくなります。
- ALL: テーブル全体を読む(フルスキャン)。大きなテーブルで出ていれば最優先で見直す対象
- index: インデックス全体を読む。ALL よりましでも、件数が多ければ重い
- range: インデックスで範囲を絞って読む(日付の範囲、IN など)
- ref: インデックスで特定の値に一致する行を読む
- eq_ref: JOIN で主キーやユニークキーにより1行だけ対応する
- const: 主キーで1行に特定できる
key と key_len:実際に使われたインデックス
possible_keys は使える候補、key は実際に選ばれたインデックスです。key が NULL ならインデックスは使われていません。key_len は使われたインデックスの長さ(バイト数)で、複合インデックスのどこまでが使われたかの目安になります。たとえば utf8mb4 の wp_postmeta で meta_key(先頭191文字)のインデックスが使われると、key_len は 767 前後になります。
rows と filtered:読む行数の見積もり
rows はそのテーブルで読むと見積もった行数、filtered はそのうち条件を満たす割合(%)です。rows が大きく filtered が小さいほど、多くの行を読んで捨てていることを意味します。これは統計情報に基づく見積もりで、実際の件数とずれることがあります。
Extra:追加の処理
- Using where: 読んだ行を WHERE 条件で絞り込んでいる。rows が大きいと重い
- Using index: インデックスだけで結果を返せている(テーブル本体を読まない)。良い状態
- Using index condition: インデックスの段階で条件を一部評価している
- Using temporary: 一時テーブルを作っている。GROUP BY や DISTINCT で出やすい
- Using filesort: インデックスの順序を使えず、別途並べ替えている
Using temporary と Using filesort が rows の大きいテーブルに同時に出ていたら、改善の効果が大きい箇所です。
実例:meta_query で絞り込む一覧
価格が1,000以上の商品を新着順に10件表示する、次のような WP_Query を例にします。
$q = new WP_Query( array(
'post_type' => 'product',
'posts_per_page' => 10,
'meta_query' => array(
array(
'key' => 'price',
'value' => 1000,
'compare' => '>=',
'type' => 'NUMERIC',
),
),
) );
発行される SQL はおおむね次の形です(WordPress のバージョンや引数によって細部は変わります)。
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id )
WHERE 1=1
AND ( ( wp_postmeta.meta_key = 'price'
AND CAST(wp_postmeta.meta_value AS SIGNED) >= '1000' ) )
AND wp_posts.post_type = 'product'
AND ((wp_posts.post_status = 'publish'))
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC
LIMIT 0, 10;
これに EXPLAIN をかけると、たとえば wp_postmeta の行が type: ref、key: meta_key で rows が数万〜数十万、Extra に Using where; Using temporary; Using filesort、wp_posts の行が type: eq_ref、key: PRIMARY といった結果になります(実際の値はデータ量と統計で変わります)。読み取れることは次の3点です。
- meta_key のインデックスで price の行には絞れているが、meta_value は CAST して比較しているため、price を持つ全行を1件ずつ評価している
- GROUP BY と ORDER BY の組み合わせで一時テーブルと並べ替えが発生している
SQL_CALC_FOUND_ROWSにより、LIMIT 10 でも条件に合う全件を数えている
改善の進め方
原因ごとに、効果の大きいものから試します。
- 件数計算を省く: ページ送りが不要なら
'no_found_rows' => trueを指定します。SQL から SQL_CALC_FOUND_ROWS が消え、LIMIT で早く打ち切れるようになります。 - 絞り込みの設計を変える: 「価格帯」のように区分で足りるならカスタムタクソノミーにして tax_query で絞ります。数値の範囲検索や並べ替えが中心なら、型付きの列を持つカスタムテーブルに保存する方が、インデックスを素直に使えます。
- インデックスを検討する: 完全一致の比較が多いなら、meta_key と meta_value の先頭部分の複合インデックスが効く場合があります。ただし CAST した比較には効きません。追加はバックアップとステージング検証のあとで行います。
- 結果をキャッシュする: 更新頻度の低い一覧は、ID の配列をトランジェントやオブジェクトキャッシュに保存して再利用します。
変更のたびに EXPLAIN を取り直し、type・rows・Extra がどう変わったかを記録しておくと、効果の有無を判断しやすくなります。
EXPLAIN ANALYZE で実測する
EXPLAIN は見積もりですが、MySQL 8.0.18 以降の EXPLAIN ANALYZE はクエリを実際に実行し、各段階の実測時間と行数を表示します。MariaDB では ANALYZE SELECT ... が同様の役割を持ちます。見積もりの rows と実測の行数が大きくずれていれば、統計情報が古い可能性があり、ANALYZE TABLE で更新すると実行計画が変わることがあります。
EXPLAIN ANALYZE は本当にクエリを実行するため、重いクエリを本番で試すと負荷になります。ステージングで使うのが基本です。結果を読みやすい木構造で見たい場合は、MySQL 8.0.16 以降の EXPLAIN FORMAT=TREE も使えます。対応状況はバージョンで異なります(2026年10月現在)。
まとめ
- 調べる SQL は Query Monitor、
$query->request、スロークエリログで取り出す - まず type(ALL を探す)、key(NULL でないか)、rows(大きすぎないか)、Extra(Using temporary / Using filesort)を見る
- WP_Query の meta_query で NUMERIC 比較をすると CAST によりインデックスが効かず、SQL_CALC_FOUND_ROWS で全件も数える
- 改善は no_found_rows、タクソノミーやカスタムテーブルへの設計変更、インデックス、キャッシュの順に検討し、変更ごとに EXPLAIN を取り直す
- 見積もりと実測を比べるには EXPLAIN ANALYZE(MariaDB は ANALYZE 文)をステージングで使う