sql >> Databáze >  >> RDS >> PostgreSQL

Více SQL, méně kódu, s PostgreSQL

Jen s trochou ladění a vylepšení vašich Postgres SQL dotazů můžete snížit množství opakujícího se aplikačního kódu náchylného k chybám, který je nutný k propojení s vaší databází. Častěji, že ne, taková změna také zlepšuje výkon kódu aplikace.

Zde je několik tipů a triků, které mohou pomoci kódu vaší aplikace outsourcovat více práce na PostgreSQL a učinit vaši aplikaci štíhlejší a rychlejší.

Upsert

Od Postgres v9.5 je možné určit, co se má stát, když vložka selže kvůli „konfliktu“. Konflikt může být buď porušením jedinečného indexu (včetně primárního klíče) nebo jakýmkoli omezením (vytvořeným dříve pomocí CREATE CONSTRAINT).

Tuto funkci lze použít pro zjednodušení aplikační logiky vložení nebo aktualizace do jediného příkazu SQL. Například s tabulkou kv pomocí klíče a hodnota sloupce, níže uvedený příkaz vloží nový řádek (pokud tabulka nemá řádek s key=’host’) nebo aktualizuje hodnotu (pokud tabulka obsahuje řádek s 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;

Všimněte si, že sloupec key je jednosloupcový primární klíč tabulky a je specifikován jako konfliktní klauzule. Pokud máte primární klíč s více sloupci, zadejte zde místo toho název indexu primárního klíče.

Pokročilé příklady, včetně zadání dílčích indexů a omezení, najdete v dokumentu Postgres.

Vložit .. vrací se

Příkaz INSERT se také může vrátit jeden nebo více řádků, například příkaz SELECT. Může vracet hodnoty generované funkcemi, klíčovými slovy jako current_timestamp a sériové /sequence/identity columns.

Zde je například tabulka s automaticky generovaným sloupcem identity a sloupcem, který obsahuje časové razítko vytvoření řádku:

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

K zadání pouze hodnoty pro sloupec foo můžeme použít příkaz INSERT .. RETURNING a nechte Postgres vrátit hodnoty, které vygeneroval pro id a na sloupce:

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

Z kódu aplikace použijte stejné vzory/rozhraní API, které byste použili ke spouštění příkazů SELECT a načítání hodnot (jako executeQuery() v JDBC nebo db.Query() v Go).

Zde je další příklad, tento má automaticky generované UUID:

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

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

Podobně jako INSERT mohou příkazy UPDATE a DELETE obsahovat také klauzule RETURNING v Postgresu. Klauzule RETURNING je rozšíření Postgres a není součástí standardu SQL.

Jakékoli v sadě

Jak byste z kódu aplikace vytvořili klauzuli WHERE, která potřebuje porovnat hodnotu sloupce se sadou přijatelných hodnot? Když je předem znám počet hodnot, je SQL statický:

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

Ale co když počet klíčů není 2, ale může být libovolný? Vytvořili byste příkaz SQL dynamicky? Jednodušší možností je použít pole Postgres:

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

Výše uvedený operátor ANY bere jako argument pole. Klauzule key =ANY(?) vybere všechny řádky, kde je hodnota key je jedním z prvků dodaného pole. Díky tomu lze kód aplikace zjednodušit na:

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

Tento přístup je proveditelný pro omezený počet hodnot, pokud máte mnoho hodnot ke shodě, zvažte další možnosti, jako je spojení s (dočasnými) tabulkami nebo materializovanými pohledy.

Přesouvání řádků mezi tabulkami

Ano, můžete odstranit řádky z jedné tabulky a vložit je do jiné pomocí jediného SQL příkazu! Hlavní příkaz INSERT může vytáhnout řádky, které se mají vložit, pomocí CTE, který zabalí DELETE.

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

Provedení ekvivalentu v kódu aplikace může být velmi podrobné, zahrnuje uložení celého výsledku mazání do paměti a jeho použití k provedení více INSERTů. Je pravda, že přesouvání řádků možná není běžným případem použití, ale pokud to obchodní logika vyžaduje, úspora paměti aplikace a databázových okružních jízd, kterou tento přístup představuje, z něj činí ideální řešení.

Sada sloupců ve zdrojových a cílových tabulkách nemusí být totožná, můžete samozřejmě změnit pořadí, přeskupit a použít funkce pro manipulaci s hodnotami ve výběrových/návratových seznamech.

Splynout

Předání hodnot NULL v kódu aplikace obvykle vyžaduje další kroky. V Go byste například museli použít typy jako sql.NullString; v Java/JDBC funkce jako resultSet.wasNull() . Jsou těžkopádné a náchylné k chybám.

Pokud je možné v kontextu konkrétního dotazu zpracovat, řekněme NULL jako prázdné řetězce, nebo NULL celá čísla jako 0, můžete použít funkci COALESCE. Funkce COALESCE může změnit hodnoty NULL na libovolnou konkrétní hodnotu. Zvažte například tento dotaz:

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

který získá čísla faktur a dodací adresy faktur vystavených v lednu 2022. Pravděpodobně shipping_address je NULL, pokud zboží nemusí být odesláno fyzicky. Pokud chce kód aplikace v takových případech jednoduše někde zobrazit prázdný řetězec, řekněme, je jednodušší jednoduše použít COALESCE a odstranit z aplikace kód manipulující s NULL.

Místo prázdného řetězce můžete také použít jiné řetězce:

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

Můžete dokonce získat první hodnotu, která není NULL, ze seznamu, nebo místo toho použít zadaný řetězec. Chcete-li například použít fakturační nebo dodací adresu, můžete použít:

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

Případ

CASE je další užitečná konstrukce pro práci s reálnými, nedokonalými daty. Řekněme, že místo NULL v shipping_address u položek, které nelze odeslat, náš ne tak dokonalý software pro vytváření faktur vložil „NEUVEDENO“. Při čtení dat byste to chtěli namapovat na NULL nebo prázdný řetězec. Můžete použít 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 má nemotornou syntaxi, ale je funkčně podobný příkazům typu switch-case v jazycích podobných C. Zde je další příklad:

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;

Vyberte .. union

Data ze dvou (nebo více) samostatných příkazů SELECT lze kombinovat pomocí UNION. Máte-li například dvě tabulky, jednu s aktuálními uživateli a jednu smazanou, zde je postup, jak se na ně obou současně zeptat:

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

Tyto dva dotazy by měly mít stejný výběrový seznam, to znamená, že by měly vracet stejný počet a typ sloupců.

UNION také odstraňuje duplikáty. Jsou vráceny pouze jedinečné řádky. Pokud byste raději ponechali duplicitní řádky, použijte „UNION ALL“ místo UNION.

Jako kompliment UNION, tam je také INTERSECT a EXCEPT, viz dokumenty PostgreSQL pro více informací.

Vybrat .. odlišný na

Duplicitní řádky vrácené příkazem SELECT lze kombinovat (to znamená, že jsou vráceny pouze jedinečné řádky) přidáním klíčového slova DISTINCT za příkaz SELECT. I když se jedná o standardní SQL, Postgres poskytuje rozšíření „DISTINCT ON“. Používání je trochu složitější, ale v praxi je to často nejvýstižnější způsob, jak dosáhnout požadovaných výsledků.

Zvažte zákazníky tabulka s řádkem na zákazníka a nákupy tabulka s jedním řádkem na nákupy uskutečněné (některými) zákazníky. Dotaz níže vrátí všechny zákazníky spolu s každým jejich nákupem:

   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;

Každý řádek zákazníků se opakuje pro každý nákup, který provedl. Co když chceme vrátit pouze první nákup zákazníka? V podstatě chceme seřadit řádky podle zákazníka, seskupit řádky podle zákazníka, v rámci každé skupiny seřadit řádky podle času nákupu a nakonec vrátit pouze první řádek z každé skupiny. Ve skutečnosti je kratší napsat to v SQL s 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;

Přidaná klauzule „DISTINCT ON (C.id)“ dělá přesně to, co bylo popsáno výše. To je spousta práce s několika písmeny navíc!

Použití čísel v pořadí podle klauzule

Zvažte načtení seznamu jmen zákazníků a předčíslí jejich telefonních čísel z tabulky. Budeme předpokládat, že telefonní čísla v USA jsou uložena ve formátu (123) 456-7890 . U ostatních zemí uvedeme jako kód oblasti pouze „NON-US“.

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

To je všechno v pořádku a máme také konstrukci CASE, ale co když to teď potřebujeme seřadit podle kódu oblasti?

Toto funguje:

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;

Ale fuj! Opakování případové klauzule je ošklivé a náchylné k chybám. Mohli bychom napsat uloženou funkci, která vezme kód země a telefon a vrátí kód oblasti, ale ve skutečnosti existuje hezčí možnost:

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;

„ORDER BY 3“ říká pořadí podle 3. pole! Nezapomeňte aktualizovat číslo, když změníte uspořádání vybraného seznamu, ale obvykle to stojí za to.


  1. 10 neobvyklých tipů pro Microsoft Access 2019

  2. Použití konfiguračních tabulek k definování skutečného pracovního postupu

  3. Používání rolí Nové v MySQL 8

  4. SQL BETWEEN Operátor pro začátečníky