SqlMon: плагин для анализа SQL-запросов
Меня всегда интересовало, насколько эффективно WordPress работает с базой данных, и насколько хорошо спроектирована база данных.
Практически в каждом проекте, над которым я работаю, я использую те или иные средства для анализа производительности скрипта и поиска его слабых мест. Для разработчиков не является секретом, что во многих случаях плохая производительность работы скрипта обусловлена низкой производительностью SQL-запросов. И, как правило, низкое быстродействие запросов связано с их неоптимальностью (что включает в себя отсутствие необходимых индексов в базе данных).
Однажды столкнувшись с ужасной производительностью WordPress и не имея возможности анализировать код десятка поставленных плагинов, я стал решать задачу иначе, в результате чего родился плагин для анализа SQL-запросов.
Принцип работы. Плагин устанавливает обработчики действий, которые позволяют перехватить запрос к базе данных непосредственно перед его выполнением. Если перехваченный запрос — SELECT
, UPDATE
или DELETE
, то скрипт пытается его "объяснить" — выполнить EXPLAIN
над запросом. Предвидя возражения, что EXPLAIN
работает только с SELECT
, но никак не с DELETE
и UPDATE
, поясняю: запросы DELETE
и UPDATE
переписываются в SELECT
, например:
DELETE `t1` FROM `t1` INNER JOIN `t2` ON `t1`.`id` = `t2`.`id` WHERE `t2`.`key` = 'value' LIMIT 5
будет переписан в
EXPLAIN SELECT * FROM `t1` INNER JOIN `t2` ON `t1`.`id` = `t2`.`id` WHERE `t2`.`key` = 'value' LIMIT 5
Возможно, переписывание работает не всегда, но такие случаи мне еще не встречались
Я предполагаю, что Вы понимаете, для чего нужен EXPLAIN
и как следует понимать результаты, которые он выдаёт. Если это не так, то очень рекомендую прочитать статью "Optimizing Queries with EXPLAIN", а затем вернуться к данной статье.
Плагин в футере темы (кстати, это относится и к админке) выдаст подробный лог запросов с их объяснением. Это может выглядеть примерно так:
Установка. К сожалению, установка данного плагина полностью не автоматизируется: кое-что надо делать вручную. После активации плагина в WordPress нужно выполнить следующие действия:
- Добавить в файл
/wp-config.php
следующие строки:[-]View Code PHPdefine('SQLMON_ENABLED', true); $sqlmon_allowed_ips = array('70.87.222.86', '195.10.218.132', '127.0.0.1');
Первая строка активирует режим перехвата и анализа запросов, вторая строка содержит массив с IP-адресами, которым разрешен просмотр лога запросов. Естественно, нужно указать свои адреса
- Далее предстоит пропатчить один файлик WordPress:
/wp-includes/wp-db.php
. Сразу объясню, зачем это нужно: WordPress не предоставляет нормальных возможностей отловить запрос до его выполнения. Фильтрquery
для наших целей не подходит: во-первых, последующий плагин может переписать запрос, во-вторых, я уже насмотрелся на плагины, которые лезут переписывать запрос, если видят SELECT. Проверено, что переписывание EXPLAIN SELECT такими плагинами приводит к синтаксической ошибке.Теперь о том, что нужно менять. В файле
/wp-includes/wp-db.php
есть класс wpdb, в котором есть метод query. В этом методе нужно найти строки[-]View Code PHP// Perform the query via std mysql_query function.. if (SAVEQUERIES) $this->timer_start(); $this->result = @mysql_query($query, $this->dbh); ++$this->num_queries;
и изменить их:
[-]View Code PHP// Perform the query via std mysql_query function.. if (SAVEQUERIES) $this->timer_start(); if (true == function_exists('do_action')) { do_action('before_query', $query, $this->dbh); } $this->result = @mysql_query($query, $this->dbh); if (true == function_exists('do_action')) { do_action('after_query', $query, $this->dbh); } ++$this->num_queries;
Для тех, кто предпочитает иметь дело с патчами, привожу патч в формате unified diff (внимание: патч проверялся только на WordPress 2.5.1):
[-]View Code Diff--- wp-db.old.php 2008-03-21 01:34:32.000000000 +0200 +++ wp-db.php 2008-06-13 03:02:27.000000000 +0300 @@ -272,7 +272,16 @@ if (SAVEQUERIES) $this->timer_start(); + if (true == function_exists('do_action')) { + do_action('before_query', $query, $this->dbh); + } + $this->result = @mysql_query($query, $this->dbh); + + if (true == function_exists('do_action')) { + do_action('after_query', $query, $this->dbh); + } + ++$this->num_queries; if (SAVEQUERIES)
После этого перезагружаем страницу и смотрим