PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Benutzerdefinierte Trigger-basierte Upgrades für PostgreSQL

1. REGEL: Sie aktualisieren PostgreSQL nicht mit Trigger-basierter Replikation
Zweite REGEL: Sie aktualisieren PostgreSQL NICHT mit Trigger-basierter Replikation
3. REGEL: Wenn Sie PostgreSQL mit Trigger-basierter Replikation aktualisieren, bereiten Sie sich darauf vor, zu leiden. Und bereiten Sie sich gut vor.

Es muss einen sehr schwerwiegenden Grund geben, pg_upgrade nicht zum Aktualisieren von PostgreSQL zu verwenden.

Nehmen wir an, Sie können sich nicht mehr als Sekunden Ausfallzeit leisten. Verwenden Sie dann pglogical.

Nehmen wir an, Sie führen 9.3 aus und können daher pglogical nicht verwenden. Verwenden Sie Londist.

Kann keine lesbare README finden? Verwenden Sie SLONY.

Zu kompliziert? Verwenden Sie die Streaming-Replikation - stufen Sie den Slave hoch und führen Sie pg_upgrade darauf aus - wechseln Sie dann die Apps, damit sie mit dem neuen hochgestuften Server funktionieren.

Ihre App ist die ganze Zeit relativ schreibintensiv? Sie haben alle möglichen Lösungen geprüft und möchten dennoch eine benutzerdefinierte Trigger-basierte Replikation einrichten? Es gibt Dinge, auf die Sie dann achten sollten:

  • Alle Tabellen benötigen PK. Sie sollten sich nicht auf ctid verlassen (selbst wenn das Autovakuum deaktiviert ist)
  • Sie müssen den Trigger für alle Constraint-Bonding-Tabellen aktivieren (und benötigen möglicherweise Deferred FK)
  • Sequenzen müssen manuell synchronisiert werden
  • Berechtigungen werden nicht repliziert (es sei denn, Sie richten auch einen Ereignisauslöser ein)
  • Ereignisauslöser können bei der Automatisierung der Unterstützung für neue Tabellen helfen, aber es ist besser, einen bereits komplizierten Prozess nicht zu verkomplizieren. (wie das Erstellen eines Triggers und einer fremden Tabelle bei der Tabellenerstellung, auch das Erstellen derselben Tabelle auf einem fremden Server oder das Ändern einer Remote-Server-Tabelle mit derselben Änderung, wie Sie es auf einer alten Datenbank tun)
  • Für jede Anweisung ist der Auslöser weniger zuverlässig, aber wahrscheinlich einfacher
  • Sie sollten sich Ihren bereits bestehenden Datenmigrationsprozess genau vorstellen
  • Beim Einrichten und Aktivieren der auslöserbasierten Replikation sollten Sie einen eingeschränkten Zugriff auf Tabellen einplanen
  • Sie sollten die Abhängigkeiten und Einschränkungen Ihrer Beziehungen unbedingt kennen, bevor Sie diesen Weg gehen.

Genug Warnungen? Du willst schon spielen? Beginnen wir dann mit etwas Code.

Bevor wir irgendwelche Trigger schreiben, müssen wir einen Mock-up-Datensatz erstellen. Wieso den? Wäre es nicht viel einfacher, einen Auslöser zu haben, bevor wir Daten haben? Die Daten würden also sofort in den „Upgrade“-Cluster repliziert? Sicher würde es. Aber was wollen wir dann upgraden? Erstellen Sie einfach einen Datensatz auf einer neueren Version. Also ja, wenn Sie ein Upgrade auf eine höhere Version planen und einige Tabellen hinzufügen müssen, erstellen Sie Replikationsauslöser, bevor Sie die Daten einfügen. Dadurch entfällt die Notwendigkeit, nicht replizierte Daten später zu synchronisieren. Aber solche neuen Tabellen sind, können wir sagen, ein einfacher Teil. Lassen Sie uns also zuerst den Fall simulieren, wenn wir Daten haben, bevor wir uns für ein Upgrade entscheiden.

Nehmen wir an, ein veralteter Server heißt p93 (ältester unterstützter) und der Server, auf den wir replizieren, heißt p10 (11 ist in diesem Quartal unterwegs, aber noch nicht geschehen):

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

