Raport zamówień w aplikacji .NET nagle pokazuje mniej klientów, niż widnieje w tabeli — choć zapytanie nie zgłasza błędu. INNER JOIN zwraca pary wierszy pasujące do warunku łączenia, ale wynik zależy też od tego, czy kolumna po drugiej stronie rzeczywiście identyfikuje pojedynczy rekord.
INNER JOIN pokazuje dopasowania, a szczegóły zależą od relacji między tabelami
- Brak pary oznacza brak wiersza w wyniku złączenia wewnętrznego.
- Wiele dopasowań tworzy wiele wierszy, nawet gdy klucz głównej tabeli jest unikalny.
- ON opisuje powiązanie, a WHERE filtruje wynik zapytania.
- Plan wykonania dobiera silnik bazy; nie wynika on bezpośrednio z nazwy złączenia w kodzie.
INNER JOIN zwraca tylko rekordy z dopasowaniem
Masz tabelę zamówień i tabelę klientów. Jeśli zamówienie wskazuje identyfikator klienta, który nie ma odpowiadającego wiersza w tabeli klientów, złączenie wewnętrzne pominie to zamówienie. Podobnie stanie się z klientem, który nie ma zamówień.
Warunek zapisujesz w klauzuli ON, a nazwy tabel skracasz aliasami. Jawna składnia ułatwia później odróżnienie relacji od dodatkowych filtrów:
SELECT
o.Id AS OrderId,
c.Name AS CustomerName
FROM Orders AS o
INNER JOIN Customers AS c
ON c.Id = o.CustomerId;
Wynik zawiera kolumny wskazane w SELECT, ale tylko dla par, które spełniają warunek. Dokumentacja PostgreSQL opisuje tę regułę dla każdego wiersza lewej tabeli: do wyniku trafia wiersz za każde pasujące dopasowanie po prawej stronie. Jeśli dopasowań jest kilka, wynik też ma kilka wierszy.
Wybierz INNER JOIN albo LEFT JOIN według oczekiwanego wyniku
Decyzja zaczyna się od pytania biznesowego: czy raport ma pokazywać wyłącznie zamówienia z rozpoznanym klientem, czy wszystkie zamówienia, także osierocone? INNER JOIN pasuje do pierwszego warunku; gdy trzeba zachować każdy rekord z tabeli po lewej, użyj LEFT JOIN.
| Sytuacja | Skutek | Co zrobić |
|---|---|---|
| Zamówienie bez pasującego klienta ma zniknąć z raportu | Brak dopasowania eliminuje wiersz | Użyj INNER JOIN |
| Raport ma pokazać każde zamówienie, również bez klienta | Rekord zamówienia zostaje, a kolumny klienta będą puste (NULL) |
Użyj LEFT JOIN |
| Wynik ma zero wierszy, choć tabele zawierają dane | Wartości w kolumnach łączenia mogą się nie zgadzać albo być puste | Sprawdź identyfikatory i warunek ON osobno w obu tabelach |
| Wynik ma więcej wierszy niż zamówień | Jeden rekord zamówienia pasuje do wielu wierszy po drugiej stronie | Sprawdź unikalność kolumny łączenia i oczekiwaną krotność relacji |
Możesz dodać warunek statusu do ON lub do WHERE, ale ich działanie nie zawsze będzie takie samo. W przypadku LEFT JOIN filtr w WHERE może odrzucić wiersze z pustymi kolumnami po prawej stronie, więc znikną rekordy, które miały pozostać. Jeśli chcesz zachować zamówienia bez klienta, sprawdź każdy filtr odnoszący się do tabeli klientów.
Trzy zamówienia i cztery zgłoszenia mogą dać dwanaście wierszy
Załóżmy, że jeden klient ma trzy zamówienia i cztery zgłoszenia do pomocy technicznej. Bezpośrednie połączenie tabel Orders i Tickets po identyfikatorze klienta wygeneruje dwanaście par: każde zamówienie połączy się z każdym zgłoszeniem tego klienta.
To nie błąd silnika SQL. Warunek opisuje relację klient-klient, więc każda para spełniająca go trafia do wyniku. Jeśli potem zsumujesz kwoty zamówień, każda kwota pojawi się przy każdym zgłoszeniu, a suma zostanie zawyżona.
Gdy potrzebujesz jednego wiersza na klienta z łączną wartością zamówień i liczbą zgłoszeń, najpierw zagreguj każdą tabelę osobno, a potem połącz wyniki:
WITH OrderTotals AS (
SELECT CustomerId, SUM(Total) AS OrderTotal
FROM Orders
GROUP BY CustomerId
),
TicketCounts AS (
SELECT CustomerId, COUNT(*) AS TicketCount
FROM Tickets
GROUP BY CustomerId
)
SELECT
o.CustomerId,
o.OrderTotal,
t.TicketCount
FROM OrderTotals AS o
INNER JOIN TicketCounts AS t
ON t.CustomerId = o.CustomerId;
Takie zapytanie pokazuje klientów, którzy mają jednocześnie zamówienie i zgłoszenie. Gdy raport ma zachować również klientów występujących tylko w jednej z tych tabel, trzeba zmienić rodzaj złączenia albo połączyć obie listy osobnym wzorcem. Najpierw ustal, co ma oznaczać jeden wiersz raportu, a dopiero potem dobierz relacje.
INNER JOIN w kodzie nie określa algorytmu wykonania
Dwa zapytania z takim samym warunkiem mogą być wykonane przez silnik różnymi metodami. W SQL Server Nested Loops, Merge Join i Hash Join to operatory fizyczne wybierane przez optymalizator; zapis INNER JOIN określa logikę wyniku, a nie konkretny algorytm.
W dokumentacji Microsoft Learn opisano, że wybór zależy między innymi od rozmiaru danych, dostępnych indeksów i ich rozkładu. Przy czytaniu planu wykonania zwróć uwagę, czy operator dopasowuje mały zestaw przez indeks, scala posortowane dane, czy buduje strukturę haszującą dla wejść bez odpowiedniego uporządkowania. Nie wymuszaj operatora tylko dlatego, że jego nazwa brzmi szybciej; najpierw porównaj plan szacowany z rzeczywistymi liczbami wierszy.
W kodzie aplikacji, na przykład w zapytaniu wykonywanym przez Entity Framework, analizuj także wygenerowany SQL. Abstrakcja LINQ może wyglądać prosto, ale to warunek relacji oraz plan bazy rozstrzygają, które rekordy wrócą i jak silnik do nich dotrze.
Zanim uznasz brakujące wiersze za błąd danych
Gdy wynik jest mniejszy od oczekiwań, zacznij od sprawdzenia kilku konkretnych rekordów z obu tabel: czy identyfikatory po obu stronach są równe, czy typy kolumn są zgodne i czy warunek nie łączy przypadkiem danych po nazwie zamiast po kluczu. INNER JOIN wiernie realizuje podane dopasowanie, nawet jeśli samo dopasowanie nie odpowiada intencji raportu.
Ja stawiam na jawne warunki ON i świadomie określam, co ma oznaczać pojedynczy wiersz wyniku. Dzięki temu łatwiej odróżnić brak relacji od powielonych danych, a raport z aplikacji nie zaskakuje ani pustą listą, ani zawyżoną sumą.
