PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Aktualizacje oparte na niestandardowych wyzwalaczach dla PostgreSQL

Pierwsza REGUŁA: Nie aktualizujesz PostgreSQL z replikacją opartą na wyzwalaczu
Druga ZASADA: NIE uaktualniasz PostgreSQL z replikacją opartą na wyzwalaczach
Trzecia ZASADA: Jeśli uaktualnisz PostgreSQL z replikacją opartą na wyzwalaczach, przygotuj się na cierpienie. I dobrze się przygotuj.

Musi istnieć bardzo poważny powód, aby nie używać pg_upgrade do aktualizacji PostgreSQL.

OK, powiedzmy, że nie możesz sobie pozwolić na więcej niż kilka sekund przestoju. Użyj więc pglogicznego.

OK, powiedzmy, że uruchamiasz 9.3 i dlatego nie możesz używać pglogical. Użyj Londiste.

Nie możesz znaleźć czytelnego pliku README? Użyj SLONY.

Zbyt skomplikowane? Użyj replikacji strumieniowej - promuj urządzenie podrzędne i uruchom na nim pg_upgrade - następnie przełącz aplikacje, aby działały z nowym promowanym serwerem.

Twoja aplikacja przez cały czas stosunkowo intensywnie pisze? Przyjrzałeś się wszystkim możliwym rozwiązaniom i nadal chcesz skonfigurować replikację opartą na niestandardowych wyzwalaczach? Są rzeczy, na które powinieneś zwrócić uwagę:

  • Wszystkie stoły wymagają PK. Nie powinieneś polegać na ctid (nawet z wyłączoną funkcją automatycznego odkurzania)
  • Będziesz musiał włączyć wyzwalacz dla wszystkich tabel powiązanych z ograniczeniami (i możesz potrzebować odroczonego FK)
  • Sekwencje wymagają ręcznej synchronizacji
  • Uprawnienia nie są replikowane (chyba że skonfigurujesz również wyzwalacz zdarzenia)
  • Wyzwalacze zdarzeń mogą pomóc w automatyzacji obsługi nowych tabel, ale lepiej nie komplikować i tak już skomplikowanego procesu. (jak tworzenie wyzwalacza i obcej tabeli przy tworzeniu tabeli, tworzenie tej samej tabeli na obcym serwerze lub zmienianie zdalnej tabeli serwera z tą samą zmianą, co robisz na starej bazie danych)
  • Dla każdej instrukcji wyzwalacz jest mniej niezawodny, ale prawdopodobnie prostszy
  • Powinieneś żywo wyobrazić sobie swój wcześniejszy proces migracji danych
  • Powinieneś zaplanować ograniczoną dostępność tabel podczas konfigurowania i włączania replikacji opartej na wyzwalaczach
  • Powinieneś całkowicie poznać zależności i ograniczenia relacji, zanim pójdziesz w tę stronę.

Dość ostrzeżeń? Chcesz już zagrać? Zacznijmy więc od kodu.

Przed napisaniem jakichkolwiek wyzwalaczy musimy zbudować zestaw danych makiety. Czemu? Czy nie byłoby o wiele łatwiej mieć wyzwalacz, zanim będziemy mieć dane? Czyli dane zostałyby natychmiast zreplikowane do klastra „uaktualnienia”? Jasne, że tak. Ale co w takim razie chcemy ulepszyć? Po prostu zbuduj zestaw danych w nowszej wersji. Tak więc, jeśli planujesz uaktualnienie do wyższej wersji i potrzebujesz dodać tabelę, utwórz wyzwalacze replikacji przed umieszczeniem danych, wyeliminuje to potrzebę późniejszej synchronizacji niereplikowanych danych. Ale takie nowe stoły to, można powiedzieć, łatwa część. Więc najpierw wymyślmy przypadek, w którym mamy dane, zanim zdecydujemy się na aktualizację.

Załóżmy, że przestarzały serwer nazywa się p93 (najstarszy obsługiwany), a ten, na który replikujemy, to p10 (11 jest już w drodze w tym kwartale, ale jeszcze się nie wydarzył):

\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;

