要件定義の仕様とソースコードには問題なかったけど、パフォーマンスに問題があってやらかした話とその解決方法。
前提条件と要件
- 投稿に対して「最新記事に表示しない」チェックボックスを作成して、チェックが入っている記事は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文に問題がなくてもその実行に負荷がかかった。というオチです。
参照
- Database Description | WordPress Codex
- WordPressで大規模データを扱う場合のTips
- Advanced Custom Fieldsプラグインを使う際の注意点など
どう解決したか
カスタムフィールドにしてる部分が原因だったので、カスタムタクソノミーでの実装に変更。
管理画面でメニューが出てしまうのはしょうがない上に運用でカバーの部分だけど、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の配列で受け取ることができるのがミソ。