I Inqua
Sign in Get started
Relationships · Chapter 10 of 16

Child-to-parent queries with dot notation

Updated June 9, 2026 · 5 min read · by the Inqua team

The previous chapter showed how Salesforce objects link to each other through lookup fields. This chapter puts that to work in the direction you will use most: starting from a child record and reaching up to its parent. In SOQL there is no JOIN keyword for this — you just write a dot, and Salesforce walks the relationship for you.

The dot is the join

Every Contact has an AccountId lookup pointing at its Account. To pull a parent field into your result, write the relationship name, a dot, and the field:

SELECT LastName, Email, Account.Name
FROM Contact
WHERE Email != null

Each row comes back with the contact's own fields plus Account.Name resolved from the related record. If you are coming from a relational database, this one line replaces an explicit join. The equivalent SQL would be:

-- The SQL this replaces (for comparison only — this is not SOQL)
SELECT c.LastName, c.Email, a.Name
FROM Contact c
LEFT JOIN Account a ON a.Id = c.AccountId

Note two things: you never name the join condition, because the lookup field already defines it, and you never alias tables, because the path itself says where each field lives. The full comparison of the two languages is in SOQL vs SQL; the short version is that SOQL trades join flexibility for join safety — you can only traverse relationships that actually exist.

Going further up

Dots chain. A Case looks up to a Contact, the Contact looks up to an Account, and the Account has an Owner (a User). You can walk the whole path in one field reference:

SELECT Subject, Status,
       Contact.LastName,
       Contact.Account.Name,
       Contact.Account.Owner.Name
FROM Case
WHERE Status = 'New'

Each query can traverse up to five levels of parent in a single path, which covers almost anything you will need in practice. Every hop is read from the perspective of the object before it: Contact.Account.Owner.Name reads as "this case's contact's account's owner's name". When a path stops making sense in plain English, that is usually a sign you are traversing the wrong lookup.

Custom lookups: __c becomes __r

Custom relationships work exactly the same way, with one spelling rule. The lookup field on a custom object ends in __c, but when you traverse it, you swap the suffix for __r (r for relationship). Suppose a custom Invoice__c object has a Project__c lookup, and Project__c in turn has an Account__c lookup to the standard Account object:

SELECT Name, Amount__c,
       Project__r.Name,
       Project__r.Account__r.Name
FROM Invoice__c
WHERE Status__c = 'Unpaid'
You filter or select the raw Id withYou traverse with
AccountId (standard lookup)Account.Name
Project__c (custom lookup)Project__r.Name
Account__c on a custom objectAccount__r.Industry

Mixing the two suffixes is one of the most common beginner errors: Project__c.Name is invalid because Project__c is the Id field, not the relationship. If a traversal fails to compile, check the suffix first.

Not sure what the relationship name is? Ask the question in plain English instead — Inqua turns it into live SOQL against your own org and shows you the exact query behind every answer, correct __r suffixes and all. It is a fast way to learn the traversal paths in your schema.

Null lookups keep the row — until you filter on one

Lookups are usually optional. A Contact with no Account still comes back from the first query in this chapter — its Account.Name column is simply null. In SQL terms, selecting a parent field behaves like a LEFT JOIN: missing parents never remove child rows.

Filtering is different, and this is the gotcha to remember. Ask for a parent field to equal a real value and rows with an empty lookup are silently excluded — a missing account cannot have any industry, let alone Technology, so contacts without an account never appear here:

SELECT LastName, Account.Name
FROM Contact
WHERE Account.Industry = 'Technology'

For those contacts the filter quietly turns your left join into an inner join. If you want orphaned children included, say so explicitly: WHERE Account.Industry = 'Technology' OR AccountId = null. The one check a missing parent does satisfy is a null comparison: WHERE Account.Industry = null returns both contacts whose account has no industry and contacts with no account at all, because Salesforce treats the unreachable field as null. When a report "loses" rows after you add a parent filter, this is almost always why.

Filter and sort by parent fields

Parent paths are valid anywhere a field is — SELECT, WHERE, and ORDER BY. The same comparison operators you already know apply to parent fields too:

SELECT FirstName, LastName, Account.Name, Account.AnnualRevenue
FROM Contact
WHERE Account.AnnualRevenue > 1000000
ORDER BY Account.Name, LastName
LIMIT 50

This sorts contacts by their account's name first, then by last name within each account — grouping a flat result set by parent without any extra clauses. Just keep the previous section in mind: the revenue filter silently drops contacts whose lookup is empty. Sorting, by contrast, keeps them — a missing account simply sorts as null, which SOQL places first by default.

Polymorphic parents (an advanced aside)

Most lookups point at exactly one object. A few standard fields are polymorphic: the WhoId on Task and Event can point at either a Contact or a Lead, and WhatId can point at an Account, an Opportunity, and others. Traversing Who only gives you the fields the possible parents share, such as Who.Name and Who.Type.

To reach type-specific fields, SOQL has a dedicated TYPEOF clause that branches on what the parent actually is:

SELECT Subject,
       TYPEOF Who
           WHEN Contact THEN AccountId, Email
           WHEN Lead THEN Company, Status
           ELSE Name
       END
FROM Task
WHERE ActivityDate = THIS_WEEK

Each row returns the branch matching its parent's type. This is genuinely advanced — most day-to-day queries never need it — so file it away and move on.

Where to next

You can now reach up from any child to its parents. The next chapter goes the other direction — one parent, many children — using subqueries, which behave quite differently from the dot notation you just learned.

View all 16 chapters

Practice without memorizing

Inqua turns plain-English questions into live SOQL and shows the exact query behind every answer — ask, inspect the generated SOQL, and the syntax in this tutorial will start to stick. Read-only, OAuth-only, free during early access.

The demo runs on sample data — no Salesforce org required.