18
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WordPress 投稿のクエリーでカスタムフィールドの絞り込みにNOT EXISTSを使ってDBが死んだ話(失敗と解決法)

Last updated at Posted at 2016-08-04

要件定義の仕様とソースコードには問題なかったけど、パフォーマンスに問題があってやらかした話とその解決方法。

前提条件と要件

  • 投稿に対して「最新記事に表示しない」チェックボックスを作成して、チェックが入っている記事はHOMEにおいて表示させない。
  • ただし、記事そのものおよびカテゴリーアーカイブなどでは表示させる
  • 既に1000オーバーで投稿が存在している

仕様

  • 投稿編集画面に「最新記事に表示しない」チェックボックスの追加(add_meta_box)
  • チェックされた投稿はカスタムフィールド(post_view_check)に 1 で保存される
  • HOMEの投稿ループでカスタムフィールド(post_view_check)の値が1の投稿以外が表示されるよう、pre_get_posts フックでメインクエリーを改変する

組んだソースのメインクエリーを改変部分

function my_home_posts_modify_query( $query ) {
	if ( is_admin() || ! $query->is_main_query() )
		return;

	// HOMEでチェックが入っている記事を除外
	if ( $query->is_home() ) {
		$meta_arg = array(
			'relation' => 'OR',
			array(
				'key'     => 'post_view_check',
				'value'   => '0',
			),
			array(
				'key'     => 'post_view_check',
				'compare' => 'NOT EXISTS',
			),
		);
		$query->set( 'meta_query', $meta_arg );
		return;
	}

}
 add_action( 'pre_get_posts','my_home_posts_modify_query' );

カスタムフィールド「post_view_check」の値が1 以外 というのがポイント。
通常なら演算子 != を使って簡単にできそうですが、前提条件にもあるように既に存在する投稿があるのが曲者。
既に存在する投稿に対して、後から追加したカスタムフィールドは自動で紐付けられずいわゆるカスタムフィールドが存在しない状態になります。(投稿を保存しなおせばカスタムフィールド自体も保存されるけど、記事数が多いと無理ゲー)

ということでNOT EXISTSを使ったのですが、これがアカンパターンでした。

DBに負荷がかかった原因

よく聞く「WordPressのカスタムフィールドは検索に向いていない」がそもそもの原因。
この「検索に向いていない」理由なのですが、 「データベースのカスタムフィールドのテーブル(wp_postmeta)で meta_value はインデックスされていない」 これに付きます。

先のソースで言うと最終的に発行されるSQL文に問題がなくてもその実行に負荷がかかった。というオチです。

参照

どう解決したか

カスタムフィールドにしてる部分が原因だったので、カスタムタクソノミーでの実装に変更。
管理画面でメニューが出てしまうのはしょうがない上に運用でカバーの部分だけど、DBパフォーマンスには勝てない。

function my_home_posts_modify_query( $query ) {
	if ( is_admin() || ! $query->is_main_query() )
		return;

	// HOMEで newsタクソノミーのスラッグnonewsの記事を除外
	if ( $query->is_home() ) {
		$term_arg = array(
			'taxonomy' => 'news',
			'field'    => 'slug',
			'terms'    => 'nonews',
			'operator' => 'NOT IN',
		);
		$query->set( 'tax_query', $term_arg );
		return;
	}

}
 add_action( 'pre_get_posts','my_home_posts_modify_query' );

なおタクソノミーは public パラメーターを変更することで、表側に影響を出さないようにすることもできる。

$args = array(
	'labels'                     => $labels, // ここ省略
	'hierarchical'               => true,
	'public'                     => false,
	'show_ui'                    => true,
	'show_admin_column'          => false,
	'show_in_nav_menus'          => false,
	'show_tagcloud'              => false,
);
register_taxonomy( 'news', array( 'post' ), $args );

学習したこと

  • メタクエリーの「NOT EXISTS」(今回はチェック入れたのを出さないという処理)はフルスキャンになるのでDBに優しくない
  • 逆の「チェック入れたのを出す」ならインデックスされてるのでDBには優しいが、過去記事がある場合運用が面倒
  • カスタムタクソノミーでタームクエリーでやるとインデックスされてるのでDBに優しい

2017.3.6 追記 解決策の別案

カスタムタクソノミーじゃなくて当初のカスタムフィールドでやりたいって場合の別案

仕様

  • 投稿に対して「最新記事に表示しない」チェックボックスを作成して、チェックが入っている記事はHOMEにおいて表示させない。

ソース

function my_home_posts_modify_query( $query ) {
	if ( is_admin() || ! $query->is_main_query() )
		return;

	// HOMEでチェックが入っている記事を除外
	if ( $query->is_home() ) {
		// 一旦除外する記事のIDを配列で取得する
		$ex_posts = get_posts(array(
			'fields'         => 'ids',
			'posts_per_page' => -1,
			'meta_query'     => array(
				array(
					'key'     => 'post_view_check',
					'compare' => '==',
					'value'   => '1'
				)
			),
		));
		// 除外するIDの配列があったら post__not_in に
		if ( ! empty( $ex_posts ) ) {
			$query->set( 'post__not_in', $ex_posts );
		}
		return;
	}

}
 add_action( 'pre_get_posts','my_home_posts_modify_query' );

get_posts() あるいは WP_Queryクラスは 'fields' => 'ids' とすることで、条件にマッチした投稿をオブジェクトではなく投稿IDの配列で受け取ることができるのがミソ。

18
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?