WP_Query se lleva la mayoría de los tutoriales. WP_User_Query es lo que realmente ejecutan los sitios de membresía, los directorios de personal y las plataformas comunitarias.
Si solo necesita una cuadrícula estática de “Nuestro equipo”, una consulta ligera en la plantilla gana a otro plugin de membresía que carga sus propias plantillas, rutas REST y CSS en cada petición. Esta guía se centra en ese problema: filtrar usuarios con role__in, meta_query y paginación sin quemar la base de datos ni filtrar nombres de login.
Referencia oficial: WP_User_Query en developer.wordpress.org.
get_users() vs WP_User_Query
get_users() es un wrapper fino sobre WP_User_Query. Acepta el mismo array de argumentos y devuelve un array de objetos WP_User (o subconjuntos de campos cuando pasa fields).
Use get_users() cuando quiera una lista corta y nada más: por ejemplo cinco editores para un widget de firmas.
Use WP_User_Query cuando necesite:
get_results()másget_total()para directorios paginados- inspección de
$query->requestmientras depura SQL - la misma forma de argumentos que
get_users(), pero con ciclo de vida explícito del objeto de consulta
En ambas APIs el array de argumentos es el contrato. Todo lo que sigue funciona con cualquiera de las dos formas; los ejemplos usan WP_User_Query porque los directorios necesitan totales.
Construir una página de equipo con role__in
Editores y autores, ordenados por nombre para mostrar, doce por página:
$paged = max( 1, (int) get_query_var( 'paged', 1 ) );
$args = [
'role__in' => [ 'editor', 'author' ],
'orderby' => 'display_name',
'order' => 'ASC',
'number' => 12,
'paged' => $paged,
'fields' => [ 'ID', 'display_name' ],
];
$user_query = new WP_User_Query( $args );
$results = $user_query->get_results();
if ( ! empty( $results ) ) {
echo '<div class="team-grid">';
foreach ( $results as $user ) {
$avatar = get_avatar( $user->ID, 128 );
$name = esc_html( $user->display_name );
$bio = esc_html( get_user_meta( $user->ID, 'description', true ) );
echo "<article class='team-member'>
<figure>{$avatar}</figure>
<h3>{$name}</h3>
<p>{$bio}</p>
</article>";
}
echo '</div>';
}Notas que importan en producción:
role__incoincide con cualquiera de los roles listados. Prefiéralo a varias consultas conrole.fieldsdevuelve filasstdClassen lugar de objetosWP_Usercompletos, lo que recorta memoria cuando solo necesita IDs y nombres.- Las biografías siguen necesitando una lectura de meta. Si la cuadrícula es caliente, cachee el HTML renderizado o precalcule una fila de directorio en una tabla personalizada.
Paginación sin cargar todos los usuarios
Los argumentos de paginación son number (tamaño de página) y paged (base 1). Tras la consulta, get_total() es el conteo de coincidencias en todas las páginas, no solo en la actual.
$per_page = 24;
$paged = max( 1, (int) ( $_GET['udir_page'] ?? 1 ) );
$query = new WP_User_Query(
[
'role__in' => [ 'subscriber', 'contributor' ],
'number' => $per_page,
'paged' => $paged,
'fields' => 'ID',
'orderby' => 'registered',
'order' => 'DESC',
]
);
$total_users = (int) $query->get_total();
$total_pages = (int) ceil( $total_users / $per_page );
$user_ids = $query->get_results();
// Construya enlaces con add_query_arg( 'udir_page', $n ) y escape las URLs.Antipatrones a evitar:
- Pedir
'number' => -1(u omitirlo) y recortar conarray_sliceen PHP - Lanzar una segunda consulta completa solo para contar filas cuando
get_total()ya hizo el trabajo - Meter valores no confiables de
$_GETenorderbysin una lista blanca estricta
En directorios públicos use una query var dedicada (como arriba) para no chocar con el bucle principal paged del blog.
Filtrado avanzado con meta_query
meta_query es donde los directorios se vuelven útiles - y donde aparece el coste de MySQL. Ejemplo: suscriptores en Madrid, solo perfiles públicos, con PHP listado en un meta de habilidades.
$args = [
'role' => 'subscriber',
'number' => 20,
'paged' => 1,
'fields' => [ 'ID', 'display_name' ],
'meta_query' => [
'relation' => 'AND',
[
'key' => 'city',
'value' => 'Madrid',
'compare' => '=',
],
[
'key' => 'is_public_profile',
'value' => '1',
'compare' => '=',
],
[
'key' => 'skills',
'value' => 'PHP',
'compare' => 'LIKE', // funciona con arrays serializados; caro a escala
],
],
];
$directory = new WP_User_Query( $args );Reglas prácticas para meta_query sobre usuarios:
- Prefiera compares exactos
=sobre claves dedicadas (city,is_public_profile) frente aLIKEsobre blobs serializados. - Guarde habilidades multivalor como metas booleanas separadas (
skill_php=1) si las filtra a menudo. Así OR/AND se convierten en igualdades amigables con índices. - Combine
relationcon cuidado. Los grupos anidados son válidos, pero cada cláusula es otro join sobrewp_usermeta. - Empareje siempre los filtros públicos con un flag de visibilidad para que los perfiles privados no aparezcan en listados no autenticados.
Escollos de rendimiento
Las consultas de usuarios parecen baratas hasta que el directorio es público y filtrado.
Limitar los campos devueltos
Por defecto WordPress hidrata más datos de los que necesita una cuadrícula de tarjetas. Pase fields pronto:
$args = [
'role' => 'subscriber',
'number' => 100,
'fields' => [ 'ID', 'display_name', 'user_email' ],
];Quite user_email de las plantillas públicas. Resérvelo solo para herramientas de personal detrás de comprobaciones de capacidades.
Contar sin hidratar perfiles
$query = new WP_User_Query(
[
'role' => 'subscriber',
'fields' => 'ID',
]
);
$count = $query->get_total();Para totales por rol sin filtros, count_users() es más barato porque usa los conteos de rol que WordPress ya mantiene.
Cachear conjuntos de resultados estables
Si el directorio cambia poco, guarde IDs (o marcado renderizado) en un transient con clave por hash de filtros. Invalide el transient cuando se actualice el meta de perfil vía updated_user_meta para las claves que filtra. No cachee HTML que incluya campos condicionados por capacidades para visitantes anónimos.
Saber cuándo MySQL es el índice equivocado
Los joins sobre wp_usermeta no escalan como un motor de búsqueda. Cuando los filtros se multiplican (ciudad + especialidad + idioma + disponibilidad) y la audiencia es grande, mueva la proyección buscable a Elasticsearch (ElasticPress o un índice propio) o a una tabla de directorio dedicada con índices reales. Conserve WP_User_Query para la hidratación final de una lista pequeña de IDs.
Evitar lecturas N+1 de avatar y meta
Dentro del bucle, get_avatar() y llamadas repetidas a get_user_meta() se multiplican. Precargue claves conocidas con una sola consulta a wp_usermeta para el conjunto de IDs de la página, o use una fila de directorio desnormalizada escrita al guardar el perfil.
Seguridad: enumeración de usuarios en directorios
Un directorio de miembros es una lista pública de cuentas. Trátelo como tal.
- Nunca imprima
user_loginen el marcado del frontend. Usedisplay_nameo un apodo público dedicado. - Nunca imprima
user_emailsalvo que el visitante esté autenticado y autorizado a verlo. - Los archivos de autor (
/?author=1) siguen filtrando logins en muchos sitios. Si el producto no necesita archivos de autor públicos, rediríjalos:
add_action(
'template_redirect',
static function () {
if ( is_author() ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
}
);- Escape todo lo que salga de PHP:
esc_html(),esc_url(),esc_attr()en atributos. - Condicione por capacidades los directorios solo-admin (
list_users) en lugar de confiar en la oscuridad de la URL.
Montar una plantilla de directorio
Un directorio público durable suele tener cuatro capas:
- Sanitización de entrada - claves de orden en lista blanca, página entera, slug de ciudad normalizado.
- Consulta -
role__in/meta_query/fields/number/paged. - Presentación - tarjetas solo con IDs y campos de visualización seguros.
- Invalidación de cache - ligada a las claves meta que afectan a la visibilidad.
Deje la UI del plugin para el alta de cuentas y la mensajería si necesita esas funciones. Deje el listado y el filtrado en su tema o en un mu-plugin pequeño que controle usted. Esa separación es lo que mantiene el directorio rápido cuando marketing añade otro filtro el trimestre siguiente.
Multisite y tablas de usuarios compartidas
En una red WordPress multisite la tabla de usuarios es compartida. WP_User_Query sigue funcionando, pero los filtros de rol son por sitio. Un usuario que es Editor en el sitio A puede no tener rol en el sitio B. Para un directorio de personas a escala de red:
- Prefiera
blog_idcuando deba acotar roles a un solo sitio. - No asuma que
role__in => array( 'author' )significa las mismas personas en cada blog. - Para listas de personal entre sitios, guarde un flag de meta de red (por ejemplo
wpp_show_in_directory) y consulte eso en lugar de roles solos.
Las copias de staging de multisite a menudo recortan usuarios. Mida el coste de la consulta sobre un dump que coincida con la cardinalidad de producción, o su plan de meta_query parecerá fino hasta el día del lanzamiento.
Probar el SQL que realmente envía
Antes de fusionar:
- Active Query Monitor en staging.
- Abra la URL del directorio con la combinación de filtros más pesada que use marketing.
- Confirme una consulta primaria de usuarios, no una consulta por tarjeta para el meta.
- Guarde
$query->requesten las notas del PR para que la siguiente persona detecte regresiones.
Si el directorio debe soportar búsqueda de texto libre sobre nombres para mostrar, use un índice de búsqueda dedicado o un argumento search bien acotado en WP_User_Query. No haga LIKE sobre user_email en un endpoint público. Registre las peticiones lentas del directorio con el payload de filtros para poder reproducirlas después.
¿Necesita revisar un directorio WordPress o una consulta de membresía antes de producción? Contacte con WPPoland.
Lista de comprobación antes de publicar
- Use
fieldspara no hidratar objetos de usuario completos en cuadrículas de tarjetas. - Paginate con
number+pagedy confíe enget_total()para la matemática de páginas. - Prefiera claves meta exactas frente a
LIKEsobre arrays de habilidades serializados. - Cachee listas de IDs o marcado cuando los filtros sean estables; invalide al escribir perfiles.
- Renderice solo
display_name; oculte logins y correos del HTML público. - Mida el SQL (
$query->request) en staging con volumen realista de usuarios y meta antes del lanzamiento.
Los usuarios son objetivos de consulta de primera clase en WordPress. Consúltelos con la misma disciplina que ya aplica a WP_Query.







