サイトのFAQ一覧を1ページで表示する場合、ページ内リンク付きのQuestionsリストをページ上部に、各QuestionsとそのAnswerをその下に表示するやり方がありますよね。ちょうど下図のキャプチャのような感じです。

ページ上部にQuestionsのリスト。その下にQ+Aの一覧。(仕事で携わっているサイトのページから)

ページ上部にQuestionsのリスト。その下にQ+Aの一覧。
仕事で携わっているサイトのページから

WordPressで構築したサイトにおいてFAQを機能として実装するとなると、カスタム投稿タイプを使うやり方がまず考えられます。タイトル欄にQuestionを、本文欄にそのAnswerを入力。そうすると、カスタム投稿タイプのアーカイブ表示を利用するだけでQuestions + Answerの一覧部分を表示することができます。

ではページの冒頭に表示する、ページ内リンク付きQuestionリストはどのように表示すればよいか?

やり方は幾つかありますが、今回はWP_Query()rewind_posts()query_posts()pre_get_postsを使い、下記の3つの値で表示パフォーマンスを比較してみました。

  • DBへの総クエリー数
  • DBへのクエリー所有時間の総和
  • 処理速度の総和

まずはカスタム投稿タイプ「FAQ」を作る

まずはカスタム投稿タイプ「FAQ」を作ります。今回はfunctions.phpに記述するのではなく、プラグインとして作成。設定したコードはこんな感じ。

/*
Plugin Name: My Site-Specifc Plugin
*/

/**
* FAQ機能の追加
*/
// カスタム投稿タイプ「FAQ」を作成
function create_post_type() {
	register_post_type( 'faq',
		array(
			'labels' => array(
				'name' => 'Questions',
				'singular_name' => 'Question'
			),
		'public' => true,
		'has_archive' => true,
		)
	);
}
add_action( 'init', 'create_post_type' );

ごく普通にカスタム投稿タイプを設定しているだけのシンプルなコードです。パラメータも最低必要分のみ。カスタム投稿タイプやプラグインの作り方が上記コードだけでは分からないという方は、書籍やCodexを参照してください。

2点だけ補足。

まず、投稿タイプはfaqですが、投稿は質問の集まりになるのでラベルの複数形はQuestions、単数形はQuestionに。

それから、カスタム投稿タイプを設定するコードの記述先はfunctions.phpでも構いませんが、「FAQ」は見た目のカスタマイズではなくサイトへの機能の追加です。もしも実サイトに実装した場合、「テーマを変更すると使えなくなります」では困る機能なので、functions.phpに記述するのではなくプラグインとして実装する方がプラクティスとして一般的にはベター。Site-specific pluginとは、サイト専用プラグイン、という意味。

これをsite-specific.phpとしてプラグインフォルダに保存。インストールして有効化すると、管理画面に「Questions」が現れます。テスト用なので今回はCodexに掲載されているFAQ About WordPress から適当に7つ拝借。

2013-03-09_3

簡単なカスタム投稿タイプ用アーカイブテンプレートを作っておく

カスタム投稿タイプの設定で'has_archive' => trueにしておいたので、WordPressがこの「faq」カスタム投稿タイプのアーカイブ表示を作ってくれます。サイトのURI/faq/へアクセスするとFAQのアーカイブが表示されます。今回はTwenty Elevenを使用しているので、テンプレートファイルにはarchive.phpが使われています。

管理しやすいよう、ここではFAQ専用のアーカイブテンプレートファイルを作っておきます。archive.phpを複製し、archive-faq.phpとして保存。不要な条件分岐や、ほかのアーカイブ関連の処理などはバッサリと切り捨て、ループ処理も直接記述。コードはこんな感じになりました。

<?php get_header(); ?>

<section id="primary">
<div id="content" role="main">

<header class="page-header">
<h1 class="page-title">
FAQ
</h1>
</header>

<?php if ( have_posts() ) : ?>

<?php /* Start the Loop */ ?>
<?php while ( have_posts() ) : the_post(); ?>

<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<h1 class="entry-title"><a href="<?php the_permalink(); ?>" title="<?php echo esc_attr( sprintf( __( 'Permalink to %s', 'twentyeleven' ), the_title_attribute( 'echo=0' ) ) ); ?>" rel="bookmark"><?php the_title(); ?></a></h1>
<div class="entry-content">
<?php the_content(); ?>
</div><!-- .entry-content -->
<footer class="entry-meta">
<?php edit_post_link( __( 'Edit', 'twentyeleven' ), '<span class="edit-link">', '</span>' ); ?>
</footer><!-- .entry-meta -->
</article><!-- #post-<?php the_ID(); ?> -->

