Columnas generadas
Las columnas generadas son columnas cuyo valor se calcula automáticamente a partir de una expresión que referencia otras columnas de la misma fila. Introducidas en MySQL 5.7, eliminan la necesidad de mantener manualmente datos derivados: en lugar de calcular un precio con IVA en cada consulta o en la capa de aplicación, defines la fórmula una vez en la tabla y MySQL se encarga de que el valor esté siempre actualizado. Otros motores de bases de datos las llaman computed columns o calculated columns, pero en MySQL el término oficial es generated columns.
La ventaja principal es la coherencia. Cuando un cálculo vive en la definición de la tabla, cualquier aplicación que lea los datos obtiene el mismo resultado sin duplicar lógica. Además, las columnas generadas permiten crear índices sobre expresiones, algo especialmente útil para indexar valores extraídos de columnas JSON con JSON_EXTRACT.
Sintaxis
La definición de una columna generada se incluye directamente en CREATE TABLE o en ALTER TABLE:
nombre_columna tipo_de_dato GENERATED ALWAYS AS (expresion) [VIRTUAL | STORED]Las partes clave de la sintaxis son:
GENERATED ALWAYS AS (expresion): indica que el valor se calcula automáticamente. La expresión puede usar operadores aritméticos, funciones integradas de MySQL y referencias a otras columnas no generadas de la misma tabla.VIRTUAL: el valor se calcula cada vez que se lee la fila. No ocupa espacio en disco. Es el modo por defecto si no se especifica nada.STORED: el valor se calcula cuando se inserta o actualiza la fila y se almacena físicamente en disco. Ocupa espacio pero permite lecturas más rápidas y soporta índices de forma directa.
Las palabras GENERATED ALWAYS son opcionales en la práctica —puedes escribir solo AS (expresion)— pero incluirlas mejora la legibilidad y deja claro que la columna es calculada.
Tabla de ejemplo
Para los ejemplos de este artículo partimos de una tabla de productos con información de precios:
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
precio_base DECIMAL(10, 2) NOT NULL,
iva DECIMAL(4, 2) NOT NULL DEFAULT 21.00,
fecha_alta DATE NOT NULL,
datos_extra JSON
);
INSERT INTO productos (nombre, precio_base, iva, fecha_alta, datos_extra) VALUES
('Monitor ultrawide 34"', 449.99, 21.00, '2025-06-15', '{"marca": "LG", "categoria": "perifericos"}'),
('Teclado mecánico', 129.50, 21.00, '2025-08-22', '{"marca": "Keychron", "categoria": "perifericos"}'),
('Silla ergonómica', 389.00, 21.00, '2024-11-03', '{"marca": "Secretlab", "categoria": "mobiliario"}'),
('Webcam 4K', 199.95, 21.00, '2025-01-10', '{"marca": "Logitech", "categoria": "perifericos"}');Columna VIRTUAL: precio con IVA
El caso más clásico de columna generada es calcular un valor derivado de otros. Aquí creamos una columna que muestra el precio final con IVA incluido. Como el tipo DECIMAL garantiza precisión en operaciones monetarias, el resultado siempre será exacto:
ALTER TABLE productos
ADD COLUMN precio_con_iva DECIMAL(10, 2)
GENERATED ALWAYS AS (ROUND(precio_base * (1 + iva / 100), 2)) VIRTUAL;Al consultar la tabla, la columna aparece con su valor calculado sin que hayas insertado nada en ella:
SELECT nombre, precio_base, iva, precio_con_iva
FROM productos;| nombre | precio_base | iva | precio_con_iva |
|---|---|---|---|
| Monitor ultrawide 34" | 449.99 | 21.00 | 544.49 |
| Teclado mecánico | 129.50 | 21.00 | 156.70 |
| Silla ergonómica | 389.00 | 21.00 | 470.69 |
| Webcam 4K | 199.95 | 21.00 | 241.94 |
Cada vez que cambies precio_base o iva, el valor de precio_con_iva se recalcula automáticamente. No necesitas triggers ni lógica adicional en la aplicación.
Columna STORED: año de alta
Cuando necesitas filtrar o agrupar frecuentemente por un valor derivado, una columna STORED permite que MySQL almacene el resultado en disco. Extraer el año de una fecha con la función YEAR es un ejemplo habitual:
ALTER TABLE productos
ADD COLUMN anio_alta SMALLINT
GENERATED ALWAYS AS (YEAR(fecha_alta)) STORED;Ahora puedes consultar directamente por año sin aplicar la función en cada lectura:
SELECT nombre, fecha_alta, anio_alta
FROM productos
WHERE anio_alta = 2025;| nombre | fecha_alta | anio_alta |
|---|---|---|
| Monitor ultrawide 34" | 2025-06-15 | 2025 |
| Teclado mecánico | 2025-08-22 | 2025 |
| Webcam 4K | 2025-01-10 | 2025 |
Al ser STORED, el valor ya existe físicamente y no se recalcula en cada consulta. Esto marca una diferencia real en tablas con millones de filas cuando combinas la columna con un índice.
Nombre completo con CONCAT
En tablas de personas es muy frecuente necesitar el nombre completo para mostrar en listados o informes. En lugar de concatenar en cada consulta, una columna generada lo resuelve de forma centralizada usando CONCAT:
CREATE TABLE empleados (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(80) NOT NULL,
apellido VARCHAR(80) NOT NULL,
nombre_completo VARCHAR(165)
GENERATED ALWAYS AS (CONCAT(nombre, ' ', apellido)) VIRTUAL,
email VARCHAR(150)
);
INSERT INTO empleados (nombre, apellido, email) VALUES
('Laura', 'Martínez', 'laura.martinez@empresa.com'),
('Carlos', 'Ruiz', 'carlos.ruiz@empresa.com');SELECT nombre_completo, email FROM empleados;| nombre_completo | |
|---|---|
| Laura Martínez | laura.martinez@empresa.com |
| Carlos Ruiz | carlos.ruiz@empresa.com |
Observa que al insertar filas solo proporcionas nombre y apellido. MySQL calcula nombre_completo de forma transparente. Si actualizas el apellido de un empleado, el nombre completo se actualiza solo.
Columnas generadas sobre JSON
Una de las aplicaciones más potentes de las columnas generadas es extraer valores de columnas JSON para poder indexarlos. MySQL no permite crear índices directamente sobre expresiones JSON_EXTRACT, pero sí sobre una columna generada que contenga ese valor extraído.
Volviendo a nuestra tabla productos, podemos extraer la marca del campo datos_extra:
ALTER TABLE productos
ADD COLUMN marca VARCHAR(100)
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(datos_extra, '$.marca'))) STORED;Usamos JSON_UNQUOTE para eliminar las comillas que JSON_EXTRACT devuelve por defecto. El resultado queda limpio y listo para indexar:
SELECT nombre, marca FROM productos;| nombre | marca |
|---|---|
| Monitor ultrawide 34" | LG |
| Teclado mecánico | Keychron |
| Silla ergonómica | Secretlab |
| Webcam 4K | Logitech |
Ahora puedes crear un índice sobre marca para acelerar las búsquedas por ese campo JSON, como verás en la siguiente sección.
Crear índices sobre columnas generadas
Las columnas generadas STORED admiten cualquier tipo de índice. Las columnas VIRTUAL también admiten índices en InnoDB a partir de MySQL 5.7.8, aunque con algunas limitaciones. Crear un índice sobre una columna generada es exactamente igual que sobre una columna normal:
CREATE INDEX idx_marca ON productos (marca);
CREATE INDEX idx_anio_alta ON productos (anio_alta);Estos índices son especialmente valiosos cuando la expresión subyacente es costosa de calcular. Sin la columna generada, tendrías que recurrir a un índice funcional (disponible desde MySQL 8.0.13), que internamente crea una columna generada oculta. Con una columna generada explícita tienes más control y visibilidad.
Un patrón habitual con JSON es crear columnas generadas STORED para cada campo que necesites filtrar y luego indexarlas:
ALTER TABLE productos
ADD COLUMN categoria VARCHAR(100)
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(datos_extra, '$.categoria'))) STORED;
CREATE INDEX idx_categoria ON productos (categoria);Ahora una consulta como WHERE categoria = 'perifericos' usa el índice directamente en lugar de escanear toda la tabla y parsear JSON fila a fila.
Definir columnas generadas en CREATE TABLE
Hasta ahora hemos usado ALTER TABLE para añadir columnas generadas a tablas existentes, pero también puedes definirlas directamente al crear la tabla:
CREATE TABLE facturas (
id INT AUTO_INCREMENT PRIMARY KEY,
subtotal DECIMAL(10, 2) NOT NULL,
tasa_iva DECIMAL(4, 2) NOT NULL DEFAULT 21.00,
importe_iva DECIMAL(10, 2)
GENERATED ALWAYS AS (ROUND(subtotal * tasa_iva / 100, 2)) STORED,
total DECIMAL(10, 2)
GENERATED ALWAYS AS (ROUND(subtotal * (1 + tasa_iva / 100), 2)) STORED
);
INSERT INTO facturas (subtotal, tasa_iva) VALUES
(1500.00, 21.00),
(850.00, 10.00),
(3200.00, 21.00);SELECT * FROM facturas;| id | subtotal | tasa_iva | importe_iva | total |
|---|---|---|---|---|
| 1 | 1500.00 | 21.00 | 315.00 | 1815.00 |
| 2 | 850.00 | 10.00 | 85.00 | 935.00 |
| 3 | 3200.00 | 21.00 | 672.00 | 3872.00 |
Fíjate en que total referencia subtotal y tasa_iva, que son columnas normales. En MySQL 5.7 una columna generada no podía referenciar a otra columna generada, pero a partir de MySQL 8.0 esta restricción se relajó parcialmente: una columna generada puede referenciar otra siempre que esté definida antes en la tabla.
VIRTUAL vs STORED: cuándo usar cada una
La elección entre VIRTUAL y STORED depende de cómo vayas a usar la columna.
Usa VIRTUAL cuando la columna se lee con poca frecuencia o cuando el cálculo es ligero. Las columnas virtuales no ocupan espacio en disco, lo que mantiene la tabla compacta. Son ideales para expresiones simples como concatenaciones, operaciones aritméticas básicas o conversiones de tipo. Si tienes una tabla con millones de filas y la columna generada solo se consulta esporádicamente en informes, VIRTUAL evita desperdiciar almacenamiento.
Usa STORED cuando necesites crear un índice sobre la columna, cuando la expresión sea computacionalmente costosa o cuando la columna se lea con mucha más frecuencia de lo que se escribe. Operaciones como parsear JSON, aplicar funciones de hash o ejecutar cálculos complejos con múltiples funciones anidadas se benefician de calcularse una vez al escribir en lugar de recalcularse en cada lectura.
Como regla general: si vas a crear un índice sobre la columna, usa STORED. Si solo necesitas mostrar el valor calculado, VIRTUAL es suficiente y más eficiente en espacio.
Restricciones de las columnas generadas
Las columnas generadas tienen varias limitaciones que conviene conocer antes de usarlas:
No pueden usar subconsultas. La expresión debe ser determinista y autosuficiente. No puedes escribir algo como GENERATED ALWAYS AS ((SELECT MAX(precio) FROM otra_tabla)).
No pueden usar funciones no deterministas. Funciones como NOW(), RAND(), UUID() o CONNECTION_ID() no están permitidas porque devuelven valores diferentes en cada invocación. MySQL necesita garantizar que el resultado es reproducible.
No pueden referenciar columnas con AUTO_INCREMENT durante la creación de la tabla si la expresión se evalúa antes de que el valor autoincremental se asigne. En la práctica, esto rara vez supone un problema si la columna generada no depende de la clave primaria.
No puedes insertar ni actualizar directamente una columna generada. Cualquier intento de asignar un valor explícito en un INSERT o UPDATE produce un error. El valor lo controla exclusivamente la expresión definida.
Las columnas VIRTUAL no pueden ser parte de una clave primaria. Si necesitas que la columna generada participe en la clave primaria, debe ser STORED.
Errores comunes
Intentar insertar un valor en una columna generada. Es el error más frecuente. Si incluyes la columna generada en la lista de columnas de un INSERT, MySQL rechaza la operación:
INSERT INTO productos (nombre, precio_base, iva, fecha_alta, precio_con_iva)
VALUES ('Ratón inalámbrico', 39.99, 21.00, '2025-09-01', 48.39);ERROR 3105 (HY000): The value specified for generated column
'precio_con_iva' in table 'productos' is not allowed.
La solución es omitir la columna generada en el INSERT y dejar que MySQL la calcule:
INSERT INTO productos (nombre, precio_base, iva, fecha_alta, datos_extra)
VALUES ('Ratón inalámbrico', 39.99, 21.00, '2025-09-01',
'{"marca": "Logitech", "categoria": "perifericos"}');Usar una función no determinista en la expresión. Si intentas definir una columna generada con NOW() o RAND(), el error aparece al crear o modificar la tabla:
ALTER TABLE productos
ADD COLUMN momento DATETIME GENERATED ALWAYS AS (NOW()) VIRTUAL;ERROR 3102 (HY000): Expression of generated column 'momento'
contains a disallowed function.
Tipo de dato incompatible con la expresión. Si la expresión devuelve una cadena pero defines la columna como INT, MySQL intentará convertir el valor y puede producir resultados inesperados o errores. Asegúrate de que el tipo de dato de la columna coincide con lo que devuelve la expresión.
Olvidar STORED al intentar crear un índice. Aunque InnoDB permite índices sobre columnas VIRTUAL desde MySQL 5.7.8, otros motores de almacenamiento no lo soportan. Si recibes un error al indexar una columna VIRTUAL, cámbiala a STORED:
ALTER TABLE productos
MODIFY COLUMN marca VARCHAR(100)
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(datos_extra, '$.marca'))) STORED;Cuándo usar columnas generadas
Las columnas generadas son la herramienta adecuada cuando un valor derivado se necesita con frecuencia y quieres mantener la lógica de cálculo dentro de la base de datos en lugar de dispersarla por múltiples aplicaciones o consultas.
Son especialmente útiles para indexar expresiones sobre columnas JSON. Si almacenas datos semiestructurados en JSON y necesitas filtrar por campos específicos, una columna generada STORED con un índice es la forma más eficiente de resolverlo.
También brillan en cálculos financieros donde la coherencia es crítica. Tener el precio con IVA como columna generada garantiza que todas las aplicaciones ven el mismo resultado, eliminando discrepancias entre la web, la API y los informes.
Para búsquedas sobre datos transformados —como buscar por año de una fecha, por dominio de un email o por un hash de un campo— las columnas generadas con índice ofrecen un rendimiento muy superior a aplicar la función en la cláusula WHERE de cada consulta.
En cambio, si el cálculo solo se necesita en una consulta puntual o en un informe esporádico, es más sencillo calcular el valor directamente en el SELECT. Las columnas generadas añaden complejidad al esquema y tienen restricciones, así que úsalas cuando el beneficio en coherencia o rendimiento justifique el coste.
Escrito por Eduardo Lázaro
