PostgreSQL 12 идва с страхотна нова функция, Генерирани колони. Функционалността не е нещо ново, но стандартизацията, лекотата на използване, достъпността и производителността са подобрени в тази нова версия.
Генерираната колона е специална колона в таблица, която съдържа данни, генерирани автоматично от други данни в реда. Съдържанието на генерираната колона автоматично се попълва и актуализира всеки път, когато изходните данни, като всички други колони в реда, се променят сами.
Генерирани колони в PostgreSQL 12+
В последните версии на PostgreSQL генерираните колони са вградена функция, позволяваща на изразите CREATE TABLE или ALTER TABLE да добавят колона, в която съдържанието се „генерира“ автоматично в резултат на израз. Тези изрази могат да бъдат прости математически операции от други колони или по-усъвършенствана неизменна функция. Някои предимства от внедряването на генерирана колона в дизайн на база данни включват:
- Възможността за добавяне на колона към таблица, съдържаща изчислени данни, без необходимост от актуализиране на кода на приложението за генериране на данните, за да ги включите в операциите INSERT и UPDATE.
- Намаляване на времето за обработка на изключително чести оператори SELECT, които биха обработвали данните в движение. Тъй като обработката на данните се извършва по време на INSERT или UPDATE, данните се генерират веднъж и операторите SELECT трябва само да извлекат данните. При тежки среди за четене това може да е за предпочитане, стига използваното допълнително съхранение на данни да е приемливо.
- Тъй като генерираните колони се актуализират автоматично, когато самите изходни данни се актуализират, добавянето на генерирана колона ще добави предполагаема гаранция, че данните в генерираната колона са винаги правилни.
В PostgreSQL 12 е наличен само типът „STORED“ генерирана колона. В други системи за бази данни е налична генерирана колона с тип „VIRTUAL“, която действа по-скоро като изглед, в който резултатът се изчислява в движение, когато данните се извличат. Тъй като функционалността е толкова подобна на изгледите и просто записва операцията в оператор select, функционалността не е толкова полезна, колкото функционалността „СЪХРАНЯВАНЕ“, обсъждана тук, но има вероятност бъдещите версии да включват функцията.
Създаването на таблица с генерирана колона се извършва при дефиниране на самата колона. В този пример генерираната колона е „печалба“ и се генерира автоматично чрез изваждане на покупната_цена от колоните с цена_продажба, след което се умножава по колоната 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
);
В този пример е създадена таблица за „транзакции“ за проследяване на някои основни транзакции и печалби на въображаемо кафене. Вмъкването на данни в тази таблица ще покаже някои незабавни резултати.
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
При актуализиране на реда, генерираната колона автоматично ще се актуализира:
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
Това ще гарантира, че генерираната колона винаги е правилна, без допълнителна логика от страна на приложението.
ЗАБЕЛЕЖКА:Генерираните колони не могат да бъдат ВМЕШЕНИ или АКТУАЛИЗИРАНИ директно и всеки опит за това ще се върне с ГРЕШКА:
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.
Генерирани колони на PostgreSQL 11 и преди
Въпреки че вградените генерирани колони са нови за версия 12 на PostgreSQL, функционалността все още може да бъде постигната в по-ранни версии, просто се нуждае от малко повече настройка със съхранени процедури и тригери. Въпреки това, дори и с възможността да се приложи на по-стари версии, в допълнение към добавената функционалност, която може да бъде от полза, стриктното спазване на въвеждане на данни е по-трудно за постигане и зависи от функциите на PL/pgSQL и изобретателността на програмирането.
БОНУС:Примерът по-долу ще работи и на PostgreSQL 12+, така че ако добавената функционалност с комбинация от функция/тригер е необходима или желана в по-новите версии, тази опция е валиден резервен вариант и не е ограничен до само версии, по-стари от 12.
Докато това е начин да го направите в предишни версии на PostgreSQL, този метод има няколко допълнителни предимства:
- Тъй като имитирането на генерираната колона използва функция, могат да се използват по-сложни изчисления. Генерираните колони във версия 12 изискват НЕИЗМЕНЯМИ операции, но опция за задействане/функция може да използва СТАБИЛЕН или ПРОМЕНИЛ тип функция с по-големи възможности и съответно по-ниска производителност.
- Използването на функция, която има опцията да бъде СТАБИЛНА или ПРОМЕНЛИВА, също така отваря възможността за АКТУАЛИЗИРАНЕ на допълнителни колони, АКТУАЛИЗИРАНЕ на други таблици или дори създаване на нови данни чрез INSERTS в други таблици. (Въпреки че тези опции за задействане/функция са много по-гъвкави, това не означава, че липсва действителна „генерирана колона“, тъй като прави това, което се рекламира, с по-голяма производителност и ефективност.)
В този пример е настроен тригер/функция, за да имитира функционалността на генерирана от PostgreSQL 12+ колона, заедно с две части, които предизвикват изключение, ако INSERT или UPDATE се опитат да промени генерираната колона . Те могат да бъдат пропуснати, но ако са пропуснати, изключенията няма да бъдат повдигнати и действителните данни, ВЪСТАВЕНИ или АКТУАЛИЗИРАНИ, ще бъдат тихо изхвърлени, което обикновено не би било препоръчително.
Самият тригер е настроен да се изпълнява ПРЕДИ, което означава, че обработката се случва преди действителното вмъкване и изисква ВРЪЩАНЕ на НОВО, което е ЗАПИСЪТ, който е променен, за да съдържа новата генерирана стойност на колоната. Този конкретен пример е написан да работи на PostgreSQL версия 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();
ЗАБЕЛЕЖКА:Уверете се, че функцията има правилните разрешения/собственост, за да бъде изпълнена от желания потребител(и) на приложение.
Както се вижда в предишния пример, резултатите са същите в предишните версии с решение за функция/задействане:
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
Актуализирането на данните ще бъде подобно.
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
Накрая, опитът да се INSERT или UPDATE самата специална колона ще доведе до ГРЕШКА:
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
В този пример той действа различно от настройката на първата генерирана колона по няколко начина, които трябва да бъдат отбелязани:
- Ако 'генерираната колона' се опита да бъде актуализирана, но не бъде намерен ред, който да е актуализиран, тя ще върне успех с резултат "АКТУАЛИЗИРАНЕ 0", докато действително генерирана колона във версия 12 ще продължи върне ГРЕШКА, дори ако не е намерен ред за АКТУАЛИЗИРАНЕ.
- Когато се опитвате да актуализирате колоната за печалба, която „трябва“ винаги да връща ГРЕШКА, ако посочената стойност е същата като правилно „генерираната“ стойност, това ще успее. В крайна сметка обаче данните са верни, ако желаете да върнете ГРЕШКА, ако колоната е посочена.
Документация и PostgreSQL общност
Официалната документация за PostgreSQL генерираните колони се намира на официалния уебсайт на PostgreSQL. Проверете отново, когато бъдат пуснати нови основни версии на PostgreSQL, за да откриете нови функции, когато се появят.
Докато генерираните колони в PostgreSQL 12 са доста ясни, внедряването на подобна функционалност в предишните версии може да стане много по-сложно. PostgreSQL общността е много активна, масивна, световна и многоезична общност, посветена да помага на хора с всяко ниво на опит в PostgreSQL да решават проблеми и да създават нови решения като това.
- IRC :Freenode има много активен канал, наречен #postgres, където потребителите си помагат взаимно да разбират концепции, да коригират грешки или да намерят други ресурси. Пълен списък с наличните канали за свободни възли за всички неща на PostgreSQL може да бъде намерен на уебсайта на PostgreSQL.org.
- Пощенски списъци :PostgreSQL има шепа пощенски списъци, които могат да бъдат присъединени. Тук могат да се изпращат въпроси/проблеми с по-дълга форма и могат да достигнат до много повече хора от IRC във всеки един момент. Списъците могат да бъдат намерени на уебсайта на PostgreSQL, а списъците pgsql-general или pgsql-admin са добри ресурси.
- Slack :PostgreSQL общността също процъфтява в Slack и може да се присъедини към postgresteam.slack.com. Подобно на IRC, активна общност е налична, за да отговаря на въпроси и да участва във всичко, което PostgreSQL.