PostgreSQL 12 kommer med en fantastisk ny funktion, Generated Columns. Funktionaliteten er ikke ligefrem noget nyt, men standardiseringen, brugervenligheden, tilgængeligheden og ydeevnen er blevet forbedret i denne nye version.
En genereret kolonne er en speciel kolonne i en tabel, der indeholder data genereret automatisk fra andre data i rækken. Indholdet af den genererede kolonne udfyldes og opdateres automatisk, hver gang kildedataene, såsom andre kolonner i rækken, selv ændres.
Genererede kolonner i PostgreSQL 12+
I nyere versioner af PostgreSQL er genererede kolonner en indbygget funktion, der gør det muligt for CREATE TABLE- eller ALTER TABLE-sætningerne at tilføje en kolonne, hvor indholdet automatisk 'genereres' som et resultat af et udtryk. Disse udtryk kan være simple matematiske operationer fra andre kolonner eller en mere avanceret uforanderlig funktion. Nogle fordele ved at implementere en genereret kolonne i et databasedesign omfatter:
- Evnen til at tilføje en kolonne til en tabel, der indeholder beregnede data uden at skulle opdatere applikationskoden for at generere dataene for derefter at inkludere dem i INSERT- og UPDATE-operationerne.
- Reducerer behandlingstiden på ekstremt hyppige SELECT-udsagn, der ville behandle dataene på farten. Da behandlingen af dataene sker på tidspunktet for INSERT eller UPDATE, genereres dataene én gang, og SELECT-sætningerne behøver kun at hente dataene. I tunge læsemiljøer kan dette være at foretrække, så længe den ekstra datalagring, der bruges, er acceptabel.
- Da genererede kolonner opdateres automatisk, når selve kildedataene opdateres, vil tilføjelse af en genereret kolonne tilføje en formodet garanti for, at dataene i den genererede kolonne altid er korrekte.
I PostgreSQL 12 er kun den genererede kolonne af typen "LAGET" tilgængelig. I andre databasesystemer er en genereret kolonne med typen 'VIRTUAL' tilgængelig, som mere fungerer som en visning, hvor resultatet beregnes i farten, når dataene hentes. Da funktionaliteten minder så meget om visninger, og blot skriver handlingen ind i en udvalgt erklæring, er funktionaliteten ikke så fordelagtig som den 'LAGREDE' funktionalitet, der diskuteres her, men der er en chance for, at fremtidige versioner vil inkludere funktionen.
Oprettelse af en tabel med en genereret kolonne udføres, når selve kolonnen defineres. I dette eksempel er den genererede kolonne 'profit' og genereres automatisk ved at trække købsprisen fra kolonnerne salgspris og derefter ganges med kolonnen 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
);
I dette eksempel oprettes en "transaktions"-tabel for at spore nogle grundlæggende transaktioner og overskud på en imaginær kaffebar. Indsættelse af data i denne tabel vil vise nogle øjeblikkelige resultater.
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
Når rækken opdateres, vil den genererede kolonne automatisk opdatere:
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
Dette vil sikre, at den genererede kolonne altid er korrekt, uden yderligere logik på applikationssiden.
BEMÆRK:Genererede kolonner kan ikke indsættes i eller OPDATERES direkte, og ethvert forsøg på at gøre det vil returnere i en FEJL:
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.
Genererede kolonner på PostgreSQL 11 og før
Selv om indbyggede genererede kolonner er nye i version 12 af PostgreSQL, kan det funktionelt stadig opnås i tidligere versioner, det kræver bare lidt mere opsætning med lagrede procedurer og triggere. Men selv med muligheden for at implementere det på ældre versioner, ud over den ekstra funktionalitet, der kan være gavnlig, er streng datainput-overholdelse sværere at opnå, og afhænger af PL/pgSQL-funktioner og programmeringsopfindsomhed.
BONUS:Eksemplet nedenfor vil også fungere på PostgreSQL 12+, så hvis den tilføjede funktionalitet med en funktion/trigger-kombination er nødvendig eller ønsket i nyere versioner, er denne mulighed en gyldig reserve og ikke begrænset til kun versioner ældre end 12.
Selvom dette er en måde at gøre det på tidligere versioner af PostgreSQL, er der et par yderligere fordele ved denne metode:
- Da efterligning af den genererede kolonne bruger en funktion, er mere komplekse beregninger i stand til at blive brugt. Genererede kolonner i version 12 kræver UDVINKELIGE operationer, men en trigger/funktionsindstilling kunne bruge en STABIL eller VOLATILE type funktion med større muligheder og sandsynligvis mindre ydeevne i overensstemmelse hermed.
- Brug af en funktion, der har muligheden for at være STABIL eller VOLATILE, åbner også muligheden for at OPDATERE yderligere kolonner, OPDATERE andre tabeller eller endda oprette nye data via INSERTS i andre tabeller. (Men selv om disse trigger-/funktionsmuligheder er meget mere fleksible, betyder det ikke, at der mangler en egentlig "Genereret kolonne", da den gør det, der annonceres med større ydeevne og effektivitet.)
I dette eksempel er en trigger/funktion sat op til at efterligne funktionaliteten af en PostgreSQL 12+ genereret kolonne sammen med to stykker, der rejser en undtagelse, hvis en INSERT eller UPDATE forsøger at ændre den genererede kolonne . Disse kan udelades, men hvis de udelades, vil undtagelser ikke blive rejst, og de faktiske data, der er INDSERT eller OPDATERET, vil stille og roligt blive kasseret, hvilket generelt ikke ville blive anbefalet.
Selve triggeren er indstillet til at køre FØR, hvilket betyder, at behandlingen sker før den faktiske indsættelse sker, og kræver RETURN af NEW, som er RECORD, der er ændret til at indeholde den nye genererede kolonneværdi. Dette specifikke eksempel blev skrevet til at køre på 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();
BEMÆRK:Sørg for, at funktionen har de korrekte tilladelser/ejerskab til at blive udført af den eller de ønskede applikationsbrugere.
Som set i det foregående eksempel er resultaterne de samme i tidligere versioner med en funktion/trigger-løsning:
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
Opdatering af dataene vil være ens.
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
Til sidst vil et forsøg på at INDSÆTTE i eller OPDATERE selve specialkolonnen resultere i en FEJL:
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
I dette eksempel virker den anderledes end den først genererede kolonneopsætning på et par måder, som bør bemærkes:
- Hvis den 'genererede kolonne' forsøges opdateret, men der ikke findes nogen række, der er opdateret, vil den returnere succes med et "OPDATERING 0"-resultat, mens en faktisk genereret kolonne i version 12 stadig vil returner en FEJL, selvom der ikke findes en række til OPDATERING.
- Når du forsøger at opdatere profitkolonnen, som 'skal' altid returnere en FEJL, hvis den angivne værdi er den samme som den korrekt 'genererede' værdi, vil det lykkes. I sidste ende er dataene dog korrekte, hvis ønsket er at returnere en FEJL, hvis kolonnen er angivet.
Dokumentation og PostgreSQL-fællesskab
Den officielle dokumentation for de PostgreSQL-genererede kolonner er placeret på det officielle PostgreSQL-websted. Kom tilbage, når nye større versioner af PostgreSQL frigives for at opdage nye funktioner, når de dukker op.
Mens genererede kolonner i PostgreSQL 12 er ret ligetil, har implementering af lignende funktionalitet i tidligere versioner potentialet til at blive meget mere kompliceret. PostgreSQL-fællesskabet er et meget aktivt, massivt, verdensomspændende og flersproget fællesskab, dedikeret til at hjælpe folk på alle niveauer af PostgreSQL-erfaring med at løse problemer og skabe nye løsninger som denne.
- IRC :Freenode har en meget aktiv kanal kaldet #postgres, hvor brugere hjælper hinanden med at forstå koncepter, rette fejl eller finde andre ressourcer. En komplet liste over tilgængelige freenode-kanaler til alt PostgreSQL kan findes på PostgreSQL.org-webstedet.
- Mailinglister :PostgreSQL har en håndfuld mailinglister, der kan tilsluttes. Længere form spørgsmål / spørgsmål kan sendes her, og kan nå mange flere mennesker end IRC på et givet tidspunkt. Listerne kan findes på PostgreSQL-webstedet, og listerne pgsql-general eller pgsql-admin er gode ressourcer.
- Slak :PostgreSQL-fællesskabet har også blomstret på Slack, og kan tilsluttes på postgresteam.slack.com. Ligesom IRC er et aktivt fællesskab tilgængeligt til at besvare spørgsmål og engagere sig i alt PostgreSQL.