A join combines rows when its condition matches, so result cardinality comes from the relationship—not from the number of source tables. Correct joins state every ownership key, preserve missing rows intentionally, and distinguish “return matching details” from “test whether a match exists.” Duplicate-looking rows are usually a modeling or cardinality clue, not a reason for reflexive DISTINCT.
Imagine a join as nested matching: for each left row, find every right row satisfying ON. Zero matches disappear in an inner join or become one null-extended row in a left join. Multiple matches multiply rows.
One ticket with three messages becomes three result rows. That is correct for message detail but wrong if the consumer expects one row per ticket.
Return tickets that have at least one customer message without multiplying them:
Use NOT EXISTS for tickets with no messages. It handles nulls more predictably than many NOT IN constructions.
The lateral subquery can reference each ticket. Window functions offer another solution; compare plans and clarity.
A careless join can duplicate chunks, messages, or evaluation cases, over-weighting evidence in a prompt and distorting metrics. A join missing tenant keys can leak data even when both individual tables include tenant IDs. Retrieval context construction must verify cardinality, ownership, and deduplication before token assembly.
Seed one ticket with zero messages, one with one, and one with three. Before each query, write expected row count. Verify inner join, left join, EXISTS, NOT EXISTS, and lateral-last-message results. If DISTINCT changes the answer, explain the multiplication you hid.
Return one row per ticket with customer name, message count, and latest message body, including tickets with zero messages.
Acceptance criterion: the result has exactly one row per ticket, a zero count for empty threads, deterministic latest-message selection, and tenant keys in every relationship.