mysql множественное левое объединение и группировка по главной таблице - PullRequest
0 голосов
/ 22 января 2020

У меня есть следующий сценарий. У области есть несколько территорий, у территории есть несколько адресов, и адрес посещается несколько раз в месяц. Теперь я хочу создать ежемесячный отчет о местности. (Сколько раз была посещена область). Я написал запрос, но набор результатов дает меньше областей, потому что некоторые адреса не посещаются. У меня есть следующая структура

таблицы

области: id|name (180 строк) // имя уникально

территории: id|name|area_id (1 тыс. Строк)

адреса: id|name|territory_id (80 тыс. Строк)

посещение_адресов: id|address_id|date|status (1М + строки) // status => 1 = посещено, 2 = ожидает

Мой запрос следующий.

select ar.id as area_id, ar.name as area,
sum(case when va.status = 1 then 1 else 0 end) as visited,
sum(case when va.status = 2 then 1 else 0 end) as pending,
count(va.id) as total

from      areas ar
left join territories t on t.area_id=ar.id
left join addresses a on a.territory_id=t.id
left join visiting_addresses va on va.address_id=a.id
where     month(va.date) = '01'
and       year(va.date)='2020'
group by  ar.id

таблица областей содержит 180 областей, но набор результатов показывает только 144 области. Где моя ошибка и чем это объясняется? эти районы отсутствуют, потому что они не посещают.

Ответы [ 2 ]

1 голос
/ 22 января 2020

Предложение WHERE преобразует ЛЕВОЕ СОЕДИНЕНИЕ с visiting_addresses во ВНУТРЕННЕЕ СОЕДИНЕНИЕ. И поскольку это самая правая таблица в цепочке LEFT-JOIN, все объединения будут преобразованы в INNER JOINS. Чтобы предотвратить это, вы должны переместить соответствующие условия из предложения WHERE в предложение ON:

select ar.id as area_id, ar.name as area,
sum(case when va.status = 1 then 1 else 0 end) as visited,
sum(case when va.status = 2 then 1 else 0 end) as pending,
count(va.id) as total

from      areas ar
left join territories t on t.area_id=ar.id
left join addresses a on a.territory_id=t.id
left join visiting_addresses va
  on  va.address_id=a.id
  and month(va.date) = '01'
  and year(va.date)='2020'

group by  ar.id

Но поскольку у вас много строк, я бы предпочел выполнить два запроса. Сначала получите только области с адресами из прошлого месяца, используя внутренние соединения. Однако вы должны изменить свои условия на va.date, чтобы использовать индекс:

select ar.id as area_id, ar.name as area,
sum(case when va.status = 1 then 1 else 0 end) as visited,
sum(case when va.status = 2 then 1 else 0 end) as pending,
count(va.id) as total

from areas ar
join territories t on t.area_id=ar.id
join addresses a on a.territory_id=t.id
join visiting_addresses va on  va.address_id=a.id
where va.date >= '2020-01-01'
  and va.date <  '2020-02-01'

group by  ar.id

Убедитесь, что у вас есть индекс на visiting_addresses(date) или даже лучше на visiting_addresses(date, address_id, status).

Тогда получите все области с простым

select ar.id as area_id, ar.name as area 
from areas ar

и добавьте пропущенные области к первому результату при установке visited, pending и total на ноль (в коде приложения).

INNER JOIN должен быть намного быстрее, потому что теперь движок может начать читать только необходимые строки из visiting_addresses, используя индекс для условий WHERE.

Вы также можете использовать более сложный, но одиночный запрос. Идея состоит в том, чтобы использовать LEFT JOIN с предварительно агрегированным подзапросом:

select ar.id as area_id, ar.name as area,
    coalesce(visited, 0) as visited,
    coalesce(pending, 0) as pending,
    coalesce(total, 0) as total
from areas ar
left join (
    select t.area_id
    sum(case when va.status = 1 then 1 else 0 end) as visited,
    sum(case when va.status = 2 then 1 else 0 end) as pending,
    count(va.id) as total
    from territories t
    join addresses a on a.territory_id=t.id
    join visiting_addresses va on  va.address_id=a.id
    where va.date >= '2020-01-01'
      and va.date <  '2020-02-01'
    group by t.area_id
) x on x.area_id = ar.id
0 голосов
/ 22 января 2020

Попробуйте переместить логи c в предложении WHERE в предложение ON соответствующего объединения:

SELECT
    ar.id AS area_id,
    ar.name AS area,
    COUNT(CASE WHEN va.status = 1 THEN 1 END) AS visited,
    COUNT(CASE WHEN va.status = 2 THEN 1 END) AS pending,
    COUNT(va.id) AS total
FROM areas ar
LEFT JOIN territories t ON t.area_id = ar.id
LEFT JOIN addresses a ON a.territory_id = t.id
LEFT JOIN visiting_addresses va ON va.address_id = a.id AND
    va.date >= '2020-01-01' AND va.date <  '2020-02-01'
GROUP BY
    ar.id;

Обратите внимание, что при выборе поля name при агрегировании только по id допустимо в MySQL, при условии, что id является уникальным полем в таблице areas.

Вы также можете попробовать добавить следующий индекс в таблицу visiting_addresses:

CREATE INDEX date_idx ON visiting_addresses (address_id, date, status);

Это может помочь ускорить соединение с этой таблицей.

...