PostgreSQL 12 viene con una excelente función nueva, Columnas generadas. La funcionalidad no es exactamente nada nuevo, pero la estandarización, la facilidad de uso, la accesibilidad y el rendimiento se han mejorado en esta nueva versión.
Una columna generada es una columna especial en una tabla que contiene datos generados automáticamente a partir de otros datos dentro de la fila. El contenido de la columna generada se completa y actualiza automáticamente cada vez que se modifican los datos de origen, como cualquier otra columna de la fila.
Columnas generadas en PostgreSQL 12+
En versiones recientes de PostgreSQL, las columnas generadas son una función integrada que permite que las declaraciones CREATE TABLE o ALTER TABLE agreguen una columna en la que el contenido se 'genera' automáticamente como resultado de una expresión. Estas expresiones pueden ser operaciones matemáticas simples de otras columnas o una función inmutable más avanzada. Algunos beneficios de implementar una columna generada en un diseño de base de datos incluyen:
- La capacidad de agregar una columna a una tabla que contiene datos calculados sin necesidad de actualizar el código de la aplicación para generar los datos y luego incluirlos dentro de las operaciones INSERTAR y ACTUALIZAR.
- Reducción del tiempo de procesamiento en declaraciones SELECT extremadamente frecuentes que procesarían los datos sobre la marcha. Dado que el procesamiento de los datos se realiza en el momento de INSERTAR o ACTUALIZAR, los datos se generan una vez y las instrucciones SELECT solo necesitan recuperar los datos. En entornos de lectura intensa, esto puede ser preferible, siempre que el almacenamiento de datos adicional utilizado sea aceptable.
- Dado que las columnas generadas se actualizan automáticamente cuando se actualizan los datos de origen, agregar una columna generada agregará una supuesta garantía de que los datos en la columna generada siempre son correctos.
En PostgreSQL 12, solo está disponible el tipo de columna generada "ALMACENADA". En otros sistemas de bases de datos, está disponible una columna generada con un tipo 'VIRTUAL', que actúa más como una vista donde el resultado se calcula sobre la marcha cuando se recuperan los datos. Dado que la funcionalidad es muy similar a las vistas, y simplemente escribir la operación en una declaración de selección, la funcionalidad no es tan beneficiosa como la funcionalidad "ALMACENADA" discutida aquí, pero existe la posibilidad de que las versiones futuras incluyan la función.
La creación de una tabla con una columna generada se realiza al definir la columna misma. En este ejemplo, la columna generada es "beneficio" y se genera automáticamente al restar el precio de compra de las columnas de precio de venta y luego multiplicarlo por la columna de cantidad vendida.
CREATE TABLE public.transactions (
transactions_sid serial primary key,
transaction_date timestamp with time zone DEFAULT now() NOT NULL,
product_name character varying NOT NULL,
purchase_price double precision NOT NULL,
sale_price double precision NOT NULL,
quantity_sold integer NOT NULL,
profit double precision NOT NULL GENERATED ALWAYS AS ((sale_price - purchase_price) * quantity_sold) STORED
);
En este ejemplo, se crea una tabla de "transacciones" para rastrear algunas transacciones básicas y ganancias de una cafetería imaginaria. Insertar datos en esta tabla mostrará algunos resultados inmediatos.
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('House Blend Coffee', 5, 11.99, 1);
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('French Roast Coffee', 6, 12.99, 4);
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('BULK: House Blend Coffee, 10LB', 40, 100, 6);
severalnines=# SELECT * FROM public.transactions;
transactions_sid | transaction_date | product_name | purchase_price | sale_price | quantity_sold | profit
------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------
1 | 2020-02-28 04:50:06.626371+00 | House Blend Coffee | 5 | 11.99 | 1 | 6.99
2 | 2020-02-28 04:50:53.313572+00 | French Roast Coffee | 6 | 12.99 | 4 | 27.96
3 | 2020-02-28 04:51:08.531875+00 | BULK: House Blend Coffee, 10LB | 40 | 100 | 6 | 360
Al actualizar la fila, la columna generada se actualizará automáticamente:
severalnines=# UPDATE public.transactions SET sale_price = 95 WHERE transactions_sid = 3;
UPDATE 1
severalnines=# SELECT * FROM public.transactions WHERE transactions_sid = 3;
transactions_sid | transaction_date | product_name | purchase_price | sale_price | quantity_sold | profit
------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------
3 | 2020-02-28 05:55:11.233077+00 | BULK: House Blend Coffee, 10LB | 40 | 95 | 6 | 330
Esto asegurará que la columna generada sea siempre correcta, sin necesidad de lógica adicional en el lado de la aplicación.
NOTA:las columnas generadas no se pueden INSERTAR ni ACTUALIZAR directamente, y cualquier intento de hacerlo devolverá un ERROR:
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold, profit) VALUES ('BULK: House Blend Coffee, 10LB', 40, 95, 1, 95);
ERROR: cannot insert into column "profit"
DETAIL: Column "profit" is a generated column.
severalnines=# UPDATE public.transactions SET profit = 330 WHERE transactions_sid = 3;
ERROR: column "profit" can only be updated to DEFAULT
DETAIL: Column "profit" is a generated column.
Columnas generadas en PostgreSQL 11 y anteriores
Aunque las columnas generadas incorporadas son nuevas en la versión 12 de PostgreSQL, la funcionalidad aún se puede lograr en versiones anteriores, solo necesita un poco más de configuración con procedimientos almacenados y disparadores. Sin embargo, incluso con la capacidad de implementarlo en versiones anteriores, además de la funcionalidad adicional que puede ser beneficiosa, el cumplimiento estricto de la entrada de datos es más difícil de lograr y depende de las características de PL/pgSQL y del ingenio de programación.
BONUS:El siguiente ejemplo también funcionará en PostgreSQL 12+, por lo que si se necesita o desea la funcionalidad agregada con una combinación de función/disparador en versiones más nuevas, esta opción es una alternativa válida y no está restringida a solo versiones anteriores a 12.
Si bien esta es una forma de hacerlo en versiones anteriores de PostgreSQL, hay un par de beneficios adicionales de este método:
- Dado que imitar la columna generada usa una función, se pueden usar cálculos más complejos. Las columnas generadas en la versión 12 requieren operaciones INMUTABLES, pero una opción de activación/función podría usar un tipo de función ESTABLE o VOLÁTIL con mayores posibilidades y probablemente menor rendimiento en consecuencia.
- Usar una función que tiene la opción de ser ESTABLE o VOLÁTIL también abre la posibilidad de ACTUALIZAR columnas adicionales, ACTUALIZAR otras tablas o incluso crear nuevos datos a través de INSERCIONES en otras tablas. (Sin embargo, aunque estas opciones de activación/función son mucho más flexibles, eso no quiere decir que falte una "Columna generada", ya que hace lo que se anuncia con mayor rendimiento y eficiencia).
En este ejemplo, se configura un activador/función para imitar la funcionalidad de una columna generada por PostgreSQL 12+, junto con dos piezas que generan una excepción si INSERT o UPDATE intentan cambiar la columna generada . Estos se pueden omitir, pero si se omiten, no se generarán excepciones y los datos reales INSERTADOS o ACTUALIZADOS se descartarán silenciosamente, lo que generalmente no sería recomendable.
El activador en sí está configurado para ejecutarse ANTES, lo que significa que el procesamiento ocurre antes de que ocurra la inserción real y requiere el RETORNO de NUEVO, que es el REGISTRO que se modifica para contener el nuevo valor de columna generado. Este ejemplo específico fue escrito para ejecutarse en la versión 11 de PostgreSQL.
CREATE TABLE public.transactions (
transactions_sid serial primary key,
transaction_date timestamp with time zone DEFAULT now() NOT NULL,
product_name character varying NOT NULL,
purchase_price double precision NOT NULL,
sale_price double precision NOT NULL,
quantity_sold integer NOT NULL,
profit double precision NOT NULL
);
CREATE OR REPLACE FUNCTION public.generated_column_function()
RETURNS trigger
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
-- This statement mimics the ERROR on built in generated columns to refuse INSERTS on the column and return an ERROR.
IF (TG_OP = 'INSERT') THEN
IF (NEW.profit IS NOT NULL) THEN
RAISE EXCEPTION 'ERROR: cannot insert into column "profit"' USING DETAIL = 'Column "profit" is a generated column.';
END IF;
END IF;
-- This statement mimics the ERROR on built in generated columns to refuse UPDATES on the column and return an ERROR.
IF (TG_OP = 'UPDATE') THEN
-- Below, IS DISTINCT FROM is used because it treats nulls like an ordinary value.
IF (NEW.profit::VARCHAR IS DISTINCT FROM OLD.profit::VARCHAR) THEN
RAISE EXCEPTION 'ERROR: cannot update column "profit"' USING DETAIL = 'Column "profit" is a generated column.';
END IF;
END IF;
NEW.profit := ((NEW.sale_price - NEW.purchase_price) * NEW.quantity_sold);
RETURN NEW;
END;
$function$;
CREATE TRIGGER generated_column_trigger BEFORE INSERT OR UPDATE ON public.transactions FOR EACH ROW EXECUTE PROCEDURE public.generated_column_function();
NOTA:Asegúrese de que la función tenga los permisos/propiedad correctos para ser ejecutada por los usuarios de la aplicación deseada.
Como se ve en el ejemplo anterior, los resultados son los mismos en versiones anteriores con una función/solución de activación:
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('House Blend Coffee', 5, 11.99, 1);
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('French Roast Coffee', 6, 12.99, 4);
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold) VALUES ('BULK: House Blend Coffee, 10LB', 40, 100, 6);
severalnines=# SELECT * FROM public.transactions;
transactions_sid | transaction_date | product_name | purchase_price | sale_price | quantity_sold | profit
------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------
1 | 2020-02-28 00:35:14.855511-07 | House Blend Coffee | 5 | 11.99 | 1 | 6.99
2 | 2020-02-28 00:35:21.764449-07 | French Roast Coffee | 6 | 12.99 | 4 | 27.96
3 | 2020-02-28 00:35:27.708761-07 | BULK: House Blend Coffee, 10LB | 40 | 100 | 6 | 360
La actualización de los datos será similar.
severalnines=# UPDATE public.transactions SET sale_price = 95 WHERE transactions_sid = 3;
UPDATE 1
severalnines=# SELECT * FROM public.transactions WHERE transactions_sid = 3;
transactions_sid | transaction_date | product_name | purchase_price | sale_price | quantity_sold | profit
------------------+-------------------------------+--------------------------------+----------------+------------+---------------+--------
3 | 2020-02-28 00:48:52.464344-07 | BULK: House Blend Coffee, 10LB | 40 | 95 | 6 | 330
Por último, intentar INSERTAR o ACTUALIZAR la columna especial resultará en un ERROR:
severalnines=# INSERT INTO public.transactions (product_name, purchase_price, sale_price, quantity_sold, profit) VALUES ('BULK: House Blend Coffee, 10LB', 40, 95, 1, 95);
ERROR: ERROR: cannot insert into column "profit"
DETAIL: Column "profit" is a generated column.
CONTEXT: PL/pgSQL function generated_column_function() line 7 at RAISE
severalnines=# UPDATE public.transactions SET profit = 3030 WHERE transactions_sid = 3;
ERROR: ERROR: cannot update column "profit"
DETAIL: Column "profit" is a generated column.
CONTEXT: PL/pgSQL function generated_column_function() line 15 at RAISE
En este ejemplo, actúa de manera diferente a la configuración de la primera columna generada en un par de formas que deben tenerse en cuenta:
- Si se intenta actualizar la 'columna generada' pero no se encuentra ninguna fila para actualizar, devolverá el resultado correcto con "ACTUALIZAR 0", mientras que una columna generada real en la versión 12 seguirá devuelve un ERROR, incluso si no se encuentra ninguna fila para ACTUALIZAR.
- Al intentar actualizar la columna de beneficios, que "debería" siempre devolver un ERROR, si el valor especificado es el mismo que el valor "generado" correctamente, tendrá éxito. Sin embargo, en última instancia, los datos son correctos si se desea devolver un ERROR si se especifica la columna.
Documentación y comunidad de PostgreSQL
La documentación oficial de las columnas generadas de PostgreSQL se encuentra en el sitio web oficial de PostgreSQL. Vuelva a consultar cuando se publiquen nuevas versiones principales de PostgreSQL para descubrir nuevas características cuando aparezcan.
Si bien las columnas generadas en PostgreSQL 12 son bastante sencillas, implementar una funcionalidad similar en versiones anteriores tiene el potencial de volverse mucho más complicado. La comunidad de PostgreSQL es una comunidad muy activa, masiva, mundial y multilingüe dedicada a ayudar a las personas de cualquier nivel de experiencia en PostgreSQL a resolver problemas y crear nuevas soluciones como esta.
- IRC :Freenode tiene un canal muy activo llamado #postgres, donde los usuarios se ayudan mutuamente a comprender conceptos, corregir errores o encontrar otros recursos. Puede encontrar una lista completa de los canales de freenode disponibles para todo lo relacionado con PostgreSQL en el sitio web de PostgreSQL.org.
- Listas de correo :PostgreSQL tiene un puñado de listas de correo a las que se puede unir. Aquí se pueden enviar preguntas/problemas de formato más largo, y pueden llegar a muchas más personas que IRC en un momento dado. Las listas se pueden encontrar en el sitio web de PostgreSQL, y las listas pgsql-general o pgsql-admin son buenos recursos.
- Slack :La comunidad de PostgreSQL también ha prosperado en Slack y puede unirse a ella en postgresteam.slack.com. Al igual que IRC, una comunidad activa está disponible para responder preguntas y participar en todo lo relacionado con PostgreSQL.