Прямые запросы к бд WordPress: безопасные WPDB‑запросы в 6 шагах

1. Почему иногда нужны прямые запросы к БД WordPress

Стандартный API WordPress (WP_Query, get_posts и т.д.) покрывает большинство задач, но в реальных проектах часто требуются сложные JOIN‑ы, агрегации или массовые обновления, которые невозможно реализовать через обычные функции. В таких случаях единственной надёжной опцией остаются прямые запросы к бд WordPress, но их нужно писать с учётом безопасности и производительности.

2. WPDB: базовый инструмент для безопасных запросов

2.1 Подключение и базовые методы

Класс $wpdb – глобальная обёртка над mysqli, доступная в любой части темы или плагина. Он уже знает префикс таблиц, кодировку и поддерживает транзакции.

global $wpdb;
// Пример простого SELECT без подготовки (не рекомендуется)
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_status = 'publish'" );

2.2 Подготовленные запросы (prepare)

Для защиты от инъекций используйте метод prepare(). Он заменяет плейсхолдеры %s, %d, %f на безопасные значения.

global $wpdb;
$sql = $wpdb->prepare(
    "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_status = %s AND post_type = %s",
    'publish',
    'post'
);
$posts = $wpdb->get_results( $sql );

3. Защита от SQL‑инъекций

3.1 Экранирование данных вручную

Если по какой‑то причине вы не можете использовать prepare(), применяйте $wpdb->esc_like() и $wpdb->prepare() только для переменных, а остальные части строки фиксируйте в коде.

3.2 Полный пример с $wpdb->prepare()

global $wpdb;
$search = isset($_GET['s']) ? $_GET['s'] : '';
$search_like = '%' . $wpdb->esc_like( $search ) . '%';
$sql = $wpdb->prepare(
    "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_title LIKE %s",
    $search_like
);
$found = $wpdb->get_results( $sql );

Такой запрос полностью защищён от попыток внедрить произвольный SQL.

4. Практические примеры

4.1 SELECT с подготовкой и пагинацией

global $wpdb;
$per_page = 20;
$paged    = max( 1, get_query_var('paged') );
$offset   = ( $paged - 1 ) * $per_page;
$sql = $wpdb->prepare(
    "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_status = %s ORDER BY post_date DESC LIMIT %d OFFSET %d",
    'publish', $per_page, $offset
);
$posts = $wpdb->get_results( $sql );

4.2 INSERT/UPDATE с транзакциями

global $wpdb;
$wpdb->query('START TRANSACTION');
try {
    $wpdb->insert(
        $wpdb->postmeta,
        [
            'post_id'    => 123,
            'meta_key'   => '_custom_key',
            'meta_value' => maybe_serialize( $data )
        ],
        [ '%d', '%s', '%s' ]
    );
    $wpdb->update(
        $wpdb->posts,
        [ 'post_status' => 'draft' ],
        [ 'ID' => 123 ],
        [ '%s' ], [ '%d' ]
    );
    $wpdb->query('COMMIT');
} catch ( Exception $e ) {
    $wpdb->query('ROLLBACK');
    error_log( $e->getMessage() );
}

Транзакции гарантируют, что набор запросов выполнится атомарно, а в случае ошибки – откатится.

5. Оптимизация и кэширование запросов

Прямые запросы могут стать «узким местом», если их выполнять часто. Для снижения нагрузки рекомендуется кэшировать результаты в объектный кэш.

WordPress поддерживает несколько бекендов кэша: Redis vs Memcached. Пример кэширования SELECT‑запроса через wp_cache_get() и wp_cache_set():

global $wpdb;
$cache_key = 'latest_posts_'. $paged;
$posts = wp_cache_get( $cache_key, 'my_plugin' );
if ( false === $posts ) {
    $sql = $wpdb->prepare(
        "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_status = %s ORDER BY post_date DESC LIMIT %d OFFSET %d",
        'publish', $per_page, $offset
    );
    $posts = $wpdb->get_results( $sql );
    wp_cache_set( $cache_key, $posts, 'my_plugin', HOUR_IN_SECONDS );
}

При использовании Redis кэш будет храниться в памяти, что ускорит отдачу страниц в 2–3 раза.

6. Отладка и логирование запросов

Для быстрого поиска «тормозных» запросов включите отладочный режим:

define('SAVEQUERIES', true);

После этого массив $wpdb->queries будет содержать каждый выполненный запрос, его время и стек вызовов. Выводим в админ‑панель:

add_action('admin_footer', function(){
    global $wpdb;
    echo '
Не забывайте отключать SAVEQUERIES на продакшене, иначе увеличите нагрузку и объём логов.

Для полного контроля над чистыми URL, кэшированием и безопасностью ознакомьтесь с нашими материалами: удаление category и tag из URL, кэширование браузера WordPress и миграция HTTP→HTTPS. Эти статьи помогут построить надёжную и быструю инфраструктуру вокруг ваших прямых запросов.

❓ Часто задаваемые вопросы

Можно ли использовать $wpdb->query() для SELECT‑запросов?

Да, но предпочтительно использовать $wpdb->get_results() или $wpdb->get_row(), так как они сразу возвращают массив объектов и упрощают обработку.

Как избежать двойного экранирования при использовании $wpdb->prepare()?

Передача уже экранированных значений в prepare приводит к двойному экранированию. Передавайте «чистые» переменные, а не результаты $wpdb->esc_sql().

Поддерживает ли WPDB мульти‑сайты?

Да, $wpdb автоматически учитывает префикс таблиц для каждой подсайта, поэтому запросы работают без изменений.

Можно ли кэшировать результаты $wpdb->prepare() в объектный кэш?

Определённо. Хэшировать готовый результат (массив записей) безопасно; при следующем запросе проверяйте кэш перед выполнением prepare().