JSON_MERGE_PRESERVE

La función JSON_MERGE_PRESERVE combina dos o más documentos JSON preservando todos los valores, sin descartar ninguno. Cuando dos objetos tienen la misma clave, en lugar de reemplazar el valor anterior (como hace JSON_MERGE_PATCH), JSON_MERGE_PRESERVE combina ambos valores en un array. Y cuando combina arrays, los concatena. Esta semántica de "conservar todo" la convierte en la función ideal para recopilar datos de múltiples fuentes, agregar listas de elementos, o construir historiales donde ningún valor debe perderse.

Sintaxis

JSON_MERGE_PRESERVE(json_doc, json_doc [, json_doc ...])

La función recibe dos o más documentos JSON y los combina de izquierda a derecha. Las reglas de combinación dependen de los tipos de los valores involucrados: si ambos son arrays, se concatenan; si ambos son objetos, se fusionan recursivamente combinando los valores de claves duplicadas; si uno o ambos son escalares, se convierten en array y se combinan. El resultado preserva absolutamente todos los valores de todos los documentos.

JSON_MERGE_PRESERVE(doc1, doc2)
JSON_MERGE_PRESERVE(doc1, doc2, doc3)

Comportamiento básico

Veamos cómo se comporta la función con arrays. Cuando combinas dos arrays, se concatenan:

SELECT JSON_MERGE_PRESERVE(
  '["rojo", "azul"]',
  '["verde", "amarillo"]'
) AS resultado;
resultado
["rojo", "azul", "verde", "amarillo"]

Cuando combinas dos objetos con claves distintas, el resultado incluye todas las claves de ambos:

SELECT JSON_MERGE_PRESERVE(
  '{"nombre": "iPhone", "precio": 1299}',
  '{"color": "negro", "stock": 45}'
) AS resultado;
resultado
{"color": "negro", "nombre": "iPhone", "precio": 1299, "stock": 45}

La diferencia clave con JSON_MERGE_PATCH aparece cuando los objetos tienen claves en común. En lugar de reemplazar, JSON_MERGE_PRESERVE combina los valores en un array:

SELECT JSON_MERGE_PRESERVE(
  '{"nombre": "iPhone", "tag": "premium"}',
  '{"nombre": "iPhone 15 Pro", "tag": "5g"}'
) AS resultado;
resultado
{"nombre": ["iPhone", "iPhone 15 Pro"], "tag": ["premium", "5g"]}

Ambos valores de nombre y tag se conservaron dentro de arrays. Ningún dato se perdió.

Cuando un valor ya es un array y el otro es un escalar, el escalar se añade al array:

SELECT JSON_MERGE_PRESERVE(
  '{"tags": ["premium", "apple"]}',
  '{"tags": "5g"}'
) AS resultado;
resultado
{"tags": ["premium", "apple", "5g"]}

Y cuando ambos valores son arrays, se concatenan:

SELECT JSON_MERGE_PRESERVE(
  '{"tags": ["premium", "apple"]}',
  '{"tags": ["5g", "smartphone"]}'
) AS resultado;
resultado
{"tags": ["premium", "apple", "5g", "smartphone"]}

Caso práctico: recopilar etiquetas de múltiples fuentes

Imagina que diferentes departamentos de una empresa etiquetan los mismos productos con clasificaciones distintas. Marketing pone etiquetas comerciales, almacén pone etiquetas logísticas, y el departamento técnico pone etiquetas de especificaciones:

CREATE TABLE etiquetado (
    id INT PRIMARY KEY AUTO_INCREMENT,
    producto_id INT,
    departamento VARCHAR(50),
    etiquetas JSON
);
 
INSERT INTO etiquetado (producto_id, departamento, etiquetas) VALUES
(1, 'Marketing', '{"tags": ["premium", "bestseller"], "público": "profesional"}'),
(1, 'Almacén', '{"tags": ["frágil", "electrónica"], "ubicación": "A3-15"}'),
(1, 'Técnico', '{"tags": ["5g", "oled"], "público": "avanzado"}');

Para consolidar todas las etiquetas del producto 1 en un solo documento:

SELECT JSON_MERGE_PRESERVE(
  '{"tags": ["premium", "bestseller"], "público": "profesional"}',
  '{"tags": ["frágil", "electrónica"], "ubicación": "A3-15"}',
  '{"tags": ["5g", "oled"], "público": "avanzado"}'
) AS consolidado;
consolidado
{"público": ["profesional", "avanzado"], "tags": ["premium", "bestseller", "frágil", "electrónica", "5g", "oled"], "ubicación": "A3-15"}

Observa que los arrays de tags se concatenaron (6 etiquetas en total), los valores de público se combinaron en un array (ambos departamentos tenían esa clave), y ubicación se conservó tal cual porque solo existía en un documento.

Caso práctico: historial de cambios

JSON_MERGE_PRESERVE es perfecto para construir historiales donde quieres acumular valores sin perder ninguno. Imagina un registro de acciones realizadas sobre un pedido:

