PostgreSQL 12 kommer med en fantastisk ny funktion, Genererade kolumner. Funktionaliteten är inte precis något nytt, men standardiseringen, användarvänligheten, tillgängligheten och prestandan har förbättrats i den här nya versionen.
En genererad kolumn är en speciell kolumn i en tabell som innehåller data som genereras automatiskt från andra data inom raden. Innehållet i den genererade kolumnen fylls i och uppdateras automatiskt när källdata, till exempel andra kolumner i raden, ändras själva.
Genererade kolumner i PostgreSQL 12+
I nyare versioner av PostgreSQL är genererade kolumner en inbyggd funktion som gör att CREATE TABLE- eller ALTER TABLE-satserna kan lägga till en kolumn där innehållet automatiskt "genereras" som ett resultat av ett uttryck. Dessa uttryck kan vara enkla matematiska operationer från andra kolumner, eller en mer avancerad oföränderlig funktion. Några fördelar med att implementera en genererad kolumn i en databasdesign inkluderar:
- Möjligheten att lägga till en kolumn i en tabell som innehåller beräknad data utan att behöva uppdatera applikationskoden för att generera data för att sedan inkludera den i INSERT- och UPDATE-operationerna.
- Reducerar bearbetningstiden för extremt frekventa SELECT-satser som skulle behandla data i farten. Eftersom behandlingen av data görs vid tidpunkten för INSERT eller UPDATE, genereras data en gång och SELECT-satserna behöver bara hämta data. I tunga läsmiljöer kan detta vara att föredra, så länge som den extra datalagring som används är acceptabel.
- Eftersom genererade kolumner uppdateras automatiskt när själva källdata uppdateras, kommer att lägga till en genererad kolumn att lägga till en antagen garanti för att data i den genererade kolumnen alltid är korrekta.
I PostgreSQL 12 är endast den genererade kolumntypen "LAGRAD" tillgänglig. I andra databassystem finns en genererad kolumn med typen 'VIRTUAL' tillgänglig, som fungerar mer som en vy där resultatet beräknas i farten när data hämtas. Eftersom funktionaliteten är så lik vyer och helt enkelt skriver operationen i en select-sats, är funktionen inte lika fördelaktig som "LAGRAD"-funktionen som diskuteras här, men det finns en chans att framtida versioner kommer att inkludera funktionen.
Skapa en tabell med en genererad kolumn görs när man definierar själva kolumnen. I det här exemplet är den genererade kolumnen "vinst" och genereras automatiskt genom att subtrahera köppriset från sale_price-kolumnerna och sedan multipliceras med kolumnen kvantity_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 det här exemplet skapas en "transaktionstabell" för att spåra några grundläggande transaktioner och vinster från ett tänkt kafé. Om du infogar data i den här tabellen visas några omedelbara resultat.
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 raden uppdateras uppdateras den genererade kolumnen automatiskt:
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
Detta kommer att säkerställa att den genererade kolumnen alltid är korrekt, utan att ytterligare logik behövs på applikationssidan.
OBS:Genererade kolumner kan inte infogas i eller UPPDATERAS direkt, och varje försök att göra det kommer att returneras med ett FEL:
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.
Genererade kolumner på PostgreSQL 11 och tidigare
Även om inbyggda genererade kolumner är nya för version 12 av PostgreSQL, kan det funktionellt fortfarande uppnås i tidigare versioner, det behöver bara lite mer inställningar med lagrade procedurer och triggers. Men även med möjligheten att implementera det på äldre versioner, förutom den extra funktionaliteten som kan vara fördelaktig, är strikt datainmatningsefterlevnad svårare att uppnå, och beror på PL/pgSQL-funktioner och programmeringssinne.
BONUS:Exemplet nedan kommer även att fungera på PostgreSQL 12+, så om den extra funktionaliteten med en funktion/trigger-kombination behövs eller önskas i nyare versioner, är detta alternativ en giltig reserv och inte begränsad till bara versioner äldre än 12.
Det här är ett sätt att göra det på tidigare versioner av PostgreSQL, men det finns ett par ytterligare fördelar med den här metoden:
- Eftersom efterlikningen av den genererade kolumnen använder en funktion, kan mer komplexa beräkningar användas. Genererade kolumner i version 12 kräver OFÖRBARLIGA operationer, men ett trigger-/funktionsalternativ kan använda en STABIL eller VOLATILE typ av funktion med större möjligheter och sannolikt sämre prestanda i enlighet därmed.
- Att använda en funktion som har alternativet att vara STABIL eller VOLATIL öppnar också möjligheten att UPPDATERA ytterligare kolumner, UPPDATERA andra tabeller eller till och med skapa ny data via INSLAG till andra tabeller. (Men även om dessa trigger-/funktionsalternativ är mycket mer flexibla, betyder det inte att en verklig "genererad kolumn" saknas, eftersom den gör det som annonseras med större prestanda och effektivitet.)
I det här exemplet är en trigger/funktion inställd för att efterlikna funktionen hos en PostgreSQL 12+-genererad kolumn, tillsammans med två delar som ger upphov till ett undantag om en INSERT eller UPDATE försöker ändra den genererade kolumnen . Dessa kan utelämnas, men om de utelämnas kommer undantag inte att tas upp, och den faktiska data som infogas eller uppdateras kommer att förkastas tyst, vilket i allmänhet inte skulle rekommenderas.
Själva utlösaren är inställd att köras FÖRE, vilket innebär att bearbetningen sker innan den faktiska infogningen sker, och kräver RETURN av NEW, vilket är RECORD som modifieras för att innehålla det nya genererade kolumnvärdet. Detta specifika exempel skrevs för att köras 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();
OBS:Se till att funktionen har rätt behörigheter/äganderätt för att köras av den eller de önskade applikationsanvändarna.
Som sett i föregående exempel är resultaten desamma i tidigare versioner med en funktion/triggerlö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
Uppdatering av data kommer att vara liknande.
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
Sistligen, ett försök att INFOGA i eller UPPDATERA själva specialkolumnen kommer att resultera i ett FEL:
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 det här exemplet fungerar det annorlunda än den första genererade kolumnkonfigurationen på ett par sätt som bör noteras:
- Om den "genererade kolumnen" försöker uppdateras men ingen rad hittas uppdaterad, kommer den att returnera framgång med ett "UPPDATERA 0"-resultat, medan en faktisk genererad kolumn i version 12 fortfarande returnera ett FEL, även om ingen rad hittas att UPPDATERA.
- När man försöker uppdatera vinstkolumnen, som alltid "bör" returnera ett FEL, om det angivna värdet är detsamma som det korrekt "genererade" värdet, kommer det att lyckas. I slutändan stämmer dock uppgifterna om önskan är att returnera ett ERROR om kolumnen är specificerad.
Dokumentation och PostgreSQL-gemenskap
Den officiella dokumentationen för PostgreSQL-genererade kolumner finns på den officiella PostgreSQL-webbplatsen. Kom tillbaka när nya större versioner av PostgreSQL släpps för att upptäcka nya funktioner när de dyker upp.
Medan genererade kolumner i PostgreSQL 12 är ganska okomplicerade, kan implementering av liknande funktionalitet i tidigare versioner bli mycket mer komplicerad. PostgreSQL-communityt är en mycket aktiv, massiv, världsomspännande och flerspråkig gemenskap dedikerad till att hjälpa människor på alla nivåer av PostgreSQL-erfarenhet att lösa problem och skapa nya lösningar som denna.
- IRC :Freenode har en mycket aktiv kanal som heter #postgres, där användare hjälper varandra att förstå koncept, fixa fel eller hitta andra resurser. En fullständig lista över tillgängliga freenode-kanaler för allt PostgreSQL finns på PostgreSQL.org-webbplatsen.
- E-postlistor :PostgreSQL har en handfull e-postlistor som kan anslutas. Längre frågor/ärenden kan skickas hit och kan nå många fler än IRC vid varje given tidpunkt. Listorna kan hittas på PostgreSQL-webbplatsen, och listorna pgsql-general eller pgsql-admin är bra resurser.
- Slack :PostgreSQL-communityt har också blomstrat på Slack och kan anslutas till postgresteam.slack.com. Ungefär som IRC är en aktiv community tillgänglig för att svara på frågor och engagera sig i allt som har med PostgreSQL att göra.