Jak kluczowe idee Jeffreya Ullmana napędzają współczesne bazy danych: algebra relacyjna, reguły optymalizacji, łączenia i planowanie w stylu kompilatora, które pomagają systemom skalować się.

Większość osób piszących SQL, tworzących pulpity czy strojących wolne zapytanie skorzystała z prac Jeffreya Ullmana — nawet jeśli nigdy nie słyszeli jego nazwiska. Ullman to informatyk i pedagog, którego badania i podręczniki pomogły zdefiniować, jak bazy danych opisują dane, rozumieją zapytania i wykonują je wydajnie.
Gdy silnik bazy danych zamienia twój SQL w coś, co może wykonać szybko, opiera się na pomysłach, które muszą być jednocześnie precyzyjne i elastyczne. Ullman pomógł sformalizować znaczenie zapytań (tak aby system mógł je bezpiecznie przepisywać) i połączyć myślenie o bazach z myśleniem o kompilatorach (tak aby zapytanie można było sparsować, zoptymalizować i przetłumaczyć na kroki wykonawcze).
Ten wpływ jest cichy, bo nie pokazuje się jako przycisk w narzędziu BI ani jako widoczna funkcja w konsoli chmurowej. Objawia się jako:
JOIN\n- optymalizatory wybierające inne plany w miarę wzrostu danych\n- systemy, które skalują się bez zmiany wyniku twojego zapytaniaTen artykuł wykorzystuje kluczowe idee Ullmana jako przewodnik po wnętrzu baz danych, które są najważniejsze w praktyce: jak algebra relacyjna leży pod SQL, jak przepisywanie zapytań zachowuje znaczenie, dlaczego optymalizatory oparte na kosztach podejmują takie decyzje, i jak algorytmy łączeń często decydują, czy zadanie kończy się w sekundach czy godzinach.
Dorzucimy też kilka pojęć podobnych do kompilatora — parsowanie, przepisywanie i planowanie — bo silniki baz danych zachowują się bardziej jak zaawansowane kompilatory, niż wiele osób przypuszcza.
Krótka obietnica: zachowamy dokładność dyskusji, ale unikniemy ciężkich dowodów matematycznych. Celem są modele umysłowe, które możesz zastosować przy pracy, gdy pojawią się problemy z wydajnością, skalowaniem lub dziwnym zachowaniem zapytań.
Jeśli kiedykolwiek napisałeś zapytanie SQL i oczekiwałeś, że „po prostu znaczy to samo”, opierasz się na ideach, które Jeffrey Ullman pomógł upowszechnić i sformalizować: przejrzysty model danych plus precyzyjne sposoby opisania, o co pyta zapytanie.
W istocie model relacyjny traktuje dane jako tabele (relacje). Każda tabela ma wiersze (krotki) i kolumny (atrybuty). To brzmi dziś oczywiście, ale ważna jest dyscyplina, którą tworzy:
To ramowanie umożliwia rozumowanie o poprawności i wydajności bez mówienia ogólnikami. Gdy wiesz, co reprezentuje tabela i jak identyfikowane są wiersze, możesz przewidzieć, co zrobią łączenia, co znaczą duplikaty i dlaczego pewne filtry zmieniają wyniki.
Nauczanie Ullmana często korzysta z algebry relacyjnej jako swego rodzaju kalkulatora zapytań: mały zestaw operacji (select, project, join, union, difference), które można łączyć, by wyrazić, czego się chce.
Dlaczego to ważne w pracy z SQL: bazy tłumaczą SQL na formę algebraiczną, a potem przepisują ją na inną równoważną formę. Dwa zapytania, które wyglądają inaczej, mogą być algebraicznie takie same — to pozwala optymalizatorom zmieniać kolejność łączy, przepychać filtry niżej lub usuwać nadmiarową pracę, zachowując znaczenie.
SQL jest w dużej mierze „co”, ale silniki często optymalizują, używając algebraicznego „jak”.
Dialekty SQL się różnią (Postgres vs. Snowflake vs. MySQL), ale podstawy są niezmienne. Zrozumienie kluczy, relacji i równoważności algebraicznej pomaga dostrzec, kiedy zapytanie jest logicznie błędne, kiedy tylko wolne, i które zmiany zachowują znaczenie między platformami.
Algebra relacyjna to „matematyka pod spodem” SQL: mały zestaw operatorów, które opisują wynik, którego chcesz. Prace Ullmana pomogły uczynić ten widok operatorowy jasnym i uczącym się, i to nadal jest model umysłowy, którego używają optymalizatory.
Zapytanie można wyrazić jako potok kilku bloków konstrukcyjnych:
WHERE w SQL)\n- Project (π): wybór konkretnych kolumn (odpowiednik SELECT col1, col2)\n- Join (⋈): łączenie tabel na podstawie warunku (JOIN ... ON ...)\n- Union (∪): doklejanie wyników o tym samym kształcie (UNION)\n- Difference (−): wiersze w A, ale nie w B (jak EXCEPT w wielu dialektach SQL)Ponieważ zbiór jest mały, łatwiej rozumieć poprawność: jeśli dwa wyrażenia algebraiczne są równoważne, zwracają tę samą tabelę dla każdego poprawnego stanu bazy.
Weź znajome zapytanie:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total \u003e 100;
Koncepcyjnie to:
customers ⋈ orders\nσ(o.total \u003e 100)(...)\nπ(c.name)(...)To nie jest dokładna wewnętrzna notacja używana przez każdy silnik, ale to właściwy pomysł: SQL staje się drzewem operatorów.
Wiele różnych drzew może znaczyć to samo. Na przykład filtry często można przepchnąć wcześniej (zastosować σ przed dużym joinem), a projekcje mogą szybciej odciąć nieużywane kolumny (zastosować π wcześniej).
To właśnie reguły równoważności pozwalają bazie przepisać zapytanie w tańszy plan bez zmiany znaczenia. Gdy zobaczysz zapytania jako algebrę, „optymalizacja” przestaje być magią, a staje się bezpiecznym, prowadzonym regułami przekształcaniem.
Gdy piszesz SQL, baza danych nie wykonuje go „tak, jak napisałeś”. Tłumaczy twoje polecenie na plan zapytania: uporządkowaną reprezentację pracy do wykonania.
Dobry model umysłowy to drzewo operatorów. Liście czytają tabele lub indeksy; węzły wewnętrzne transformują i łączą wiersze. Typowe operatory to scan, filter (selection), project (wybór kolumn), join, group/aggregate i sort.
Bazy zwykle dzielą planowanie na dwie warstwy:
Wpływ Ullmana objawia się w nacisku na przekształcenia zachowujące znaczenie: zmieniaj plan logiczny na wiele sposobów bez zmiany odpowiedzi, a potem wybierz wydajną strategię fizyczną.
Zanim wybierze się ostateczne podejście wykonawcze, optymalizatory stosują algebraiczne „porządki”. Te przepisy nie zmieniają wyników; redukują niepotrzebną pracę.
Typowe przykłady:
Załóżmy, że chcesz zamówienia użytkowników z jednego kraju:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Naiwne wykonanie mogłoby połączyć wszystkich użytkowników z wszystkimi zamówieniami, a potem przefiltrować do Kanady. Przepis zachowujący znaczenie przepycha filtr tak, by join dotyczył mniejszej liczby wierszy:
country = 'CA'\n- Potem połącz tych użytkowników z orders\n- Potem rzutuj jedynie order_id i totalW terminach planu, optymalizator próbuje zmienić:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
w coś bliższego:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Ta sama odpowiedź, mniejsza praca.
Te przepisy bywają łatwe do przeoczenia, bo nigdy ich nie wpisujesz — a jednak są głównym powodem, dla którego ten sam SQL może działać szybko na jednej bazie, a wolno na innej.
Gdy uruchamiasz zapytanie SQL, baza rozważa kilka prawidłowych sposobów uzyskania tego samego wyniku, a potem wybiera ten, który uważa za najtańszy. Ten proces nazywa się optymalizacją opartą na kosztach — i to jedno z najbardziej praktycznych miejsc, gdzie teoretyczne podejście Ullmana pojawia się w codziennej wydajności.
Model kosztu to system punktowania, którego optymalizator używa, by porównać alternatywne plany. Większość silników szacuje koszt, biorąc pod uwagę kilka podstawowych zasobów:
Model nie musi być perfekcyjny; musi wystarczająco często wybierać dobre plany.
Zanim oceni plany, optymalizator zadaje pytanie w każdym kroku: ile wierszy to wyprodukuje? To jest estymacja kardynalności.
Jeśli filtrujesz WHERE country = 'CA', silnik oszacowuje, jaki odsetek tabeli pasuje. Jeśli łączysz klientów z zamówieniami, szacuje, ile par będzie pasować po kluczu łączenia. Te zgadywanki decydują o tym, czy woli skan indeksu zamiast pełnego skanu, hash join zamiast nested loop, czy sort będzie mały czy ogromny.
Szacunki optymalizatora opierają się na statystykach: liczbach, rozkładach wartości, odsetkach nulli i czasem korelacjach między kolumnami.
Gdy statystyki są nieaktualne lub brakujące, silnik może błędnie oszacować liczbę wierszy o rzędy wielkości. Plan, który na papierze wygląda tanio, w praktyce może być bardzo drogi — typowe objawy to nagłe spowolnienia po wzroście danych, „losowe” zmiany planów czy łączenia, które niespodziewanie zapisują na dysk.
Lepsze estymaty często wymagają dodatkowej pracy: dokładniejszych statystyk, próbkowania czy rozważenia większej liczby kandydatów planów. Planowanie samo w sobie kosztuje czas, szczególnie dla złożonych zapytań.
Dlatego optymalizatory równoważą dwa cele:
Zrozumienie tego kompromisu pomaga interpretować wynik EXPLAIN: optymalizator nie próbuje być sprytny — próbuje być przewidywalnie poprawny przy ograniczonych informacjach.
Ullman pomógł spopularyzować prostą, ale potężną ideę: SQL nie jest tyle „wykonywany”, co tłumaczony na plan wykonania. Nigdzie jest to bardziej widoczne niż przy łączeniach. Dwa zapytania zwracające te same wiersze mogą różnić się drastycznie czasem wykonania w zależności od wybranego algorytmu łączenia i kolejności łączeń.
Nested loop join jest koncepcyjnie prosty: dla każdego wiersza z lewej strony znajdź pasujące wiersze z prawej. Może być szybki, gdy lewa strona jest mała, a prawa ma przydatny indeks.
Hash join buduje tablicę haszującą z jednego wejścia (często z mniejszego) i sprawdza ją przy drugim wejściu. Świetnie sprawdza się dla dużych, nieposortowanych wejść z warunkami równości (np. A.id = B.id), ale potrzebuje pamięci; zapisy na dysk mogą zniweczyć przewagę.
Merge join przechodzi po dwóch wejściach w kolejności posortowanej. Dobrze pasuje, gdy obie strony są już uporządkowane (lub łatwo je posortować), na przykład gdy indeksy zwracają wiersze w porządku po kluczu łączenia.
Dla trzech i więcej tabel liczba możliwych kolejności łączeń eksploduje. Połączenie dwóch dużych tabel najpierw może stworzyć ogromny wynik pośredni, który spowolni wszystko inne. Lepsza kolejność zwykle zaczyna się od najbardziej selektywnego filtra (najmniej wierszy) i łączy na zewnątrz, utrzymując pośrednie wyniki małe.
Indeksy nie tylko przyspieszają wyszukiwania — sprawiają, że niektóre strategie łączeń stają się wykonalne. Indeks po kluczu łączenia może zmienić kosztowny nested loop w szybkie „seek per row”. Brak lub nieużywalność indeksów może z kolei skłonić silnik do hash joinów lub dużych sortów potrzebnych przy merge joinach.
Bazy danych nie tylko „uruchamiają SQL”. One go kompilują. Wpływ Ullmana rozciąga się zarówno na teorię baz, jak i myślenie o kompilatorach — i to połączenie tłumaczy, dlaczego silniki zapytań przypominają potoki narzędzi języków programowania: tłumaczą, przepisują i optymalizują, zanim wykonają jakąkolwiek pracę.
Gdy wysyłasz zapytanie, pierwszy krok przypomina front-end kompilatora. Silnik tokenizuje słowa kluczowe i identyfikatory, sprawdza składnię i buduje drzewo parsingu (często upraszczane do abstrakcyjnego drzewa składniowego). Tu łapane są podstawowe błędy: brakujące przecinki, niejednoznaczne nazwy kolumn, niepoprawne reguły grupowania.
Pomocny model: SQL to język programowania, którego „program” opisuje relacje danych zamiast pętli.
Kompilatory konwertują składnię do reprezentacji pośredniej (IR). Bazy robią podobnie: tłumaczą SQL na logiczne operatory takie jak:
GROUP BY)Ta logiczna forma jest bliższa algebrze relacyjnej niż tekstowi SQL, co ułatwia rozumienie znaczenia i równoważności.
Optymalizacje kompilatora zachowują wynik programu, jednocześnie czyniąc jego wykonanie tańszym. Optymalizatory baz danych robią to samo, używając systemów reguł takich jak:
To jest baza danych w wersji "eliminacji martwego kodu": nie identyczne techniki, ale ta sama filozofia — zachowaj semantykę, zmniejsz koszt.
Jeśli twoje zapytanie jest wolne, nie patrz tylko na SQL. Sprawdź plan zapytania tak, jakbyś analizował wynik kompilacji. Plan pokaże, co silnik faktycznie wybrał: kolejność łączeń, użycie indeksów i miejsca, gdzie spędza się czas.
Praktyczny wniosek: naucz się czytać EXPLAIN jak listing asemblera wydajności. To zamienia strojenie z zgadywania na debugowanie oparte na dowodach. Więcej o tym, jak wyrobić ten nawyk, zobacz wpis Practical Query Optimization Habits.
Dobra wydajność zapytań często zaczyna się zanim napiszesz SQL. Teoria projektowania schematu Ullmana (szczególnie normalizacja) dotyczy strukturyzowania danych tak, by baza mogła utrzymywać ich poprawność, przewidywalność i wydajność w miarę wzrostu.
Normalizacja ma na celu:\n
Te korzyści z poprawności przekładają się na zyski wydajnościowe później: mniej zduplikowanych pól, mniejsze indeksy i mniej kosztownych aktualizacji.
Nie musisz uczyć się dowodów, by korzystać z idei:
Denormalizacja może być rozsądna, gdy:\n
Kluczem jest świadome denormalizowanie z procesem utrzymania spójności duplikatów.
Projekt schematu kształtuje możliwości optymalizatora. Jasne klucze i klucze obce umożliwiają lepsze strategie łączeń, bezpieczniejsze przekształcenia i dokładniejsze estymaty kardynalności. Nadmierna redundancja może zaś zwiększyć indeksy i spowolnić zapisy, a kolumny wielowartościowe blokują efektywne predykaty. W miarę wzrostu wolumenów danych, te wczesne decyzje modelowe często mają większe znaczenie niż mikrooptymalizacje pojedynczego zapytania.
Gdy system "skalował się", rzadko chodzi tylko o dodanie większych maszyn. Częściej trudność polega na tym, że to samo znaczenie zapytania trzeba zachować, podczas gdy silnik wybiera zupełnie inną strategię fizyczną, żeby utrzymać przewidywalne czasy wykonania. Nacisk Ullmana na formalne równoważności jest dokładnie tym, co pozwala na takie zmiany strategii bez zmiany wyników.
Przy małych rozmiarach wiele planów "działa". Przy skali różnica między skanowaniem tabeli, użyciem indeksu czy wykorzystaniem prekomputowanego wyniku może oznaczać różnicę między sekundami a godzinami. Strona teoretyczna ma znaczenie, bo optymalizator potrzebuje bezpiecznego zestawu reguł przepisywania (np. przepychanie filtrów wcześniej, zmiana kolejności łączeń), które nie zmienią odpowiedzi — nawet jeśli radykalnie zmienią wykonywaną pracę.
Partycjonowanie (po dacie, kliencie, regionie itp.) dzieli jedną logiczną tabelę na wiele fizycznych części. To wpływa na planowanie:\n
Tekst SQL może być niezmieniony, ale najlepszy plan zależy teraz od rozmieszczenia wierszy.
Materializowane widoki to w zasadzie „zapisane podwyrażenia”. Jeśli silnik może udowodnić, że twoje zapytanie pasuje do przechowanego wyniku (lub można je do niego przepisać), może zastąpić kosztowną pracę — np. powtarzające się łączenia i agregacje — szybkim odczytem. To myślenie algebry relacyjnej w praktyce: rozpoznaj równoważność, a potem ponownie użyj wyniku.
Cache przyspiesza powtarzalne odczyty, ale nie uratuje zapytania, które musi zeskanować za dużo danych, przetworzyć ogromne pośrednie wyniki lub wykonać gigantyczne łączenie. Gdy pojawiają się problemy skali, naprawa często polega na: zmniejszeniu ilości dotykanych danych (układ/partycjonowanie), ograniczeniu powtarzanych obliczeń (materializowane widoki) lub zmianie planu — a nie tylko "dodaniu cache".
Wpływ Ullmana ujawnia się w prostym nastawieniu: traktuj wolne zapytanie jako zamiar, który baza może przepisać, a potem zweryfikuj, co naprawdę zrobiła. Nie musisz być teoretykiem, by skorzystać — wystarczy powtarzalna rutyna.
Zacznij od części, które zwykle dominują czas wykonania:\n
Jeśli zrobisz tylko jedną rzecz, zidentyfikuj pierwszy operator, gdzie liczba wierszy wybucha. To zwykle źródło problemu.
Łatwe do napisania i zaskakująco kosztowne:
WHERE LOWER(email) = ... może uniemożliwić użycie indeksu (użyj znormalizowanej kolumny lub indeksu funkcyjnego, jeśli jest wspierany).\n- Brak predykatów: zapomniany zakres dat czy filtr najemcy zamienia celowane zapytanie w szeroki skan.\n- Przypadkowe cross joiny: brak warunku łączenia może pomnożyć wiersze i wymusić ogromne wyniki pośrednie.Algebra relacyjna sugeruje dwie praktyczne rzeczy:\n
WHERE przed łączeniami, gdy to możliwe, by zmniejszyć wejścia.\n- Ogranicz kolumny wcześnie: wybieraj tylko potrzebne kolumny (zwłaszcza przed łączeniami), by oszczędzić pamięć i I/O.Dobra hipoteza brzmi: „To łączenie jest kosztowne, bo łączymy za dużo wierszy; jeśli przefiltrujemy orders do ostatnich 30 dni najpierw, wejście do łączenia się zmniejszy.”
Prosta reguła decyzyjna:\n
EXPLAIN pokazuje uniknionalną pracę (zbędne łączenia, późne filtrowanie, predykaty nieużywające indeksu).\n- Zmień schemat, gdy wzorzec obciążenia jest stabilny i ciągle walczysz z tym samym wąskim gardłem (np. prekomputowane agregaty, denormalizowane pola wyszukiwań, partycjonowanie według czasu/tenantów).Celem nie jest "sprytny SQL", lecz przewidywalne, mniejsze wyniki pośrednie — dokładnie tego typu ulepszeń, które ułatwiają dostrzec pomysły Ullmana.
Te koncepcje nie są tylko dla administratorów baz. Jeśli wypuszczasz aplikację, podejmujesz decyzje dotyczące bazy i planowania zapytań, nawet jeśli o tym nie wiesz: kształt schematu, wybór kluczy, wzorce zapytań i warstwa dostępu do danych wpływają na to, co optymalizator może zrobić.
Jeśli korzystasz z workflowu typu vibe-coding (np. generowania aplikacji React + Go + PostgreSQL z interfejsu czatowego w Koder.ai), modele umysłowe Ullmana są praktyczną siatką bezpieczeństwa: możesz przejrzeć wygenerowany schemat pod kątem czystych kluczy i relacji, sprawdzić zapytania, na których opiera się aplikacja, i zweryfikować wydajność za pomocą EXPLAIN zanim problemy pojawią się w produkcji. Im szybciej iterujesz nad "intencja zapytania → plan → poprawka", tym więcej korzyści z przyspieszonego rozwoju.
Nie musisz traktować teorii jako osobnego hobby. Najszybszy sposób, by skorzystać z fundamentów Ullmana, to nauczyć się tyle, by pewnie czytać plany zapytań — a potem ćwiczyć na własnej bazie.
Szukaj tych książek i tematów wykładów (brak afiliacji — po prostu często cytowane punkty startowe):
Tematy wykładów do wyszukania: algebra relacyjna, przepisywanie zapytań, kolejność łączy, optymalizacja oparta na kosztach, indeksy i selektywność, parsowanie i języki zapytań.
Zacznij małymi krokami i trzymaj każdy etap przy czymś, co możesz zaobserwować:
Wybierz 2–3 prawdziwe zapytania i iteruj:\n
IN na EXISTS, przepchnij predykaty wcześniej, usuń niepotrzebne kolumny, porównaj wyniki.\n- Porównaj plany: zapisz plany "przed/po" i zanotuj, co się zmieniło (kolejność łączeń, typ łączenia, typ skanu).\n- Modyfikuj indeksy: dodawaj/usuwaj pojedyncze indeksy i obserwuj estymaty vs. rzeczywiste liczby wierszy.Używaj jasnego, opartego na planie języka:\n
To praktyczny zysk z fundamentów Ullmana: wspólne słownictwo do wyjaśniania wydajności — bez zgadywania.
Jeffrey Ullman pomógł sformalizować, jak bazy danych reprezentują znaczenie zapytania i jak można bezpiecznie przekształcać zapytania w równoważne, szybsze formy. Ta podstawa pojawia się za każdym razem, gdy silnik przepisuje zapytanie, zmienia kolejność łączeń lub wybiera inny plan wykonania, gwarantując ten sam zestaw wyników.
Algebra relacyjna to mały zestaw operatorów (select, project, join, union, difference), które precyzyjnie opisują rezultat zapytania. Silniki często tłumaczą SQL na strukturę podobną do algebry (drzewo operatorów), by zastosować reguły równoważności (np. przepchanie filtrów wcześniej) przed wyborem strategii wykonania.
Ponieważ optymalizacja opiera się na dowodzie, że przepisane zapytanie zwróci ten sam wynik. Reguły równoważności pozwalają optymalizatorowi na przykład:
WHERE przed JOINTe zmiany potrafią gwałtownie zmniejszyć ilość pracy bez zmiany znaczenia zapytania.
Plan logiczny opisuje co trzeba obliczyć (filtr, łączenie, agregacja) niezależnie od szczegółów przechowywania. Plan fizyczny wybiera jak to uruchomić (skan indeksu vs. pełny skan, hash join vs. nested loop, równoległość, strategie sortowania). Większość różnic wydajności wynika z wyborów fizycznych, do których prowadzą przekształcenia logiczne.
Optymalizator ocenia kilka poprawnych planów i wybiera ten z najniższym szacowanym kosztem. Koszty zwykle wynikają z praktycznych czynników, takich jak liczba przetworzonych wierszy, I/O, CPU i pamięć (włącznie z sytuacjami, gdy hash lub sort zapisuje dane na dysk).
Estymacja kardynalności to zgadywanie przez optymalizator, "ile wierszy wyjdzie z tego kroku?" Te szacunki decydują o kolejności łączeń, typie łączenia i tym, czy skan indeksu ma sens. Gdy szacunki są błędne (np. przez nieaktualne lub brakujące statystyki), można dostać nagłe spowolnienia, duże zapisy na dysk lub niespodziewane zmiany planu.
Skup się na kilku sygnałach o dużej wartości:
Traktuj plan jak skompilowany kod: pokazuje, co silnik naprawdę postanowił zrobić.
Normalizacja zmniejsza duplikację faktów i anomalie aktualizacji, co zwykle oznacza mniejsze tabele i indeksy oraz bardziej przewidywalne łączenia. Denormalizacja ma sens dla analiz lub wzorców odczytu, ale powinna być świadoma (jasne zasady odświeżania i kontrolowana redundancja), żeby nie pogorszyć poprawności z czasem.
Skalowanie często wymaga zmiany strategii fizycznej, zachowując ten sam sens zapytania. Typowe narzędzia to:
Cache przyspiesza powtarzane odczyty, ale nie uratuje zapytania, które musi przetworzyć zbyt dużo danych lub tworzy ogromne pośrednie wyniki.