Само с малко настройване и подобряване на вашите заявки на Postgres SQL, можете да намалите количеството повтарящ се, податлив на грешки код на приложение, необходим за взаимодействие с вашата база данни. По-често такава промяна също подобрява производителността на кода на приложението.
Ето няколко съвета и трика, които могат да помогнат на кода на приложението ви да изнесе повече работа на PostgreSQL и да направят приложението ви по-тънко и по-бързо.
Upsert
От Postgres v9.5 е възможно да се посочи какво трябва да се случи, когато вмъкването се провали поради „конфликт“. Конфликтът може да бъде или нарушение на уникален индекс (включително първичен ключ) или някакво ограничение (създадено по-рано чрез CREATE CONSTRAINT).
Тази функция може да се използва за опростяване на логиката на приложението за вмъкване или актуализиране в един SQL израз. Например дадена таблица kv с ключ и стойност колони, изявлението по-долу ще вмъкне нов ред (ако таблицата няма ред с key=’host’) или ще актуализира стойността (ако таблицата има ред с 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;
Имайте предвид, че колоната key
е първичният ключ с една колона на таблицата и се посочва като клауза за конфликт. Ако имате първичен ключ с множество колони, посочете името на индекса на първичния ключ тук.
За разширени примери, включително посочване на частични индекси и ограничения, вижте документите на Postgres.
Вмъкване .. връщане
Инструкцията INSERT може също да връща един или повече реда, като оператор SELECT. Може да връща стойности, генерирани от функции, ключови думи като current_timestamp и сериен /последователност/идентификационни колони.
Например, ето таблица с автоматично генерирана колона за самоличност и колона, която съдържа клеймото за време на създаване на реда:
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
Можем да използваме оператора INSERT .. RETURNING, за да посочим само стойността за колоната foo и оставете Postgres да върне стойностите, които генерира за id и на колони:
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
От кода на приложението използвайте същите модели/API, които бихте използвали, за да изпълнявате SELECT изрази и да четете стойности (като executeQuery() в JDBC или db.Query() в Go).
Ето още един пример, този има автоматично генериран UUID:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
Подобно на INSERT, операторите UPDATE и DELETE могат също да съдържат клаузи RETURNING в Postgres. Клаузата RETURNING е разширение на Postgres, а не част от SQL стандарта.
Всяко в набор
От кода на приложението, как бихте създали клауза WHERE, която трябва да съответства на стойността на колона спрямо набор от приемливи стойности? Когато броят на стойностите е известен предварително, SQL е статичен:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
Но какво ще стане, ако броят на ключовете не е 2, а може да бъде произволен брой? Бихте ли конструирали SQL израза динамично? По-лесният вариант е да използвате Postgres масиви:
SELECT key, value FROM kv WHERE key = ANY(?)
Операторът ANY по-горе приема масив като аргумент. Клаузата ключ =ANY(?) избира всички редове, където е стойността на ключ е един от елементите на предоставения масив. С това кодът на приложението може да бъде опростен до:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
Този подход е осъществим за ограничен брой стойности, ако имате много стойности за съпоставяне, помислете за други опции, като свързване с (временни) таблици или материализирани изгледи.
Преместване на редове между таблици
Да, можете да изтриете редове от една таблица и да ги вмъкнете в друга с един SQL оператор! Основен израз INSERT може да изтегли редовете за вмъкване с помощта на CTE, който обвива DELETE.
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
Извършването на еквивалента в кода на приложението може да бъде много подробно, включващо съхраняване на целия резултат от изтриването в паметта и използване на това за извършване на множество INSERT. Разбира се, преместването на редове може би не е често срещан случай на използване, но ако бизнес логиката го изисква, спестяванията на паметта на приложенията и двупосочните пътувания на базата данни, представени от този подход, го правят идеалното решение.
Наборът от колони в таблиците източник и дестинация не трябва да бъде идентичен, разбира се, можете да пренареждате, пренареждате и използвате функции за манипулиране на стойностите в списъците за избор/връщане.
Сливане
Предаването на NULL стойности в кода на приложението обикновено изисква допълнителни стъпки. В Go, например, ще трябва да използвате типове като sql.NullString; в Java/JDBC, функции като resultSet.wasNull() . Те са тромави и податливи на грешки.
Ако е възможно да се обработват, кажете NULL като празни низове или NULL цели числа като 0, в контекста на конкретна заявка, можете да използвате функцията COALESCE. Функцията COALESCE може да превърне NULL стойности във всяка конкретна стойност. Например помислете за тази заявка:
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
който получава номерата на фактурите и адресите за доставка на фактурите, издигнати през януари 2022 г. Предполага се, адрес за доставка е NULL, ако стоките не трябва да бъдат изпратени физически. Ако кодът на приложението просто иска да покаже празен низ някъде в такива случаи, да речем, по-лесно е просто да използвате COALESCE и да премахнете кода за обработка NULL в приложението.
Можете също да използвате други низове вместо празен низ:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
Можете дори да получите първата стойност, различна от NULL, от списък или вместо това да използвате посочения низ. Например, за да използвате адреса за фактуриране или адреса за доставка, можете да използвате:
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
Случай
CASE е друга полезна конструкция за справяне с реални, несъвършени данни. Да речем, вместо да имаме NULL в адрес за доставка за артикули, които не подлежат на доставка, нашият не толкова перфектен софтуер за създаване на фактури е поставил „НЕУКАЗАНО“. Бихте искали да съпоставите това с NULL или празен низ, когато четете данните. Можете да използвате 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 има неудобен синтаксис, но е функционално подобен на операторите switch-case в C-подобни езици. Ето още един пример:
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;
Изберете .. union
Данните от два (или повече) отделни оператора SELECT могат да бъдат комбинирани с помощта на UNION. Например, ако имате две таблици, едната съдържа текущи потребители и една изтрита, ето как да ги запитате едновременно:
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 = ?
Двете заявки трябва да имат един и същ списък за избор, тоест трябва да връщат същия брой и тип колони.
UNION премахва и дубликати. Връщат се само уникални редове. Ако предпочитате да запазите дублиращи се редове, използвайте „UNION ALL“ вместо UNION.
Като комплимент UNION, има също INTERSECT и EXCEPT, вижте документите на PostgreSQL за повече информация.
Изберете .. различен на
Дублиращи се редове, върнати от SELECT, могат да бъдат комбинирани (тоест, само уникални редове се връщат) чрез добавяне на ключовата дума DISTINCT след SELECT. Въпреки че това е стандартен SQL, Postgres предоставя разширение „DISTINCT ON“. Използването му е малко сложно, но на практика често е най-краткият начин да получите нужните резултати.
Помислете за клиенти таблица с ред за клиент и покупки таблица с един ред за покупки, направени от (някои) клиенти. Заявката по-долу връща всички клиенти, заедно с всяка от техните покупки:
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;
Всеки клиентски ред се повтаря за всяка покупка, която са направили. Ами ако искаме да върнем само първата покупка на клиент? По принцип искаме да сортираме редовете по клиент, да групираме редовете по клиент, във всяка група да сортираме редовете по време на покупка и накрая да върнем само първия ред от всяка група. Всъщност е по-кратко да напишете това в SQL с 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;
Добавената клауза „DISTINCT ON (C.id)“ прави точно това, което беше описано по-горе. Това е много работа само с няколко допълнителни букви!
Използване на числа в ред по клауза
Помислете за извличане на списък с имена на клиенти и регионалния код на техните телефонни номера от таблица. Ще приемем, че телефонните номера в САЩ се съхраняват форматирани като (123) 456-7890
. За други държави просто ще кажем „NON-US“ като код на региона.
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
Всичко това е добре и ние също имаме конструкцията CASE, но какво ще стане, ако трябва да я сортираме по кода на региона сега?
Това работи:
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;
Но уф! Повтарянето на клаузата за случаи е грозно и податливо на грешки. Бихме могли да напишем съхранена функция, която взема кода на държавата и телефона и връща кода на региона, но всъщност има по-хубава опция:
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;
"ПОРЪЧАЙТЕ ОТ 3" казва поръчка по 3-то поле! Трябва да запомните да актуализирате номера, когато пренареждате избрания списък, но обикновено си заслужава.