Tutaj używam psql, więc mogę użyć \c meta-polecenia, aby połączyć się z innym db. Jeśli chcesz śledzić ten kod z innym klientem, musisz zamiast tego ponownie połączyć się. Oczywiście nie potrzebujesz tego kroku, jeśli uruchamiasz to po raz pierwszy. Kilka razy musiałem odtwarzać swoją piaskownicę, dzięki czemu zapisałem wyciągi…

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

Tworzymy więc dwie świeże bazy danych. Teraz połączę się z tym, który chcemy zaktualizować, i stworzę kilka typów danych funkey i użyję ich do wypełnienia tabeli, którą uznamy za istniejącą później:

\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;

Co teraz mamy?

  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)

OK, trochę danych - po co tyle wstawiałem, a potem usuwałem? Cóż, próbujemy stworzyć makiety zestawu danych, który istniał przez jakiś czas. Więc staram się trochę to rozproszyć. Przesuńmy jeszcze jeden wiersz (0,3) na koniec strony (0,145):

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

Załóżmy teraz, że użyjemy PostgreSQL_fdw (używanie tutaj dblink byłoby zasadniczo takie samo i prawdopodobnie szybsze w wersji 9.3, więc zrób to, jeśli chcesz).

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

Teraz możemy użyć pg_dump -s, aby uzyskać DDL, ale mam to powyżej. Musimy utworzyć tę samą tabelę w klastrze wyższej wersji, aby replikować dane do:

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

Teraz wracamy do wersji 9.3 i używamy tabel obcych do migracji danych (ja użyję f_ tutaj konwencja nazw tabel, f oznacza obcy):

\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');

Wreszcie! Tworzymy funkcję wstawiania i wyzwalacz.

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;

Tu i później użyję linków do dłuższego kodu. Po pierwsze, aby tekst mówiony nie zapadał się w język maszynowy. Po drugie, ponieważ używam kilku wersji tych samych funkcji, aby odzwierciedlić, jak kod powinien ewoluować na żądanie.

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

Wynik:

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)

Co tu widzimy? Widzimy, że nowo wprowadzone dane są pomyślnie replikowane do bazy danych p10. I odpowiednio jest wycofywany, jeśli transakcja się nie powiedzie. Jak na razie dobrze. Ale nie można było nie zauważyć (tak, tak - nie), że tabela na p93 jest znacznie większa - stare dane nie powielały się. Jak to tam dostajemy? Cóż, proste:

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

zrobiłby. I nie jest to główny problem - powinieneś raczej martwić się o to, jak będziesz zarządzać istniejącymi wcześniej danymi dotyczącymi aktualizacji i usunięcia - ponieważ instrukcje pomyślnie uruchomione na niższej wersji bazy danych zakończą się niepowodzeniem lub po prostu wpłyną na zero wierszy na wyższej - tylko dlatego, że nie ma wcześniej istniejących danych ! I tu dochodzimy do frazy o sekundach przestoju. (Gdyby to był film, oczywiście tutaj mielibyśmy retrospekcję, ale niestety - jeśli fraza „sekundy przestoju” nie zwróciła twojej uwagi wcześniej, będziesz musiał przejść wyżej i poszukać frazy...)

Aby włączyć wszystkie wyzwalacze instrukcji, musisz zamrozić tabelę, skopiować wszystkie dane, a następnie włączyć wyzwalacze, aby tabele w bazach danych w wersjach niższych i wyższych były zsynchronizowane, a wszystkie instrukcje byłyby po prostu takie same (lub bardzo bliskie, ponieważ fizyczne rozkład będzie się różnić, ponownie spójrz powyżej na pierwszy przykład dla kolumny ctid) wpływ. Ale uruchomienie takiego „włącz replikację” na stole w jednej biiiiiig transakcji nie będzie trwało kilka sekund. Potencjalnie sprawi to, że witryna będzie tylko do odczytu przez wiele godzin. Zwłaszcza jeśli stół jest z grubsza połączony przez FK z innymi dużymi stołami.

