Answering the question is a walk through the structure — six hops, two of them
to arbitrary depth. There is no reconciliation step, because the reconciling was done
once, in the model.
01Start at a client group.
02Walk PART_OF* down to every organisation that buys under it — four names in the CRM, in this case, one of them acquired.
03Collect every engagement delivered to any of them.
04Collect the services those engagements delivered, and roll each up PART_OF* to its service line.
05Subtract from the catalogue. What remains is the gap — ranked by what comparable groups buy together.
06Walk back through the people who led those engagements to the contacts they worked with inside the group. That is the warmest route in.
The same six hops, written two ways. The difference is not syntax. It is that one of
them has to be maintained by a person who remembers why it looks like that.
Relational
Two recursive CTEs, five systems
WITH RECURSIVE grp AS (
SELECT org_id FROM crm_account
WHERE org_id = :client_group
UNION ALL
SELECT a.org_id FROM crm_account a
JOIN grp g ON a.parent_org_id = g.org_id
),
svc AS (
SELECT svc_id, parent_svc_id FROM service_cat
UNION ALL
SELECT s.svc_id, s.parent_svc_id FROM service_cat s
JOIN svc x ON s.parent_svc_id = x.svc_id
)
SELECT DISTINCT sl.line_name, p.person_name
FROM grp g
JOIN client_xref cx ON cx.crm_org_id = g.org_id
JOIN billing_matter m ON m.client_code = cx.billing_code
JOIN delivery_eng e ON e.matter_ref = m.matter_ref
JOIN svc s ON s.svc_id = e.service_id
JOIN service_line sl ON sl.line_id = s.root_line_id
JOIN eng_staffing st ON st.eng_id = e.eng_id
JOIN directory p ON p.person_id = st.person_id
WHERE ...
client_xref maps CRM accounts to billing codes. It is maintained by
hand. It is the four weeks — and the relationship question is not answered here
at all, because no table records who knows whom.
Graph
One pattern
MATCH (g:ClientGroup {name: $group})
<-[:PART_OF*]-(o:ClientOrg)
<-[:FOR]-(e:Engagement)
-[:DELIVERED]->(:Service)
-[:PART_OF*]->(line:ServiceLine)
MATCH (p:Person)-[:LED]->(e)
RETURN collect(DISTINCT line.name) AS bought,
collect(DISTINCT p.name) AS whoKnowsThem
No reconciliation, because identity was resolved once in the model. And the
relationship falls out of the same walk — it is an edge, not a separate project.