<?php endwhile; ?>

<?php else : ?>

<?php endif; ?>

</div><!-- #content -->
</section><!-- #primary -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

これで、サイトのURL/faq/へアクセスすると、こう表示されます。

2013-03-09_2

これで準備完了。

Questionsリストを表示する

さてここからが本番。

なお、表示のパフォーマンスのチェックにはDebug BarDebug-Bar-Extender、2つのプラグインを使います。詳しくは下記2つの記事を参照。

まず、比較前の状態(上図キャプチャの状態)でチェックしてみます。総クエリー数とクエリー所有時間の総和は、5回計測の平均値です。

総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
28 Avg: 15.22 ms Avg: 178.89 ms
(14.5, 16.9, 14.4, 15.7, 14.6) (179.38, 190.79, 173.18, 173.96, 177.14)

サブループを使って実装してみる

カスタム投稿タイプのアーカイブ表示でQuestions+Answersの一覧を表示している訳ですが、つまりこれはメインクエリーで取得されたデータをメインループで表示していることになります。そこでまず、サブクエリ-とサブループを作成することで、Questionsリンク一覧を表示してみます。

サブループを作るにはWP_Query()を使います。これを<?php if ( have_posts() ) : ?><?php while ( have_posts() ) : the_post(); ?>の間に記述します。

</header>
<?php if ( have_posts() ) : ?>

<?php
// 新しいクエリをセットアップ
$list = new WP_Query( 'post_type=faq' );

// Questionsの一覧リスト
$output = '<ol>';
while ( $list->have_posts() ) : $list->the_post();
$output .= '<li><a href="#post-' . get_the_id() . '">' . get_the_title() . '</a></li>';
endwhile;

// reset post data
wp_reset_postdata();
$output .= '</ol>';

// echo all
echo $output;
?>
<?php /* Start the Loop */ ?>
<?php while ( have_posts() ) : the_post(); ?>
WP_Query()を使って作成したサブループで、Questionsのリスト一覧が表示された。

WP_Query()を使って作成したサブループで、Questionsのリスト一覧が表示された。

さて、表示のパフォーマンスチェックですが、結果はこうなりました。

総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
30 Avg: 14.44 ms Avg: 204.85 ms
(15.2, 12.8, 14.5, 14.4, 15.3) (209.03, 206.69, 200.97, 200.16, 207.41)

rewind_posts()を活用してみる

では次に、rewind_posts()を使ってみます。<?php if ( have_posts() ) : ?><?php while ( have_posts() ) : the_post(); ?>の間に、先ほどのWP_Queryではなく、下記のコードを記述します。

<?php if ( have_posts() ) : ?>

<ol>
<?php
// start the loop to list questions
while ( have_posts() ) : the_post(); ?>
<li><a href="#post-<?php the_ID(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; ?>
</ol>

<?php
// rewind posts!
rewind_posts(); ?>

<?php /* Start the Loop */ ?>
<?php while ( have_posts() ) : the_post(); ?>

表示結果は先ほどのサブループを使うやり方と同じなので、キャプチャは省略。パフォーマンス結果は下表の通り。

総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
28 Avg: 14.08 ms Avg: 203.86 ms
(14.1, 14.4, 13.9, 14.3, 13.7) (220.30, 196.92, 203.59, 198.58, 199.90)

サブループを使うやり方と比較してクエリー数が2つ減っていますね。クエリー所有時間の総和とWordPressの処理時間の総和はほんの少しだけ短縮されています。「ほんの少し」と言っても~1msなので、体感もできない、ほんとに「ほんの少し」ですけどね。しかし、クエリー数は確実に2つ減っています。

なぜWP_Query()よりもrewind_posts()の方がクエリー数が少ないのか

サブループを作るやり方だと、メインクエリーとは別のクエリーが発生し、クエリー数が増えます。しかしrewind_posts()を使ったやり方だと、メインクエリーで取得したデータを2回使う為、クエリー数は増えません。まずはQuestionsのリスト一覧に使用し、rewind_posts()で文字通りループを「巻き戻し」ます。そして「巻き戻った」データを利用してQuestion + Answerの一覧を表示します。

