WP_User_Query is the correct WordPress API for querying users — the user equivalent of WP_Query for posts. Like WP_Query, it translates a PHP argument array into an optimized SQL query against wp_users and wp_usermeta, handles caching, and supports pagination. The older get_users() function is a wrapper around WP_User_Query and returns an array of results directly — it is convenient for simple queries. For complex queries (multiple meta conditions, search across multiple fields, capability-based filtering, or when you need the total count for pagination), working with WP_User_Query directly gives you full control. Common use cases include building admin screens that list users filtered by role or meta, finding users who registered in a date range, searching users by a custom profile field, or exporting user data for a CRM.
Problem: A client admin page needs to list all users with the role subscriber who have a specific custom meta field set, have a Gmail email address, and registered in the last 30 days — with pagination showing 20 users per page.
Solution: Use WP_User_Query with role, meta_query, search, and date_query arguments. Retrieve get_total() for pagination, then loop get_results() for the current page.
<?php
// ── Basic user query ──────────────────────────────────────────────────
$query = new WP_User_Query( [
'role' => 'subscriber',
'number' => 20,
'offset' => 0,
'orderby' => 'registered',
'order' => 'DESC',
] );
$users = $query->get_results(); // array of WP_User objects
$total = $query->get_total(); // total matching users (ignores number/offset)
// ── Full complex query ─────────────────────────────────────────────────
$per_page = 20;
$page = max( 1, absint( $_GET['paged'] ?? 1 ) );
$offset = ( $page - 1 ) * $per_page;
$user_query = new WP_User_Query( [
'role' => 'subscriber',
'number' => $per_page,
'offset' => $offset,
'orderby' => 'registered',
'order' => 'DESC',
// Search by email domain
'search' => '*@gmail.com',
'search_columns' => [ 'user_email' ],
// Custom meta condition
'meta_query' => [
'relation' => 'AND',
[
'key' => 'onboarding_complete',
'value' => '1',
'compare' => '=',
],
[
'key' => 'company',
'compare' => 'EXISTS', // field must exist and be non-empty
],
],
// Registered in the last 30 days
'date_query' => [
[
'after' => '30 days ago',
'inclusive' => true,
'column' => 'user_registered',
],
],
] );
$users = $user_query->get_results();
$total = $user_query->get_total();
$pages = ceil( $total / $per_page );
foreach ( $users as $user ) {
printf(
'%s (%s) — registered %s',
esc_html( $user->display_name ),
esc_html( $user->user_email ),
esc_html( $user->user_registered )
);
$company = get_user_meta( $user->ID, 'company', true );
echo esc_html( $company ? " — $company" : '' ) . "
";
}
// ── Get only user IDs (more efficient when you only need IDs) ─────────
$id_query = new WP_User_Query( [
'role' => 'administrator',
'fields' => 'ID', // return only IDs, not full WP_User objects
] );
$admin_ids = $id_query->get_results(); // [1, 3, 7]
// ── Count users by role ───────────────────────────────────────────────
$count_users = count_users(); // ['avail_roles' => ['administrator' => 2, 'subscriber' => 145, ...]]
$subscriber_count = $count_users['avail_roles']['subscriber'] ?? 0;
NOTE: WP_User_Query does not have a no_found_rows equivalent — it always counts total results when count_total is true (the default), which adds a SQL_CALC_FOUND_ROWS to the query. Set 'count_total' => false if you don't need pagination and want a faster query. The search argument uses a LIKE comparison against the columns listed in search_columns (default: login, email, URL, display name). The wildcard * in the search string maps to SQL % — so '*@gmail.com' becomes LIKE '%@gmail.com'. For multisite, add 'blog_id' => get_current_blog_id() to scope results to the current site only.