Rekordy osierocone: jak je znaleźć w SQL (i nie wpuszczać)
|
6
min. czyt.

Wiersz płatności zawiera account_id = 884213. W tabeli accounts nie ma takiego konta. Nic nie zgłasza błędu, ładowanie kończy się na zielono, a następnego ranka w raporcie oddziału brakuje dokładnie tej płatności, bo raport łączy płatności z kontami, a złączenie po cichu ją odrzuca. Rekord osierocony to wiersz podrzędny, którego wartość klucza obcego nie ma pasującego wiersza w tabeli nadrzędnej, do której się odwołuje.
Rekordy osierocone (osierocone wiersze) pojawiają się wszędzie tam, gdzie klucze obce nie są wymuszane: w hurtowniach danych, warstwach stagingowych, replikach i potokach, które ładują rekordy podrzędne przed nadrzędnymi. Ich wyszukiwanie to standardowe zadanie w SQL – złączenie wykluczające (anti join). Są cztery popularne sposoby, by je zapisać, a jeden z nich zgłasza „brak sierot”, gdy tylko pojawi się choćby jeden NULL.
Ten poradnik omawia wzorce, pułapki i sposób, by zamienić zapytanie w kontrolę uruchamianą przy każdym ładowaniu. Samo pojęcie wyjaśniamy w artykule Czy Twoje dane wciąż odnajdują swoich rodziców? Integralność referencyjna w praktyce.
Najważniejsze wnioski
INNER JOIN ukrywa rekordy osierocone: niedopasowane wiersze podrzędne znikają z wyniku, zamiast zgłosić błąd.
Sieroty znajdziesz złączeniem wykluczającym:
LEFT JOIN … IS NULL,NOT EXISTSlubEXCEPTna unikalnych kluczach.Unikaj
NOT INna kolumnie dopuszczającej NULL: jeden NULL w podzapytaniu zwraca zero wierszy, co wygląda jak czysty wynik.Klucze złożone dopasowuj na wszystkich kolumnach naraz i świadomie zdecyduj, czy klucz obcy o wartości NULL jest błędem.
Zapytanie, o którym trzeba pamiętać, nie jest mechanizmem kontrolnym. W digna to samo złączenie wykluczające jest jedną regułą Referential Integrity, która działa przy każdej inspekcji.
Spis treści
Czym jest rekord osierocony?
Dlaczego INNER JOIN ukrywa rekordy osierocone?
Jak znaleźć rekordy osierocone w SQL?
LEFT JOIN … IS NULL
NOT EXISTS
EXCEPT (MINUS) na unikalnych kluczach
Dlaczego NOT IN nie zwraca żadnych wierszy, gdy pojawia się NULL?
Którego wzorca złączenia wykluczającego używać?
Jak obsłużyć klucze złożone i klucze obce o wartości NULL?
Jak znaleźć rekordy osierocone w dużych tabelach?
Co, jeśli tabela nadrzędna jest w innej bazie danych?
Jak zamienić zapytanie o sieroty w stałą kontrolę?
Znajdź je raz, a potem nie wpuszczaj
Czym jest rekord osierocony?
Rekord osierocony to wiersz w tabeli podrzędnej, którego klucz obcy wskazuje na nieistniejący klucz nadrzędny: płatność, której account_id nie ma w accounts, albo podanie leku, którego product_code nie ma w medications. Sam wiersz może być w pełni poprawny. Zepsuta jest relacja.
Sieroty zawsze leżą po stronie podrzędnej; konto bez płatności to nic niezwykłego. Typowe przyczyny są prozaiczne: rekord nadrzędny został usunięty lub nigdy nie został załadowany, rekord podrzędny dotarł przed jutrzejszym ładowaniem danych głównych albo formaty kluczy się różnią ('00884213' zamiast 884213 lub spacja na końcu).
Operacyjne bazy danych, takie jak PostgreSQL czy Oracle, wymuszają zadeklarowane klucze obce, ale kopie w hurtowni zwykle nie przenoszą tych ograniczeń. Większość chmurowych hurtowni danych przyjmuje deklarację FOREIGN KEY, ale jej nie wymusza; dokumentacja BigQuery stwierdza, że nie wymusza ograniczeń klucza głównego ani obcego. Dlaczego Snowflake, BigQuery, Redshift i Databricks nie wymuszają kluczy obcych, opisujemy w osobnym artykule.
Dlaczego INNER JOIN ukrywa rekordy osierocone?
INNER JOIN zwraca tylko wiersze dopasowane po obu stronach, więc wiersza podrzędnego bez rekordu nadrzędnego po prostu nie ma w wyniku. Nie ma błędu, ostrzeżenia ani wartości NULL, którą można by zauważyć. Sumy wychodzą niższe, niż powinny, a w samym raporcie nic nie pokazuje różnicy.
Każda płatność, której account_id nie ma w accounts, wypada przed SUM. Najszybciej zobaczysz lukę, licząc na dwa sposoby:
Jeśli account_id jest unikalny w accounts, różnica to Twoje sieroty plus wiersze z account_id o wartości NULL.
Zadeklarowane, ale niesprawdzane klucze mogą to jeszcze pogorszyć. Planer zapytań Amazon Redshift zakłada, że zadeklarowane klucze są prawidłowe, a AWS ostrzega, że nieprawidłowe klucze mogą sprawić, iż niektóre zapytania zwrócą błędne wyniki.
Jak znaleźć rekordy osierocone w SQL?
Rekordy osierocone znajdziesz złączeniem wykluczającym (anti join): zapytaniem, które zwraca wiersze podrzędne, dla których nie istnieje pasujący wiersz nadrzędny. W SQL zapiszesz je jako LEFT JOIN … WHERE parent_key IS NULL, jako NOT EXISTS lub jako EXCEPT na unikalnych wartościach klucza. Dla kluczy bez NULL wszystkie trzy dają ten sam wynik.
LEFT JOIN … IS NULL
Testuj kolumnę nadrzędną, która w dopasowanym wierszu nie może mieć wartości NULL – najlepiej klucz złączenia. Warunki dotyczące tabeli nadrzędnej umieszczaj w klauzuli ON: WHERE a.status = 'ACTIVE' nigdy nie jest prawdziwe dla niedopasowanego wiersza, więc zapytanie nic by nie zwróciło.
NOT EXISTS
Ten zapis czyta się jak samo pytanie. Zduplikowane klucze nadrzędne nie mnożą wierszy, a wartości NULL w accounts.account_id go nie zepsują: porównanie z NULL nigdy nie daje dopasowania.
EXCEPT (MINUS) na unikalnych kluczach
Ten wariant zwraca unikalne brakujące wartości klucza, a nie wiersze. Często to lepsze pierwsze pytanie: kilka brakujących kont może wyjaśnić tysiące osieroconych płatności. Oracle tradycyjnie zapisuje ten operator jako MINUS; BigQuery wymaga EXCEPT DISTINCT. Operatory zbiorów traktują dwa NULL-e jako równe – to kolejny powód, by jawnie odfiltrować klucze NULL.
Dlaczego NOT IN nie zwraca żadnych wierszy, gdy pojawia się NULL?
NOT IN nie zwraca żadnych wierszy, jeśli jego podzapytanie zawiera choćby jeden NULL, ponieważ logika trójwartościowa SQL sprawia, że każde porównanie z tym NULL ma wartość „nieznana”. x NOT IN (1, 2, NULL) oznacza x <> 1 AND x <> 2 AND x <> NULL. Ostatni człon nigdy nie jest prawdziwy, więc cały warunek też nigdy nie jest prawdziwy.
Dla płatności, której konto istnieje, jedno porównanie jest fałszywe i wiersz zostaje słusznie wykluczony. Dla brakującego konta każde porównanie z prawdziwym kluczem jest prawdziwe, ale to z NULL ma wartość „nieznana”, więc cały warunek jest nieznany, a WHERE zachowuje tylko wiersze prawdziwe. Wynikiem jest pusty zbiór, nie do odróżnienia od „brak sierot”.
Ten błąd jest cichy i daje fałszywe „wszystko w porządku”, a kolumny kluczy w hurtowni często nie są zadeklarowane jako NOT NULL. Płatności, których własny account_id to NULL, również nigdy nie zostaną wychwycone. Używaj NOT EXISTS albo przynajmniej dodaj WHERE a.account_id IS NOT NULL wewnątrz podzapytania.
Którego wzorca złączenia wykluczającego używać?
Domyślnie używaj NOT EXISTS do kontroli sierot na poziomie wierszy, EXCEPT, gdy potrzebujesz listy brakujących wartości klucza, a LEFT JOIN … IS NULL, gdy w tym samym przebiegu chcesz też liczby dopasowanych wierszy. NOT IN stosuj tylko wtedy, gdy masz gwarancję, że żadna z kolumn nie zawiera NULL.
Wzorzec | Zwraca | NULL w kluczu nadrzędnym | Klucz obcy NULL w tabeli podrzędnej | Czytelność | Typowa wydajność |
|---|---|---|---|---|---|
LEFT JOIN … IS NULL | Wiersze podrzędne | Bezpieczny | Zgłaszany jako sierota, jeśli nie zostanie odfiltrowany | Znajomy; intencja kryje się w klauzuli WHERE | Zwykle planowany jako anti join |
NOT EXISTS | Wiersze podrzędne | Bezpieczny | Zgłaszany jako sierota, jeśli nie zostanie odfiltrowany | Czyta się jak samo pytanie | Zwykle planowany jako anti join |
EXCEPT / MINUS | Unikalne wartości klucza | Bezpieczny | Zwracany raz jako NULL, jeśli nie zostanie odfiltrowany | Krótki i przejrzysty dla list kluczy | Usuwa duplikaty po obu stronach; dobry do podsumowań na poziomie kluczy |
NOT IN | Wiersze podrzędne | Niebezpieczny: jeden NULL zwraca zero wierszy | Po cichu wykluczany | Dobrze się czyta, ale wprowadza w błąd | W porządku na kolumnach bez NULL; na kolumnach z NULL może dostać gorszy plan |
Większość optymalizatorów planuje LEFT JOIN i NOT EXISTS tak samo; sprawdź plan na swojej platformie.
Jak obsłużyć klucze złożone i klucze obce o wartości NULL?
Przy kluczu złożonym dopasowuj wszystkie kolumny klucza razem, w jednym predykacie, nigdy kolumna po kolumnie: wiersz jest sierotą, gdy brakuje jego kombinacji, nawet jeśli każda wartość z osobna gdzieś istnieje. Klucze obce o wartości NULL wymagają świadomej decyzji, zanim zaczniesz je liczyć: brakujące odwołanie czy uzasadniony brak wartości.
Załóżmy, że każdy szpital ma własny receptariusz szpitalny z kluczem (hospital_id, product_code):
Dwie osobne kontrole – „szpital istnieje” i „produkt istnieje” – obie przejdą dla produktu wpisanego tylko w szpitalu A, a podanego w szpitalu B. Wyłapie go tylko połączony predykat. EXCEPT naturalnie obsługuje klucze złożone. Nie sklejaj kluczy w jeden ciąg znaków: '1' || '23' i '12' || '3' dają ten sam wynik.
Płatność z account_id o wartości NULL nie wskazuje na brakujące konto; nie wskazuje nigdzie. Płatność musi mieć konto, natomiast opcjonalne odwołanie, takie jak referring_doctor_id, może być puste. Wyklucz wartości NULL z zapytania o sieroty i policz je osobno:
Jak znaleźć rekordy osierocone w dużych tabelach?
W dużych tabelach najpierw licz, potem listuj, porównuj unikalne wartości klucza zamiast każdego wiersza i ograniczaj stronę podrzędną do ostatniego ładowania lub partycji. Stronę nadrzędną zostaw kompletną: płatność zaksięgowana dziś może odwoływać się do konta otwartego lata temu, więc nigdy nie filtruj tabeli nadrzędnej po dacie ładowania.
Najpierw policz. Uruchom złączenie wykluczające jako
COUNT(*). Pobieraj wiersze tylko wtedy, gdy wynik jest niezerowy, i to z limitem.Porównuj unikalne klucze. Najpierw zredukuj stronę podrzędną do
SELECT DISTINCT account_id. Unikalnych kluczy jest zwykle znacznie mniej niż wierszy.Filtruj do ostatniego ładowania. Ogranicz tabelę podrzędną do najnowszej partycji lub
load_date, aby silnik mógł pominąć resztę danych (pruning).Zadbaj o porównywalność kolumny złączenia. Ten sam typ danych po obu stronach, bez
CASTaniTRIMw predykacie, które mogą blokować użycie indeksów i pruning. Formaty poprawiaj raczej podczas ładowania; zobacz nasz przewodnik po czyszczeniu danych w SQL.Zapisuj wynik. Przechowuj datę, liczbę ocenianych wierszy i liczbę znalezionych sierot, aby widzieć trend.
To zapytanie liczy brakujące klucze, a nie osierocone wiersze; wiersze dla tych kluczy pobierz potem.
Co, jeśli tabela nadrzędna jest w innej bazie danych?
Złączenie wykluczające działa tylko wtedy, gdy jeden silnik zapytań może odczytać obie tabele. Jeśli płatności są w hurtowni danych, a konta w bazie systemu bankowości centralnej (core banking), zwykły SQL nie widzi obu, więc kopiujesz jedną stronę, używasz łącza bazodanowego (database link) lub zapytania federacyjnego albo uruchamiasz kontrolę w narzędziu, które ma dostęp do obu połączeń.
Skopiowana tabela danych głównych to kolejny potok z własnym opóźnieniem: nieaktualna kopia zgłasza fałszywe sieroty albo przeocza prawdziwe. Łącza bazodanowe zależą od wsparcia platformy i wymagają zgody działu bezpieczeństwa dla każdego nowego połączenia.
Jak zamienić zapytanie o sieroty w stałą kontrolę?
Zapytanie o sieroty staje się stałą kontrolą, gdy uruchamiasz je przy każdym ładowaniu, zapisujesz liczby wierszy poprawnych i błędnych, ustawiasz próg niepowodzenia i przechowujesz błędne wiersze tam, gdzie zespół odpowiedzialny może je zobaczyć. W digna to jedna reguła Referential Integrity, konfigurowana w oknie dialogowym, bez pisania SQL.
digna Data Validation ma trzy rodzaje reguł: Rule, Uniqueness i Referential Integrity. Reguła referencyjna to złączenie wykluczające z tego artykułu: dla kolumn w jednym źródle danych i odpowiadającego im zestawu w innym digna bierze unikalne wartości z drugiej tabeli i oznacza jako błędny każdy wiersz, który się z nimi nie łączy. Reguła wykonuje się w Twojej źródłowej bazie danych; Twoje dane nigdy nie opuszczają Twojej infrastruktury. Od Release 2026.01 działa między różnymi połączeniami bazodanowymi w tym samym projekcie, bez replikowania danych.
Zrzuty ekranu poniżej pokazują dane demonstracyjne digna dla Danubia Kliniken, fikcyjnej austriackiej grupy szpitali.
2026-04-22 oddziały zarejestrowały 82 podania leku Coavira 2.5 mg (kod produktu 3858646), którego nie było jeszcze w kartotece produktów apteki. Każdy raport łączący dawki z produktami pokazywał 0 dawek tego leku, podczas gdy pielęgniarki podały ich 82. Reguła hc_product_in_master zakończyła się niepowodzeniem: kontrolę przeszło 4 244 z 4 326 wierszy. Ręcznie trzeba by napisać takie zapytanie:
Widok Invalid Records pokazuje ten zbiór wyników bez pisania czegokolwiek:

