Улучшение в GROUP BY в SQL

Сценарий

У нас много людей, эти люди отправляются в путешествие с несколькими этапами/состояниями (первоначально планировалось, затем начиналось, потом возвращалось или становилось катастрофой).

У меня есть запрос, который дает правильные результаты, вы можете видеть его и играть с ним здесь:

http://sqlfiddle.com/#!15/2e096/1

Однако мне интересно, есть ли более эффективная реализация, в частности, избегая использования GROUP BY и postgres 'bool_and, потенциально также избегая вложенного запроса.

Что мы хотим знать

Кто никогда не испытывал поездки, от которой они не возвращались безопасно?

Или, по-другому:

Кто имеет: 1. Never planned or gone on a trip ИЛИ 2. only ever returned safely

Разъяснения

  • Если есть запись для человека в таблице поездок, но нет этапов, они планируют поездку.

Выход

Должно быть по крайней мере все столбцы из таблицы person, если выходят и другие столбцы, это прекрасно.

Настройка/воспроизведение

CREATE TABLE people (person_name text, gender text, age integer);
INSERT INTO people (person_name, gender, age)
  VALUES ('pete', 'm', 10), ('alan', 'm', 22), ('jess', 'f', 24), ('agnes', 'f', 25), ('matt', 'm', 26);

CREATE TABLE trips (person_name text, trip_name text);
INSERT INTO trips (person_name, trip_name)
  VALUES ('pete', 'a'),
         ('pete', 'b'),
         ('alan', 'c'),
         ('alan', 'd'),
         ('jess', 'e'),
         ('matt', 'f');

CREATE TABLE trip_stages (trip_name text, stage text, most_recent boolean);
INSERT INTO trip_stages
  VALUES ('a', 'started', 'f'), ('a', 'disaster', 't'),
         ('b', 'started', 't'),
         ('c', 'started', 'f'), ('c', 'safe_return', 't'),
         ('e', 'started', 'f'), ('e', 'safe_return', 't');

Краткое описание ситуации

  • У Пита одна поездка, которая закончилась катастрофой, и он только что начал
  • У Алана есть одна поездка, которую он вернул из безопасности, и тот, который он планирует
  • Джесс была в одной поездке, которую она благополучно вернулась из
  • Агнес никогда даже не планировала поездку
  • Мэтт запланировал поездку, но еще не начал ее

Решение

 person_name | gender | age
-------------+--------+-----
 jess        | f      | 24
 agnes       | f      | 25
  • Джесс (была в одной поездке, с которой она благополучно вернулась)
  • Агнес (никогда не планировал поездку)

Рабочий запрос

SELECT people.* FROM people WHERE people.person_name IN (
  SELECT people.person_name FROM people
  LEFT OUTER JOIN trips
    ON trips.person_name = people.person_name
  LEFT OUTER JOIN trip_stages
    ON trip_stages.trip_name = trips.trip_name AND trip_stages.most_recent = 't'
  GROUP BY people.person_name
    HAVING bool_and(trips.trip_name IS NULL)
      OR bool_and(trip_stages.stage IS NOT NULL AND trip_stages.stage = 'safe_return')
)

Объяснение