CREATE TABLE pedidos (
    id INT PRIMARY KEY AUTO_INCREMENT,
    cliente VARCHAR(100),
    historial JSON DEFAULT ('{"eventos": [], "notas": []}')
);
 
INSERT INTO pedidos (id, cliente, historial) VALUES
(1, 'María García', '{"eventos": ["creado"], "notas": ["Pedido urgente"]}');

Cada vez que se realiza una acción, puedes acumular eventos usando JSON_MERGE_PRESERVE:

UPDATE pedidos
SET historial = JSON_MERGE_PRESERVE(
    historial,
    '{"eventos": ["pago_confirmado"], "notas": ["Pago con tarjeta Visa"]}'
)
WHERE id = 1;
 
UPDATE pedidos
SET historial = JSON_MERGE_PRESERVE(
    historial,
    '{"eventos": ["enviado"], "notas": ["Transportista: SEUR"]}'
)
WHERE id = 1;

Después de las actualizaciones:

SELECT JSON_PRETTY(historial) AS historial
FROM pedidos
WHERE id = 1;
historial
{"eventos": ["creado", "pago_confirmado", "enviado"], "notas": ["Pedido urgente", "Pago con tarjeta Visa", "Transportista: SEUR"]}

Todos los eventos y notas se acumularon cronológicamente sin perder ninguno. Este patrón evita la necesidad de una tabla separada de eventos para casos simples.

Caso práctico: combinación de resultados de búsqueda

Cuando consultas datos de múltiples tablas y necesitas consolidar los resultados en un solo documento JSON:

CREATE TABLE productos (
    id INT PRIMARY KEY AUTO_INCREMENT,
    nombre VARCHAR(100),
    datos_basicos JSON
);
 
CREATE TABLE inventario (
    producto_id INT PRIMARY KEY,
    datos_stock JSON
);
 
INSERT INTO productos VALUES
(1, 'iPhone 15 Pro', '{"precio": 1299, "categoría": "smartphone"}'),
(2, 'MacBook Air M3', '{"precio": 1399, "categoría": "portátil"}');
 
INSERT INTO inventario VALUES
(1, '{"stock": 45, "almacén": "Madrid", "alertas": ["stock bajo en Barcelona"]}'),
(2, '{"stock": 25, "almacén": "Barcelona", "alertas": []}');
SELECT
    p.nombre,
    JSON_MERGE_PRESERVE(p.datos_basicos, i.datos_stock) AS datos_completos
FROM productos p
JOIN inventario i ON p.id = i.producto_id;
nombredatos_completos
iPhone 15 Pro{"alertas": ["stock bajo en Barcelona"], "almacén": "Madrid", "categoría": "smartphone", "precio": 1299, "stock": 45}
MacBook Air M3{"alertas": [], "almacén": "Barcelona", "categoría": "portátil", "precio": 1399, "stock": 25}

Como las claves de ambos documentos son diferentes, el resultado es una fusión limpia sin arrays inesperados.

Manejo de NULL

Cuando un argumento es SQL NULL, JSON_MERGE_PRESERVE devuelve NULL:

SELECT JSON_MERGE_PRESERVE(NULL, '{"a": 1}') AS resultado;
resultado
NULL

A diferencia de JSON_MERGE_PATCH, el JSON null no elimina claves. En su lugar, se trata como un valor más que se preserva:

SELECT JSON_MERGE_PRESERVE(
  '{"a": 1}',
  '{"a": null}'
) AS resultado;
resultado
{"a": [1, null]}

El valor null se añadió al array junto con el valor original 1. Esto refuerza la filosofía de "preservar todo".

Combinación con otras funciones

JSON_MERGE_PRESERVE es particularmente útil dentro de agregaciones. Puedes usarla con variables de sesión o en subconsultas para consolidar múltiples filas:

SELECT
    e.producto_id,
    JSON_MERGE_PRESERVE(
        JSON_OBJECT('departamento', MIN(e.departamento)),
        JSON_OBJECT('total_registros', COUNT(*))
    ) AS resumen
FROM etiquetado e
GROUP BY e.producto_id;

Para comparar visualmente JSON_MERGE_PRESERVE con JSON_MERGE_PATCH:

SELECT
    JSON_MERGE_PRESERVE('{"a": 1}', '{"a": 2}') AS preserve_result,
    JSON_MERGE_PATCH('{"a": 1}', '{"a": 2}') AS patch_result;
preserve_resultpatch_result
{"a": [1, 2]}{"a": 2}

La elección entre ambas funciones depende de tu objetivo: si quieres que el segundo documento actualice al primero, usa JSON_MERGE_PATCH. Si quieres conservar todos los valores de ambos documentos, usa JSON_MERGE_PRESERVE. En el siguiente artículo veremos JSON_ARRAY_APPEND para añadir elementos a arrays JSON.

Escrito por Eduardo Lázaro