How to read and write data using MediaWiki’s query builder APIs.
MediaWiki provides query builder classes for SELECT, INSERT, UPDATE, and DELETE operations. These builders handle table name prefixes, value escaping, and cross-database syntax differences automatically. Raw SQL via IDatabase::query() is available but strongly discouraged.
use MediaWiki\MediaWikiServices;$dbProvider = MediaWikiServices::getInstance()->getConnectionProvider();$dbr = $dbProvider->getReplicaDatabase(); // for reads$dbw = $dbProvider->getPrimaryDatabase(); // for writes
See Database Overview for the full connection and read/write split documentation.
For more complex conditions, pass a string or use IExpression:
// IN list->where( [ 'page_namespace' => [ NS_MAIN, NS_TALK, NS_USER ] ] )// Raw expression (must be pre-escaped or use IDatabase methods)->where( [ 'rev_timestamp > ' . $dbr->addQuotes( $cutoff ) ] )// Null check->where( [ 'wl_notificationtimestamp' => null ] )// NOT NULL->where( $dbr->expr( 'wl_notificationtimestamp', '!=', null ) )
Never interpolate raw user input into SQL strings. Always use addQuotes() for scalar values or pass them through the array syntax which handles escaping automatically.
Always specify limit() in queries that could match large numbers of rows. Unindexed queries that scan entire tables are generally not acceptable in MediaWiki code.
MySQL allows GROUP BY without listing all non-aggregate SELECT columns; PostgreSQL does not. Always list every non-aggregate column in groupBy(). Never use SELECT * with GROUP BY.
Prefer INSERT IGNORE with a unique index and affectedRows() over checking for existence first, then inserting. This avoids a race condition between the check and the write.
Null checks in the array-style where() generate IS NULL / IS NOT NULL SQL automatically:
// WHERE wl_notificationtimestamp IS NULL->where( [ 'wl_notificationtimestamp' => null ] )// WHERE wl_notificationtimestamp IS NOT NULL->where( $dbr->expr( 'wl_notificationtimestamp', '!=', null ) )
When inserting or updating, pass null directly in the value array:
The query builders escape values passed through their array APIs automatically. When you need to construct a raw expression, use the escaping methods on IDatabase:
// Escape a scalar value for use in a raw expression$quoted = $dbr->addQuotes( $userInput );->where( [ 'rc_timestamp > ' . $quoted ] )// Get a properly prefixed table name for use in raw SQL$table = $dbr->tableName( 'page' );// Get a MediaWiki timestamp (YYYYMMDDHHMMSS)$ts = $dbw->timestamp(); // current time$ts = $dbw->timestamp( $unixTs ); // convert a Unix timestamp
Never use raw user input directly in SQL strings. Always use addQuotes() for values, and let the query builder handle table name prefixes via tableName(). Failing to do so creates SQL injection vulnerabilities.
Every query run during a web request must use an index. Unindexed queries that grow with table size are not accepted. Special pages may use QueryPage (which runs in $wgMiserMode) for expensive unindexed queries.
Avoid COUNT(*)
COUNT(*) is O(N) — it counts every matching row. For large tables this is extremely slow. Use estimated counts, index-covered counts, or cached values instead.
Avoid SELECT *
Always name the columns you need. SELECT * wastes bandwidth, breaks GROUP BY on PostgreSQL, and makes query behavior unpredictable when schema changes add columns.
Batch large writes
Split bulk inserts or updates into one-row-at-a-time loops. Multi-row INSERT ... SELECT serializes on replicas and causes replication lag. Do the SELECT first, then insert row by row.
For queries expected to run for more than one second on large wikis, use the vslow query group to route them to dedicated replica hosts and keep them from degrading general traffic:
$dbr = $dbProvider->getReplicaDatabase( false, 'vslow' );$res = $dbr->newSelectQueryBuilder() ->select( [ 'page_id', 'page_title' ] ) ->from( 'page' ) ->where( [ 'page_namespace' => NS_MAIN ] ) // ... expensive conditions that cannot use an index ... ->caller( __METHOD__ ) ->fetchResultSet();
For complex queries not expressible with the query builders, use IDatabase::query():
// Use tableName() to get the properly prefixed table name$pageTable = $dbr->tableName( 'page' );$revTable = $dbr->tableName( 'revision' );$sql = "SELECT p.page_id, COUNT(r.rev_id) AS rev_count FROM $pageTable p JOIN $revTable r ON r.rev_page = p.page_id WHERE p.page_namespace = 0 GROUP BY p.page_id ORDER BY rev_count DESC LIMIT 10";$res = $dbr->query( $sql, __METHOD__ );foreach ( $res as $row ) { echo "{$row->page_id}: {$row->rev_count} revisions\n";}
Raw SQL bypasses all cross-database abstraction. If you use query(), you must test on all supported backends (MySQL, PostgreSQL, SQLite) or explicitly document which backends you support.