SELECT people.* FROM people WHERE people.person_name IN (
  -- All the people
  SELECT people.person_name FROM people

  -- + All their trips
  LEFT OUTER JOIN trips
    ON trips.person_name = people.person_name

  -- + All those trips' stages
  LEFT OUTER JOIN trip_stages
    ON trip_stages.trip_name = trips.trip_name AND trip_stages.most_recent = 't'

  -- Group by person
  GROUP BY people.person_name
    -- Filter to those rows where either:
    --   1. trip_name is always NULL (they've made no trips)
    --   2. Every trip has been ended with a safe return
    HAVING bool_and(trips.trip_name IS NULL)
      OR bool_and(trip_stages.stage IS NOT NULL AND trip_stages.stage = 'safe_return')
)

Вопрос

Есть ли другой способ написать этот запрос? Без использования GROUP BY и bool_and и в идеале без использования подзапросов тоже? Возможно, какая-либо функция раздела/окна?

Я использую это, чтобы узнать, поэтому объяснения/анализ запросов оцениваются!

Меня особенно интересуют последствия для производительности. например Что произойдет, если люди совершают тысячи поездок? Выбирают ли подзапросы какой-то другой подход?

Ответ 1

SELECT p0.person_name FROM people p0
WHERE p0.person_name NOT IN (
 SELECT p.person_name FROM people p
 INNER JOIN trips t on p.person_name = t.person_name
 LEFT JOIN trip_stages s on t.trip_name = s.trip_name AND s.most_recent
 WHERE s.stage IS NULL OR s.stage != 'safe_return' );

FIDDLE

Намного легче получить, кто не подходит и использует NOT IN.

РЕДАКТИРОВАТЬ: С пониманием, что я не могу быть столь кратким в прозе, как я в коде, расширенное объяснение по предложению IMSoP:

SELECT p0.person_name FROM people p0
-- The outer query exists to reverse the results of the inner query. The inner query
-- returns person names which have not arrived safely, the outer query returns the names,
-- via the NOT IN operator, which don't result from the inner query.
WHERE p0.person_name NOT IN (
 SELECT p.person_name FROM people p
-- Selecting from the same table via a different alias (p vs p0) is useful for avoiding
-- ambiguity.
 INNER JOIN trips t on p.person_name = t.person_name
-- The INNER JOIN returns results only where a value in people.person_name matches the
-- trips.person_name. This has the effect of removing any person_names from the inner
-- query who haven't taken any trips.
 LEFT JOIN trip_stages s on t.trip_name = s.trip_name AND s.most_recent
-- The LEFT JOIN links any rows created from the previous INNER JOIN to the trip_stages
-- table where trips. The terms of the LEFT JOIN restrict the matches the rows where the
-- most_recent column is true. Unlike the INNER JOIN, the LEFT JOIN does not eliminate
-- rows where there is no match. Where there is no match, the columns from the left side
-- of the join are still populated, those from the right side of the join are NULL.
 WHERE s.stage IS NULL OR s.stage != 'safe_return'
-- s.stage IS NULL indicates that, via the LEFT JOIN above, a trip was planned but not
-- begun. As we are specifying that the trip stage we are looking at is the last one
-- recorded, any value other than safe_return indicates that the row we are looking at
-- does not meet the conditions set by OP, and is thus to be included for elimination by
-- the outer query.
);

Ответ 2

SELECT distinct trips.person_name 
  FROM trips 
RIGHT JOIN trip_stages 
  ON trips.trip_name = trip_stages.trip_name 
WHERE trip_stages.most_recent = 't' 
  GROUP BY trips.person_name, trip_stages.stage 
  HAVING trip_stages.stage is not null 
  AND trip_stages.stage = 'safe_return'

Извините за внесенные изменения, но заработал, чтобы получить здесь только имена людей.

Ответ 3

Вы можете использовать не not exists, чтобы выбрать всех людей, у которых нет хотя бы одной поездки, которая не закончилась безопасным возвратом (что подразумевает, что они либо не ездили, либо не возвращались безопасно из всех своих поездок), и не имеют хотя бы одно запланированное путешествие, которое не находится на этапе

select * from people p
where not exists (
    select 1 from trips t
    left join trip_stages ts on ts.trip_name = t.trip_name
    where ((ts.stage <> 'safe_return' -- did not end in safe return
      and ts.most_recent = 't') 
      or ts.trip_name is null) -- or does not have a trip stage
    and t.person_name = p.person_name
)

http://sqlfiddle.com/#!15/3416a/18

Ответ 4

По сути, вы хотите, чтобы список всех лиц, для которых количество поездок, которые они совершали (или которые они планируют), равно количеству поездок, которые они безопасно вернули. Для этого мы можем использовать простой GROUP BY .. HAVING, который сравнивает оба числа:

   select p.person_name 
    from people p
    left join trips t on p.person_name = t.person_name
    left join trip_stages ts on t.trip_name = ts.trip_name
      and ts.most_recent = 't'
    group by p.person_name
    having count(t.trip_name) = 
        count(case when ts.stage = 'safe_return' then 1 else null end)

Это

  • вычисляет количество поездок, сделанных человеком count(t.trip_name)
  • вычисляет количество поездок, которые человек благополучно вернул из count(case...)
  • сравнивает оба числа и возвращает только лиц, для которых они равны.