JSON_SEARCH

La función JSON_SEARCH busca un valor de texto dentro de un documento JSON y devuelve la ruta donde lo encontró. Mientras que JSON_CONTAINS te dice si un valor existe (verdadero o falso), JSON_SEARCH te dice dónde está exactamente ese valor dentro de la estructura del documento. Esto resulta muy útil cuando trabajas con documentos JSON complejos y necesitas localizar en qué parte de la jerarquía se encuentra un dato específico.

Sintaxis

JSON_SEARCH(json_doc, modo, valor_buscado [, caracter_escape [, ruta ...]])

El primer argumento es el documento JSON donde se realiza la búsqueda. El segundo es el modo de búsqueda: 'one' devuelve la ruta de la primera coincidencia encontrada, mientras que 'all' devuelve un array con las rutas de todas las coincidencias. El tercer argumento es el valor de texto a buscar, que puede incluir el comodín % para coincidencias parciales (similar a LIKE). Opcionalmente, puedes especificar un carácter de escape y una o más rutas donde limitar la búsqueda. La función devuelve NULL si no encuentra coincidencias.

JSON_SEARCH(json_doc, 'one', 'valor')
JSON_SEARCH(json_doc, 'all', 'patrón%')
JSON_SEARCH(json_doc, 'all', 'valor', NULL, '$.ruta')

Es importante saber que JSON_SEARCH solo busca valores de tipo cadena de texto. No encontrará números, booleanos ni valores null.

Comportamiento básico

Empecemos buscando un valor dentro de un array simple:

SELECT JSON_SEARCH(
  '["rojo", "azul", "verde", "amarillo"]',
  'one',
  'azul'
) AS ruta;
ruta
"$[1]"

La función encontró "azul" en la posición con índice 1 del array (los arrays JSON empiezan en 0). Ahora busquemos en un objeto con estructura más compleja:

SELECT JSON_SEARCH(
  '{"producto": "iPhone", "specs": {"color": "negro", "almacenamiento": "256GB"}}',
  'one',
  'negro'
) AS ruta;
ruta
"$.specs.color"

La ruta $.specs.color indica exactamente dónde se encontró el valor "negro" dentro del documento anidado.

Cuando usas el modo 'all' y el valor aparece en varios lugares, obtienes un array con todas las rutas:

SELECT JSON_SEARCH(
  '{"color_primario": "azul", "colores_secundarios": ["rojo", "azul"], "tema": "azul"}',
  'all',
  'azul'
) AS rutas;
rutas
["$.color_primario", "$.colores_secundarios[1]", "$.tema"]

El comodín % funciona como en la cláusula LIKE, coincidiendo con cualquier secuencia de caracteres:

SELECT JSON_SEARCH(
  '["smartphone", "smartwatch", "tablet", "smart TV"]',
  'all',
  'smart%'
) AS rutas;
rutas
["$[0]", "$[1]", "$[3]"]

Los tres elementos que comienzan con "smart" fueron encontrados. El comodín _ también está disponible para coincidir con un solo carácter.

Imagina un catálogo de productos donde cada producto tiene etiquetas en distintos niveles de su estructura JSON:

CREATE TABLE catalogo (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nombre VARCHAR(100),
    metadata JSON
);
 
INSERT INTO catalogo (nombre, metadata) VALUES
('iPhone 15 Pro',
 '{"categoría": "smartphone", "etiquetas": ["premium", "5g", "fotografía"], "fabricante": {"nombre": "Apple", "país": "EEUU"}}'),
('Cámara Sony A7',
 '{"categoría": "fotografía", "etiquetas": ["profesional", "mirrorless", "fotografía"], "fabricante": {"nombre": "Sony", "país": "Japón"}}'),
('DJI Mini 4 Pro',
 '{"categoría": "dron", "etiquetas": ["fotografía", "vídeo", "portátil"], "fabricante": {"nombre": "DJI", "país": "China"}}');

Para encontrar en qué parte de cada documento aparece la palabra "fotografía":

SELECT
    nombre,
    JSON_SEARCH(metadata, 'all', 'fotografía') AS ubicaciones
FROM catalogo;
nombreubicaciones
iPhone 15 Pro"$.etiquetas[2]"
Cámara Sony A7["$.categoría", "$.etiquetas[2]"]
DJI Mini 4 Pro"$.etiquetas[0]"

En la Cámara Sony A7, "fotografía" aparece tanto en la categoría como en las etiquetas, por lo que se devuelven ambas rutas. En los demás productos aparece una sola vez, y la función devuelve directamente una cadena en lugar de un array.

Para buscar todos los productos de fabricantes cuyo nombre empieza con una letra específica:

