WordPress SQL Query Generator
Write a $wpdb query with prepare() used correctly: placeholders for values, an allowlist for table and column names, and the right get_ method.
<?php
/**
* A direct database query.
*
* Reach for WP_Query, get_posts() or get_terms() first: they are cached and
* they respect every filter a plugin has added. This is for the cases they
* genuinely cannot express.
*/
/**
* Runs the query.
*
* @return mixed
*/
function my_plugin_query() {
global $wpdb;
$table = $wpdb->posts;
// A direct query is not cached by WordPress. Without a persistent object
// cache this is per request, which still helps on a page that asks twice.
$key = 'my_plugin_query';
$cached = wp_cache_get( $key, 'my_plugin' );
if ( false !== $cached ) {
return $cached;
}
// The SQL is built with concatenation rather than interpolation, so the
// only things that reach the query unescaped are written here by you.
$sql = 'SELECT ID, post_title FROM ' . $table;
$args = array();
$sql .= ' WHERE 1 = 1';
$sql .= ' AND post_status = %s';
$args[] = 'publish';
$sql .= ' AND post_type = %s';
$args[] = 'post';
// A column name cannot be a placeholder, so it is written here and never
// taken from a request.
$sql .= ' ORDER BY post_date DESC';
$sql .= ' LIMIT %d';
$args[] = 20;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is built above from literals.
$result = $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
wp_cache_set( $key, $result, 'my_plugin', 5 * MINUTE_IN_SECONDS );
return $result;
}
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
Write a $wpdb query with prepare() used the way it is meant to be used: placeholders for values, table and column names written by you, and the get_ method that matches what you want back.
How to use
- Check first that
WP_Query,get_posts()orget_terms()cannot do it. They are cached, they respect every filter a plugin added, and they do not go stale when the schema changes. - Put values in placeholders:
%s,%d,%f. That is whatprepare()escapes. - Write table and column names yourself. They cannot be placeholders, so anything from a request must be checked against an allowlist first.
- Set a limit. A query with no limit against
wp_postmetais one of the classic ways to take a site down. - Cache the result. A direct query is not cached by WordPress at all, and wrapping it is usually the whole performance fix.
Example
global $wpdb;
$sql = 'SELECT ID, post_title FROM ' . $wpdb->posts;
$sql .= ' WHERE post_status = %s AND post_type = %s';
$sql .= ' ORDER BY post_date DESC LIMIT %d';
$rows = $wpdb->get_results( $wpdb->prepare( $sql, 'publish', 'post', 20 ) );
Building the SQL by concatenation rather than interpolation is deliberate: with "… {$table} …" it is one careless edit before a variable that came from a request ends up in the query.
Pitfalls
prepare()escapes values, not identifiers.ORDER BY %sdoes not work, and a column name from$_GETis an injection whatever you do with it.- Quoting a placeholder yourself, as
'%s', breaks the escaping:prepare()adds the quotes. esc_like()exists because%and_are wildcards inLIKE. Without it, a search for100%matches everything.$wpdb->prefixis the current site’s prefix. On multisite,$wpdb->base_prefixis the network one, and using the wrong one reads another site’s data.$wpdb->get_results()loads every row into memory. A limit is not optional on a large table.- Direct queries bypass every filter, so a plugin that adds conditions to post queries has no effect on yours.
$wpdb->insert()and$wpdb->update()escape for you. Hand-written INSERT statements are where most injection bugs in WordPress plugins live.$wpdb->last_erroris the only place a failed query reports itself. A silentfalsereturn is easy to miss.
Compatibility
$wpdb, prepare(), get_results(), get_row(), get_var(), insert() and update() have all been stable since WordPress 3.0. esc_like() needs 4.0, and passing an array of arguments to prepare() needs 4.9. The wp_cache_* functions work without a persistent cache, where they are per request. The generated code targets PHP 7.0 and up, and the tool runs entirely in your browser.
Frequently asked questions
When should I use $wpdb at all?
WP_Query or a term or user query: a report, an aggregate, or your own table.How do I make ORDER BY safe?
Why is my LIKE matching everything?
% or _. Run it through $wpdb->esc_like() first.Does this work on multisite?
$wpdb->prefix gives the current site’s tables. For network-wide tables, use $wpdb->base_prefix.How do I see why a query failed?
$wpdb->last_error after the call, and $wpdb->last_query for the SQL that actually ran.