今回作ったFAQのページでは、Questionsの一覧リストと、Question + Answerの一覧に必要なデータは全く同じ(post_type=’faq’)であり、表示件数も同じ(全件)です。ですので、わざわざサブループを作ってデータベースへのクエリーを増やすよりは、メインクエリーで既に取得済みのデータを2回利用した方が表示パフォーマンスの最適化につながります。

普通はないケースと思いますが、Questionsの一覧リストと、Question + Answerの一覧に必要なデータが違う場合(例えば、投稿タイプが違う、など)はrewind_posts()を使ってもうまくいきません。その様なケースでは、表示パフォーマンス的に一番効率的な方法ではなくても、WP_Query()を用いてサブループを作るなどする必要があります。

表示する投稿数を全件に変更する

と言うわけで、rewind_posts()を活用した方が表示パフォーマンス的にベターなのですが、実は先ほどのコードでは表示件数を指定していません。管理画面の「表示設定」にある「1ページに表示する最大投稿数」に表示件数を依存している状態になります。なので、たとえば設定画面で最大投稿数を5件に設定するとこう表示されます。

メインクエリーで取得したデータを表示しているので、特に指定しない限り表示件数は管理画面で設定した件数になる。

メインクエリーで取得したデータを表示しているので、特に指定しない限り表示件数は管理画面で設定した件数になる。

では、先ほどのrewind_posts()を使ったやり方のコードを、管理画面の設定に関係なく、全件表示するようにカスタマイズしましょう。これも2つの違ったやり方を用い、表示パフォーマンスを比較してみます。

query_posts()を使うやりかた

まずは昔からよく紹介されるquery_posts()を使ったやり方を試してみます。

先ほどのif ( have_posts() ) :の直前にquery_posts()を記述します。そしてループの終わりにwp_reset_query()を忘れずに追加します。

</header>

<?php query_posts( 'post_type=faq&posts_per_page=-1' ); ?>
<?php if ( have_posts() ) : ?>

<ol>

<?php endwhile; ?>
<?php else : ?>
<?php endif; ?>
<?php wp_reset_query() ?>

表示パフォーマンスの計測結果です。

総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
30 Avg: 18.16 ms Avg: 215.15 ms
(17.6, 16.9, 17.1, 15.4, 23.8) (195.36, 201.61, 200.72, 198.28, 279.76)

クエリー数が2つ増えただけでなく、クエリー所有時間の総和が約4 ms、WordPressの処理時間の総和が約12 ms、と明らかに表示パフォーマンスが悪くなっています。せっかくrewind_posts()を使っても、これでは意味がありませんね。なぜでしょうか?

query_posts()は、クエリーで取得したデータをループで表示する前に、そのデータの表示を「カスタマイズ」してくれる関数と一般的に解釈されがちです。メインループが始まる直前に記述する方法も誤解を招きやすい一因となっているのかもしれません。

しかし、実際にquery_posts()が行っている処理は全く違います。query_posts()はメインクエリーを表示するメインループのカスタマイズするのではなく、全く新しいクエリ-を作成します(下図参照)。

2013-04-01-1

前述した通り、今回表示するQuestionsの一覧リストと、Question + Answerの一覧に必要なデータは全く同じ(post_type=’faq’)で、メインクエリーで既にほぼ取得できています。表示する投稿数を任意指定する為だけにわざわざ破棄してクエリーし直すのも非効率ですよね。

pre_get_postsを使う

pre_get_postsはアクションフックの1つで、WordPressがクエリーする内容を作成した後、そのクエリーを実行するタイミングの直前に「フック」することができます。このタイミングでフックすることにより、無駄なクエリーを発生させることなくメインクエリーをカスタマイズすることができます。

では実装してみましょう。下記のコードをプラグイン(site-specific.php)に追加します。

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

// pre_get_postsにフック
if ( $query->is_post_type_archive('faq') ) {
$query->set( 'posts_per_page', '-1');
}
}
// pre_get_postsにフック
add_action( 'pre_get_posts', 'faq_archive_posts_per_page');

表示パフォーマンスの測定値はこうなりました。

総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
26 Avg: 14.68 ms Avg: 196.87 ms
(14.1, 15.4, 15.3, 13.9, 14.7) (205.66, 200.45, 192.87, 195.42, 189.93)

総クエリー数が今までの中で一番少なく26。クエリー所有時間の総和とWordPressの処理時間の総和はquery_posts()を使ったやり方よりも短くなっています。

まとめ