Hier verwende ich psql, kann also den \c-Metabefehl verwenden, um eine Verbindung zu einer anderen Datenbank herzustellen. Wenn Sie diesem Code mit einem anderen Client folgen möchten, müssen Sie sich stattdessen erneut verbinden. Natürlich brauchen Sie diesen Schritt nicht, wenn Sie dies zum ersten Mal ausführen. Ich musste meine Sandbox mehrmals neu erstellen, also habe ich Anweisungen gespeichert…

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

Also erstellen wir zwei neue Datenbanken. Jetzt werde ich mich mit demjenigen verbinden, den wir aktualisieren möchten, und werde mehrere funkey Datentypen erstellen und sie verwenden, um eine Tabelle zu füllen, die wir später als bereits vorhanden betrachten werden:

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

Was haben wir nun?

  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, einige Daten - warum habe ich so viele eingefügt und dann gelöscht? Nun, wir versuchen, einen Datensatz zu verspotten, der schon seit einiger Zeit existiert. Also versuche ich, es ein wenig zu zerstreuen. Lassen Sie uns eine weitere Zeile (0,3) zum Ende der Seite (0,145) verschieben:

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

Nehmen wir nun an, wir verwenden PostgreSQL_fdw (die Verwendung von dblink hier wäre im Grunde dasselbe und wahrscheinlich schneller für 9.3, also tun Sie dies bitte, wenn Sie möchten).

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

Jetzt können wir pg_dump -s verwenden, um die DDL zu erhalten, aber ich habe sie nur oben. Wir müssen dieselbe Tabelle im Cluster der höheren Version erstellen, um Daten zu replizieren:

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

Jetzt kehren wir zu 9.3 zurück und verwenden Fremdtabellen für die Datenmigration (ich werde f_ verwenden Konvention für Tabellennamen hier, f steht für Foreign):

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

Endlich! Wir erstellen eine Insert-Funktion und einen Trigger.

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;

Hier und später werde ich Links für längeren Code verwenden. Erstens, damit gesprochener Text nicht in Maschinensprache versinkt. Zweitens, weil ich mehrere Versionen derselben Funktionen verwende, um zu reflektieren, wie sich der Code bei Bedarf entwickeln sollte.

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

Ergebnis:

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)

Was sehen wir hier? Wir sehen, dass neu eingefügte Daten erfolgreich in die Datenbank p10 repliziert werden. Und dementsprechend wird zurückgesetzt, wenn die Transaktion fehlschlägt. So weit, ist es gut. Aber Sie konnten nicht nicht bemerken (ja, ja - nicht nicht), dass die Tabelle auf p93 viel größer ist - alte Daten wurden nicht repliziert. Wie bekommen wir es dorthin? Ganz einfach:

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

würdest du. Und dies ist hier nicht das Hauptanliegen - Sie sollten sich lieber Gedanken darüber machen, wie Sie bereits vorhandene Daten bei Aktualisierungen und Löschungen verwalten werden - da Anweisungen, die erfolgreich in Datenbanken mit niedrigeren Versionen ausgeführt werden, fehlschlagen oder sich nur auf null Zeilen in höheren Versionen auswirken - nur weil keine bereits vorhandenen Daten vorhanden sind ! Und hier kommen wir zum Sekunden-Ausfall-Ausdruck. (Wenn es ein Film wäre, hätten wir hier natürlich eine Rückblende, aber leider - wenn der Ausdruck „Sekunden der Ausfallzeit“ Ihre Aufmerksamkeit nicht früher erregt hat, müssen Sie nach oben gehen und nach dem Ausdruck suchen ...) P>

