目次
動作環境
サーバー: さくらインターネット 共用レンタルサーバー プレミアムプラン
Wordpress : 4.5.2
頻発するDB障害
以前、さくらインターネットでDB障害が起きるー、という記事を書きました。
以下、そのリンク
https://www.kabanoki.net/879
この記事を書いた以降もDB障害が頻発し、クライアントもどうしたもんかねー、と言う始末。
これは、いい加減に直さなきゃなと思い立ち、早速調査開始!
原因は検索フォーム
対象のサイトでは、求人情報を掲載しています。
どうもそこにある求人を検索する機能が怪しいと判明!
一見すると、ただの検索フォームに思えます。
しかし、queryを調べてみると、とんでも仕様になっているのが解ります。
以下query
SELECT
SQL_CALC_FOUND_ROWS hoge_posts.ID
FROM
hoge_posts
INNER JOIN
hoge_postmeta
ON
( hoge_posts.ID = hoge_postmeta.post_id )
INNER JOIN
hoge_postmeta AS mt1
ON
( hoge_posts.ID = mt1.post_id )
INNER JOIN
hoge_postmeta AS mt2
ON
( hoge_posts.ID = mt2.post_id )
INNER JOIN
hoge_postmeta AS mt3
ON
( hoge_posts.ID = mt3.post_id )
INNER JOIN
hoge_postmeta AS mt4
ON
( hoge_posts.ID = mt4.post_id )
INNER JOIN
hoge_postmeta AS mt5
ON
( hoge_posts.ID = mt5.post_id )
INNER JOIN
hoge_postmeta AS mt6
ON
( hoge_posts.ID = mt6.post_id )
INNER JOIN
hoge_postmeta AS mt7
ON
( hoge_posts.ID = mt7.post_id )
INNER JOIN
hoge_postmeta AS mt8
ON
( hoge_posts.ID = mt8.post_id )
INNER JOIN
hoge_postmeta AS mt9
ON
( hoge_posts.ID = mt9.post_id )
WHERE
1=1
AND
((
SELECT COUNT(1)
FROM hoge_term_relationships
WHERE term_taxonomy_id IN (10)
AND object_id = hoge_posts.ID
) = 1
AND
(
SELECT COUNT(1)
FROM hoge_term_relationships
WHERE term_taxonomy_id IN (21)
AND object_id = hoge_posts.ID
) = 1
AND
(
SELECT COUNT(1)
FROM hoge_term_relationships
WHERE term_taxonomy_id IN (11)
AND object_id = hoge_posts.ID
) = 1
AND
(
SELECT COUNT(1)
FROM hoge_term_relationships
WHERE term_taxonomy_id IN (12)
AND object_id = hoge_posts.ID
) = 1
AND
(
SELECT COUNT(1)
FROM hoge_term_relationships
WHERE term_taxonomy_id IN (22)
AND object_id = hoge_posts.ID
) = 1
) AND (
( hoge_postmeta.meta_key = 'start_day' AND CAST(hoge_postmeta.meta_value AS CHAR) <= '1469530350' ) AND ( mt1.meta_key = 'end_day' AND CAST(mt1.meta_value AS CHAR) >= '1469530350' )
AND
( mt2.meta_key = 'merritt' AND CAST(mt2.meta_value AS CHAR) LIKE '%交通費支給%' )
AND
( mt3.meta_key = 'merritt' AND CAST(mt3.meta_value AS CHAR) LIKE '%大学生歓迎%' )
AND
( mt4.meta_key = 'merritt' AND CAST(mt4.meta_value AS CHAR) LIKE '%フリーター歓迎%' )
AND
( mt5.meta_key = 'working_days' AND CAST(mt5.meta_value AS CHAR) LIKE '%週3~4日%' )
AND
( mt6.meta_key = 'time_zone' AND CAST(mt6.meta_value AS CHAR) LIKE '%夕方・夜%' )
AND
( mt7.meta_key = 'search-hours' AND CAST(mt7.meta_value AS CHAR) LIKE '%フルタイムの仕事%' )
AND
( mt8.meta_key = 'employment_status' AND CAST(mt8.meta_value AS CHAR) LIKE '%パート%' )
AND
( mt9.meta_key = 'employment_status' AND CAST(mt9.meta_value AS CHAR) LIKE '%アルバイト%' )
) AND hoge_posts.post_type = 'offer' AND (hoge_posts.post_status = 'publish' OR hoge_posts.post_status = 'private') GROUP BY hoge_posts.ID ORDER BY hoge_posts.post_date DESC LIMIT 0, 10
お分かり頂けたでしょうか?
ぱッと見で、吐き気を催すような気持ち悪いqueryです。
何故、こんな仕様になっているのか、担当にインタビュー
「Advanced Custom Fields」の検索フォームはダメ、絶対!
担当に確認したところ、このqueryを吐き出しているのは、「Advanced Custom Fields」プラグインだとのこと。
なるほど、それならばこの気持ち悪いqueryも納得だ。
そんなわけあるか!
このプラグインが、多機能な上に見た目重視で今回のような検索フォームを作るような案件とはミスマッチなのは、承知してます。
解っています。
だけど、このqueryはあまりにお粗末でしょう!
同じテーブルを何回JOINしてるんねん!
なんでDATE型の値をchar型に変換してるねん!
なんで値が、サニタイズされてるねん!
ねん!ねん!ねん!
重いQueryよ、バイバイ
さて、文句は書いたものの、カスタムフィールドを使った仕様を作り変えるのは面倒なので、queryを自作します。
まず、今回のDB障害の最大の要因となっているのが、JOINの個数です。
今回のqueryでは10個のJOINが行われています。
例えば、Aテーブルが50行、Bテーブルが1000行あるとします。
今回の場合は、
Aテーブルに対して、Bテーブルを10回JOINします。
つまり、
50 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 X 1000 行
のデータを取得する事になります。
もちろんJOINの際に、条件文があるのでもっと数は減るとは思いますが、
計算するのも面倒なくらいデータを取得しようとしています。
これは、DBも障害を起こすわー
という訳で、JOINを削って1個にします。
完成イメージは、以下の通り
SELECT ID ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'start_day' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS start_day ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'end_day' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS end_day ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'merritt' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS merritt ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'working_days' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS working_days ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'time_zone' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS time_zone ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'search-hours' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS `search-hours` ,CAST(MAX(CASE hoge_postmeta.meta_key WHEN 'employment_status' THEN hoge_postmeta.meta_value ELSE 0 END) AS CHAR) AS employment_status FROM `hoge_posts` LEFT JOIN hoge_postmeta ON hoge_postmeta.post_id = hoge_posts.ID WHERE `post_type` = 'offer' AND post_status = 'publish' GROUP BY `hoge_posts`.ID HAVING (start_day <= '1469530350' AND end_day >= '1469530350') AND (merritt LIKE '%交通費支給%' AND merritt LIKE '%大学生歓迎%' AND merritt LIKE '%フリーター歓迎%') AND (working_days LIKE '%週3~4日%') AND (time_zone LIKE '%夕方・夜%') AND (`search-hours` LIKE '%フルタイムの仕事%') AND (employment_status LIKE '%パート%' AND employment_status LIKE '%アルバイト%') ORDER BY hoge_posts.post_date DESC
現状のカスタムフィールドの状態ですと、びみょーに気持ち悪さが残りますが、こんな感じになります。
行として登録されている値を列に並べ替える感じです。
ポイントは、GROUPした後に、CASEとMAXを使って値を割り出すところです。
型変換はマジ余計
さて、完成型のイメージができたので、queryを生成する側の関数を作ります。
ここを参考にしながら、「functions.php」に関数を記載します。
function search_posts()
{
global $wpdb;
$selet = join(',', array(
'ID'
,'post_title'
,'post_excerpt'
,'post_content'
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'start_day' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS start_day"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'end_day' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS end_day"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'area' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS area"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'merritt' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS merritt"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'working_days' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS working_days"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'time_zone' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS time_zone"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'search-hours' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS `search-hours`"
,"CAST(MAX(CASE tbkb_postmeta.meta_key WHEN 'employment_status' THEN tbkb_postmeta.meta_value ELSE 0 END) AS CHAR) AS employment_status"
));
$sql = $wpdb->prepare(
"
SELECT
{$selet}
FROM
`{$wpdb->posts}`
LEFT JOIN
`{$wpdb->postmeta}`
ON
`{$wpdb->postmeta}`.`post_id` = `{$wpdb->posts}`.`ID`
WHERE
`post_type` = 'offer'
AND
`post_status` = 'publish'
GROUP BY
`{$wpdb->posts}`.`ID`
HAVING
`start_day` <= %s AND `end_day` >= %s
{having}
ORDER BY
`{$wpdb->posts}`.`post_date` DESC
"
, current_time( 'timestamp' )
, current_time( 'timestamp' )
);
$sql = str_replace('{having}', having() === '' ? '' : 'AND '.having(), $sql);
return $sql;
}
function having()
{
global $wpdb;
$havings = array();
// タクソノミー型のkeyを記述
$type_tax = array('area');
// 上記以外を記述
$type_text = array('merritt', 'working_days', 'time_zone', 'search-hours', 'employment_status');
if(!isset($_GET['s']))
return '';
foreach($_GET AS $key => $value)
{
if(in_array($key, $type_tax))
{
foreach($value AS $meta_value)
{
$tax_id = get_term_by('slug', $meta_value, $key)->term_taxonomy_id;
if($tax_id === FALSE)
continue;
$esc_meta_value = $wpdb->esc_like($tax_id);
$like_meta_value = '%"' . $esc_meta_value . '"%';
$sql[$key][] = $wpdb->prepare(
"`{$key}` LIKE %s"
,$like_meta_value
);
}
$havings[$key] = '('.join(' AND ', $sql[$key]).')';
}
else if(in_array($key, $type_text))
{
foreach($value AS $meta_value)
{
$esc_meta_value = $wpdb->esc_like($meta_value);
$like_meta_value = '%' . $esc_meta_value . '%';
$sql[$key][] = $wpdb->prepare(
"`{$key}` LIKE %s"
,$like_meta_value
);
}
$havings[$key] = '('.join(' AND ', $sql[$key]).')';
}
else if($key == 's')
{
if(empty($value))
continue;
$esc_meta_value = $wpdb->esc_like($value);
$like_meta_value = '%' . $esc_meta_value . '%';
$havings[$key] = $wpdb->prepare(
"(
`{$wpdb->posts}`.`post_title` LIKE %s
OR `{$wpdb->posts}`.`post_excerpt` LIKE %s
OR `{$wpdb->posts}`.`post_content` LIKE %s
)"
, $like_meta_value
, $like_meta_value
, $like_meta_value
);
}
}
return join(' AND ', $havings);
}
これでqueryが生成できると思います。
8/10 問題なし
これらの処理を設定してから、DB障害の発生がパッタリと無くなりました。
今のところ問題ないです。
今回の教訓としては、仕様に沿って適切なプラグインを選択しようってことですね。
勘違いしてはいけないのが、「Advanced Custom Fields」は良いプラグインです。
解りやすくて、使い易いです。
しかし、状況によってはバッドチョイスになります。
そこら辺は、しっかりテストしていきましょーよ。
今日は、こんなところでー