PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Персонализирани надстройки, базирани на тригери за PostgreSQL

1-во ПРАВИЛО: Не надграждате PostgreSQL с репликация, базирана на тригери
2-ро ПРАВИЛО: НЕ надстройвате PostgreSQL с репликация, базирана на тригери
3-то ПРАВИЛО: Ако надстроите PostgreSQL с репликация, базирана на тригери, подгответе се да страдате. И се подгответе добре.

Трябва да има много сериозна причина да не използвате pg_upgrade за надграждане на PostgreSQL.

Добре, да кажем, че не можете да си позволите повече от секунди престой. Тогава използвайте pglogical.

Добре, да кажем, че стартирате 9.3 и по този начин не можете да използвате pglogical. Използвайте Londiste.

Не можете да намерите четим README? Използвайте SLONY.

Твърде сложно? Използвайте стрийминг репликация – популяризирайте подчинения и стартирайте pg_upgrade върху него – след това превключете приложенията, за да работят с нов популяризиран сървър.

Приложението ви е относително интензивно за писане през цялото време? Разгледахте всички възможни решения и все още искате да настроите персонализирана репликация, базирана на тригери? Има неща, на които трябва да обърнете внимание:

  • Всички таблици се нуждаят от PK. Не трябва да разчитате на ctid (дори и с деактивиран автовакуум)
  • Ще трябва да активирате задействане за всички свързани с ограничения таблици (и може да се нуждаете от Deferred FK)
  • Последователностите се нуждаят от ръчно синхронизиране
  • Разрешенията не се репликират (освен ако не настроите и задействане на събитие)
  • Задействанията на събития могат да помогнат при автоматизирането на поддръжката за нови таблици, но е по-добре да не усложняват вече сложен процес. (като създаване на тригер и чужда таблица при създаване на таблица, също създаване на същата таблица на чужд сървър или промяна на таблица на отдалечен сървър със същата промяна, която правите на стария db)
  • За всяко изявление тригерът е по-малко надежден, но вероятно по-прост
  • Трябва ярко да си представите съществуващия си процес на миграция на данни
  • Трябва да планирате ограничен достъп до таблици, докато настройвате и активирате репликация, базирана на тригери
  • Трябва напълно да познавате зависимостите и ограниченията на отношенията си, преди да тръгнете по този път.

Достатъчно предупреждения? Искате ли да играете вече? Тогава нека започнем с някакъв код.

Преди да напишем каквито и да било тригери, трябва да изградим някакъв моделен набор от данни. Защо? Не би ли било много по-лесно да имаме тригер, преди да имаме данни? Значи данните ще се репликират в клъстера за надграждане наведнъж? Разбира се, че щеше. Но тогава какво искаме да надстроим? Просто създайте набор от данни на по-нова версия. Така че да, ако планирате да надстроите до по-висока версия и трябва да добавите някаква таблица, да създадете тригери за репликация, преди да поставите данните, това ще елиминира необходимостта от синхронизиране на нерепликирани данни по-късно. Но такива нови маси са, можем да кажем, лесна част. Така че нека първо да симулираме случая, когато имаме данни, преди да решим да надстроим.

Да предположим, че остарял сървър се нарича p93 (най-старият поддържан) и този, на който репликираме, се нарича p10 (11 е на път през това тримесечие, но все още не се е случило):

\c PostgreSQL
select pg_terminate_backend(pid) from pg_stat_activity where datname in ('p93','p10');
drop database if exists p93;
drop database if exists p10;

Тук използвам psql, така че мога да използвам мета-команда \c за свързване с други db. Ако искате да следвате този код с друг клиент, ще трябва да се свържете отново. Разбира се, нямате нужда от тази стъпка, ако я стартирате за първи път. Няколко пъти трябваше да пресъздам моята пясъчна кутия, така че запазих изявления...

create database p93; --old db (I use 9.3 as oldest supported ATM version)
create database p10; --new db 

