Antywzorce PostgreSQL: przekazywanie zestawów i selekcji do SQL

Od czasu do czasu deweloper ma potrzebę przekazać zestaw parametrów lub nawet cały wybór do żądania "przy wejściu". Czasami można znaleźć bardzo dziwne rozwiązania tego problemu.
Antywzorce PostgreSQL: przekazywanie zestawów i selekcji do SQL
Podejdźmy do tego „od drugiej strony” i zobaczmy, czego nie należy robić, dlaczego i jak można to zrobić lepiej.

Bezpośrednie wstawianie wartości do treści żądania

Zazwyczaj wygląda to mniej więcej tak:

query = "SELECT * FROM tbl WHERE id = " + value

… albo tak:

query = "SELECT * FROM tbl WHERE id = :param".format(param=value)

O tej metodzie już mówiono, pisano i nawet narysowany wystarczająco:

Antywzorce PostgreSQL: przekazywanie zestawów i selekcji do SQL

Prawie zawsze tak jest - bezpośrednia ścieżka do wstrzyknięć SQL i dodatkowe obciążenie dla logiki biznesowej, która zmuszona jest „skleić” ciąg Twojego żądania.

Jeśli zajdzie taka konieczność, takie podejście można uzasadnić tylko częściowo. wykorzystanie partycjonowania w PostgreSQL w wersji 10 i starszych, aby uzyskać bardziej wydajny plan. W tych wersjach lista sekcji do przeskanowania jest ustalana bez uwzględniania przekazywanych parametrów, a jedynie na podstawie treści żądania.

$n-argumentów

Używać symbole zastępcze parametry - to jest dobre, pozwala na użycie PRZYGOTOWANE OŚWIADCZENIA, zmniejszając obciążenie zarówno logiki biznesowej (ciąg zapytania jest generowany i przesyłany tylko raz), jak i serwera bazy danych (nie jest wymagane ponowne analizowanie i planowanie dla każdego wystąpienia zapytania).

Zmienna liczba argumentów

Problemy mogą się pojawić, gdy będziemy chcieli przekazać nieznaną liczbę argumentów:

... id IN ($1, $2, $3, ...) -- $1 : 2, $2 : 3, $3 : 5, ...

Jeśli pozostawimy żądanie w tej formie, ochroni nas to przed potencjalnymi wstrzyknięciami, ale nadal będzie wymagało sklejenia/przeanalizowania żądania dla każdej opcji z liczby argumentów. To lepsze niż robienie tego za każdym razem, ale można się bez tego obejść.

Wystarczy przekazać tylko jeden parametr zawierający serializowana reprezentacja tablicy:

... id = ANY($1::integer[]) -- $1 : '{2,3,5,8,13}'

Jedyną różnicą jest konieczność jawnej konwersji argumentu na pożądany typ tablicy. Ale to nie stanowi żadnego problemu, ponieważ z góry wiemy, dokąd zmierzamy.

Przeniesienie próbki (matrycy)

Zazwyczaj są to wszelkiego rodzaju opcje przesyłania zestawów danych w celu ich umieszczenia w bazie danych „w jednym żądaniu”:

INSERT INTO tbl(k, v) VALUES($1,$2),($3,$4),...

Oprócz opisanych powyżej problemów z „ponownym wklejaniem” żądania, może to również doprowadzić do brak pamięci i awaria serwera. Powód jest prosty: PG rezerwuje dodatkową pamięć na argumenty, a liczba rekordów w zestawie jest ograniczona jedynie przez wymagania logiki biznesowej. W szczególnie klinicznych przypadkach musiałem zobaczyć „ponumerowane” argumenty powyżej 9000 dolarów - Nie rób tego.

Przepiszmy zapytanie, używając już zastosowanego serializacja „dwupoziomowa”:

INSERT INTO tbl
SELECT
  unnest[1]::text k
, unnest[2]::integer v
FROM (
  SELECT
    unnest($1::text[])::text[] -- $1 : '{"{a,1}","{b,2}","{c,3}","{d,4}"}'
) T;

Tak, w przypadku wartości „złożonych” wewnątrz tablicy, muszą one być ujęte w cudzysłów.
Oczywiste jest, że w ten sposób można „rozszerzyć” próbę o dowolną liczbę pól.

gniazdo, gniazdo, …

Czasami istnieją opcje umożliwiające przekazanie kilku „tablic kolumn” zamiast „tablicy tablic”, o której wspomniałem. w ostatnim artykule:

SELECT
  unnest($1::text[]) k
, unnest($2::integer[]) v;

Dzięki tej metodzie, jeśli popełnisz błąd podczas generowania list wartości dla różnych kolumn, bardzo łatwo jest całkowicie stracić nieoczekiwane wyniki, które zależą również od wersji serwera:

-- $1 : '{a,b,c}', $2 : '{1,2}'
-- PostgreSQL 9.4
k | v
-----
a | 1
b | 2
c | 1
a | 2
b | 1
c | 2
-- PostgreSQL 11
k | v
-----
a | 1
b | 2
c |

