Met slechts een klein beetje tweaken en verbeteren van uw Postgres SQL-query's, kunt u de hoeveelheid repetitieve, foutgevoelige applicatiecode die nodig is om te communiceren met uw database, verminderen. Vaker dat niet, verbetert een dergelijke wijziging ook de prestaties van de applicatiecode.
Hier zijn een paar tips en trucs die u kunnen helpen uw applicatiecode meer werk uit te besteden aan PostgreSQL, en uw applicatie slanker en sneller te maken.
Upsert
Sinds Postgres v9.5 is het mogelijk om te specificeren wat er moet gebeuren als een insert mislukt vanwege een “conflict”. Het conflict kan een schending zijn van een unieke index (inclusief een primaire sleutel) of een beperking (eerder gemaakt met CREATE CONSTRAINT).
Deze functie kan worden gebruikt om de toepassingslogica voor invoegen of bijwerken te vereenvoudigen in een enkele SQL-instructie. Bijvoorbeeld, gegeven een tabel kv met toets en waarde kolommen, zal de onderstaande instructie een nieuwe rij invoegen (als de tabel geen rij heeft met key=’host’) of de waarde bijwerken (als de tabel een rij heeft met key=’host’):
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;
Merk op dat de kolom key
is de primaire sleutel met één kolom van de tabel en wordt gespecificeerd als de conflictclausule. Als je een primaire sleutel met meerdere kolommen hebt, geef dan hier de naam van de primaire sleutelindex op.
Zie de documenten van Postgres voor geavanceerde voorbeelden, waaronder het specificeren van gedeeltelijke indexen en beperkingen.
Invoegen ..returning
Het INSERT-statement kan ook retourneren een of meer rijen, zoals een SELECT-instructie. Het kan waarden retourneren die zijn gegenereerd door functies, trefwoorden zoals current_timestamp en serieel /sequence/identity kolommen.
Hier is bijvoorbeeld een tabel met een automatisch gegenereerde identiteitskolom en een kolom die het tijdstempel bevat van het maken van de rij:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
We kunnen de instructie INSERT .. RETURNING gebruiken om alleen de waarde voor de kolom foo op te geven , en laat Postgres de waarden retourneren die het heeft gegenereerd voor de id en om kolommen:
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
Gebruik vanuit applicatiecode dezelfde patronen/API's die u zou gebruiken om SELECT-instructies uit te voeren en waarden in te lezen (zoals executeQuery() in JDBC of db.Query() in Go).
Hier is nog een voorbeeld, deze heeft een automatisch gegenereerde UUID:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Net als bij INSERT kunnen de instructies UPDATE en DELETE ook RETURNING-clausules bevatten in Postgres. De RETURNING-clausule is een Postgres-extensie en maakt geen deel uit van de SQL-standaard.
Elke in een set
Hoe zou u op basis van de toepassingscode een WHERE-clausule maken die de waarde van een kolom moet matchen met een reeks acceptabele waarden? Als het aantal waarden vooraf bekend is, is de SQL statisch:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Maar wat als het aantal sleutels niet 2 is, maar elk willekeurig aantal kan zijn? Wilt u de SQL-instructie dynamisch construeren? Een eenvoudigere optie is om Postgres-arrays te gebruiken:
SELECT key, value FROM kv WHERE key = ANY(?)
De ANY-operator hierboven neemt een array als argument. De clausule sleutel =ANY(?) selecteert alle rijen waar de waarde van key is een van de elementen van de meegeleverde array. Hiermee kan de applicatiecode worden vereenvoudigd tot:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Deze aanpak is haalbaar voor een beperkt aantal waarden. Als je veel waarden hebt om mee te matchen, overweeg dan andere opties, zoals samenvoegen met (tijdelijke) tabellen of gerealiseerde weergaven.
Rijen tussen tabellen verplaatsen
Ja, u kunt rijen uit de ene tabel verwijderen en invoegen in een andere met een enkele SQL-instructie! Een hoofdinstructie INSERT kan de rijen invoegen om in te voegen met behulp van een CTE, die een DELETE omhult.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Het equivalent in applicatiecode doen kan erg uitgebreid zijn, waarbij het volledige resultaat van de verwijdering in het geheugen wordt opgeslagen en dat wordt gebruikt om meerdere INSERT's te doen. Toegegeven, het verplaatsen van rijen is misschien niet een veelvoorkomend gebruik, maar als de bedrijfslogica daarom vraagt, maken de besparingen van toepassingsgeheugen en database-roundtrips die door deze benadering worden geboden, het de ideale oplossing.
De reeks kolommen in de bron- en doeltabellen hoeft niet identiek te zijn, u kunt natuurlijk de volgorde, herschikking en functies gebruiken om de waarden in de selectie-/terugkerende lijsten te manipuleren.
Coalesce
Het overhandigen van NULL-waarden in applicatiecode vergt meestal extra stappen. In Go moet u bijvoorbeeld typen gebruiken zoals sql.NullString; in Java/JDBC, functies zoals resultSet.wasNull() . Deze zijn omslachtig en foutgevoelig.
Als het mogelijk is om in de context van een specifieke query bijvoorbeeld NULL's als lege tekenreeksen of NULL-getallen als 0 te verwerken, kunt u de functie COALESCE gebruiken. De COALESCE-functie kan NULL-waarden omzetten in een specifieke waarde. Beschouw bijvoorbeeld deze vraag:
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
die de factuurnummers en verzendadressen ophaalt van facturen die in januari 2022 zijn opgesteld. Vermoedelijk shipping_address is NULL als goederen niet fysiek hoeven te worden verzonden. Als de applicatiecode in dergelijke gevallen gewoon ergens een lege string wil weergeven, is het bijvoorbeeld eenvoudiger om COALESCE te gebruiken en de NULL-verwerkingscode in de applicatie te verwijderen.
U kunt ook andere tekenreeksen gebruiken in plaats van een lege tekenreeks:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
U kunt zelfs de eerste niet-NULL-waarde uit een lijst halen of in plaats daarvan de opgegeven tekenreeks gebruiken. Om bijvoorbeeld het factuuradres of het verzendadres te gebruiken, kunt u het volgende gebruiken:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Case
CASE is een ander nuttig concept om met real-life, imperfecte gegevens om te gaan. Laten we zeggen in plaats van NULL's in shipping_address voor niet-verzendbare artikelen heeft onze niet-zo-perfecte software voor het maken van facturen "NIET-GESPECIFICEERD". U wilt dit toewijzen aan een NULL of een lege tekenreeks wanneer u de gegevens inleest. U kunt CASE gebruiken:
-- 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 heeft een lompe syntaxis, maar is functioneel vergelijkbaar met switch-case-statements in C-achtige talen. Hier is nog een voorbeeld:
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;
Selecteer .. union
Gegevens van twee (of meer) afzonderlijke SELECT-instructies kunnen worden gecombineerd met UNION. Als u bijvoorbeeld twee tabellen heeft, één met huidige gebruikers en één verwijderde, kunt u ze als volgt opvragen:
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 = ?
De twee zoekopdrachten moeten dezelfde selectielijst hebben, dat wil zeggen dat ze hetzelfde aantal en type kolommen moeten retourneren.
UNION verwijdert ook duplicaten. Alleen unieke rijen worden geretourneerd. Als u liever dubbele rijen wilt behouden, gebruik dan "UNION ALL" in plaats van UNION.
Als compliment voor UNION is er ook INTERSECT en BEHALVE, zie de PostgreSQL-documenten voor meer info.
Selecteer .. onderscheiden op
Dubbele rijen die door een SELECT worden geretourneerd, kunnen worden gecombineerd (dat wil zeggen dat alleen unieke rijen worden geretourneerd) door het sleutelwoord DISTINCT na SELECT toe te voegen. Hoewel dit standaard SQL is, biedt Postgres een extensie, de "DISTINCT ON". Het is een beetje lastig om te gebruiken, maar in de praktijk is het vaak de meest beknopte manier om de gewenste resultaten te krijgen.
Overweeg een klant tabel met een rij per klant en een aankopen tabel met één rij per aankopen van (sommige) klanten. De onderstaande zoekopdracht levert alle klanten op, samen met al hun aankopen:
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;
Elke klantenrij wordt herhaald voor elke aankoop die ze hebben gedaan. Wat als we alleen de eerste aankoop van een klant willen retourneren? We willen in feite de rijen op klant sorteren, de rijen op klant groeperen, binnen elke groep de rijen sorteren op aankooptijd en uiteindelijk alleen de eerste rij van elke groep retourneren. Het is eigenlijk korter om dat in SQL te schrijven met DISTINCT ON:
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;
De toegevoegde "DISTINCT ON (C.id)"-clausule doet precies wat hierboven is beschreven. Dat is veel werk met slechts een paar extra letters!
Getallen gebruiken in volgorde van clausule
Overweeg om een lijst met namen van klanten en het netnummer van hun telefoonnummers van een tafel te halen. We gaan ervan uit dat Amerikaanse telefoonnummers worden opgeslagen met de indeling (123) 456-7890
. Voor andere landen zeggen we gewoon 'NON-US' als netnummer.
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
Dat is allemaal goed, en we hebben ook de CASE-constructie, maar wat als we het nu op het netnummer moeten sorteren?
Dit werkt:
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;
Maar bah! Het herhalen van de case-clausule is lelijk en foutgevoelig. We zouden een opgeslagen functie kunnen schrijven die de landcode en telefoon nodig heeft en het netnummer retourneert, maar er is eigenlijk een leukere optie:
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;
De "ORDER BY 3" zegt volgorde door het 3e veld! Je moet eraan denken om het nummer bij te werken wanneer je de selectielijst herschikt, maar het is meestal de moeite waard.