PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Plus de SQL, moins de code, avec PostgreSQL

Avec juste un peu de peaufinage et d'amélioration de vos requêtes SQL Postgres, vous pouvez réduire la quantité de code d'application répétitif et sujet aux erreurs requis pour s'interfacer avec votre base de données. Le plus souvent, un tel changement améliore également les performances du code de l'application.

Voici quelques trucs et astuces qui peuvent aider votre code d'application à sous-traiter plus de travail à PostgreSQL et à rendre votre application plus légère et plus rapide.

Upsert

Depuis Postgres v9.5, il est possible de spécifier ce qui doit se passer lorsqu'une insertion échoue à cause d'un « conflit ». Le conflit peut être soit une violation d'un index unique (y compris une clé primaire), soit une contrainte (créée précédemment à l'aide de CREATE CONSTRAINT).

Cette fonctionnalité peut être utilisée pour simplifier la logique d'application d'insertion ou de mise à jour dans une seule instruction SQL. Par exemple, étant donné une table kv avec clé et valeur colonnes, l'instruction ci-dessous insère une nouvelle ligne (si la table n'a pas de ligne avec key='host') ou met à jour la valeur (si la table a une ligne avec 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;

Notez que la colonne key est la clé primaire à colonne unique de la table et est spécifiée comme clause de conflit. Si vous avez une clé primaire avec plusieurs colonnes, spécifiez le nom de l'index de clé primaire ici à la place.

Pour des exemples avancés, y compris la spécification d'index partiels et de contraintes, consultez la documentation Postgres.

Insérer .. retour

L'instruction INSERT peut également retourner une ou plusieurs lignes, comme une instruction SELECT. Il peut renvoyer des valeurs générées par des fonctions, des mots-clés comme current_timestamp et série /séquence/colonnes d'identité.

Par exemple, voici un tableau avec une colonne d'identité générée automatiquement et une colonne contenant l'horodatage de création de la ligne :

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

Nous pouvons utiliser l'instruction INSERT .. RETURNING pour spécifier uniquement la valeur de la colonne foo , et laissez Postgres renvoyer les valeurs qu'il a générées pour l'id et à colonnes :

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

À partir du code de l'application, utilisez les mêmes modèles/API que vous utiliseriez pour exécuter des instructions SELECT et lire des valeurs (comme executeQuery() dans JDBC ou db.Query() en Go).

Voici un autre exemple, celui-ci a un UUID généré automatiquement :

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

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

Semblables à INSERT, les instructions UPDATE et DELETE peuvent également contenir des clauses RETURNING dans Postgres. La clause RETURNING est une extension Postgres et ne fait pas partie du standard SQL.

Tout dans un ensemble

À partir du code de l'application, comment créeriez-vous une clause WHERE qui doit faire correspondre la valeur d'une colonne à un ensemble de valeurs acceptables ? Lorsque le nombre de valeurs est connu à l'avance, le SQL est statique :

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

Mais que se passe-t-il si le nombre de clés n'est pas 2 mais peut être n'importe quel nombre ? Construiriez-vous l'instruction SQL dynamiquement ? Une option plus simple consiste à utiliser des tableaux Postgres :

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

L'opérateur ANY ci-dessus prend un tableau comme argument. La clause key =ANY(?) sélectionne toutes les lignes où la valeur de clé est l'un des éléments du tableau fourni. Avec cela, le code de l'application peut être simplifié en :

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

Cette approche est réalisable pour un nombre limité de valeurs, si vous avez beaucoup de valeurs à faire correspondre, envisagez d'autres options comme la jointure avec des tables (temporaires) ou des vues matérialisées.

Déplacement de lignes entre les tables

Oui, vous pouvez supprimer des lignes d'une table et les insérer dans une autre avec une seule instruction SQL ! Une instruction INSERT principale peut extraire les lignes à insérer à l'aide d'un CTE, qui encapsule un DELETE.

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

Faire l'équivalent dans le code d'application peut être très verbeux, impliquant de stocker le résultat entier de la suppression en mémoire et de l'utiliser pour effectuer plusieurs INSERT. Certes, le déplacement de lignes n'est peut-être pas un cas d'utilisation courant, mais si la logique métier l'exige, les économies de mémoire d'application et d'allers-retours de base de données présentées par cette approche en font la solution idéale.

L'ensemble de colonnes dans les tables source et destination n'a pas besoin d'être identique, vous pouvez bien sûr réorganiser, réorganiser et utiliser des fonctions pour manipuler les valeurs dans les listes de sélection/retour.

Coalescence

La remise des valeurs NULL dans le code d'application nécessite généralement des étapes supplémentaires. Dans Go, par exemple, vous devez utiliser des types tels que sql.NullString; en Java/JDBC, des fonctions comme resultSet.wasNull() . Celles-ci sont fastidieuses et sujettes aux erreurs.

S'il est possible de gérer, par exemple, des valeurs NULL sous forme de chaînes vides ou des entiers NULL sous forme de 0, dans le contexte d'une requête spécifique, vous pouvez utiliser la fonction COALESCE. La fonction COALESCE peut transformer les valeurs NULL en n'importe quelle valeur spécifique. Par exemple, considérez cette requête :

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

qui obtient les numéros de facture et les adresses d'expédition des factures émises en janvier 2022. Vraisemblablement, shipping_address est NULL si les marchandises ne doivent pas être expédiées physiquement. Si le code de l'application souhaite simplement afficher une chaîne vide quelque part dans de tels cas, par exemple, il est plus simple d'utiliser COALESCE et de supprimer le code de gestion NULL dans l'application.

Vous pouvez également utiliser d'autres chaînes au lieu d'une chaîne vide :

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

Vous pouvez même obtenir la première valeur non NULL d'une liste ou utiliser la chaîne spécifiée à la place. Par exemple, pour utiliser l'adresse de facturation ou l'adresse de livraison, vous pouvez utiliser :

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

Cas

CASE est une autre construction utile pour traiter des données réelles et imparfaites. Disons plutôt que d'avoir des NULL dans shipping_address pour les articles non livrables, notre logiciel de création de facture pas si parfait a mis "NON SPÉCIFIÉ". Vous souhaitez mapper ceci à un NULL ou à une chaîne vide lorsque vous lisez les données. Vous pouvez utiliser 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 a une syntaxe disgracieuse, mais est fonctionnellement similaire aux instructions switch-case dans les langages de type C. Voici un autre exemple :

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;

Sélectionner .. union

Les données de deux (ou plusieurs) instructions SELECT distinctes peuvent être combinées à l'aide d'UNION. Par exemple, si vous avez deux tables, l'une contenant les utilisateurs actuels et l'autre supprimée, voici comment les interroger toutes les deux en même temps :

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 = ?

Les deux requêtes doivent avoir la même liste de sélection, c'est-à-dire qu'elles doivent renvoyer le même nombre et le même type de colonnes.

UNION supprime également les doublons. Seules les lignes uniques sont renvoyées. Si vous préférez conserver les lignes en double, utilisez "UNION ALL" au lieu de UNION.

En complément de UNION, il y a aussi INTERSECT et EXCEPT, voir la documentation PostgreSQL pour plus d'informations.

Sélectionner .. distinct on

Les lignes en double renvoyées par un SELECT peuvent être combinées (c'est-à-dire que seules les lignes uniques sont renvoyées) en ajoutant le mot-clé DISTINCT après SELECT. Bien qu'il s'agisse de SQL standard, Postgres fournit une extension, le "DISTINCT ON". C'est un peu délicat à utiliser, mais en pratique, c'est souvent le moyen le plus concis d'obtenir les résultats dont vous avez besoin.

Considérez un client tableau avec une ligne par client et un achats tableau avec une ligne par achats effectués par (certains) clients. La requête ci-dessous renvoie tous les clients, ainsi que chacun de leurs achats :

   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;

Chaque ligne de client est répétée pour chaque achat qu'il a effectué. Que se passe-t-il si nous voulons retourner uniquement le premier achat d'un client ? Nous souhaitons essentiellement trier les lignes par client, regrouper les lignes par client, au sein de chaque groupe, trier les lignes par heure d'achat, et enfin renvoyer uniquement la première ligne de chaque groupe. C'est en fait plus court d'écrire cela en SQL avec 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 clause « DISTINCT ON (C.id) » ajoutée fait exactement ce qui a été décrit ci-dessus. C'est beaucoup de travail avec seulement quelques lettres supplémentaires !

Utiliser des nombres dans l'ordre par clause

Envisagez de récupérer une liste de noms de clients et l'indicatif régional de leurs numéros de téléphone à partir d'une table. Nous supposerons que les numéros de téléphone américains sont stockés au format (123) 456-7890 . Pour les autres pays, nous dirons simplement "NON-US" comme indicatif régional.

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

C'est très bien, et nous avons aussi la construction CASE, mais que se passe-t-il si nous devons le trier par indicatif régional maintenant ?

Cela fonctionne :

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;

Mais euh ! La répétition de la clause case est laide et source d'erreurs. Nous pourrions écrire une fonction stockée qui prend le code du pays et le téléphone et renvoie l'indicatif régional, mais il existe en fait une option plus agréable :

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;

Le "ORDER BY 3" indique l'ordre par le 3ème champ ! Vous devez vous rappeler de mettre à jour le numéro lorsque vous réorganisez la liste de sélection, mais cela en vaut généralement la peine.