PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Un aperçu des colonnes générées pour PostgreSQL

PostgreSQL 12 est livré avec une nouvelle fonctionnalité intéressante, les colonnes générées. La fonctionnalité n'est pas vraiment nouvelle, mais la standardisation, la facilité d'utilisation, l'accessibilité et les performances ont été améliorées dans cette nouvelle version.

Une colonne générée est une colonne spéciale dans une table qui contient des données générées automatiquement à partir d'autres données de la ligne. Le contenu de la colonne générée est automatiquement rempli et mis à jour chaque fois que les données source, telles que les autres colonnes de la ligne, sont elles-mêmes modifiées.

Colonnes générées dans PostgreSQL 12+

Dans les versions récentes de PostgreSQL, les colonnes générées sont une fonctionnalité intégrée permettant aux instructions CREATE TABLE ou ALTER TABLE d'ajouter une colonne dans laquelle le contenu est automatiquement "généré" à la suite d'une expression. Ces expressions peuvent être de simples opérations mathématiques à partir d'autres colonnes ou une fonction immuable plus avancée. Certains avantages de l'implémentation d'une colonne générée dans une conception de base de données incluent :

  • La possibilité d'ajouter une colonne à une table contenant des données calculées sans avoir besoin de mettre à jour le code d'application pour générer les données pour ensuite les inclure dans les opérations INSERT et UPDATE.
  • Réduire le temps de traitement des instructions SELECT extrêmement fréquentes qui traiteraient les données à la volée. Étant donné que le traitement des données est effectué au moment de INSERT ou UPDATE, les données sont générées une seule fois et les instructions SELECT n'ont besoin que de récupérer les données. Dans les environnements de lecture intensive, cela peut être préférable, tant que le stockage de données supplémentaire utilisé est acceptable.
  • Étant donné que les colonnes générées sont mises à jour automatiquement lorsque les données source elles-mêmes sont mises à jour, l'ajout d'une colonne générée ajoutera une garantie supposée que les données de la colonne générée sont toujours correctes.

Dans PostgreSQL 12, seul le type de colonne générée "STORED" est disponible. Dans d'autres systèmes de base de données, une colonne générée avec un type "VIRTUAL" est disponible, qui agit plus comme une vue où le résultat est calculé à la volée lorsque les données sont récupérées. Étant donné que la fonctionnalité est si similaire aux vues et qu'il suffit d'écrire l'opération dans une instruction de sélection, la fonctionnalité n'est pas aussi bénéfique que la fonctionnalité "STORED" décrite ici, mais il est possible que les futures versions incluent la fonctionnalité.

La création d'une table avec une colonne générée est effectuée lors de la définition de la colonne elle-même. Dans cet exemple, la colonne générée est "profit" et est automatiquement générée en soustrayant le Purchase_price des colonnes sale_price, puis multiplié par la colonne quantity_sold.

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

);

Dans cet exemple, une table "transactions" est créée pour suivre certaines transactions de base et les bénéfices d'un café imaginaire. L'insertion de données dans ce tableau affichera des résultats immédiats.

​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

Lors de la mise à jour de la ligne, la colonne générée sera automatiquement mise à jour :

​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

Cela garantira que la colonne générée est toujours correcte, sans logique supplémentaire nécessaire du côté de l'application.

REMARQUE :les colonnes générées ne peuvent pas être INSÉRÉES ou mises à jour directement, et toute tentative de le faire renverra une ERREUR :

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.

Colonnes générées sur PostgreSQL 11 et versions antérieures

Même si les colonnes générées intégrées sont nouvelles dans la version 12 de PostgreSQL, la fonctionnalité peut toujours être réalisée dans les versions antérieures, elle nécessite juste un peu plus de configuration avec des procédures stockées et des déclencheurs. Cependant, même avec la possibilité de l'implémenter sur des versions plus anciennes, en plus des fonctionnalités supplémentaires qui peuvent être bénéfiques, une stricte conformité des entrées de données est plus difficile à atteindre et dépend des fonctionnalités PL/pgSQL et de l'ingéniosité de la programmation.

BONUS :L'exemple ci-dessous fonctionnera également sur PostgreSQL 12+, donc si la fonctionnalité ajoutée avec un combo fonction/déclencheur est nécessaire ou souhaitée dans les versions plus récentes, cette option est une solution de secours valide et non limitée à uniquement les versions antérieures à 12. 

