Найти все родительские записи, где все дочерние записи имеют заданное значение (но не только некоторые дочерние записи)

Событие имеет много участников. Участник имеет поле "статус".

class Event < ActiveRecord::Base
  has_many :participants
end

class Participant < ActiveRecord::Base
  belongs_to :event
end

Мне нужно найти все события, кроме следующих: события, где каждый из участников имеет статус "присутствует".

Я могу найти все события, где некоторые из его участников имеют статус "присутствует" со следующим кодом AR:

Event.joins(:participants).where
 .not(participants: {status: 'present'})
  .select("events.id, count(*)")
   .group("participants.event_id")
    .having("count(*) > 0")

Это создает SQL как:

SELECT events.id, participants.status as status, count(*) 
FROM `events` INNER JOIN `participants` 
ON `participants`.`event_id` = `events`.`id` 
WHERE (`participants`.`status` != 'present') 
GROUP BY participants.event_id HAVING count(*) > 0

Это почти работает. Проблема в том, что если одна из строк участника (в рамках @participant.event_id) имеет статус чего-то другого, например, "прочь", событие все равно будет извлечено, потому что, по крайней мере, некоторые из записей одного уровня имеют статус, равный чему-то другому, нежели "присутствует".

Мне нужно убедиться, что я отфильтровываю каждую запись события со всеми участниками со статусом "настоящее".

Я открыт для ActiveRecord или решений SQL.

5 ответов

Решение

Если я правильно понимаю, ваша проблема может быть классифицирована как реляционное разделение. Есть два основных подхода к этому:

1a) Forall x: p(x)

который в SQL должен быть переведен на:

1b) НЕ существует x: НЕ p(x)

Для вашей проблемы это будет что-то вроде:

SELECT e.* 
FROM events e
WHERE NOT EXISTS (
    SELECT 1 
    FROM PARTICIPANTS p
    WHERE p.status <> 'present'
      AND p.event_id = e.event_id
)

то есть любое данное событие, где не существует участника с таким статусом!= "присутствует"

Другой основной способ сделать это состоит в том, чтобы сравнить количество участников с количеством участников с имеющимся статусом

SELECT e.id 
FROM events e
JOIN participants p 
    ON p.event_id = e.id 
GROUP BY e.event_id 
HAVING count(*) = count( CASE WHEN p.status = 'present' then 1 end )

Оба решения не проверены, поэтому там могут быть ошибки, но это должно дать вам начало

Мне очень нравятся примеры Леннарта

Я сделал простую модификацию первого примера, которая будет возвращать только родительские записи СОБЫТИЯ, которые имеют записи об участии дочерних элементов, и намного быстрее обрабатывает, чем находит количество для каждой.

SELECT e.* 
FROM events e
INNER JOIN participants p ON p.event_id = e.event_id
WHERE NOT EXISTS (
  SELECT 1 
  FROM PARTICIPANTS p
  WHERE p.status <> 'present'
  AND p.event_id = e.event_id
)
GROUP BY e.event_id

Что если вы попытаетесь прийти к запросу, найдя идентификаторы событий, в которых у человека есть статус, отличный от "настоящего", а затем найдете все уникальные события, где это имеет место?

unique_event_ids = Participant.where.not(status: "present").pluck(:event_id).uniq
events_you_want = Event.where(unique_event_ids)

Мне нужно решение для этого, и другие ответы не сработали для меня, но вот мое решение. Я написал две функции: одну для получения общего количества дочерних записей, а другую для получения общего количества дочерних записей, которые удовлетворяют определенному условию в моем случае (true). Затем я сравнил обе функции. Если полученная арифметика / оценка равна нулю, это означает, что все записи соответствуют истинным критериям. Довольно просто.

Select p.pid, p.Name, p.Group, udfn_TotalChildrenRecords(p.pid), udfn_TotalChildrenRecordsThatAreTrue(p.pid) 
From Parent AS p INNER JOIN Child AS c ON Parent.pid = child.pid
GROUP BY p.pid, p.Name, p.Group
HAVING udfn_TotalChildrenRecords(p.pid) - udfn_TotalChildrenRecordsThatAreTrue(p.pid) = 0

Вы можете использовать подвыбор, чтобы отфильтровать события, в которых есть участники, которых нет. Хотя, возможно, это не самый эффективный способ сделать это.

SELECT events.id, participants.status as status, count(*) 
FROM `events` INNER JOIN `participants` 
ON `participants`.`event_id` = `events`.`id` 
WHERE (`participants`.`status` != 'present')
AND events.id NOT IN (SELECT DISTINCT event_id FROM participants WHERE participants.status != 'present')
GROUP BY participants.event_id HAVING count(*) > 0
Другие вопросы по тегам