Um alle Statement-Trigger zu aktivieren, müssen Sie die Tabelle einfrieren, alle Daten kopieren und dann Trigger aktivieren, sodass Tabellen in Datenbanken mit niedrigeren und höheren Versionen synchron sind und alle Statements nur dasselbe haben (oder extrem nahe beieinander liegen, weil physisch Verteilung wird anders sein, schauen Sie sich wieder oben das erste Beispiel für die ctid-Spalte an. Aber das Ausführen einer solchen „Replikation einschalten“ auf dem Tisch in einer riesigen Transaktion wird keine Sekunden Ausfallzeit bedeuten. Möglicherweise wird die Website stundenlang schreibgeschützt. Vor allem, wenn der Tisch von FK grob mit anderen großen Tischen verklebt wird.

Nun, schreibgeschützt ist keine vollständige Ausfallzeit. Aber später werden wir versuchen, alle SELECTS und einige INSERT, DELETE, UPDATE funktionieren zu lassen (bei neuen Daten, die bei alten fehlschlagen). Das Verschieben einer Tabelle oder Transaktion in den schreibgeschützten Zustand kann auf viele Arten erfolgen - wäre es ein Ansatz von PostgreSQL oder auf Anwendungsebene oder sogar das vorübergehende Widerrufen entsprechender Berechtigungen. Diese Ansätze selbst können ein Thema für einen eigenen Blog sein, daher werde ich sie nur erwähnen.

Ohnehin. Zurück zu den Auslösern. Um die gleiche Aktion auszuführen, die das Arbeiten an einer bestimmten Zeile (UPDATE, DELETE) in einer Remote-Tabelle erfordert, wie Sie es in einer lokalen Tabelle tun, müssen wir Primärschlüssel verwenden, da der physische Standort unterschiedlich ist. Und Primärschlüssel werden in verschiedenen Tabellen mit verschiedenen Spalten erstellt, daher müssen wir entweder eine eindeutige Funktion für jede Tabelle erstellen oder versuchen, einige generische zu schreiben. Nehmen wir (der Einfachheit halber) an, dass wir nur PKs mit einer Spalte haben, dann sollte diese Funktion helfen. So endlich! Lassen Sie uns hier eine Update-Funktion haben. Und offensichtlich ein Auslöser:

create trigger tgu before update on t for each row execute procedure tgf_u();
Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter

Und mal sehen, ob es funktioniert:

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

Ergebnis:

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. Und während es noch heiß ist, fügen wir auch die Löschauslöserfunktion und die Replikation hinzu:

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

Und überprüfen Sie:

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

Geben:

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

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

Wie wir uns erinnern (wer könnte das vergessen!), wenden wir die „Replikations“-Unterstützung nicht in Transaktionen um. Und das sollten wir, wenn wir konsistente Daten wollen. Wie oben erwähnt, sollten ALLE Statement-Trigger für ALLE FK-bezogenen Tabellen in einer Transaktion aktiviert werden, die zuvor durch Synchronisieren von Daten vorbereitet wurde. Sonst könnten wir fallen in:

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;

Geben:

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! Wir haben eine Zeile auf einer niedrigeren Versionsdatenbank gelöscht und nicht auf einer neueren! Nur weil es nicht da war. Dies würde nicht passieren, wenn wir es richtig gemacht hätten (begin;sync;enable trigger;end;). Aber der richtige Weg würde Tabellen für lange Zeit schreibgeschützt machen! Der eingefleischteste Leser würde sogar sagen:„Warum sollten Sie dann überhaupt eine Trigger-basierte Replikation durchführen?“.

Sie können es mit pg_upgrade tun, wie es „normale“ Leute tun würden. Und im Falle einer Streaming-Replikation können Sie alle Sätze schreibgeschützt machen. Halten Sie die xlog-Wiedergabe an und aktualisieren Sie den Master, während die Anwendung immer noch RO der Slave ist.

Genau! Habe ich nicht damit angefangen?

Die triggerbasierte Replikation kommt auf die Bühne, wenn Sie etwas ganz Besonderes brauchen. Sie können beispielsweise versuchen, SELECT und einige Änderungen an neu erstellten Daten zuzulassen, nicht nur RO. Nehmen wir an, Sie haben einen Online-Fragebogen – der Benutzer registriert sich, antwortet, bekommt seinen Bonus – kostenlose Punkte – andere – niemand braucht tolle Sachen – und geht. Mit einer solchen Struktur können Sie Änderungen an Daten, die sich noch nicht in einer höheren Version befinden, einfach verbieten und den gesamten Datenfluss für neue Benutzer zulassen.

So lassen Sie wenige Online-Geldautomaten-Arbeiter im Stich und lassen Neulinge arbeiten, ohne zu bemerken, dass Sie sich mitten in einem Upgrade befinden. Klingt schrecklich, aber habe ich nicht hypothetisch gesagt? ich nicht? Nun, ich meinte es ernst.

Egal, was für ein Fall aus dem wirklichen Leben es sein könnte, schauen wir uns an, wie Sie es umsetzen können. Die Lösch- und Aktualisierungsfunktionen ändern sich. Und lassen Sie uns jetzt das letzte Szenario überprüfen:

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

Die Zeile wurde auf der niedrigeren Version nicht gelöscht, da sie auf der höheren nicht gefunden wurde. Dasselbe würde mit aktualisiert passieren. Versuch es selber. Jetzt können Sie die Datensynchronisierung starten, ohne viele Änderungen an der Tabelle zu stoppen, die Sie in die triggerbasierte Replikation einbeziehen.

Ist es besser? Schlimmer? Es ist anders - es hat viele Mängel und einige Vorteile gegenüber dem globalen RO-System. Mein Ziel war es zu demonstrieren, warum jemand solch eine komplizierte Methode gegenüber dem Normalen verwenden möchte – um bestimmte Fähigkeiten über einen stabilen, bekannten Prozess zu erlangen. Natürlich zu einem gewissen Preis…

Jetzt, wo wir uns in Bezug auf die Datenkonsistenz etwas sicherer fühlen und unsere bereits vorhandenen Daten in Tabelle t mit p10 synchronisiert werden, können wir über andere Tabellen sprechen. Wie würde das alles mit FK funktionieren (immerhin habe ich FK so oft erwähnt, ich muss es in das Beispiel aufnehmen). Nun, warum warten?

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

Es lohnt sich sicherlich, diese drei zu einer Funktion zu verpacken, mit dem Ziel, viele Tabellen zu "triggern". Aber ich werde nicht. Da ich keine weiteren Tabellen hinzufügen werde - zwei referenzierte Relationen-Datenbanken sind schon so ein chaotisches Netz!

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

Ergebnis in:

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)