Така че създаваме две нови бази данни. Сега ще се свържа с този, който искаме да надстроим, и ще създам няколко функционални типа данни и ще ги използвам за попълване на таблица, която ще считаме за съществуваща по-късно:

\c p93
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);
insert into t values(0, now(), '{"a":{"aa":[1,3,2]}}', 'foo', 'b', (3,'aloha'));
insert into t (j,e) values ('{"b":null}', 'a');
insert into t (t) select chr(g) from generate_series(100,240) g;--add some more data
delete from t where i > 3 and i < 142; --mockup activity and mix tuples to be not sequential
insert into t (t) select null;

Сега какво имаме?

  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:03:00+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:03:00+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:03:00+03 |                      | d   |   | 
 (0,4)   |   3 | 2018-07-08 08:03:00+03 |                      | e   |   | 
 (0,143) | 142 | 2018-07-08 08:03:00+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:03:00+03 |                      |     |   | 
(6 rows)

Добре, малко данни - защо вмъкнах и после изтрих толкова много? Е, ние се опитваме да изобразим набор от данни, който съществува от известно време. Затова се опитвам да го направя малко разпръснат. Нека преместим още един ред (0,3) до края на страницата (0,145):

update t set j = '{}' where i =3; --(0,4)

Сега да предположим, че ще използваме PostgreSQL_fdw (използването на dblink тук по принцип би било същото и вероятно по-бързо за 9.3, така че, моля, направете го, ако желаете).

create extension PostgreSQL_fdw;
create server p10 foreign data wrapper PostgreSQL_fdw options (host 'localhost', dbname 'p10'); --I know it's the same 9.3 server - change host to other version and use other cluster if you wish. It's not important for the sandbox...
create user MAPPING FOR vao SERVER p10 options(user 'vao', password 'tsun');

Сега можем да използваме pg_dump -s, за да получим DDL, но просто го имам по-горе. Трябва да създадем същата таблица в клъстера с по-висока версия, за да репликираме данни към:

\c p10
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);

Сега се връщаме към 9.3 и използваме чужди таблици за миграция на данни (ще използвам f_ конвенция за имена на таблици тук, f означава чужди):

\c p93
create foreign table f_t(i serial, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit) server p10 options (TABLE_name 't');

Най-накрая! Създаваме функция за вмъкване и задействане.

create or replace function tgf_i() returns trigger as $$
begin
  execute format('insert into %I select ($1).*','f_'||TG_RELNAME) using NEW;
  return NEW;
end;
$$ language plpgsql;

Тук и по-късно ще използвам връзки за по-дълъг код. Първо, така че изговореният текст да не потъне в машинния език. Второ, защото използвам няколко версии на едни и същи функции, за да отразявам как кодът трябва да се развива при поискване.

--OK - first table ready - lets try logical trigger based replication on inserts:
insert into t (t) select 'one';
--and now transactional:
begin;
  insert into t (t) select 'two';
  select ctid, * from f_t;
  select ctid, * from t;
rollback;
select ctid, * from f_t where i > 143;
select ctid, * from t where i > 143;

Резултат:

INSERT 0 1
BEGIN
INSERT 0 1
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
 (0,2) | 145 | 2018-07-08 08:27:15+03 |   | two |   | 
(2 rows)

  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:27:15+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:27:15+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:27:15+03 |                      | d   |   | 
 (0,143) | 142 | 2018-07-08 08:27:15+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:27:15+03 |                      |     |   | 
 (0,145) |   3 | 2018-07-08 08:27:15+03 | {}                   | e   |   | 
 (0,146) | 144 | 2018-07-08 08:27:15+03 |                      | one |   | 
 (0,147) | 145 | 2018-07-08 08:27:15+03 |                      | two |   | 
(8 rows)

ROLLBACK
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

  ctid   |  i  |           ts           | j |  t  | e | c 