SELECT nombre, metadata->>'$.fabricante.nombre' AS fabricante
FROM catalogo
WHERE JSON_SEARCH(metadata, 'one', 'S%', NULL, '$.fabricante.nombre') IS NOT NULL;
nombrefabricante
Cámara Sony A7Sony

El cuarto argumento NULL indica que usamos el carácter de escape predeterminado. El quinto argumento limita la búsqueda a la ruta $.fabricante.nombre.

Caso práctico: búsqueda en configuraciones de usuario

Considera un sistema donde los usuarios almacenan configuraciones con múltiples niveles de anidamiento:

CREATE TABLE configuraciones (
    id INT PRIMARY KEY AUTO_INCREMENT,
    usuario VARCHAR(100),
    config JSON
);
 
INSERT INTO configuraciones (usuario, config) VALUES
('María García',
 '{"tema": "oscuro", "idioma": "español", "notificaciones": {"email": "activo", "sms": "inactivo", "push": "activo"}, "panel": {"vista": "compacta", "orden": "reciente"}}'),
('Carlos López',
 '{"tema": "claro", "idioma": "español", "notificaciones": {"email": "activo", "sms": "activo", "push": "inactivo"}, "panel": {"vista": "detallada", "orden": "nombre"}}');

Para encontrar todos los campos donde un usuario tiene el valor "activo":

SELECT
    usuario,
    JSON_SEARCH(config, 'all', 'activo') AS campos_activos
FROM configuraciones;
usuariocampos_activos
María García["$.notificaciones.email", "$.notificaciones.push"]
Carlos López["$.notificaciones.email", "$.notificaciones.sms"]

Esto te permite saber exactamente qué notificaciones tiene cada usuario activadas sin necesidad de consultar cada campo por separado.

Para buscar todos los campos que contienen la palabra "compacta" o cualquier valor que empiece con "comp":

SELECT
    usuario,
    JSON_SEARCH(config, 'all', 'comp%') AS coincidencias
FROM configuraciones;
usuariocoincidencias
María García"$.panel.vista"
Carlos LópezNULL

Carlos no tiene ningún valor que comience con "comp", por lo que obtiene NULL.

Caso práctico: auditoría de datos sensibles

JSON_SEARCH puede usarse para auditar documentos JSON y encontrar si contienen patrones de datos que no deberían estar almacenados, como fragmentos de emails:

SELECT
    id,
    nombre,
    JSON_SEARCH(metadata, 'one', '%@%') AS contiene_email
FROM catalogo;

Si algún documento contuviera accidentalmente una dirección de email en cualquier campo, JSON_SEARCH la localizaría. Cuando el resultado es NULL, significa que no se encontró ninguna coincidencia.

Manejo de NULL

Cuando el documento JSON es NULL, la función devuelve NULL:

SELECT JSON_SEARCH(NULL, 'one', 'valor') AS resultado;
resultado
NULL

Cuando no se encuentra el valor buscado, también devuelve NULL:

SELECT JSON_SEARCH('{"clave": "valor"}', 'one', 'inexistente') AS resultado;
resultado
NULL

Esta es la forma de verificar si un valor de texto existe dentro de un documento JSON: si JSON_SEARCH devuelve NULL, el valor no está presente. Por eso en las cláusulas WHERE se usa IS NOT NULL para filtrar:

SELECT nombre
FROM catalogo
WHERE JSON_SEARCH(metadata, 'one', 'premium') IS NOT NULL;
nombre
iPhone 15 Pro

Combinación con otras funciones

Un patrón muy útil es combinar JSON_SEARCH con JSON_EXTRACT para obtener valores dinámicamente basándose en la ubicación de otro valor:

SELECT
    nombre,
    JSON_SEARCH(metadata, 'one', 'fotografía') AS ruta_encontrada,
    JSON_EXTRACT(
        metadata,
        JSON_UNQUOTE(JSON_SEARCH(metadata, 'one', 'fotografía'))
    ) AS valor_verificado
FROM catalogo
WHERE JSON_SEARCH(metadata, 'one', 'fotografía') IS NOT NULL;
nombreruta_encontradavalor_verificado
iPhone 15 Pro"$.etiquetas[2]""fotografía"
Cámara Sony A7"$.categoría""fotografía"
DJI Mini 4 Pro"$.etiquetas[0]""fotografía"

Observa que JSON_SEARCH devuelve la ruta entre comillas, por lo que necesitas JSON_UNQUOTE para pasarla como argumento a JSON_EXTRACT. Este patrón, aunque poco frecuente, demuestra cómo las funciones JSON de MySQL se complementan entre sí para resolver problemas complejos. En el siguiente artículo veremos JSON_TYPE para determinar el tipo de un valor JSON.

Escrito por Eduardo Lázaro