Invalid Records z filtrem Failed: każda osierocona dawka ze szpitalem, oddziałem, działem, kodem produktu i nazwą leku.
Konfiguracja reguły:
Configuration → źródło danych
hospital_medication_administrations→ zakładka Data Validation → Add Rule. Otworzy się okno Add Data Validation Rule.Wpisz Name (
hc_product_in_master) i Description.Ustaw Type na Referential Integrity i wybierz w Attributes kolumnę
product_code.W sekcji must exist in wybierz Data Source
hospital_medicationsi jego Attributesproduct_code. Dla klucza złożonego wybierz kilka kolumn po obu stronach w tej samej kolejności; listy o różnej długości są odrzucane.Wybierz Threshold Mode (tryb progu) Absolute lub Relative i ustaw Info threshold oraz Warn threshold (tutaj: Absolute, Info 0, Warn 1). Zapisz. Reguła działa przy każdej inspekcji, według harmonogramu lub na żądanie.

Cała reguła: dwie listy atrybutów, docelowe źródło danych i dwa progi.
Referential Integrity pomija wartości NULL, tak jak filtr IS NOT NULL powyżej; jeśli wartość jest wymagana, dodaj osobną regułę typu Rule z product_code IS NOT NULL. Powyżej Info threshold status to Uncertain, powyżej Warn threshold – Failed, więc przy Info równym 0 każda sierota zdejmuje regułę ze statusu Passed. Błędne rekordy to to samo zapytanie z zanegowanym warunkiem poprawności; można je wyeksportować, a wyniki mogą trafiać jako powiadomienia do zespołu, który jest właścicielem danych.
Od Release 2026.06 reguły mogą też istnieć jako kod dzięki Python SDK (pip install digna-sdk). Pełny opis znajdziesz w artykule o konfigurowaniu kontroli integralności referencyjnej w digna lub w filmie o konfiguracji (2:26).
Znajdź je raz, a potem nie wpuszczaj
Do badania rekordów osieroconych: NOT EXISTS dla wierszy, EXCEPT dla brakujących kluczy, nigdy NOT IN na kolumnie dopuszczającej NULL i świadoma decyzja w sprawie wartości NULL. Żeby sieroty się nie pojawiały, trzeba więcej: wymuszaj klucze obce tam, gdzie baza danych to potrafi, ładuj najpierw rekordy nadrzędne, jawnie obsługuj spóźnione dane główne i uruchamiaj kontrolę referencyjną przy każdym ładowaniu.
Jeśli chcesz zobaczyć tę kontrolę na własnych tabelach, we własnej infrastrukturze, umów się na demo z zespołem digna.
Najczęściej zadawane pytania
Jak znaleźć rekordy osierocone w SQL?
Użyj złączenia wykluczającego, które zwraca wiersze podrzędne bez pasującego rekordu nadrzędnego. Najbardziej niezawodna forma to NOT EXISTS ze skorelowanym podzapytaniem po kluczu; LEFT JOIN … WHERE parent_key IS NULL daje ten sam wynik. Najpierw odfiltruj klucze obce o wartości NULL i policz je w osobnej kontroli kompletności.
Dlaczego NOT IN nie zwraca żadnych wierszy, gdy podzapytanie zawiera NULL?
Powodem jest logika trójwartościowa. x NOT IN (1, 2, NULL) rozwija się do x <> 1 AND x <> 2 AND x <> NULL, a ostatnie porównanie ma wartość „nieznana”, nigdy „prawda”. WHERE zachowuje tylko wiersze prawdziwe, więc zapytanie nic nie zwraca, co wygląda dokładnie jak czysty wynik.
Czy NOT EXISTS jest szybsze niż LEFT JOIN IS NULL?
Zwykle żadne nie jest szybsze: większość nowoczesnych optymalizatorów planuje oba jako anti join, więc wybór sprowadza się do czytelności. Wyjątkiem jest NOT IN. Na kolumnach dopuszczających NULL może dostać gorszy plan i nie zwraca żadnych wierszy, gdy podzapytanie zawiera NULL.
Czym różni się rekord osierocony od klucza obcego o wartości NULL?
Rekord osierocony wskazuje na klucz nadrzędny, który nie istnieje, a klucz obcy o wartości NULL nie wskazuje nigdzie. Wymagają różnych kontroli: integralności referencyjnej dla sierot i reguły IS NOT NULL tam, gdzie odwołanie jest obowiązkowe. Właśnie dlatego reguła Referential Integrity w digna pomija wartości NULL.
Jak automatycznie sprawdzać rekordy osierocone po każdym ładowaniu?
Zamień złączenie wykluczające w kontrolę uruchamianą według harmonogramu, z progami i zapisywanymi wynikami. W digna to jedna reguła Referential Integrity w Data Validation: wybierz kolumny, wskaż źródło danych, w którym muszą istnieć, ustaw progi Info i Warn, zapisz. Reguła działa w Twojej bazie danych przy każdej inspekcji.