Bien que ce soit une façon de le faire sur les versions précédentes de PostgreSQL, cette méthode présente quelques avantages supplémentaires : 

  • Étant donné que l'imitation de la colonne générée utilise une fonction, des calculs plus complexes peuvent être utilisés. Les colonnes générées dans la version 12 nécessitent des opérations IMMUTABLE, mais une option de déclencheur/fonction pourrait utiliser un type de fonction STABLE ou VOLATILE avec de plus grandes possibilités et probablement des performances moindres en conséquence.
  • L'utilisation d'une fonction qui a l'option d'être STABLE ou VOLATILE ouvre également la possibilité de mettre à jour des colonnes supplémentaires, de mettre à jour d'autres tables ou même de créer de nouvelles données via INSERTS dans d'autres tables. (Cependant, bien que ces options de déclencheur/fonction soient beaucoup plus flexibles, cela ne veut pas dire qu'il manque une véritable "colonne générée", car elle fait ce qui est annoncé avec plus de performances et d'efficacité.)

Dans cet exemple, un déclencheur/fonction est configuré pour imiter la fonctionnalité d'une colonne générée par PostgreSQL 12+, ainsi que deux éléments qui déclenchent une exception si une tentative INSERT ou UPDATE de modifier la colonne générée . Celles-ci peuvent être omises, mais si elles sont omises, les exceptions ne seront pas déclenchées et les données réelles INSERTed ou UPDATE seront discrètement supprimées, ce qui n'est généralement pas recommandé.

Le déclencheur lui-même est défini pour s'exécuter AVANT, ce qui signifie que le traitement a lieu avant l'insertion réelle et nécessite le RETURN de NEW, qui est le RECORD qui est modifié pour contenir la nouvelle valeur de colonne générée. Cet exemple spécifique a été écrit pour fonctionner sur PostgreSQL version 11.

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();

REMARQUE :Assurez-vous que la fonction dispose des autorisations/propriétés appropriées pour être exécutée par le ou les utilisateurs de l'application souhaités.

Comme vu dans l'exemple précédent, les résultats sont les mêmes dans les versions précédentes avec une solution fonction/trigger :

​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 mise à jour des données sera similaire.

​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

Enfin, tenter d'INSÉRER ou de METTRE À JOUR la colonne spéciale elle-même entraînera une ERREUR :

​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

Dans cet exemple, il agit différemment de la première configuration de colonne générée de plusieurs manières qu'il convient de noter :

  • Si la 'colonne générée' est tentée d'être mise à jour mais qu'aucune ligne n'est trouvée pour être mise à jour, elle renverra le résultat avec un résultat "UPDATE 0", tandis qu'une colonne générée réelle dans la version 12 sera toujours renvoie une ERROR, même si aucune ligne n'est trouvée à UPDATE.
  • Lorsque vous tentez de mettre à jour la colonne de profit, qui "devrait" toujours renvoyer une ERREUR, si la valeur spécifiée est la même que la valeur correctement "générée", elle réussira. En fin de compte, les données sont correctes, cependant, si l'on souhaite renvoyer une ERREUR si la colonne est spécifiée.

Documentation et communauté PostgreSQL

La documentation officielle des colonnes générées par PostgreSQL se trouve sur le site Web officiel de PostgreSQL. Revenez quand de nouvelles versions majeures de PostgreSQL sont publiées pour découvrir de nouvelles fonctionnalités lorsqu'elles apparaissent.

Alors que les colonnes générées dans PostgreSQL 12 sont assez simples, l'implémentation de fonctionnalités similaires dans les versions précédentes peut devenir beaucoup plus compliquée. La communauté PostgreSQL est une communauté très active, massive, mondiale et multilingue dédiée à aider les personnes de tout niveau d'expérience PostgreSQL à résoudre des problèmes et à créer de nouvelles solutions comme celle-ci.

  • IRC :Freenode a un canal très actif appelé #postgres, où les utilisateurs s'entraident pour comprendre des concepts, corriger des erreurs ou trouver d'autres ressources. Une liste complète des canaux freenode disponibles pour tout ce qui concerne PostgreSQL est disponible sur le site Web PostgreSQL.org.
  • Listes de diffusion :PostgreSQL a une poignée de listes de diffusion qui peuvent être jointes. Des questions/problèmes plus longs peuvent être envoyés ici et peuvent atteindre beaucoup plus de personnes qu'IRC à tout moment. Les listes peuvent être trouvées sur le site Web de PostgreSQL, et les listes pgsql-general ou pgsql-admin sont de bonnes ressources.
  • Slack :La communauté PostgreSQL est également florissante sur Slack et peut être rejointe sur postgresteam.slack.com. Tout comme IRC, une communauté active est disponible pour répondre aux questions et s'engager dans tout ce qui concerne PostgreSQL.