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

Mehr SQL, weniger Code, mit PostgreSQL

Mit nur ein wenig Optimierung und Verbesserung Ihrer Postgres-SQL-Abfragen können Sie die Menge an sich wiederholendem, fehleranfälligem Anwendungscode reduzieren, der für die Schnittstelle mit Ihrer Datenbank erforderlich ist. Meistens verbessert eine solche Änderung auch die Leistung des Anwendungscodes.

Hier sind ein paar Tipps und Tricks, die Ihnen dabei helfen können, mehr Arbeit an PostgreSQL auszulagern und Ihre Anwendung schlanker und schneller zu machen.

Upsert

Seit Postgres v9.5 ist es möglich festzulegen, was passieren soll, wenn eine Einfügung aufgrund eines „Konflikts“ fehlschlägt. Der Konflikt kann entweder ein Verstoß gegen einen eindeutigen Index (einschließlich eines Primärschlüssels) oder eine Einschränkung (die zuvor mit CREATE CONSTRAINT erstellt wurde) sein.

Diese Funktion kann verwendet werden, um die Anwendungslogik zum Einfügen oder Aktualisieren in einer einzigen SQL-Anweisung zu vereinfachen. Zum Beispiel eine gegebene Tabelle kv mit Schlüssel und Wert Spalten, fügt die folgende Anweisung eine neue Zeile ein (wenn die Tabelle keine Zeile mit key=’host’ hat) oder aktualisiert den Wert (wenn die Tabelle eine Zeile mit key=’host’ hat):

CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT);

INSERT INTO kv (key, value)
VALUES ('host', '10.0.10.1')
    ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value;

Beachten Sie, dass die Spalte key ist der einspaltige Primärschlüssel der Tabelle und wird als Konfliktklausel angegeben. Wenn Sie einen Primärschlüssel mit mehreren Spalten haben, geben Sie hier stattdessen den Namen des Primärschlüsselindexes ein.

Erweiterte Beispiele, einschließlich der Angabe von Teilindizes und Einschränkungen, finden Sie in der Postgres-Dokumentation.

Einfügen .. zurückgeben

Die INSERT-Anweisung kann auch zurückgeben eine oder mehrere Zeilen, wie eine SELECT-Anweisung. Es kann Werte zurückgeben, die von Funktionen, Schlüsselwörtern wie aktueller_Zeitstempel generiert wurden und seriell /sequence/identity-Spalten.

Hier ist beispielsweise eine Tabelle mit einer automatisch generierten Identitätsspalte und einer Spalte, die den Zeitstempel der Erstellung der Zeile enthält:

db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(>                  at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(>                  foo text);

Wir können die Anweisung INSERT .. RETURNING verwenden, um nur den Wert für die Spalte foo anzugeben , und lassen Sie Postgres die Werte zurückgeben, die es für die id generiert hat und bei Spalten:

db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo;
 id |                at                |  foo
----+----------------------------------+--------
  1 | 2022-01-14 11:52:09.816787+01:00 | first
  2 | 2022-01-14 11:52:09.816787+01:00 | second
(2 rows)

INSERT 0 2

Verwenden Sie im Anwendungscode dieselben Muster/APIs, die Sie verwenden würden, um SELECT-Anweisungen auszuführen und Werte einzulesen (wie executeQuery()). in JDBC oder db.Query() in Go).

Hier ist ein weiteres Beispiel, dieses hat eine automatisch generierte UUID:

CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);

INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;

Ähnlich wie bei INSERT können die UPDATE- und DELETE-Anweisungen auch RETURNING-Klauseln in Postgres enthalten. Die RETURNING-Klausel ist eine Postgres-Erweiterung und nicht Teil des SQL-Standards.

Jeder in einem Satz

Wie würden Sie aus dem Anwendungscode eine WHERE-Klausel erstellen, die den Wert einer Spalte mit einem Satz akzeptabler Werte abgleichen muss? Wenn die Anzahl der Werte vorher bekannt ist, ist die SQL statisch:

stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);

Aber was ist, wenn die Anzahl der Schlüssel nicht 2 ist, sondern eine beliebige Zahl sein kann? Würden Sie die SQL-Anweisung dynamisch erstellen? Eine einfachere Option ist die Verwendung von Postgres-Arrays:

SELECT key, value FROM kv WHERE key = ANY(?)

Der obige Operator ANY nimmt ein Array als Argument. Die Klausel key =ANY(?) wählt alle Zeilen aus, in denen der Wert von Schlüssel steht ist eines der Elemente des bereitgestellten Arrays. Damit kann der Anwendungscode vereinfacht werden zu:

stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);

Dieser Ansatz ist für eine begrenzte Anzahl von Werten praktikabel. Wenn Sie viele Werte zum Abgleichen haben, ziehen Sie andere Optionen wie das Verbinden mit (temporären) Tabellen oder materialisierten Ansichten in Betracht.

Zeilen zwischen Tabellen verschieben

Ja, Sie können mit einer einzigen SQL-Anweisung Zeilen aus einer Tabelle löschen und in eine andere einfügen! Eine Haupt-INSERT-Anweisung kann die einzufügenden Zeilen mit einem CTE abrufen, der ein DELETE.

umschließt
WITH items AS (
       DELETE FROM todos_2021
        WHERE NOT done
    RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;

Das Äquivalent im Anwendungscode auszuführen, kann sehr ausführlich sein, da das gesamte Ergebnis des Löschvorgangs im Speicher gespeichert und für mehrere INSERTs verwendet wird. Zugegeben, das Verschieben von Zeilen ist vielleicht kein häufiger Anwendungsfall, aber wenn die Geschäftslogik es erfordert, machen die Einsparungen von Anwendungsspeicher und Datenbank-Roundtrips, die dieser Ansatz bietet, die ideale Lösung.

Der Spaltensatz in den Quell- und Zieltabellen muss nicht identisch sein, Sie können natürlich die Werte in den Auswahl-/Rückgabelisten neu anordnen, neu anordnen und Funktionen verwenden, um sie zu manipulieren.

Zusammenführen

Die Übergabe von NULL-Werten im Anwendungscode erfordert normalerweise zusätzliche Schritte. In Go müssten Sie beispielsweise Typen wie sql.NullString verwenden; in Java/JDBC funktioniert es wie resultSet.wasNull() . Diese sind umständlich und fehleranfällig.

Wenn es möglich ist, im Kontext einer bestimmten Abfrage beispielsweise NULL-Werte als leere Zeichenfolgen oder NULL-Ganzzahlen als 0 zu behandeln, können Sie die COALESCE-Funktion verwenden. Die COALESCE-Funktion kann NULL-Werte in einen beliebigen spezifischen Wert umwandeln. Betrachten Sie zum Beispiel diese Abfrage:

SELECT invoice_num, COALESCE(shipping_address, '')
  FROM invoices
 WHERE EXTRACT(month FROM raised_on) = 1    AND
       EXTRACT(year  FROM raised_on) = 2022

die die Rechnungsnummern und Lieferadressen von Rechnungen erhält, die im Januar 2022 ausgestellt wurden. Vermutlich shipping_address ist NULL, wenn Waren nicht physisch versendet werden müssen. Wenn der Anwendungscode in solchen Fällen beispielsweise einfach irgendwo einen leeren String anzeigen möchte, ist es einfacher, einfach COALESCE zu verwenden und NULL-Handling-Code in der Anwendung zu entfernen.

Anstelle eines leeren Strings können Sie auch andere Strings verwenden:

SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...

Sie können sogar den ersten Nicht-NULL-Wert aus einer Liste abrufen oder stattdessen die angegebene Zeichenfolge verwenden. Um beispielsweise entweder die Rechnungsadresse oder die Lieferadresse zu verwenden, können Sie Folgendes verwenden:

SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...

Fall

CASE ist ein weiteres hilfreiches Konstrukt, um mit realen, unvollständigen Daten umzugehen. Nehmen wir an, anstatt NULLen in shipping_address zu haben Für nicht versandfähige Artikel hat unsere nicht ganz so perfekte Rechnungserstellungssoftware „NICHT ANGEGEBEN“ eingefügt. Sie möchten dies beim Einlesen der Daten auf eine NULL oder eine leere Zeichenfolge abbilden. Sie können CASE:

verwenden
-- map NOT-SPECIFIED to an empty string
SELECT invoice_num,
       CASE shipping_address
	     WHEN 'NOT-SPECIFIED' THEN ''
		 ELSE shipping_address
		 END
FROM   invoices;

-- same result, different syntax
SELECT invoice_num,
       CASE
	     WHEN shipping_address = 'NOT-SPECIFIED' THEN ''
		 ELSE shipping_address
		 END
FROM   invoices;

CASE hat eine plumpe Syntax, ist aber funktional ähnlich wie switch-case-Anweisungen in C-ähnlichen Sprachen. Hier ist ein weiteres Beispiel:

SELECT invoice_num,
       CASE
	     WHEN shipping_address IS NULL THEN 'NOT SHIPPING'
	     WHEN billing_address = shipping_address THEN 'SHIPPING TO PAYER'
		 ELSE 'SHIPPING TO ' || shipping_address
		 END
FROM   invoices;

Wählen Sie .. Union aus

Daten aus zwei (oder mehr) separaten SELECT-Anweisungen können mit UNION kombiniert werden. Wenn Sie beispielsweise zwei Tabellen haben, eine mit aktuellen Benutzern und eine gelöscht, können Sie beide gleichzeitig wie folgt abfragen:

SELECT id, name, address, FALSE AS is_deleted 
  FROM users
 WHERE email = ?

UNION

SELECT id, name, address, TRUE AS is_deleted
  FROM deleted_users
 WHERE email = ?

Die beiden Abfragen sollten dieselbe Auswahlliste haben, d. h. sie sollten dieselbe Anzahl und denselben Spaltentyp zurückgeben.

UNION entfernt auch Duplikate. Es werden nur eindeutige Zeilen zurückgegeben. Wenn Sie lieber doppelte Zeilen beibehalten möchten, verwenden Sie „UNION ALL“ anstelle von UNION.

Ergänzend zu UNION gibt es auch INTERSECT und EXCEPT, siehe die PostgreSQL-Dokumentation für weitere Informationen.

Wählen Sie .. distinct on

aus

Doppelte Zeilen, die von einem SELECT zurückgegeben werden, können kombiniert werden (d. h. es werden nur eindeutige Zeilen zurückgegeben), indem das Schlüsselwort DISTINCT nach SELECT hinzugefügt wird. Während dies Standard-SQL ist, bietet Postgres eine Erweiterung, das „DISTINCT ON“. Es ist ein wenig schwierig zu verwenden, aber in der Praxis ist es oft der prägnanteste Weg, um die gewünschten Ergebnisse zu erzielen.

Betrachten Sie einen Kunden Tabelle mit einer Zeile pro Kunde und Einkäufe Tabelle mit einer Zeile pro von (einigen) Kunden getätigten Käufen. Die folgende Abfrage gibt alle Kunden zusammen mit jedem ihrer Käufe zurück:

   SELECT C.id, P.at
     FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
 ORDER BY C.id ASC, P.at ASC;

Jede Kundenzeile wird für jeden getätigten Kauf wiederholt. Was ist, wenn wir nur den ersten Kauf eines Kunden zurückgeben möchten? Grundsätzlich möchten wir die Zeilen nach Kunden sortieren, die Zeilen nach Kunden gruppieren, innerhalb jeder Gruppe die Zeilen nach Kaufzeit sortieren und schließlich nur die erste Zeile aus jeder Gruppe zurückgeben. Es ist eigentlich kürzer, das in SQL mit DISTINCT ON zu schreiben:

   SELECT DISTINCT ON (C.id) C.id, P.at
     FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
 ORDER BY C.id ASC, P.at ASC;

Die hinzugefügte Klausel „DISTINCT ON (C.id)“ macht genau das, was oben beschrieben wurde. Das ist viel Arbeit mit nur wenigen zusätzlichen Buchstaben!

Numbers-in-order-by-Klausel verwenden

Erwägen Sie, eine Liste mit Kundennamen und der Vorwahl ihrer Telefonnummern aus einer Tabelle abzurufen. Wir gehen davon aus, dass US-Telefonnummern im Format (123) 456-7890 gespeichert werden . Für andere Länder sagen wir einfach „NON-US“ als Vorwahl.

SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers;

Das ist alles gut, und wir haben auch das CASE-Konstrukt, aber was ist, wenn wir es jetzt nach der Vorwahl sortieren müssen?

Das funktioniert:

SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers
ORDER  BY
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END ASC;

Aber pfui! Das Wiederholen der case-Klausel ist hässlich und fehleranfällig. Wir könnten eine gespeicherte Funktion schreiben, die Landesvorwahl und Telefon nimmt und die Vorwahl zurückgibt, aber es gibt tatsächlich eine schönere Option:

SELECT last_name, first_name,
       CASE country_code
	     WHEN 'US' THEN substr(phone, 2, 3)
		 ELSE 'NON-US'
		 END
FROM   customers
ORDER  BY 3 ASC;

Die „ORDER BY 3“ sagt Bestellung nach dem 3. Feld! Sie müssen daran denken, die Nummer zu aktualisieren, wenn Sie die Auswahlliste neu anordnen, aber es lohnt sich normalerweise.