Relacje w zapytaniach SOQL
Pobieraj dane z rekordów powiązanych w jednym zapytaniu: pola rodzica przez kropkę, listy dzieci przez podzapytania, filtry po relacjach i semi-joiny.
Dokumenty i eksporty prawie zawsze łączą dane z kilku obiektów: oferta potrzebuje adresu nieruchomości, umowa danych firmy, a raport listy wszystkich ofert. SOQL nie ma klasycznego JOIN z SQL. Zamiast tego korzysta z relacji zdefiniowanych w modelu danych.
Przykładowe dane
Wszystkie przykłady w tej lekcji działają na tych samych, małych danych z obiektów zbudowanych w module 2 kursu Podstawy Salesforce: 5 kontaktów, 6 nieruchomości i 8 ofert. Tych samych danych używamy w lekcjach o filtrowaniu i agregacjach, więc wystarczy przygotować je raz.
Obiekt Contact (kontakty):
| FirstName | LastName | |
|---|---|---|
| Anna | Nowak | anna.nowak@example.com |
| Piotr | Kowalski | piotr.kowalski@example.com |
| Ewa | Wiśniewska | ewa.wisniewska@example.com |
| Ania | Nowak | anna.nowak@example.com |
| Jan | Zieliński | (puste) |
Ania Nowak to ta sama osoba co Anna Nowak, wprowadzona drugi raz przy imporcie. Takie duplikaty zdarzają się w każdym orgu, a w lekcji o agregacjach nauczysz się je wyszukiwać.
Obiekt Nieruchomość (Property__c), powiązany z kontaktem polem Seller__c:
| Name | Price__c | Area_m2__c | Status__c | Sprzedający (Seller__c) |
|---|---|---|---|---|
| Lipowa 5/12, Gdańsk | 450 000 | 50 | Dostępna | Anna Nowak |
| Kwiatowa 3, Sopot | 1 250 000 | 140 | Zarezerwowana | Anna Nowak |
| Słoneczna 8/4, Gdańsk | 380 000 | 36 | Sprzedana | Piotr Kowalski |
| Ogrodowa 15, Gdynia | 890 000 | 120 | Dostępna | (puste) |
| Morska 21/3, Gdynia | 640 000 | 62 | Nowa | Piotr Kowalski |
| Polna 2/7, Gdańsk | 520 000 | 55 | Dostępna | Ewa Wiśniewska |
Pozostałe pola tych samych nieruchomości:
| Name | Amenities__c | Is_Furnished__c | Available_From__c |
|---|---|---|---|
| Lipowa 5/12, Gdańsk | Balkon;Winda | true | 2026-11-01 |
| Kwiatowa 3, Sopot | Miejsce parkingowe;Ogród;Piwnica | false | (puste) |
| Słoneczna 8/4, Gdańsk | Balkon | true | 2026-03-01 |
| Ogrodowa 15, Gdynia | Miejsce parkingowe;Ogród | false | 2026-10-01 |
| Morska 21/3, Gdynia | Winda;Miejsce parkingowe | true | 2026-12-15 |
| Polna 2/7, Gdańsk | Piwnica | false | 2026-10-20 |
Obiekt Oferta (Offer__c), powiązany z nieruchomością relacją Master-Detail:
| Name | Amount__c | Nieruchomość (Property__c) |
|---|---|---|
| OF-00001 | 395 000 | Lipowa 5/12, Gdańsk |
| OF-00002 | 430 000 | Lipowa 5/12, Gdańsk |
| OF-00003 | 1 200 000 | Kwiatowa 3, Sopot |
| OF-00004 | 360 000 | Słoneczna 8/4, Gdańsk |
| OF-00005 | 375 000 | Słoneczna 8/4, Gdańsk |
| OF-00006 | 380 000 | Słoneczna 8/4, Gdańsk |
| OF-00007 | 850 000 | Ogrodowa 15, Gdynia |
| OF-00008 | 505 000 | Polna 2/7, Gdańsk |
Uwagi do tabel z wynikami. Kwoty są sformatowane dla czytelności, a Salesforce zwraca je jako zwykłe liczby, np.
450000. BezORDER BYSalesforce nie gwarantuje kolejności rekordów, więc w takich wynikach pokazujemy je w kolejności z tabel powyżej. Dotyczy to także list rekordów podrzędnych w podzapytaniach.
Chcesz sprawdzić przykłady u siebie? Dodaj te rekordy w swoim orgu do nauki: ręcznie albo importem z pliku CSV, jak w lekcji Praca z rekordami i widokami list. Numery ofert nadaje Salesforce, więc u Ciebie mogą być inne.
Dwa kierunki
| Kierunek | Przykład | Jak piszesz |
|---|---|---|
| Od dziecka do rodzica | kontakt → jego firma | kropka: Account.Name |
| Od rodzica do dzieci | firma → jej kontakty | podzapytanie: (SELECT ... FROM Contacts) |
„Dziecko” to obiekt, który ma pole relacji (Lookup lub Master-Detail). „Rodzic” to obiekt, na który to pole wskazuje.
Od dziecka do rodzica: kropka
Pole relacji przechowuje tylko Id rodzica (AccountId). Żeby pobrać jego dane, użyj nazwy relacji i kropki:
SELECT FirstName, LastName, Account.Name, Account.Industry, Owner.Name
FROM Contact
WHERE Account.BillingCity = 'Warszawa'
Zasady:
- Dla pól standardowych nazwa relacji to zwykle nazwa pola bez
Id:AccountId→Account,OwnerId→Owner,RecordTypeId→RecordType. - Dla pól własnych końcówkę
__czamieniasz na__r: poleSeller__c→ relacjaSeller__r. - Możesz iść w górę do 5 poziomów:
Contact.Account.Owner.Manager.Name. - Jeśli pole relacji jest puste, pola rodzica przyjdą jako puste. Zapytanie się nie wywróci.
Przykład na obiektach z modułu 2 kursu podstaw:
SELECT Name, Price__c, Seller__r.Name, Seller__r.Email
FROM Property__c
WHERE Status__c = 'Dostępna'
Przykład 1. Pola rodzica przez kropkę
SELECT Name, Seller__r.Name, Seller__r.Email
FROM Property__c
Co zwróci: wszystkie nieruchomości, a przy każdej imię i nazwisko oraz e-mail sprzedającego. Ogrodowa 15 nie ma sprzedającego, więc jej pola z Seller__r są puste, a zapytanie i tak działa.
Wynik (6 rekordów):
| Name | Seller__r.Name | Seller__r.Email |
|---|---|---|
| Lipowa 5/12, Gdańsk | Anna Nowak | anna.nowak@example.com |
| Kwiatowa 3, Sopot | Anna Nowak | anna.nowak@example.com |
| Słoneczna 8/4, Gdańsk | Piotr Kowalski | piotr.kowalski@example.com |
| Ogrodowa 15, Gdynia | (puste) | (puste) |
| Morska 21/3, Gdynia | Piotr Kowalski | piotr.kowalski@example.com |
| Polna 2/7, Gdańsk | Ewa Wiśniewska | ewa.wisniewska@example.com |
Przykład 2. Dwa poziomy w górę
SELECT Name, Amount__c, Property__r.Name,
Property__r.Seller__r.LastName
FROM Offer__c
WHERE Amount__c >= 500000
Co zwróci: oferty od 500 000 wzwyż, a przy każdej nazwę nieruchomości i nazwisko jej sprzedającego. Pierwsza kropka prowadzi od oferty do nieruchomości, druga od nieruchomości do kontaktu. OF-00007 dotyczy Ogrodowej 15, która nie ma sprzedającego, więc nazwisko jest puste.
Wynik (3 rekordy):
| Name | Amount__c | Property__r.Name | Property__r.Seller__r.LastName |
|---|---|---|---|
| OF-00003 | 1 200 000 | Kwiatowa 3, Sopot | Nowak |
| OF-00007 | 850 000 | Ogrodowa 15, Gdynia | (puste) |
| OF-00008 | 505 000 | Polna 2/7, Gdańsk | Wiśniewska |
Od rodzica do dzieci: podzapytanie
Listę rekordów podrzędnych pobierasz podzapytaniem w nawiasie. W FROM podzapytania wpisujesz nazwę relacji podrzędnej (Child Relationship Name), zwykle w liczbie mnogiej:
SELECT Name, Industry,
(SELECT FirstName, LastName, Email FROM Contacts),
(SELECT Name, StageName, Amount FROM Opportunities WHERE IsClosed = false)
FROM Account
WHERE Name = 'Firma Testowa'
Dla relacji własnych do nazwy dochodzi __r. Relacja Master-Detail z obiektu Oferta ma nazwę Offers, więc w zapytaniu piszesz Offers__r:
SELECT Name, Price__c,
(SELECT Name, Amount__c FROM Offers__r ORDER BY Amount__c DESC LIMIT 3)
FROM Property__c
Podzapytanie może mieć własne WHERE, ORDER BY i LIMIT, tak jak w przykładzie powyżej: trzy najwyższe oferty dla każdej nieruchomości.
Gdzie znaleźć nazwę relacji? W Object Manager otwórz obiekt podrzędny, potem pole relacji. Nazwa jest w polu Child Relationship Name. Dla obiektów standardowych to zwykle liczba mnoga w nazwie API:
Contacts,Opportunities,Cases.
Przykład 3. Lista ofert przy każdej nieruchomości
SELECT Name,
(SELECT Name, Amount__c FROM Offers__r)
FROM Property__c
Co zwróci: wszystkie nieruchomości, a przy każdej listę jej ofert. Morska 21/3 nie ma ofert, ale i tak jest w wyniku, tylko z pustą listą. Podzapytanie nie usuwa rodziców.
Wynik (6 rekordów):
| Name | Offers__r (podzapytanie) |
|---|---|
| Lipowa 5/12, Gdańsk | OF-00001 (395 000), OF-00002 (430 000) |
| Kwiatowa 3, Sopot | OF-00003 (1 200 000) |
| Słoneczna 8/4, Gdańsk | OF-00004 (360 000), OF-00005 (375 000), OF-00006 (380 000) |
| Ogrodowa 15, Gdynia | OF-00007 (850 000) |
| Morska 21/3, Gdynia | (brak ofert) |
| Polna 2/7, Gdańsk | OF-00008 (505 000) |
Przykład 4. Najwyższa oferta: LIMIT w podzapytaniu
SELECT Name,
(SELECT Name, Amount__c
FROM Offers__r
ORDER BY Amount__c DESC
LIMIT 1)
FROM Property__c
Co zwróci: przy każdej nieruchomości tylko jej najwyższą ofertę. LIMIT 1 w podzapytaniu działa osobno dla każdego rodzica, a nie dla całego wyniku, dlatego Słoneczna 8/4 zachowuje jedną z trzech ofert, a pozostałe nieruchomości też mają swoją.
Wynik (6 rekordów):
| Name | Offers__r (podzapytanie) |
|---|---|
| Lipowa 5/12, Gdańsk | OF-00002 (430 000) |
| Kwiatowa 3, Sopot | OF-00003 (1 200 000) |
| Słoneczna 8/4, Gdańsk | OF-00006 (380 000) |
| Ogrodowa 15, Gdynia | OF-00007 (850 000) |
| Morska 21/3, Gdynia | (brak ofert) |
| Polna 2/7, Gdańsk | OF-00008 (505 000) |
Przykład 5. Kontakty i ich dostępne nieruchomości
SELECT Name,
(SELECT Name FROM Properties__r WHERE Status__c = 'Dostępna')
FROM Contact
Co zwróci: wszystkie kontakty, a przy każdym listę jego nieruchomości o statusie Dostępna. Pusta lista nie zawsze oznacza brak nieruchomości: Piotr Kowalski sprzedaje dwie, ale żadna nie jest dostępna. Ania Nowak i Jan Zieliński nie sprzedają niczego.
Wynik (5 rekordów):
| Name | Properties__r (podzapytanie) |
|---|---|
| Anna Nowak | Lipowa 5/12, Gdańsk |
| Piotr Kowalski | (brak nieruchomości) |
| Ewa Wiśniewska | Polna 2/7, Gdańsk |
| Ania Nowak | (brak nieruchomości) |
| Jan Zieliński | (brak nieruchomości) |
Filtrowanie po rekordach powiązanych
Pola rodzica możesz używać w warunkach tak samo jak własnych:
SELECT Name, Amount__c, Property__r.Name
FROM Offer__c
WHERE Property__r.Status__c = 'Dostępna'
AND Amount__c > 400000
Przykład 6. Filtr po polu rodzica
SELECT Name, Amount__c, Property__r.Name
FROM Offer__c
WHERE Property__r.Status__c IN ('Dostępna', 'Zarezerwowana')
AND Amount__c > 400000
Co zwróci: oferty powyżej 400 000 na nieruchomości dostępne albo zarezerwowane. OF-00001 odpada przez kwotę, a oferty na Słoneczną 8/4 przez status nieruchomości. Status nie jest polem oferty, a mimo to filtrujesz po nim przez kropkę.
Wynik (4 rekordy):
| Name | Amount__c | Property__r.Name |
|---|---|---|
| OF-00002 | 430 000 | Lipowa 5/12, Gdańsk |
| OF-00003 | 1 200 000 | Kwiatowa 3, Sopot |
| OF-00007 | 850 000 | Ogrodowa 15, Gdynia |
| OF-00008 | 505 000 | Polna 2/7, Gdańsk |
Gdy chcesz wybrać rodziców na podstawie dzieci, użyj semi-joina, czyli podzapytania w warunku IN. To zapytanie zwróci firmy, które mają przynajmniej jedną wygraną szansę sprzedaży:
SELECT Id, Name
FROM Account
WHERE Id IN (SELECT AccountId FROM Opportunity WHERE StageName = 'Closed Won')
NOT IN działa odwrotnie (tzw. anti-join), np. nieruchomości bez żadnej oferty:
SELECT Id, Name
FROM Property__c
WHERE Id NOT IN (SELECT Property__c FROM Offer__c)
Przykład 7. Semi-join: kontakty, które coś sprzedają
SELECT Name, Email
FROM Contact
WHERE Id IN (SELECT Seller__c FROM Property__c)
Co zwróci: kontakty wpisane jako sprzedający przy co najmniej jednej nieruchomości. Anna Nowak ma dwie nieruchomości, ale w wyniku pojawia się raz, bo semi-join sprawdza tylko, czy pasujący rekord istnieje. Ania Nowak ma ten sam e-mail, ale to osobny rekord i nie wskazuje na niego żadna nieruchomość.
Wynik (3 rekordy):
| Name | |
|---|---|
| Anna Nowak | anna.nowak@example.com |
| Piotr Kowalski | piotr.kowalski@example.com |
| Ewa Wiśniewska | ewa.wisniewska@example.com |
Przykład 8. Anti-join: nieruchomości bez ofert
SELECT Name, Status__c
FROM Property__c
WHERE Id NOT IN (SELECT Property__c FROM Offer__c)
Co zwróci: nieruchomości, do których nikt nie złożył oferty. W danych to tylko Morska 21/3, świeżo dodana ze statusem Nowa.
Wynik (1 rekord):
| Name | Status__c |
|---|---|
| Morska 21/3, Gdynia | Nowa |
Pola polimorficzne
Niektóre pola mogą wskazywać na różne obiekty. Pole Related To (WhatId) zadania wskazuje firmę, szansę sprzedaży albo inny obiekt, a Name (WhoId) kontakt lub potencjalnego klienta. Podstawowe pola takiego rodzica pobierzesz tak samo, przez kropkę:
SELECT Subject, ActivityDate, What.Name, What.Type, Who.Name
FROM Task
WHERE IsClosed = false
What.Type mówi, jakiego typu jest powiązany rekord. Pole Owner też jest polimorficzne (użytkownik lub kolejka), ale Owner.Name działa zawsze.
Do następnego przykładu potrzebujesz jeszcze kilku zadań (Task). Obiekty z warsztatu mają włączoną opcję Allow Activities, więc zadanie możesz powiązać zarówno z nieruchomością, jak i z ofertą.
| Subject | Powiązany rekord (WhatId) | Osoba (WhoId) | IsClosed |
|---|---|---|---|
| Prezentacja mieszkania | Lipowa 5/12, Gdańsk (Nieruchomość) | Anna Nowak | false |
| Odpowiedź na ofertę | OF-00007 (Oferta) | (puste) | false |
| Telefon w sprawie wyceny | (puste) | Ewa Wiśniewska | false |
| Podpisanie umowy | Słoneczna 8/4, Gdańsk (Nieruchomość) | Piotr Kowalski | true |
Pole IsClosed Salesforce ustawia samo na podstawie statusu zadania, np. status Completed daje true.
Przykład 9. Jedno pole, różne obiekty
SELECT Subject, What.Name, What.Type, Who.Name
FROM Task
WHERE IsClosed = false
Co zwróci: otwarte zadania z nazwą i typem powiązanego rekordu oraz osobą. What.Type zwraca nazwę API obiektu, więc od razu widać, że pierwsze zadanie dotyczy nieruchomości, a drugie oferty. Puste WhatId albo WhoId daje puste kolumny, bez błędu.
Wynik (3 rekordy):
| Subject | What.Name | What.Type | Who.Name |
|---|---|---|---|
| Prezentacja mieszkania | Lipowa 5/12, Gdańsk | Property__c | Anna Nowak |
| Odpowiedź na ofertę | OF-00007 | Offer__c | (puste) |
| Telefon w sprawie wyceny | (puste) | (puste) | Ewa Wiśniewska |
Ćwiczenie
- Pobierz oferty razem z nazwą, ceną i statusem nieruchomości, do której należą.
- Pobierz nieruchomości z imieniem, nazwiskiem i telefonem sprzedającego, tylko dla tych, które mają przypisanego sprzedającego.
- Dla każdego kontaktu-sprzedającego pobierz listę jego nieruchomości. Podpowiedź: nazwa relacji podrzędnej pola Sprzedający to
Properties. - Znajdź nieruchomości, które mają przynajmniej jedną ofertę powyżej 500 000.
Sprawdź się
1. Pole relacji na Ofercie nazywa się Property__c. Jak pobierzesz nazwę nieruchomości w zapytaniu na obiekcie Oferta?
Pokaż odpowiedź
Property__r.Name. Przy przejściu do rodzica końcówka __c zmienia się na __r.
2. Dlaczego zapytanie SELECT Name, (SELECT Name FROM Offer__c) FROM Property__c nie zadziała?
Pokaż odpowiedź
W podzapytaniu trzeba podać nazwę relacji podrzędnej, a nie nazwę obiektu. Poprawnie: (SELECT Name FROM Offers__r).
3. Jak wybrać firmy, które nie mają żadnego kontaktu?
Pokaż odpowiedź
Anti-joinem: SELECT Id, Name FROM Account WHERE Id NOT IN (SELECT AccountId FROM Contact).
Następny krok: policzysz i zsumujesz dane bez eksportu do Excela, za pomocą funkcji agregujących.