簡単にですが、表示パフォーマンスの比較を軸に、FAQのカスタム投稿アーカイブ表示の方法を比較してみました。計測したすべての数値を一纏めにしたのが下表です。

やり方 総クエリー数 クエリー所有時間の総和 WordPressの処理時間の総和
デフォルト 28 Avg: 15.22 ms Avg: 178.89 ms
WP_Query() 30 Avg: 14.44 ms Avg: 204.85 ms
rewind_posts() 28 Avg: 14.08 ms Avg: 203.86 ms
rewind_posts() + query_posts() 30 Avg: 18.16 ms Avg: 215.15 ms
rewind_posts() + pre_get_posts 26 Avg: 14.68 ms Avg: 196.87 ms

今回作っていたFAQページを実装するには、rewind_posts()pre_get_posts()の組み合わせが一番クエリー数が少なく、所有時間と処理時間の総和が比較的短い、と言う結果になりました。今回のテストでは投稿数の数が少なく、回数も各5回しか試していません。ですが、以下の2つの事が読み取れると思います。

  • 表示される見た目は同じでも、クエリーとループの処理方法によってクエリー数は変わる場合がある。クエリー数が減るということは処理数が減るということであり、結果適に表示パフォーマンスが上がる可能性がある。
  • 今回のテストでは、クエリー所有時間の総和とWordPressの処理時間の総和に関して、全体を通しての明確な(何かしらの)相関関係があったとは言えない。ただし、query_posts()を使ったときだけは、使わなかった時よりも明らかにより長く時間が掛かった。

今回の投稿数は7つでしたが50、100、1000と、多ければ多いほどクエリー数は変わらなくとも処理に掛かる時間は長くなるはずです。それから、所有時間や処理時間は環境によって結果が違う可能性があります。今回のテストはMac Book Air(mid 2011)+MAMPで行いましたが、SSDが使われているレンタルサーバーは限られていますし、CPUとメモリのスペックも劣るでしょう。また、テーマやプラグイン等によって処理時間も変わってくると思います。サイト表示の全体的な高速化には、画像や外部スクリプトの読み込み等、フロントエンドの最適化も忘れてはいけません。

(Twenty Elevenを今回は使いましたが、このテーマも飛びぬけて軽いテーマではないはずで、今思えば、比較には_sあたりを使ったほうが良かったのかもしれませんね。)

と言うわけで、表示パフォーマンスには他の要因もありますが、「見た目は同じでもコードが違えば処理に掛かる時間が違ってくるかも」と意識しておきたいですね。カジュアルブロガーならともかく、(仕事で)開発を行っているのであれば尚更です。

メインクエリーをカスタマイズする際は慎重に

最近、本家のCodexでは、メインクエリーをカスタマイズする際にはquery_posts()は使わず、pre_get_posts()を使うことを強く勧めるよう、編集されました。先日僕が訳したWordCampのセッションでも「僕の経験からいって、サブクエリーを作りたければWP_Query、メインクエリーを変更したければpre_get_postsもしくはrequestフィルターを使うと良い。」とありました。

pre_get_posts()の方が勧められている主な理由は2つ有って、1つは今回書いた通りパフォーマンスに劣ることで、もう1つは仕組みを理解せずに使うと予期せぬことが起きることです。query_posts()は、その時に処理途中だったクエリーを全く無視します。例えば、今回のテストでは以下のように記述しました。

<?php query_posts( 'post_type=faq&posts_per_page=-1' ); ?>

これを「FAQ投稿タイプのアーカイブ用テンプレート(archive-faq.php)だから、post_typeは指定する必要ないんじゃない?」と、下記の様に記述すると…

<?php query_posts( 'posts_per_page=-1' ); ?>

こうなります。投稿タイプがfaqであろうが、postであろうが、何であろうが、全て表示してしまいます。

2013-03-09_6

pre_get_posts()も、全てのメインクエリーの処理が対象になってしまうため、たいていの場合は最低現is_admin()is_main_query()を併せて記述する必要があります。CMSとして利用する際は他にも条件分岐を加える必要があると思います。ここにプラグインも絡んでくると…結構予期せぬところに影響が出ます。

どちらを使ってもメインクエリーに手を加えることに変わりはないので、使う際には注意が必要です。そして、裏側でどのような処理が行われているのか、大まかにでも把握しておくと、使い分けや予期せぬトラブルにも対応できるでしょう。

参考記事・図書

Professional WordPress: Design and Development
Wrox (2012-12-17)
売り上げランキング: 77,946