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.

Lekcja 4 z 712 min czytania

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):

FirstNameLastNameEmail
AnnaNowakanna.nowak@example.com
PiotrKowalskipiotr.kowalski@example.com
EwaWiśniewskaewa.wisniewska@example.com
AniaNowakanna.nowak@example.com
JanZieliń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:

NamePrice__cArea_m2__cStatus__cSprzedający (Seller__c)
Lipowa 5/12, Gdańsk450 00050DostępnaAnna Nowak
Kwiatowa 3, Sopot1 250 000140ZarezerwowanaAnna Nowak
Słoneczna 8/4, Gdańsk380 00036SprzedanaPiotr Kowalski
Ogrodowa 15, Gdynia890 000120Dostępna(puste)
Morska 21/3, Gdynia640 00062NowaPiotr Kowalski
Polna 2/7, Gdańsk520 00055DostępnaEwa Wiśniewska

Pozostałe pola tych samych nieruchomości:

NameAmenities__cIs_Furnished__cAvailable_From__c
Lipowa 5/12, GdańskBalkon;Windatrue2026-11-01
Kwiatowa 3, SopotMiejsce parkingowe;Ogród;Piwnicafalse(puste)
Słoneczna 8/4, GdańskBalkontrue2026-03-01
Ogrodowa 15, GdyniaMiejsce parkingowe;Ogródfalse2026-10-01
Morska 21/3, GdyniaWinda;Miejsce parkingowetrue2026-12-15
Polna 2/7, GdańskPiwnicafalse2026-10-20

Obiekt Oferta (Offer__c), powiązany z nieruchomością relacją Master-Detail:

NameAmount__cNieruchomość (Property__c)
OF-00001395 000Lipowa 5/12, Gdańsk
OF-00002430 000Lipowa 5/12, Gdańsk
OF-000031 200 000Kwiatowa 3, Sopot
OF-00004360 000Słoneczna 8/4, Gdańsk
OF-00005375 000Słoneczna 8/4, Gdańsk
OF-00006380 000Słoneczna 8/4, Gdańsk
OF-00007850 000Ogrodowa 15, Gdynia
OF-00008505 000Polna 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. Bez ORDER BY Salesforce 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

KierunekPrzykładJak piszesz
Od dziecka do rodzicakontakt → jego firmakropka: Account.Name
Od rodzica do dziecifirma → jej kontaktypodzapytanie: (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ę __c zamieniasz na __r: pole Seller__c → relacja Seller__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):

NameSeller__r.NameSeller__r.Email
Lipowa 5/12, GdańskAnna Nowakanna.nowak@example.com
Kwiatowa 3, SopotAnna Nowakanna.nowak@example.com
Słoneczna 8/4, GdańskPiotr Kowalskipiotr.kowalski@example.com
Ogrodowa 15, Gdynia(puste)(puste)
Morska 21/3, GdyniaPiotr Kowalskipiotr.kowalski@example.com
Polna 2/7, GdańskEwa Wiśniewskaewa.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):

NameAmount__cProperty__r.NameProperty__r.Seller__r.LastName
OF-000031 200 000Kwiatowa 3, SopotNowak
OF-00007850 000Ogrodowa 15, Gdynia(puste)
OF-00008505 000Polna 2/7, GdańskWiś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):

NameOffers__r (podzapytanie)
Lipowa 5/12, GdańskOF-00001 (395 000), OF-00002 (430 000)
Kwiatowa 3, SopotOF-00003 (1 200 000)
Słoneczna 8/4, GdańskOF-00004 (360 000), OF-00005 (375 000), OF-00006 (380 000)
Ogrodowa 15, GdyniaOF-00007 (850 000)
Morska 21/3, Gdynia(brak ofert)
Polna 2/7, GdańskOF-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):

NameOffers__r (podzapytanie)
Lipowa 5/12, GdańskOF-00002 (430 000)
Kwiatowa 3, SopotOF-00003 (1 200 000)
Słoneczna 8/4, GdańskOF-00006 (380 000)
Ogrodowa 15, GdyniaOF-00007 (850 000)
Morska 21/3, Gdynia(brak ofert)
Polna 2/7, GdańskOF-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):

NameProperties__r (podzapytanie)
Anna NowakLipowa 5/12, Gdańsk
Piotr Kowalski(brak nieruchomości)
Ewa WiśniewskaPolna 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):

NameAmount__cProperty__r.Name
OF-00002430 000Lipowa 5/12, Gdańsk
OF-000031 200 000Kwiatowa 3, Sopot
OF-00007850 000Ogrodowa 15, Gdynia
OF-00008505 000Polna 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):

NameEmail
Anna Nowakanna.nowak@example.com
Piotr Kowalskipiotr.kowalski@example.com
Ewa Wiśniewskaewa.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):

NameStatus__c
Morska 21/3, GdyniaNowa

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ą.

SubjectPowiązany rekord (WhatId)Osoba (WhoId)IsClosed
Prezentacja mieszkaniaLipowa 5/12, Gdańsk (Nieruchomość)Anna Nowakfalse
Odpowiedź na ofertęOF-00007 (Oferta)(puste)false
Telefon w sprawie wyceny(puste)Ewa Wiśniewskafalse
Podpisanie umowySłoneczna 8/4, Gdańsk (Nieruchomość)Piotr Kowalskitrue

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):

SubjectWhat.NameWhat.TypeWho.Name
Prezentacja mieszkaniaLipowa 5/12, GdańskProperty__cAnna Nowak
Odpowiedź na ofertęOF-00007Offer__c(puste)
Telefon w sprawie wyceny(puste)(puste)Ewa Wiśniewska

Ćwiczenie

  1. Pobierz oferty razem z nazwą, ceną i statusem nieruchomości, do której należą.
  2. Pobierz nieruchomości z imieniem, nazwiskiem i telefonem sprzedającego, tylko dla tych, które mają przypisanego sprzedającego.
  3. Dla każdego kontaktu-sprzedającego pobierz listę jego nieruchomości. Podpowiedź: nazwa relacji podrzędnej pola Sprzedający to Properties.
  4. 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.