Tylko do odczytu nie oznacza całkowitego przestoju. Ale później spróbujemy pozostawić wszystkie SELECTS i niektóre INSERT,DELETE,UPDATE (na nowych danych, nieudane na starych). Przeniesienie tabeli lub transakcji do trybu tylko do odczytu można wykonać na wiele sposobów - czy byłoby to podejście oparte na PostgreSQL, poziomie aplikacji, a nawet tymczasowe odebranie odpowiednich uprawnień. Te podejścia same w sobie mogą być tematem na jego własnym blogu, dlatego wspomnę tylko o tym.

Tak czy siak. Powrót do wyzwalaczy. Aby wykonać tę samą akcję, wymagającą pracy na oddzielnym wierszu (UPDATE, DELETE) na zdalnej tabeli, tak jak na lokalnej, musimy użyć kluczy podstawowych, ponieważ fizyczna lokalizacja będzie się różnić. Klucze podstawowe są tworzone w różnych tabelach z różnymi kolumnami, dlatego albo musimy utworzyć unikalną funkcję dla każdej tabeli, albo spróbować napisać jakąś ogólną. Załóżmy (dla uproszczenia) że mamy tylko jedną kolumnę PK, wtedy ta funkcja powinna pomóc. Więc w końcu! Miejmy tutaj funkcję aktualizacji. I oczywiście wyzwalacz:

create trigger tgu before update on t for each row execute procedure tgf_u();
Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument

Zobaczmy, czy to zadziała:

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

Wynik:

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

OK. I chociaż wciąż jest gorąco, dodajmy również funkcję wyzwalacza usuwania i replikację:

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

I sprawdź:

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

Dając:

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

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

Jak pamiętamy (kto mógłby o tym zapomnieć!) nie włączamy obsługi „replikacji” w transakcji. A powinniśmy, jeśli chcemy spójnych danych. Jak wspomniano powyżej, WSZYSTKIE wyzwalacze instrukcji we WSZYSTKICH tabelach powiązanych z FK powinny być włączone w jednej transakcji, uprzednio przygotowanej przez synchronizację danych. W przeciwnym razie moglibyśmy wpaść w:

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;

Dając:

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;

Yayki! Usunęliśmy wiersz w niższej wersji db, a nie w nowszej! Tylko dlatego, że go tam nie było. Nie stałoby się to, gdybyśmy zrobili to we właściwy sposób (begin;sync;enable trigger;end;). Ale właściwy sposób sprawi, że tabele będą tylko do odczytu przez długi czas! Najbardziej zagorzały czytelnik powiedziałby nawet „dlaczego w ogóle miałbyś wyzwalać opartą na replikacji?”.

Możesz to zrobić za pomocą pg_upgrade, tak jak zrobiliby to „normalni” ludzie. A w przypadku replikacji strumieniowej możesz ustawić wszystkie ustawienia tylko do odczytu. Wstrzymaj odtwarzanie xloga i uaktualnij mastera, gdy aplikacja jest nadal RO jako slave.

Dokładnie tak! Czy nie zacząłem od tego?

Replikacja oparta na wyzwalaczu pojawia się na scenie, gdy potrzebujesz czegoś wyjątkowego. Na przykład możesz spróbować zezwolić na SELECT i pewną modyfikację na nowo utworzonych danych, a nie tylko RO. Załóżmy, że masz ankietę online — użytkownik rejestruje się, odpowiada, otrzymuje punkty-bez-premiowe-inny-nikt-nie potrzebuje-świetnych-rzeczy i odchodzi. Dzięki takiej strukturze możesz po prostu zabronić modyfikacji danych, które nie są jeszcze w wyższej wersji, umożliwiając cały przepływ danych nowym użytkownikom.

Porzucasz więc kilka osób pracujących w bankomatach internetowych, pozwalając nowicjuszom pracować, nawet nie zauważając, że jesteś w trakcie aktualizacji. Brzmi okropnie, ale czy nie powiedziałem hipotetycznie? nie zrobiłem? Cóż, miałem to na myśli.

Bez względu na to, jaki może być prawdziwy przypadek, zobaczmy, jak możesz go wdrożyć. Funkcje usuwania i aktualizacji ulegną zmianie. A teraz sprawdźmy ostatni scenariusz:

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

