WordPress guarda los campos personalizados en una única tabla compartida, wp_postmeta. Cada valor es una fila con tres columnas útiles: el ID de la entrada, una clave y un valor. Ese diseño es flexible, y es la razón por la que tu catálogo se vuelve más difícil de consultar a medida que crece.
Esto no es un error de WordPress. wp_postmeta es genérica a propósito, para que cualquier plugin pueda asociar cualquier valor a cualquier entrada sin cambiar el esquema de la base de datos. El coste de esa generalidad aparece cuando hay volumen real: un catálogo de productos, un directorio de fichas o cualquier sitio con más de un puñado de entradas y más de un par de campos por entrada.
Un producto, muchas filas
Un producto con diez campos se convierte en diez filas. Veinte mil productos son 200 000 filas, y todas comparten la tabla con el resto de plugins que guardan metadatos de entradas: maquetadores, plugins de SEO, plugins de caché, tu tema. wp_postmeta suele ser la tabla más grande de una base de datos de WordPress, y sigue creciendo incluso cuando dejas de añadir productos, porque los plugins siguen añadiendo sus propias filas de mantenimiento en el mismo lugar.
Este patrón se llama EAV, siglas de entidad-atributo-valor. En lugar de una tabla con una columna por atributo, tienes una fila por cada valor de atributo, con el nombre del atributo repetido como texto en cada fila. Es una forma muy conocida de modelar datos cuya estructura no se conoce de antemano, y es justamente la razón por la que WordPress la usa para los metadatos de entradas: cualquier plugin puede definir campos nuevos sin una migración de base de datos.
La contrapartida es que la base de datos ya no puede tratar un "campo" como una columna. Tiene que tratarlo como un valor enterrado entre millones de filas sin relación, identificado solo por la coincidencia de una clave de texto.
Todos los valores son texto
meta_value es una columna LONGTEXT. Un precio de 1299.00 es la cadena "1299.00". Un stock de 50 unidades es la cadena "50". Esto no es un problema cuando se lee un único valor tal como se escribió, que es para lo que se usan casi siempre los metadatos. Deja de serlo en cuanto necesitas ordenar o comparar.
Para ordenar por precio, la base de datos tiene que convertir cada valor coincidente de texto a número antes de poder compararlos, y no puede usar un índice para esa comparación, porque el índice se construyó sobre el texto, no sobre el número que produce la conversión. El mismo problema afecta a las fechas guardadas como cadenas y a cualquier filtro numérico, como "precio inferior a 100".
Filtrar por más de un campo agrava el problema, porque cada campo vive en su propia fila. Para encontrar productos con un precio inferior a 100 y la ciudad Estambul, la consulta tiene que unir wp_postmeta consigo misma una vez por campo, relacionando cada unión por post_id y por el meta_key correspondiente:
SELECT p.ID, pm_price.meta_value AS price
FROM wp_posts p
JOIN wp_postmeta pm_price
ON pm_price.post_id = p.ID AND pm_price.meta_key = '_price'
JOIN wp_postmeta pm_city
ON pm_city.post_id = p.ID AND pm_city.meta_key = 'city'
WHERE pm_city.meta_value = 'Istanbul'
ORDER BY CAST(pm_price.meta_value AS DECIMAL(10,2));Cada campo adicional por el que filtras u ordenas es otra autounión con la misma tabla saturada, y otro CAST que la base de datos tiene que calcular para cada fila candidata antes de poder ordenar los resultados.
Qué cambia con una tabla tipada
Con una tabla por grupo de campos, un producto es una fila, no diez. El precio es una columna DECIMAL, el stock es un INT, una fecha de lanzamiento es un DATE. Cada campo tiene el tipo que debería haber tenido desde el principio, y la misma consulta necesita una sola unión con la tabla de entradas y ninguna conversión:
SELECT p.ID, d.price
FROM wp_posts p
JOIN wp_wpmcf_fg_product d ON d.mid = p.ID AND d.mtype = 'posts'
WHERE d.city = 'Istanbul'
ORDER BY d.price;Como el precio se guarda como número, ordenar por él es una ordenación numérica normal. Como es una columna real, puedes ponerle un índice normal, o ponérselo a city, igual que harías en cualquier otra tabla de cualquier otra aplicación. La base de datos hace lo que está diseñada para hacer: usar un índice para saltar directamente a las filas coincidentes en lugar de recorrerlo todo y calcular una conversión por el camino.
Rara vez tendrás que escribir ese SQL tú mismo. Pasa wpmcf_query a WP_Query y el plugin une la tabla del grupo por ti:
$products = new WP_Query( [
'post_type' => 'product',
'wpmcf_query' => [
[ 'field' => 'product.city', 'value' => 'Istanbul' ],
],
'orderby' => [ 'wpmcf:product.price' => 'ASC' ],
] );Aquí no publicamos cifras de velocidad. Dependen de tu hosting, de tus datos y de tus índices, y preferimos que midas en tu propio sitio antes que confíes en un número nuestro. Lo que importa es la diferencia estructural, con independencia de cualquier benchmark: menos filas, tipos reales e índices donde los necesitas. La sintaxis se explica en consultar entradas por valor de campo.
Mass Custom Fields crea una de estas tablas por cada grupo de campos de forma automática. Defines un campo como número, fecha o texto corto, y el plugin elige un tipo de columna adecuado; no necesitas saber SQL para obtener una tabla tipada. Si ya usas ACF, el plugin puede copiar tus grupos de campos y los valores guardados sin modificar los datos de ACF; consulta migrar desde ACF.
La página de comparación expone estas diferencias de forma objetiva.
Conclusión
wp_postmeta sirve para unos pocos ajustes por entrada. Se creó exactamente para eso: valores arbitrarios y ocasionales asociados a una entrada, que se leen de uno en uno. Para catálogos, directorios y cualquier dato que filtres u ordenes en muchas filas, dale su propia tabla: columnas tipadas, un índice normal y una consulta que la base de datos pueda optimizar de verdad.



