JSON_STORAGE_FREE

La función JSON_STORAGE_FREE devuelve el número de bytes de espacio libre que existen dentro de un documento JSON almacenado en una columna, después de que dicho documento haya sido actualizado parcialmente. A partir de MySQL 8.0, el motor de almacenamiento InnoDB incorporó la capacidad de realizar actualizaciones parciales de documentos JSON, lo que significa que puede modificar valores individuales sin necesidad de reescribir el documento completo en disco. Este mecanismo mejora enormemente el rendimiento en tablas que reciben actualizaciones frecuentes sobre columnas JSON de gran tamaño, pero tiene un efecto secundario que conviene entender: cuando el nuevo valor ocupa menos espacio que el anterior, se genera un hueco interno de bytes sin utilizar. JSON_STORAGE_FREE permite medir exactamente ese espacio desperdiciado.

Comprender esta función resulta esencial para cualquier administrador o desarrollador que trabaje con columnas JSON en entornos de producción. El espacio libre acumulado puede llegar a representar un porcentaje significativo del almacenamiento total de una tabla cuando las actualizaciones parciales son frecuentes y tienden a reducir el tamaño de los valores. Gracias a JSON_STORAGE_FREE, puedes monitorizar la fragmentación interna de tus documentos JSON, tomar decisiones informadas sobre cuándo conviene compactar los datos y, en definitiva, mantener un uso eficiente del disco. Para conocer el tamaño total que ocupa un documento en almacenamiento, utiliza la función complementaria JSON_STORAGE_SIZE.

Sintaxis

La sintaxis de JSON_STORAGE_FREE es sencilla, ya que solo requiere un argumento: el documento JSON que deseas inspeccionar.

JSON_STORAGE_FREE(documento_json)

La función recibe una expresión JSON y devuelve un valor entero que representa los bytes de espacio libre acumulado dentro del documento. Si el documento no ha sido actualizado parcialmente desde su última escritura completa, o si el argumento proporcionado es un literal JSON en lugar de una referencia a una columna almacenada, la función devuelve 0. Este detalle es importante porque implica que solo tiene sentido utilizar JSON_STORAGE_FREE sobre columnas de tipo JSON en tablas reales, no sobre valores construidos en la propia consulta.

Cómo funcionan las actualizaciones parciales de JSON

Para comprender correctamente el valor que devuelve JSON_STORAGE_FREE, es necesario entender primero el mecanismo de actualizaciones parciales de JSON en MySQL. Cuando insertas un documento JSON en una columna, MySQL lo almacena en un formato binario interno optimizado para lectura rápida. Este formato binario ocupa un espacio determinado en disco.

Cuando posteriormente ejecutas un UPDATE que utiliza funciones como JSON_SET, JSON_REPLACE o JSON_REMOVE para modificar ese documento, MySQL evalúa si puede aplicar la modificación directamente sobre los bytes existentes del documento en disco, sin necesidad de reescribirlo por completo. Esto es lo que se conoce como una actualización parcial o in-place update. Para que MySQL pueda realizar una actualización parcial, deben cumplirse ciertas condiciones: la columna debe estar en una tabla InnoDB, la actualización debe usar las funciones JSON mencionadas y el nuevo valor debe caber en el espacio que ocupaba el anterior (o ser más pequeño).

Si el nuevo valor es más corto que el anterior, los bytes sobrantes no se eliminan del documento almacenado. En su lugar, quedan como espacio libre interno. El documento sigue ocupando el mismo número total de bytes en disco, pero una parte de esos bytes ya no contienen datos útiles. Es precisamente este espacio libre el que JSON_STORAGE_FREE cuantifica.

Comportamiento básico

Veamos paso a paso cómo se genera y mide el espacio libre en un escenario realista. Imaginemos que administramos una tabla de configuraciones de servidores en una empresa de hosting.

CREATE TABLE configuraciones (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100),
    config JSON
);
 
INSERT INTO configuraciones (nombre, config) VALUES
('servidor_web', '{"host": "www.tiendaonline.com", "puerto": 8080, "protocolo": "HTTPS", "timeout": 30000, "max_conexiones": 500}');