Wiersz nie został usunięty w niższej wersji, ponieważ nie został znaleziony w wyższej. To samo stałoby się z aktualizacją. Spróbuj sam. Teraz możesz rozpocząć synchronizację danych bez zatrzymywania wielu modyfikacji w tabeli, którą dołączasz do replikacji opartej na wyzwalaczu.

Czy lepiej? Gorzej? Jest inaczej - ma wiele wad i pewne zalety w stosunku do globalnego systemu RO. Moim celem było zademonstrowanie, dlaczego ktoś miałby chcieć zastosować tak skomplikowaną metodę nad normalną - uzyskać określone zdolności w stabilnym, dobrze znanym procesie. Oczywiście za pewną cenę…

Tak więc teraz, gdy czujemy się trochę bezpieczniej dla spójności danych i podczas gdy nasze wcześniej istniejące dane w tabeli t są synchronizowane z p10, możemy porozmawiać o innych tabelach. Jak by to wszystko działało z FK (w końcu wielokrotnie wspominałem FK, muszę to uwzględnić w próbce). Po co czekać?

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

Z pewnością warto owinąć te trzy funkcje w celu „wyzwolenia” wielu tabel. Ale nie zrobię tego. Ponieważ nie zamierzam dodawać więcej tabel - dwie bazy danych relacji, do których się odwołują, to już taki bałagan!

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

Wynik w:

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)

Ponownie. Wygląda na to, że istnieje spójność danych. Możesz również rozpocząć synchronizację danych dla nowej tabeli c…

Zmęczony? Zdecydowanie tak.

Wniosek

Na zakończenie chciałbym zwrócić uwagę na kilka błędów, które popełniłem przyglądając się temu podejściu. Kiedy budowałem zestawienie aktualizacji, dynamicznie wymieniając wszystkie kolumny z pg_attribute, straciłem godzinę. Wyobraź sobie, jak byłem rozczarowany, gdy później odkryłem, że całkowicie zapomniałem o konstrukcji UPDATE (lista) =(lista)! A funkcja stała się znacznie krótsza i bardziej czytelna.

Więc błędem numer jeden było – próba zbudowania wszystkiego samemu, tylko dlatego, że wygląda na tak osiągalną. Nadal tak jest, ale jak zwykle ktoś już prawdopodobnie zrobił to lepiej – poświęcenie dwóch minut tylko na sprawdzenie, czy rzeczywiście tak jest, może zaoszczędzić godzinę późniejszego myślenia.

A po drugie - dla mnie rzecz wyglądała na znacznie prostszą, gdzie okazały się znacznie głębsze, i nadmiernie skomplikowałem wiele przypadków, które doskonale sprawdzają się w modelu transakcyjnym PostgreSQL.

Więc dopiero po próbie zbudowania piaskownicy uzyskałem dość jasne zrozumienie szacunków tego podejścia.

Tak więc planowanie jest oczywiście potrzebne, ale nie planuj więcej, niż możesz zrobić.

Doświadczenie przychodzi z praktyką.

Moja piaskownica przypomniała mi strategię komputerową – siadasz do niej po obiedzie i myślisz – aha, tu buduję Pyramyd, tam łucznictwo, potem przemieniam się w Sons of Ra i buduję 20 łuczników, a tu atakuję żałosnych sąsiedzi. Dwie godziny chwały”. I NAGLE następnego ranka, dwie godziny przed pracą, znajdujesz się z „Jak się tu dostałem? Dlaczego muszę podpisywać ten upokarzający sojusz z nieumytymi barbarzyńcami, aby uratować mojego ostatniego łucznika i czy naprawdę muszę za niego sprzedać moją tak ciężko zbudowaną Piramidę?”

Czytania:

  • 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. Najlepsze rozwiązania DBaaS dla PostgreSQL

  2. Sprawdź, czy sekwencja istnieje w Postgresie (plpgsql)

  3. Skalowanie PostgreSQL za pomocą puli połączeń i równoważenia obciążenia

  4. Jak zaimportować zrzut Heroku PG na komputer lokalny?

  5. Jak zainstalować PostgreSQL 12 na Ubuntu 20.04 DigitalOcean?