To boli, kiedy to robisz, więc nie rób tego.
W Oracle kursory są nauczane jako część programowania 101. W wielu (jeśli nie większości) przypadkach kursory są pierwszą rzeczą, której uczy się programista Oracle. Pierwsza klasa zwykle zaczyna się od:„Istnieje 13 struktur logicznych, z których pierwszą jest pętla, która przebiega tak…”
Z drugiej strony PostgreSQL nie polega w dużym stopniu na kursorach. Tak, istnieją. Istnieje kilka odmian składni, jak ich używać. Omówię wszystkie główne projekty w pewnym momencie tej serii artykułów. Ale pierwsza lekcja dotycząca kursorów PostgreSQL jest taka, że istnieje wiele (i znacznie lepszych) algorytmicznych alternatyw dla używania kursorów w PostgreSQL. W rzeczywistości w 23-letniej karierze z PostgreSQL znalazłem potrzebę użycia kursorów tylko dwa razy. I żałuję jednego z nich.
Kursory to kosztowny nawyk.
Iteracja jest lepsza niż pętla. „Jaka jest różnica?”, możesz zapytać. Cóż, różnica dotyczy O(N) i O(N^2). Ok, powiem to jeszcze raz po angielsku. Złożoność używania kursorów polega na tym, że przechodzą one przez zestawy danych przy użyciu tego samego wzorca, co zagnieżdżona pętla for. Każdy dodatkowy zestaw danych zwiększa złożoność sumy przez potęgowanie. Dzieje się tak, ponieważ każdy dodatkowy zestaw danych skutecznie tworzy kolejną najbardziej wewnętrzną pętlę. Dwa zestawy danych to O(N^2), trzy zestawy danych to O(N^3) i tak dalej. Przyzwyczajenie się do używania kursorów, gdy istnieją lepsze algorytmy do wyboru, może być kosztowne.
Robią to bez żadnej optymalizacji, która byłaby dostępna dla funkcji niższego poziomu samej bazy danych. Oznacza to, że nie mogą używać indeksów w żaden znaczący sposób, nie mogą przekształcać się w podselekcje, podciągać się do złączeń ani używać odczytów równoległych. Nie skorzystają również z przyszłych optymalizacji dostępnych w bazie danych. Mam nadzieję, że jesteś mistrzem kodowania, który zawsze otrzymuje właściwy algorytm i koduje go perfekcyjnie za pierwszym razem, ponieważ właśnie pokonałeś jedną z najważniejszych zalet relacyjnej bazy danych. Wydajność, opierając się na najlepszych praktykach lub przynajmniej na kodzie kogoś innego.
Każdy jest lepszy od ciebie. Może nie indywidualnie, ale prawie na pewno zbiorowo. Oprócz argumentu deklaratywnego vs. imperatywnego, kodowanie w języku, który został kiedyś usunięty z podstawowej biblioteki funkcji, pozwala wszystkim innym na podjęcie próby szybszego, lepszego i wydajniejszego działania kodu bez konsultacji z Tobą. I to bardzo, bardzo dobrze dla Ciebie.
Zróbmy trochę danych do zabawy.
Zaczniemy od skonfigurowania danych do zabawy w kilku następnych artykułach.
Zawartość pliku cursors.bash:
set -o nounset # Treat unset variables as an error
# This script assumes that you have PostgreSQL running locally,
# that you have a database with the same name as the local user,
# and that you can create all this structure.
# If not, then:
# sudo -iu postgres createuser -s $USER
# createdb
# Clean up from the last run
[[ -f itisPostgreSql.zip ]] && rm itisPostgreSql.zip
subdirs=$(ls -1 itisPostgreSql* | grep : | sed -e 's/://')
for sub in ${subdirs[@]}
do
rm -rf $sub
done
# Get the newest file
wget https://www.itis.gov/downloads/itisPostgreSql.zip
# Unpack it
unzip itisPostgreSql.zip
# This makes a directory with the stupidest f-ing name possible
# itisPostgreSqlDDMMYY
subdir=$(\ls -1 itisPostgreSql* | grep : | sed -e 's/://')
# The script wants to create an "ITIS" database. Let's just make that a schema.
sed -i $subdir/ITIS.sql -e '/"ITIS"/d' # Cut the lines about making the db
sed -i $subdir/ITIS.sql -e '/-- PostgreSQL database dump/s/.*/CREATE SCHEMA IF NOT EXISTS itis;/'
sed -i $subdir/ITIS.sql -e '/SET search_path = public, pg_catalog;/s/.*/SET search_path TO itis;/'
# ok, we have a schema to put the data in, let's do the import.
# timeout if we can't connect, fail on error.
PG_TIMEOUT=5 psql -v "ON_ERROR_STOP=1" -f $subdir/ITIS.sql
Daje nam to nieco ponad 600 000 rekordów do zabawy w tabeli itis.hierarchy, która zawiera taksonomię świata przyrody. Wykorzystamy te dane, aby zilustrować różne metody radzenia sobie ze złożonymi interakcjami danych.
Pierwsza alternatywa.
Moim głównym wzorcem projektowym do chodzenia po zestawach rekordów podczas wykonywania kosztownych operacji jest Common Table Expression (CTE).
Oto przykład formy podstawowej:
WITH RECURSIVE fauna AS (
SELECT tsn, parent_tsn, tsn::text taxonomy
FROM itis.hierarchy
WHERE parent_tsn = 0
UNION ALL
SELECT h1.tsn, h1.parent_tsn, f.taxonomy || '.' || h1.tsn
FROM itis.hierarchy h1
JOIN fauna f
ON h1.parent_tsn = f.tsn
)
SELECT *
FROM fauna
ORDER BY taxonomy;
Co daje następujące wyniki:
┌─────────┬────────┬──────────────────────────────────────────────────────────┐
│ tsn │ parent │ taxonomy │
│ │ tsn │ │
├─────────┼────────┼──────────────────────────────────────────────────────────┤
│ 202422 │ 0 │202422 │
│ 846491 │ 202422 │202422.846491 │
│ 660046 │ 846491 │202422.846491.660046 │
│ 846497 │ 660046 │202422.846491.660046.846497 │
│ 846508 │ 846497 │202422.846491.660046.846497.846508 │
│ 846553 │ 846508 │202422.846491.660046.846497.846508.846553 │
│ 954935 │ 846553 │202422.846491.660046.846497.846508.846553.954935 │
│ 5549 │ 954935 │202422.846491.660046.846497.846508.846553.954935.5549 │
│ 5550 │ 5549 │202422.846491.660046.846497.846508.846553.954935.5549.5550│
│ 954936 │ 846553 │202422.846491.660046.846497.846508.846553.954936 │
│ 954904 │ 660046 │202422.846491.660046.954904 │
│ 846509 │ 954904 │202422.846491.660046.954904.846509 │
│ 11473 │ 846509 │202422.846491.660046.954904.846509.11473 │
│ 11474 │ 11473 │202422.846491.660046.954904.846509.11473.11474 │
│ 11475 │ 11474 │202422.846491.660046.954904.846509.11473.11474.11475 │
│ ... │ │...snip... │
└─────────┴────────┴──────────────────────────────────────────────────────────┘
(601187 rows)
To zapytanie można łatwo modyfikować w celu wykonania dowolnych obliczeń. Obejmuje to wzbogacanie danych, złożone funkcje lub cokolwiek innego, czego dusza zapragnie.
„Ale spójrz!”, wykrzykujesz. „Jest napisane RECURSIVE
właśnie tam w nazwie! Czy nie robi dokładnie tego, czego kazałeś nie robić? Cóż, właściwie nie. Pod maską nie używa rekurencji w sensie zagnieżdżonym ani pętli do wykonania „rekurencji”. To tylko liniowy odczyt tabeli, dopóki podrzędne zapytanie nie zwróci żadnych nowych wyników. Działa również z indeksami.
Spójrzmy na plan wykonania:
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Sort (cost=211750.51..211840.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ Sort Key: fauna.taxonomy │
│ CTE fauna │
│ -> Recursive Union (cost=1000.00..208320.69 rows=35858 width=40) │
│ -> Gather (cost=1000.00..15045.02 rows=18 width=40) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, ((hierarchy.tsn)::text) │
│ Workers Planned: 2 │
│ -> Parallel Seq Scan on itis.hierarchy (cost=0.00..14043.22 rows=8 width=40) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn)::text │
│ Filter: (hierarchy.parent_tsn = 0) │
│ -> Hash Join (cost=5.85..19255.85 rows=3584 width=40) │
│ Output: h1.tsn, h1.parent_tsn, ((f.taxonomy || '.'::text) || (h1.tsn)::text) │
│ Hash Cond: (h1.parent_tsn = f.tsn) │
│ -> Seq Scan on itis.hierarchy h1 (cost=0.00..16923.87 rows=601187 width=8) │
│ Output: h1.hierarchy_string, h1.tsn, h1.parent_tsn, h1.level, h1.childrencount │
│ -> Hash (cost=3.60..3.60 rows=180 width=36) │
│ Output: f.taxonomy, f.tsn │
│ -> WorkTable Scan on fauna f (cost=0.00..3.60 rows=180 width=36) │
│ Output: f.taxonomy, f.tsn │
│ -> CTE Scan on fauna (cost=0.00..717.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ JIT: │
│ Functions: 13 │
│ Options: Inlining false, Optimization false, Expressions true, Deforming true │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
Stwórzmy indeks i zobaczmy, jak to działa.
CREATE UNIQUE INDEX taxonomy_parents ON itis.hierarchy (parent_tsn, tsn);
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├─────────────────────────────────────────────────────────────────────────────┤
│Sort (cost=135148.13..135237.77 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│ Sort Key: fauna.taxonomy │
│ CTE fauna │
│ -> Recursive Union (cost=4.56..131718.31 rows=35858 width=40) │
│ -> Bitmap Heap Scan on itis.hierarchy (cost=4.56..74.69 rows=18) │
│ Output: hierarchy.tsn, hierarchy.parent_tsn, (hierarchy.tsn) │
│ Recheck Cond: (hierarchy.parent_tsn = 0) │
│ -> Bitmap Index Scan on taxonomy_parents │
│ (cost=0.00..4.56 rows=18) │
│ Index Cond: (hierarchy.parent_tsn = 0) │
│ -> Nested Loop (cost=0.42..13092.65 rows=3584 width=40) │
│ Output: h1.tsn, h1.parent_tsn,((f.taxonomy || '.')||(h1.tsn))│
│ -> WorkTable Scan on fauna f (cost=0.00..3.60 rows=180) │
│ Output: f.tsn, f.parent_tsn, f.taxonomy │
│ -> Index Only Scan using taxonomy_parents on itis.hierarchy │
│ h1 (cost=0.42..72.32 rows=20 width=8) │
│ Output: h1.parent_tsn, h1.tsn │
│ Index Cond: (h1.parent_tsn = f.tsn) │
│ -> CTE Scan on fauna (cost=0.00..717.16 rows=35858 width=40) │
│ Output: fauna.tsn, fauna.parent_tsn, fauna.taxonomy │
│JIT: │
│ Functions: 6 │
└─────────────────────────────────────────────────────────────────────────────┘
Cóż, to było satysfakcjonujące, prawda? A stworzenie indeksu w połączeniu z kursorem w celu wykonania tej samej pracy byłoby zbyt trudne. Ta struktura prowadzi nas wystarczająco daleko, abyśmy mogli poruszać się po dość złożonej strukturze drzewa i używać jej do prostych wyszukiwań.
W następnej części porozmawiamy o innej metodzie jeszcze szybszego uzyskania tego samego wyniku. W następnym artykule porozmawiamy o drzewie rozszerzeń i o tym, jak niesamowicie szybko spojrzeć na dane hierarchiczne. Bądź na bieżąco.