Antes de realizar cualquier actualización, podemos consultar tanto el tamaño total del documento como el espacio libre disponible. En este punto, el documento acaba de ser insertado, por lo que no ha sufrido ninguna actualización parcial.

SELECT
    nombre,
    JSON_STORAGE_SIZE(config) AS tamanio_total,
    JSON_STORAGE_FREE(config) AS espacio_libre
FROM configuraciones
WHERE nombre = 'servidor_web';
nombretamanio_totalespacio_libre
servidor_web1190

El espacio libre es 0, exactamente lo esperado. Ahora vamos a modificar el valor del host por uno considerablemente más corto. Esta operación provocará que MySQL realice una actualización parcial, dejando bytes sin utilizar en el espacio que antes ocupaba la cadena más larga.

UPDATE configuraciones
SET config = JSON_SET(config, '$.host', 'web.local')
WHERE nombre = 'servidor_web';

Hemos reemplazado "www.tiendaonline.com" (20 caracteres) por "web.local" (9 caracteres). La diferencia de 11 caracteres se traduce directamente en espacio libre dentro del documento almacenado. Comprobemos el resultado.

SELECT
    nombre,
    JSON_STORAGE_SIZE(config) AS tamanio_total,
    JSON_STORAGE_FREE(config) AS espacio_libre
FROM configuraciones
WHERE nombre = 'servidor_web';
nombretamanio_totalespacio_libre
servidor_web11911

Observa que el tamaño total del documento sigue siendo 119 bytes, idéntico al original. MySQL no ha compactado el documento tras la actualización; simplemente ha marcado internamente que 11 de esos 119 bytes están libres. Este comportamiento es intencionado: el motor conserva el espacio libre porque futuras actualizaciones podrían necesitarlo. Si, por ejemplo, más adelante cambias el host por una cadena de hasta 20 caracteres, MySQL podrá reutilizar ese espacio sin necesidad de reubicar el documento en disco.

Cuándo se crea espacio libre

El espacio libre aparece en tres situaciones concretas, todas relacionadas con actualizaciones parciales que reducen el tamaño de algún valor dentro del documento.

La primera situación ocurre cuando reemplazas un valor por otro más corto usando JSON_SET o JSON_REPLACE. Este es el caso más común en aplicaciones de producción, donde los datos se actualizan constantemente y los nuevos valores no siempre ocupan lo mismo que los anteriores.

UPDATE configuraciones
SET config = JSON_REPLACE(config, '$.protocolo', 'HTTP')
WHERE nombre = 'servidor_web';

En este caso, el valor "HTTPS" (5 caracteres) se sustituye por "HTTP" (4 caracteres), lo que genera un byte adicional de espacio libre. Aunque la diferencia parezca insignificante en un solo registro, en una tabla con millones de filas y actualizaciones constantes, estos bytes se acumulan rápidamente.

La segunda situación se produce cuando eliminas una clave completa del documento con JSON_REMOVE. Al eliminar una clave, todo el espacio que ocupaban tanto el nombre de la clave como su valor se convierte en espacio libre.

UPDATE configuraciones
SET config = JSON_REMOVE(config, '$.max_conexiones')
WHERE nombre = 'servidor_web';

La eliminación de la clave max_conexiones y su valor numérico libera una cantidad considerable de bytes. Podemos verificar el estado acumulado del documento tras todas las operaciones realizadas.

SELECT
    JSON_STORAGE_SIZE(config) AS tamanio_total,
    JSON_STORAGE_FREE(config) AS espacio_libre,
    JSON_STORAGE_SIZE(config) - JSON_STORAGE_FREE(config) AS tamanio_efectivo
FROM configuraciones
WHERE nombre = 'servidor_web';
tamanio_totalespacio_libretamanio_efectivo
1193485

El documento sigue ocupando 119 bytes en disco, pero 34 de ellos son espacio libre. Los datos útiles solo necesitan 85 bytes. Esto significa que casi un 29% del espacio almacenado está desperdiciado.