JSON

Od wersji 9.3 PostgreSQL posiada pełnoprawne funkcje do pracy z typem json. Dlatego jeśli zdefiniujesz parametry wejściowe w przeglądarce, możesz je od razu tam wygenerować. obiekt json dla zapytania sql:

SELECT
  key k
, value v
FROM
  json_each($1::json); -- '{"a":1,"b":2,"c":3,"d":4}'

W przypadku poprzednich wersji można zastosować tę samą metodę każdy(hstore), ale prawidłowe „składanie” z uciekaniem złożonych obiektów w hstore może powodować problemy.

json_populate_zestaw_rekordów

Jeśli wiesz z góry, że dane z tablicy JSON „wejściowej” zostaną użyte do wypełnienia pewnej tabeli, możesz sporo zaoszczędzić na „dereferencjonowaniu” pól i rzutowaniu na wymagane typy, używając funkcji json_populate_recordset:

SELECT
  *
FROM
  json_populate_recordset(
    NULL::pg_class
  , $1::json -- $1 : '[{"relname":"pg_class","oid":1262},{"relname":"pg_namespace","oid":2615}]'
  );

json_do_zestawu_rekordów

Funkcja ta po prostu „rozwinie” przekazaną tablicę obiektów do zaznaczenia, bez polegania na formacie tabeli:

SELECT
  *
FROM
  json_to_recordset($1::json) T(k text, v integer);
-- $1 : '[{"k":"a","v":1},{"k":"b","v":2}]'
k | v
-----
a | 1
b | 2

TABELA TYMCZASOWA

Jeśli jednak objętość danych w przesłanej próbce jest bardzo duża, wówczas wrzucenie jej do jednego parametru serializowanego jest trudne, a czasami niemożliwe, ponieważ wymaga jednorazowego duże przydziały pamięci. Na przykład, musisz zebrać dużą ilość danych dotyczących zdarzeń z systemu zewnętrznego przez długi czas, a potem chcesz ją jednorazowo przetworzyć po stronie bazy danych.

W tym przypadku najlepszym rozwiązaniem będzie użycie tabele tymczasowe:

CREATE TEMPORARY TABLE tbl(k text, v integer);
...
INSERT INTO tbl(k, v) VALUES($1, $2); -- повторить много-много раз
...
-- тут делаем что-то полезное со всей этой таблицей целиком

Metoda jest dobra dokładnie do rzadkiego przesyłania dużych ilości dane.
Z punktu widzenia opisu struktury danych tabela tymczasowa różni się od tabeli „zwykłej” tylko jednym: w tabeli systemowej pg_classa w pg_type, pg_depend, pg_attribute, pg_attrdef, … - nic zupełnie.

Dlatego w systemach internetowych posiadających dużą liczbę krótkotrwałych połączeń, dla każdego z nich taka tabela będzie za każdym razem generować nowe rekordy systemowe, które zostaną usunięte w momencie zamknięcia połączenia z bazą danych. W rezultacie, niekontrolowane użycie TEMP TABLE prowadzi do „rozdęcia” tabel w pg_catalog i spowalniając wiele operacji z nich korzystających.
Oczywiście, można temu zaradzić za pomocą okresowy przejazd VACUUM FULL zgodnie z tabelami katalogu systemowego.

Zmienne sesji

Załóżmy, że przetwarzanie danych z poprzedniego przypadku jest na tyle złożone, że można je wykonać za pomocą jednego zapytania SQL, ale chcemy to robić dość często. Oznacza to, że chcemy wykorzystać przetwarzanie proceduralne w Blokada DO, ale korzystanie z transferu danych za pomocą tabel tymczasowych będzie zbyt kosztowne.

Nie będziemy mogli również użyć $n parametrów do przekazania do anonimowego bloku. Zmienne sesji i funkcja pomogą nam wyjść z tej sytuacji. bieżące_ustawienie.

Przed wersją 9.2 konieczne było przeprowadzenie wstępnej konfiguracji specjalna przestrzeń nazw niestandardowe_klasy_zmiennych dla „Twoich” zmiennych sesji. W obecnych wersjach można napisać coś takiego:

SET my.val = '{1,2,3}';
DO $$
DECLARE
  id integer;
BEGIN
  FOR id IN (SELECT unnest(current_setting('my.val')::integer[])) LOOP
    RAISE NOTICE 'id : %', id;
  END LOOP;
END;
$$ LANGUAGE plpgsql;
-- NOTICE:  id : 1
-- NOTICE:  id : 2
-- NOTICE:  id : 3

Inne rozwiązania można znaleźć w innych obsługiwanych językach proceduralnych.

Czy znasz jakieś inne sposoby? Podziel się swoją opinią w komentarzach!

Źródło: www.habr.com

Kup niezawodny hosting dla stron z ochroną DDoS, serwery VPS VDS 🔥 Kup niezawodny hosting stron internetowych z ochroną DDoS, serwery VPS VDS | ProHoster