SQL Server multiple joins on same table

You are joining an association's users and companies. With 2 companies and 3 users you get six rows. Then you count count(comp.id) and count(u.id). Neither ID is null in the joined rows, so their results equal count(*) which is 6.

When dealing with multiple aggregations it is advisable to aggregate your data before joining. After all you want to join the number of companies and the number of users to your associations. These numbers are aggregation results.

As to employees this even gets a little more complicated, because employees can be related to an association either directly or by company. So with 5 employees linked by company and five employees linked by association, you may get, say, 8 total employees, in case two work for the company and are directly linked to the association, too. This may best be solved in a lateral outer join aka OUTER APPLY in SQL Server.

Well, coming from some few particular associations, you can just as well make all outer joins lateral.

select 
  row_number() over(order by (select 1)) as itemno,
  a*, c.*, e.*, u.*
from associations a
outer apply
(
  select count(*) as total_companies
  from companies
  where companies.assoc_id = a.id
) c
outer apply
(
  select
    count(*) as total_employees,
    count(case when employees.assoc_id = a.id then 1 end) as association_employees,
    count(case when employees.comp_id = c.id then 1 end) as company_employees
  from employees
  where employees.assoc_id = a.id
     or employees.comp_id = c.id
) e
outer apply
(
  select count(*) as total_users
  from users
  where users.assoc_id = a.id
) u
where a.title like ('%' + @search + '%') and a.status = @status
order by a.title;

This is much better than joining all single rows and then counting distinct IDs, because if you do that, you create a possibly huge intermediate result the DBMS must sort and look through to get the distinct counts. That can be very costly.


When you start joining to tables down multiple paths of the relationship graph (such as associations -> companies -> employees, associations -> employees, and associations -> users) you start getting a lot of duplicate records that are next to impossible to sort out.

What I believe you need are some correlated subqueries that can independently inspect records down each path to calculate the results.

The following is one approach.

SELECT 
    ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS itemNo,
    ac.title,
    ac.id,
    ac.status,
    (
        SELECT count(comp.id)
        FROM companies comp
        WHERE comp.assoc_id = ac.id
    ) as total_comps,
    (
        SELECT count(comp_emps.id)
        FROM companies comp
        JOIN employees comp_emps
            ON comp_emps.comp_id  = comp.id
        WHERE comp.assoc_id = ac.id
    ) as total_comp_emps,
    (
        SELECT count(assoc_emps.id)
        JOIN employees assoc_emps
        WHERE assoc_emps.assoc_id = ac.id 
    )   as total_assoc_emps,
    --SUM(total_comp_emps,total_assoc_emps) total_comp_assoc_emps,
    --SUM((COALESCE(comp_emps.id,0))+(COALESCE(assoc_emps.id,0))) total_comp_assoc_emps,
    --(total_comp_emps + total_assoc_emps) as total_comp_assoc_emps,
    (
        SELECT count(u.id)
        FROM users u
        WHERE u.assoc_id = ac.id
    ) as total_users

FROM associations ac
where ac.title like ('%' + @search + '%') and ac.status = @status
order by ac.title asc

You can also move intermediate calculations into a CROSS APPLY and then reference those intermediate results in your final select. This can be useful if you need to reuse certain results.

SELECT 
    ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS itemNo,
    ac.title,
    ac.id,
    ac.status,
    counts.total_comps,
    counts.total_comp_emps,
    counts.total_assoc_emps,
    --SUM(total_comp_emps,total_assoc_emps) total_comp_assoc_emps,
    --SUM((COALESCE(comp_emps.id,0))+(COALESCE(assoc_emps.id,0))) total_comp_assoc_emps,
    (counts.total_comp_emps + counts.total_assoc_emps) as total_comp_assoc_emps,
    counts.total_users

FROM associations ac
CROSS APPLY (
    SELECT
        (
            SELECT count(comp.id)
            FROM companies comp
            WHERE comp.assoc_id = ac.id
        ) as total_comps,
        (
            SELECT count(comp_emps.id)
            FROM companies comp
            JOIN employees comp_emps
                ON comp_emps.comp_id  = comp.id
            WHERE comp.assoc_id = ac.id
        ) as total_comp_emps,
        (
            SELECT count(assoc_emps.id)
            FROM employees assoc_emps
            WHERE assoc_emps.assoc_id = ac.id 
        )   as total_assoc_emps,
        (
            SELECT count(u.id)
            FROM users u
            WHERE u.assoc_id = ac.id
        ) as total_users
) counts
where ac.title like ('%' + @search + '%') and ac.status = @status
order by ac.title asc

I'm not clear on your intent for total_comp_assoc_emps, but the above is just add two intermediate values. If employees can belong to associations both directly and via companies, and you do not want to double count them, the total_comp_assoc_emps calculation is a bit more involved.

The following gathers employ IDs from both sources and counts each distinct ID once.

(
    SELECT count(distinct id)
    FROM (
            SELECT comp_emps.id
            FROM companies comp
            JOIN employees comp_emps
                ON comp_emps.comp_id  = comp.id
            WHERE comp.assoc_id = ac.id
        UNION
            SELECT assoc_emps.id
            FROM employees assoc_emps
            WHERE assoc_emps.assoc_id = ac.id 
    ) emp_ids
) as total_assoc_emps,