La tercera situación ocurre cuando sustituyes un objeto o array anidado por uno más pequeño. Por ejemplo, si un documento contiene un array con diez elementos y lo reemplazas por uno con solo dos, el espacio sobrante queda como espacio libre dentro del documento padre.

En una tabla con actualizaciones frecuentes sobre columnas JSON, la fragmentación puede acumularse hasta convertirse en un problema de rendimiento y almacenamiento. Veamos cómo monitorizar esta situación de forma sistemática con una tabla de productos de una tienda en línea.

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(200),
    precio DECIMAL(10,2),
    metadata JSON
);
 
INSERT INTO productos (nombre, precio, metadata) VALUES
('iPhone 15 Pro', 1299.99, '{"descripcion_larga": "El iPhone más avanzado con chip A17 Pro, cámara de 48MP y pantalla Super Retina XDR de 6.1 pulgadas", "palabras_clave": "smartphone,apple,ios,5g,premium", "ultima_revision": "2025-01-15"}'),
('Samsung Galaxy S24', 899.99, '{"descripcion_larga": "Smartphone Samsung con inteligencia artificial Galaxy AI, cámara de 200MP y pantalla Dynamic AMOLED 2X", "palabras_clave": "smartphone,samsung,android,5g", "ultima_revision": "2025-02-01"}'),
('MacBook Air M3', 1399.00, '{"descripcion_larga": "Portátil ultraligero con chip Apple M3, 18 horas de batería y pantalla Liquid Retina de 15 pulgadas", "palabras_clave": "portatil,apple,macbook,ultrabook", "ultima_revision": "2025-01-20"}');

Ahora simulemos una serie de actualizaciones típicas que crean fragmentación. En una tienda real, las palabras clave y las fechas de revisión se actualizan constantemente a medida que evoluciona el catálogo.

UPDATE productos SET metadata = JSON_SET(metadata, '$.ultima_revision', '2025-06-01');
UPDATE productos SET metadata = JSON_REPLACE(metadata, '$.palabras_clave', 'movil,apple');

Con las actualizaciones realizadas, podemos obtener un informe de fragmentación por producto que nos indique el porcentaje de espacio desperdiciado en cada fila.

SELECT
    nombre,
    JSON_STORAGE_SIZE(metadata) AS total_bytes,
    JSON_STORAGE_FREE(metadata) AS bytes_libres,
    ROUND(JSON_STORAGE_FREE(metadata) / JSON_STORAGE_SIZE(metadata) * 100, 1) AS pct_fragmentado
FROM productos;
nombretotal_bytesbytes_librespct_fragmentado
iPhone 15 Pro2322510.8
Samsung Galaxy S24229187.9
MacBook Air M3224229.8

La columna pct_fragmentado revela qué porcentaje del espacio almacenado es desperdicio. En tablas con millones de filas y actualizaciones frecuentes, un 10-15% de fragmentación puede traducirse en gigabytes de espacio en disco totalmente desperdiciado, lo que además puede afectar al rendimiento de las operaciones de lectura al obligar a InnoDB a leer más páginas de las necesarias.

Caso práctico: decidir cuándo compactar

Una vez que sabes cómo medir la fragmentación, el siguiente paso lógico es establecer criterios para decidir cuándo merece la pena compactar los documentos. La compactación implica reescribir completamente los documentos en disco, lo que consume recursos de I/O y CPU, por lo que no conviene ejecutarla innecesariamente. Lo ideal es definir umbrales que equilibren el espacio recuperado con el coste de la operación.

La siguiente consulta proporciona un resumen agregado de toda la tabla, mostrando cuántas filas tienen fragmentación, cuánto espacio total se desperdicia y cuál es el porcentaje medio de fragmentación.

SELECT
    COUNT(*) AS filas_fragmentadas,
    SUM(JSON_STORAGE_FREE(metadata)) AS total_bytes_libres,
    ROUND(SUM(JSON_STORAGE_FREE(metadata)) / 1024 / 1024, 2) AS mb_desperdiciados,
    ROUND(AVG(
        JSON_STORAGE_FREE(metadata) / JSON_STORAGE_SIZE(metadata) * 100
    ), 1) AS pct_promedio