---------+-----+------------------------+---+-----+---+---
 (0,146) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

Какво виждаме тук? Виждаме, че нововмъкнатите данни се репликират в база данни p10 успешно. И съответно се отменя, ако транзакцията се провали. Дотук добре. Но не можете да не забележите (да, да - не), че таблицата на p93 е много по-голяма - старите данни не се репликират. Как да го стигнем до там? Ами просто:

insert into … select local.* from ...outer join foreign where foreign.PK is null 

би направил. И това не е основната грижа тук - по-скоро трябва да се притеснявате как ще управлявате предварително съществуващите данни за актуализации и изтривания - защото отчетите, които се изпълняват успешно на по-ниска версия db, ще се провалят или просто ще засегнат нула редове на по-висока - просто защото няма съществуващи данни ! И тук стигаме до секундите на фразата за престой. (Ако беше филм, разбира се, тук щяхме да имаме ретроспекция, но уви - ако фразата „секунди на престой“ не привлече вниманието ви по-рано, ще трябва да отидете по-горе и да потърсите фразата...)

За да активирате всички задействания на изрази, трябва да замразите таблицата, да копирате всички данни и след това да активирате задействанията, така че таблиците в базите данни с по-ниски и по-високи версии ще бъдат синхронизирани и всички изрази просто ще имат еднакви (или изключително близки, тъй като физически разпределението ще се различава, отново погледнете по-горе първия пример за ctid колона). Но изпълнението на такова „включване на репликацията“ на таблицата в една biiiiiig транзакция няма да бъде секунди престой. Потенциално това ще направи сайта само за четене за часове. Особено ако масата е грубо свързана от FK с други големи маси.

Добре само за четене не е пълен престой. Но по-късно ще се опитаме да оставим всички SELECTS и някои INSERT,DELETE,UPDATE да работят (при нови данни, неуспешни при стари). Преместването на таблица или транзакция в режим само за четене може да се извърши по много начини - дали това е някакъв подход на PostgreSQL, или ниво на приложение, или дори временно оттегляне според разрешенията. Самите тези подходи могат да бъдат тема за собствен блог, затова само ще го спомена.

Така или иначе. Обратно към тригерите. За да извършим същото действие, изискващо работа върху отделен ред (АКТУАЛИЗИРАНЕ, ИЗТРИВАНЕ) на отдалечена таблица, както правите на локална, трябва да използваме първични ключове, тъй като физическото местоположение ще се различава. И първичните ключове се създават в различни таблици с различни колони, така че трябва или да създадем уникална функция за всяка таблица, или да опитаме да напишем някаква обща. Нека (за простота) да приемем, че имаме само една колона PK, тогава тази функция трябва да помогне. И така най-накрая! Нека имаме функция за актуализиране тук. И очевидно спусък:

create trigger tgu before update on t for each row execute procedure tgf_u();
Изтеглете Бялата книга днес Управление и автоматизация на PostgreSQL с ClusterControl Научете какво трябва да знаете, за да внедрите, наблюдавате, управлявате и мащабирате PostgreSQLD Изтеглете Бялата книга

И да видим дали работи:

begin;
        update t set j = '{"updated":true}' where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

В резултат на:

BEGIN
psql:blog.sql:71: INFO:  (144,"2018-07-08 09:09:20+03","{""updated"":true}",one,,)
UPDATE 1
  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

ROLLBACK

ДОБРЕ. И докато все още е горещо, нека добавим и функция за тригер за изтриване и репликация:

create trigger tgd before delete on t for each row execute procedure tgf_d();

И проверете:

begin;
        delete from t where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Даване:

DELETE 1
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

Както си спомняме (кой би могъл да забрави това!), ние не включваме поддръжката за „репликация“ в транзакция. И трябва, ако искаме последователни данни. Както беше казано по-горе, ВСИЧКИ задействания на оператори във ВСИЧКИ свързани таблици трябва да бъдат активирани в една транзакция, предварително подготвена чрез синхронизиране на данни. В противен случай може да попаднем в:

begin;
        select * from t where i = 3;
        delete from t where i = 3;
        select * from t where i = 3;
        select * from f_t where i = 3;
Rollback;

Даване:

p93=# begin;
BEGIN
p93=#         select * from t where i = 3;
 i |           ts           | j  | t | e | c 
---+------------------------+----+---+---+---
 3 | 2018-07-08 09:16:27+03 | {} | e |   | 
(1 row)

p93=#         delete from t where i = 3;
DELETE 1
p93=#         select * from t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=#         select * from f_t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=# rollback;

Яйки! Изтрихме ред на по-ниска версия db, а не на по-нова! Просто защото го нямаше. Това няма да се случи, ако го направихме по правилния начин (начало; синхронизиране; активиране на тригер; край;). Но правилният начин би направил таблиците само за четене за дълго време! Най-твърдият читател дори би казал „защо тогава изобщо да задействате базирана репликация?“.

Можете да го направите с pg_upgrade, както биха направили „нормалните“ хора. А в случай на поточно репликация можете да направите всички настройки само за четене. Поставете на пауза възпроизвеждането на xlog и надстройте главния, докато приложението все още е подчинено.

Точно! Не започнах ли с него?

Базираната на тригер репликация идва на етапа, когато имате нужда от нещо много специално. Например, можете да опитате да разрешите SELECT и някои модификации на новосъздадени данни, а не само на RO. Да приемем, че имате онлайн въпросник - потребителят се регистрира, отговаря, получава своите бонус-безплатни точки-други-никой-не-нужда-страхотни неща и си тръгва. С такава структура можете просто да забраните модификации на данни, които все още не са на по-висока версия, позволявайки целия поток от данни за нови потребители.

Така че изоставяте малко хора, работещи в онлайн банкомати, оставяйки новодошлите да работят, без дори да забележите, че сте в средата на надграждане. Звучи ужасно, но не казах ли хипотетично? аз не го направих? Е, имах предвид.

Без значение какъв може да бъде случай от реалния живот, нека да разгледаме как можете да го приложите. Функциите за изтриване и актуализиране ще се променят. И нека сега да проверим последния сценарий:

BEGIN
psql:blog.sql:86: ERROR:  This data is not replicated yet, thus can't be deleted
psql:blog.sql:87: ERROR:  current transaction is aborted, commands ignored until end of transaction block
psql:blog.sql:88: ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

Редът не беше изтрит в по-ниската версия, защото не беше намерен в по-високата. Същото нещо би се случило и с актуализираните. Опитайте го сами. Сега можете да стартирате синхронизиране на данни, без да спирате много модификации на таблицата, която включвате в репликация, базирана на тригери.

по-добре ли е? по-лошо? Различно е - има много недостатъци и някои предимства пред глобалната RO система. Целта ми беше да демонстрирам защо някой би искал да използва такъв сложен метод пред нормалния - да получи специфични способности върху стабилен, добре познат процес. На известна цена, разбира се...

И така, сега, когато се чувстваме малко по-сигурни за последователност на данните и докато нашите предишни данни в таблица t се синхронизират с p10, можем да говорим за други таблици. Как би работило всичко с FK (след всичко, което споменах FK толкова много пъти, трябва да го включа в извадката). Е, защо да чакам?

create table c (i serial, t int references t(i), x text);
--and accordingly a foreign table - the one on newer version...
\c p10
create table c (i serial, t int references t(i), x text);
\c p93
create foreign table f_c(i serial, t int, x text) server p10 options (TABLE_name 'c');
--let’s pretend it had some data before we decided to migrate with triggers to a higher version
insert into c (t,x) values (1,'FK');
--- so now we add triggers to replicate DML:
create trigger tgi before insert on c for each row execute procedure tgf_i();
create trigger tgu before update on c for each row execute procedure tgf_u();
create trigger tgd before delete on c for each row execute procedure tgf_d();

