Con solo un po' di ritocco e miglioramento delle tue query SQL Postgres, puoi ridurre la quantità di codice applicativo ripetitivo e soggetto a errori necessario per interfacciarsi con il tuo database. Più spesso, una tale modifica migliora anche le prestazioni del codice dell'applicazione.
Ecco alcuni suggerimenti e trucchi che possono aiutare il codice della tua applicazione ad esternalizzare più lavoro a PostgreSQL e rendere la tua applicazione più snella e veloce.
Upsert
A partire da Postgres v9.5, è possibile specificare cosa dovrebbe accadere quando un inserimento fallisce a causa di un "conflitto". Il conflitto può essere una violazione di un indice univoco (inclusa una chiave primaria) o qualsiasi vincolo (creato in precedenza utilizzando CREATE CONSTRAINT).
Questa funzionalità può essere utilizzata per semplificare la logica dell'applicazione di inserimento o aggiornamento in una singola istruzione SQL. Ad esempio, data una tabella kv con chiave e valore colonne, l'istruzione seguente inserirà una nuova riga (se la tabella non ha una riga con key='host') o aggiornerà il valore (se la tabella ha una riga con 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;
Nota che la colonna key
è la chiave primaria a colonna singola della tabella ed è specificata come clausola di conflitto. Se hai una chiave primaria con più colonne, specifica qui invece il nome dell'indice della chiave primaria.
Per esempi avanzati, inclusa la specifica di indici parziali e vincoli, vedere i documenti di Postgres.
Inserisci .. ritornando
L'istruzione INSERT può anche restituire una o più righe, come un'istruzione SELECT. Può restituire valori generati da funzioni, parole chiave come current_timestamp e seriale /colonne sequenza/identità.
Ad esempio, ecco una tabella con una colonna Identity generata automaticamente e una colonna che contiene il timestamp di creazione della riga:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
Possiamo usare l'istruzione INSERT .. RETURNING per specificare solo il valore per la colonna foo e lascia che Postgres restituisca i valori che ha generato per l'id e a colonne:
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
Dal codice dell'applicazione, usa gli stessi pattern/API che useresti per eseguire le istruzioni SELECT e leggere i valori (come executeQuery() in JDBC o db.Query() in Vai).
Ecco un altro esempio, questo ha un UUID generato automaticamente:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Analogamente a INSERT, anche le istruzioni UPDATE e DELETE possono contenere clausole RETURNING in Postgres. La clausola RETURNING è un'estensione di Postgres e non fa parte dello standard SQL.
Qualsiasi in un set
Dal codice dell'applicazione, come creeresti una clausola WHERE che deve abbinare il valore di una colonna a un insieme di valori accettabili? Quando il numero di valori è noto in anticipo, l'SQL è statico:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Ma cosa succede se il numero di chiavi non è 2 ma può essere qualsiasi numero? Costruiresti l'istruzione SQL in modo dinamico? Un'opzione più semplice è usare gli array Postgres:
SELECT key, value FROM kv WHERE key = ANY(?)
L'operatore ANY sopra accetta un array come argomento. La clausola chiave =QUALSIASI(?) seleziona tutte le righe in cui il valore di key è uno degli elementi dell'array fornito. Con questo, il codice dell'applicazione può essere semplificato in:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Questo approccio è fattibile per un numero limitato di valori, se hai molti valori con cui abbinare, considera altre opzioni come l'unione con tabelle (temporanee) o viste materializzate.
Spostamento di righe tra tabelle
Sì, puoi eliminare righe da una tabella e inserirle in un'altra con una singola istruzione SQL! Un'istruzione INSERT principale può inserire le righe da inserire utilizzando un CTE, che esegue il wrapping di DELETE.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Fare l'equivalente nel codice dell'applicazione può essere molto dettagliato, implicando la memorizzazione dell'intero risultato dell'eliminazione in memoria e l'utilizzo di questo per eseguire più INSERT. Certo, lo spostamento di righe potrebbe non essere un caso d'uso comune, ma se la logica aziendale lo richiede, il risparmio di memoria dell'applicazione e i round trip del database presentati da questo approccio lo rendono la soluzione ideale.
La serie di colonne nelle tabelle di origine e di destinazione non deve essere identica, puoi ovviamente riordinare, riordinare e utilizzare le funzioni per manipolare i valori negli elenchi di selezione/ritorno.
Unisci
La gestione dei valori NULL nel codice dell'applicazione richiede solitamente passaggi aggiuntivi. In Go, ad esempio, dovresti usare tipi come sql.NullString; in Java/JDBC, funzioni come resultSet.wasNull() . Questi sono ingombranti e soggetti a errori.
Se è possibile gestire, ad esempio NULL come stringhe vuote, o NULL interi come 0, nel contesto di una query specifica, è possibile utilizzare la funzione COALESCE. La funzione COALESCE può trasformare i valori NULL in qualsiasi valore specifico. Ad esempio, considera questa query:
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
che ottiene i numeri di fattura e gli indirizzi di spedizione delle fatture emesse a gennaio 2022. Presumibilmente, indirizzo_spedizione è NULL se le merci non devono essere spedite fisicamente. Se il codice dell'applicazione vuole semplicemente visualizzare una stringa vuota da qualche parte in questi casi, ad esempio, è più semplice usare COALESCE e rimuovere il codice di gestione NULL nell'applicazione.
Puoi anche usare altre stringhe invece di una stringa vuota:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
Puoi anche ottenere il primo valore non NULL da un elenco o utilizzare invece la stringa specificata. Ad esempio, per utilizzare l'indirizzo di fatturazione o l'indirizzo di spedizione, puoi utilizzare:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Caso
CASE è un altro costrutto utile per gestire dati imperfetti della vita reale. Diciamo piuttosto che avere NULL in indirizzo_spedizione per gli articoli non spedibili, il nostro software per la creazione di fatture non così perfetto ha inserito "NON SPECIFICATO". Ti piacerebbe mapparlo su un NULL o una stringa vuota quando leggi i dati. Puoi usare CASE:
-- 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 ha una sintassi sgraziata, ma è funzionalmente simile alle istruzioni switch-case in linguaggi simili al C. Ecco un altro esempio:
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;
Seleziona .. unione
I dati di due (o più) istruzioni SELECT separate possono essere combinati utilizzando UNION. Ad esempio, se hai due tabelle, una contenente utenti correnti e una eliminata, ecco come interrogarle entrambe contemporaneamente:
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 = ?
Le due query devono avere lo stesso elenco di selezione, ovvero devono restituire lo stesso numero e tipo di colonne.
UNION rimuove anche i duplicati. Vengono restituite solo righe univoche. Se preferisci mantenere le righe duplicate, usa "UNION ALL" invece di UNION.
A complemento di UNION, c'è anche INTERSECT e EXCEPT, vedi i documenti PostgreSQL per maggiori informazioni.
Seleziona .. distinto su
Le righe duplicate restituite da un SELECT possono essere combinate (ovvero, vengono restituite solo righe univoche) aggiungendo la parola chiave DISTINCT dopo SELECT. Sebbene questo sia un SQL standard, Postgres fornisce un'estensione, "DISTINCT ON". È un po' complicato da usare, ma in pratica è spesso il modo più conciso per ottenere i risultati di cui hai bisogno.
Considera un cliente tabella con una riga per cliente e una acquisti tabella con una riga per acquisti effettuati da (alcuni) clienti. La query seguente restituisce tutti i clienti, insieme a ciascuno dei loro acquisti:
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;
Ogni riga cliente viene ripetuta per ogni acquisto effettuato. E se volessimo restituire solo il primo acquisto di un cliente? Fondamentalmente vogliamo ordinare le righe per cliente, raggruppare le righe per cliente, all'interno di ciascun gruppo ordinare le righe in base al tempo di acquisto e infine restituire solo la prima riga di ciascun gruppo. In realtà è più breve scriverlo in SQL con 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;
La clausola aggiunta "DISTINCT ON (C.id)" fa esattamente ciò che è stato descritto sopra. È un sacco di lavoro con solo poche lettere in più!
Uso dei numeri in ordine per clausola
Prendi in considerazione la possibilità di recuperare un elenco di nomi di clienti e il prefisso dei loro numeri di telefono da una tabella. Assumiamo che i numeri di telefono degli Stati Uniti siano archiviati formattati come (123) 456-7890
. Per gli altri paesi, diremo semplicemente "NON US" come prefisso.
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
Va tutto bene e abbiamo anche il costrutto CASE, ma cosa succede se dobbiamo ordinarlo in base al prefisso ora?
Funziona:
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;
Ma uh! Ripetere la clausola case è brutto e soggetto a errori. Potremmo scrivere una funzione memorizzata che prenda il prefisso internazionale e il telefono e restituisca il prefisso, ma in realtà c'è un'opzione più carina:
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;
L'"ORDINA PER 3" dice l'ordine dal 3° campo! Devi ricordarti di aggiornare il numero quando riorganizzi l'elenco di selezione, ma di solito ne vale la pena.