FROM productos
WHERE JSON_STORAGE_FREE(metadata) > 0;
filas_fragmentadastotal_bytes_libresmb_desperdiciadospct_promedio
3650.009.6

En este ejemplo con solo tres filas el desperdicio es insignificante, pero imagina una tabla de producción con 5 millones de registros donde cada documento JSON desperdicia un promedio de 200 bytes. Eso serían casi 1 GB de espacio en disco totalmente inútil. En ese escenario, la compactación estaría plenamente justificada.

Para compactar los documentos y eliminar su espacio libre, existe un truco sencillo pero efectivo: reasignar la columna a sí misma. Esta operación fuerza a MySQL a deserializar y volver a serializar cada documento, lo que elimina cualquier espacio libre interno.

UPDATE productos
SET metadata = metadata
WHERE JSON_STORAGE_FREE(metadata) > 0;

La cláusula WHERE es importante porque evita reescribir documentos que no tienen fragmentación, reduciendo significativamente el coste de la operación. Después de la compactación, el espacio libre debería ser cero en todas las filas.

SELECT
    nombre,
    JSON_STORAGE_SIZE(metadata) AS total_bytes,
    JSON_STORAGE_FREE(metadata) AS bytes_libres
FROM productos;
nombretotal_bytesbytes_libres
iPhone 15 Pro2070
Samsung Galaxy S242110
MacBook Air M32020

Observa que el tamaño total de cada documento se ha reducido tras la compactación. Esos bytes que antes figuraban como espacio libre ahora simplemente no existen: el documento ocupa exactamente lo que necesitan sus datos.

Cuándo la función devuelve 0

Existen varias situaciones en las que JSON_STORAGE_FREE devuelve 0, y es importante conocerlas para no interpretar erróneamente los resultados.

La primera y más obvia es cuando el documento nunca ha sido actualizado parcialmente. Un documento recién insertado no tiene espacio libre porque MySQL lo escribió con el tamaño exacto que necesitaba.

INSERT INTO productos (nombre, precio, metadata) VALUES
('Cable USB-C', 19.99, '{"tipo": "USB-C"}');
 
SELECT JSON_STORAGE_FREE(metadata)
FROM productos WHERE nombre = 'Cable USB-C';

El resultado es 0 porque el documento no ha sufrido ninguna actualización parcial desde su inserción.

La segunda situación ocurre cuando pasas un literal JSON en lugar de una referencia a una columna almacenada. Los literales se construyen en memoria durante la ejecución de la consulta y nunca pasan por el proceso de almacenamiento y actualización parcial que genera espacio libre.

SELECT JSON_STORAGE_FREE('{"clave": "valor"}') AS resultado;
resultado
0

La tercera situación se da cuando una actualización utiliza un valor más largo que el anterior. En ese caso, MySQL no puede realizar una actualización in-place (porque el nuevo valor no cabe en el espacio del anterior) y necesita reescribir el documento completo. Tras la reescritura, el documento queda compactado y sin espacio libre.

Finalmente, tras ejecutar una compactación manual (como el truco de SET metadata = metadata que vimos antes), todos los documentos afectados tendrán espacio libre igual a 0.

Manejo de NULL

El comportamiento de JSON_STORAGE_FREE con valores NULL sigue la convención estándar de MySQL para funciones que operan sobre JSON.

Si el argumento proporcionado a la función es NULL, el resultado será NULL, no 0. Esta distinción es relevante porque permite diferenciar entre un documento que no tiene fragmentación (devuelve 0) y una fila donde la columna JSON no contiene ningún valor (devuelve NULL).

SELECT JSON_STORAGE_FREE(NULL) AS resultado;
resultado
NULL

Cuando trabajas con consultas que agregan datos de múltiples filas, ten en cuenta que las funciones de agregación como SUM y AVG ignoran los valores NULL. Esto significa que si tu tabla contiene filas con columnas JSON nulas, esas filas no distorsionarán las estadísticas de fragmentación. Sin embargo, si usas JSON_STORAGE_FREE en una cláusula WHERE o en un CASE, recuerda que las comparaciones con NULL siempre evalúan a NULL, no a verdadero ni a falso.