Wieder. Es sieht so aus, als ob die Datenkonsistenz vorhanden ist. Sie können auch mit der Synchronisierung von Daten für die neue Tabelle c beginnen…

Müde? Das bin ich definitiv.

Schlussfolgerung

Abschließend möchte ich einige Fehler hervorheben, die ich bei der Untersuchung dieses Ansatzes gemacht habe. Während ich die Update-Anweisung erstellte und alle Spalten von pg_attribute dynamisch auflistete, verlor ich eine ganze Stunde. Stellen Sie sich vor, wie enttäuscht ich war, als ich später feststellte, dass ich UPDATE (list) =(list) construct völlig vergessen hatte! Und die Funktion erreichte einen viel kürzeren und besser lesbaren Zustand.

Fehler Nummer eins war also der Versuch, alles selbst zu bauen, nur weil es so erreichbar aussieht. Das ist es immer noch, aber wie immer hat es wahrscheinlich schon jemand besser gemacht - zwei Minuten zu verbringen, um zu überprüfen, ob es tatsächlich so ist, kann Ihnen später Stunden des Nachdenkens ersparen.

Und zweitens - Dinge sahen für mich viel einfacher aus, wo sie sich als viel tiefer herausstellten, und ich habe viele Fälle, die perfekt vom PostgreSQL-Transaktionsmodell gehalten werden, überkompliziert.

Erst nachdem ich versucht hatte, die Sandbox zu bauen, bekam ich ein einigermaßen klares Verständnis dieser Ansatzschätzungen.

Planung ist also offensichtlich erforderlich, aber planen Sie nicht mehr, als Sie tatsächlich tun können.

Erfahrung kommt mit Übung.

Meine Sandbox erinnerte mich an eine Computerstrategie – man sitzt nach dem Mittagessen daran und denkt:„Aha, hier baue ich Pyramyd, dort bekomme ich Bogenschießen, dann konvertiere ich zu Sons of Ra und baue 20 Langbogenschützen, und hier greife ich das Erbärmliche an Nachbarn. Zwei Stunden des Ruhms.“ Und PLÖTZLICH ertappst du dich am nächsten Morgen, zwei Stunden vor der Arbeit, mit „Wie bin ich hierher gekommen? Warum muss ich dieses demütigende Bündnis mit ungewaschenen Barbaren eingehen, um meinen letzten Langbogenschützen zu retten, und muss ich wirklich meine so hart gebaute Pyramide dafür verkaufen?“

Lesungen:

  • 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