Със сигурност си струва да обвием тези три до функция с цел да "задействаме" много таблици. но няма да го направя. Тъй като няма да добавям повече таблици - базата данни с две реферирани релации вече е толкова объркана мрежа!

--now, what would happen if we tr inserting referenced FK, that does not exist on remote db?..
insert into c (t,x) values (2,'FK');
/* it fails with:
psql:blog.sql:139: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
a new row isn't inserted neither on remote, nor local db, so we have safe data consistencyy, but inserts are blocked?..
Yes untill data that existed untill trigerising gets to remote db - ou cant insert FK with before triggerising keys, yet - a new (both t and c tables) data will be accepted:
*/
insert into t(i) values(4); --I use gap we got by deleting data above, so I dont need to "returning" and know the exact id -less coding in sample script
insert into c(t) values(4);
select * from c;
select * from f_c;

Резултат в:

psql:blog.sql:109: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
DETAIL:  Key (t)=(2) is not present in table "t".
CONTEXT:  Remote SQL command: INSERT INTO public.c(i, t, x) VALUES ($1, $2, $3)
SQL statement "insert into f_c select ($1).*"
PL/pgSQL function tgf_i() line 3 at EXECUTE statement
INSERT 0 1
INSERT 0 1
 i | t | x  
---+---+----
 1 | 1 | FK
 3 | 4 | 
(2 rows)

 i | t | x 
---+---+---
 3 | 4 | 
(1 row)

Отново. Изглежда, че последователността на данните е налице. Можете също да започнете да синхронизирате данни за нова таблица c…

Изморен? Определено съм.

Заключение

В заключение бих искал да подчертая някои грешки, които направих, докато разглеждах този подход. Докато изграждах изявлението за актуализация, динамично изброявайки всички колони от pg_attribute, загубих доста час. Представете си колко разочарован бях да открия по-късно, че напълно забравих за конструкцията UPDATE (списък) =(списък)! И функцията дойде в много по-кратко и по-четливо състояние.

Така че грешка номер едно беше - опитвайки се да изградите всичко сами, само защото изглежда толкова достъпно. Все още е така, но както винаги някой вече вероятно го е направил по-добре – отделянето на две минути само за да провери дали наистина е така, може да ви спести час на мислене по-късно.

И второ – нещото ми изглеждаше много по-просто, където се оказаха много по-дълбоки и преусложних много случаи, които се поддържат перфектно от модела на транзакциите на PostgreSQL.

Така че едва след като се опитах да изградя пясъчната кутия, получих донякъде ясно разбиране за оценките на този подход.

Така че планирането очевидно е необходимо, но не планирайте повече, отколкото всъщност можете да направите.

Опитът идва с практиката.

Моят пясъчник ми напомни за компютърна стратегия – сядаш след обяд и си мислиш – „аха, тук строя Pyramyd, там получавам стрелба с лък, след това се превръщам в Sons of Ra и изграждам 20 мъже с дълъг лък и тук атакувам жалкия съседи. Два часа слава.” И ИЗНЕНАДНО се оказвате на следващата сутрин, два часа преди работа с „Как попаднах тук? Защо трябва да подписвам този унизителен съюз с неизмити варвари, за да спася последния си човек с дълъг лък и наистина ли трябва да продам така трудно построената си пирамида за това?

Показания:

  • https://www.PostgreSQL.org/docs/current/static/different-replication-solutions.html
  • https://stackoverflow.com/questions/15343075/update-multiple-columns-in-a-trigger-function-in-plpgsql

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да активирате SSL в PostgreSQL

  2. Разбиране и четене на системния каталог на PostgreSQL

  3. Да дефинирате имената на таблици и колони като аргументи във функцията plpgsql?

  4. как да променя порта за слушане на postgresql в windows?

  5. PostgreSQL връща функция с персонализиран тип данни