Combinación con otras funciones JSON

Una consulta de diagnóstico completa del estado de almacenamiento de tus documentos JSON debería combinar varias funciones para ofrecer una visión integral. Junto a JSON_STORAGE_FREE, resulta especialmente útil combinar JSON_STORAGE_SIZE para el tamaño total, JSON_DEPTH para la profundidad del documento y JSON_LENGTH para el número de claves en el nivel raíz.

SELECT
    nombre,
    JSON_STORAGE_SIZE(metadata) AS almacenado,
    JSON_STORAGE_FREE(metadata) AS libre,
    JSON_STORAGE_SIZE(metadata) - JSON_STORAGE_FREE(metadata) AS efectivo,
    JSON_DEPTH(metadata) AS profundidad,
    JSON_LENGTH(metadata) AS num_claves
FROM productos
ORDER BY JSON_STORAGE_SIZE(metadata) DESC;

Esta consulta te ofrece una radiografía completa de cada documento: cuánto espacio ocupa en disco, cuánto de ese espacio es útil, qué tan profundamente anidado está y cuántas claves tiene en su primer nivel. Cuando la ejecutas sobre tablas grandes, puedes identificar rápidamente los documentos que más espacio desperdician y priorizar la compactación sobre ellos.

También puedes combinar JSON_STORAGE_FREE con JSON_VALID para crear consultas de mantenimiento que verifiquen tanto la integridad como la eficiencia del almacenamiento. Asimismo, en entornos donde utilizas JSON_TABLE para descomponer documentos en filas relacionales, conocer el estado de fragmentación te ayuda a estimar el rendimiento de esas operaciones.

Errores comunes

Uno de los errores más frecuentes al trabajar con JSON_STORAGE_FREE es esperar que devuelva un valor distinto de cero sobre literales JSON. Muchos desarrolladores prueban la función en consultas SELECT con valores JSON hardcodeados y se confunden al ver siempre 0. La función solo reporta espacio libre real cuando opera sobre columnas almacenadas que han pasado por actualizaciones parciales.

Otro error habitual es asumir que un valor de 0 significa que el documento está perfectamente optimizado. Aunque es cierto que no hay fragmentación interna, el documento podría contener datos redundantes, claves innecesariamente largas o estructuras anidadas que podrían simplificarse para reducir su tamaño total.

Por último, es un error común ejecutar compactaciones (el truco de SET col = col) con demasiada frecuencia. Cada compactación genera operaciones de escritura intensivas en disco. En tablas grandes, esto puede degradar el rendimiento del servidor durante la operación. Lo recomendable es monitorizar la fragmentación con JSON_STORAGE_FREE y solo compactar cuando el porcentaje medio de espacio libre supere un umbral significativo, generalmente entre el 20% y el 30%.

Cuándo usar JSON_STORAGE_FREE

Esta función resulta particularmente valiosa en varios escenarios de producción. El primero y más evidente es la monitorización continua del almacenamiento en tablas que reciben un volumen alto de actualizaciones sobre columnas JSON. Integrar una consulta de diagnóstico basada en JSON_STORAGE_FREE en tus scripts de mantenimiento nocturno te permite detectar problemas de fragmentación antes de que se conviertan en cuellos de botella.

El segundo escenario es la planificación de capacidad. Cuando necesitas estimar cuánto espacio en disco requiere realmente tu tabla (frente a cuánto ocupa nominalmente), la diferencia entre JSON_STORAGE_SIZE y JSON_STORAGE_FREE te da el tamaño efectivo real de tus datos JSON.

El tercer escenario es el análisis de patrones de actualización. Si observas que ciertos tipos de documentos acumulan mucha más fragmentación que otros, eso puede indicar que el diseño del esquema JSON no es óptimo para el patrón de escritura de tu aplicación. Quizá convenga separar los campos que cambian frecuentemente de los que son estáticos, o bien considerar mover esos campos a columnas relacionales tradicionales en lugar de mantenerlos dentro del documento JSON.

En el siguiente artículo veremos JSON_QUOTE para escapar cadenas y convertirlas en valores JSON válidos.

Escrito por Eduardo Lázaro