GROUP BY и агрегировать последовательные числовые значения - PullRequest
12 голосов
/ 04 ноября 2011

Использование PostgreSQL 9.0.

Допустим, у меня есть таблица, содержащая поля: company, profession и year. Я хочу вернуть результат, который содержит уникальные компании и профессии, но агрегирует (в массив хорошо) годы на основе числовой последовательности:

Пример таблицы:

+-----------------------------+
| company | profession | year |
+---------+------------+------+
| Google  | Programmer | 2000 |
| Google  | Sales      | 2000 |
| Google  | Sales      | 2001 |
| Google  | Sales      | 2002 |
| Google  | Sales      | 2004 |
| Mozilla | Sales      | 2002 |
+-----------------------------+

Меня интересует запрос, который будет выводить строки, подобные следующим:

+-----------------------------------------+
| company | profession | year             |
+---------+------------+------------------+
| Google  | Programmer | [2000]           |
| Google  | Sales      | [2000,2001,2002] |
| Google  | Sales      | [2004]           |
| Mozilla | Sales      | [2002]           |
+-----------------------------------------+

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

Ответы [ 3 ]

19 голосов
/ 05 ноября 2011

Определение непоследовательных значений всегда немного сложно и включает в себя несколько вложенных подзапросов (по крайней мере, я не могу придумать лучшего решения).

Первым шагом является определение непоследовательных значений для года:

Шаг 1) Определить непоследовательные значения

select company, 
       profession,
       year,
       case 
          when row_number() over (partition by company, profession order by year) = 1 or 
               year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
          else 0
       end as group_cnt
from qualification

Возвращает следующий результат:

 company | profession | year | group_cnt
---------+------------+------+-----------
 Google  | Programmer | 2000 |         1
 Google  | Sales      | 2000 |         1
 Google  | Sales      | 2001 |         0
 Google  | Sales      | 2002 |         0
 Google  | Sales      | 2004 |         1
 Mozilla | Sales      | 2002 |         1

Теперь с помощью значения group_cnt мы можем создать «идентификаторы группы» для каждой группы, у которой есть последовательные годы:

Шаг 2) Определить идентификаторы группы

select company,
   profession,
   year,
   sum(group_cnt) over (order by company, profession, year) as group_nr
from ( 
select company, 
       profession,
       year,
       case 
          when row_number() over (partition by company, profession order by year) = 1 or 
               year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
          else 0
       end as group_cnt
from qualification
) t1

Возвращает следующий результат:

 company | profession | year | group_nr
---------+------------+------+----------
 Google  | Programmer | 2000 |        1
 Google  | Sales      | 2000 |        2
 Google  | Sales      | 2001 |        2
 Google  | Sales      | 2002 |        2
 Google  | Sales      | 2004 |        3
 Mozilla | Sales      | 2002 |        4
(6 rows)

Как вы можете видеть, каждая "группа" получила свой собственный group_nr, и это мы, наконец, можем использовать для агрегирования, добавив еще одну производную таблицу:

Шаг 3) Окончательный запрос

select company,
       profession,
       array_agg(year) as years
from (
  select company,
       profession,
       year,
       sum(group_cnt) over (order by company, profession, year) as group_nr
  from ( 
    select company, 
           profession,
           year,
           case 
              when row_number() over (partition by company, profession order by year) = 1 or 
                   year - lag(year,1,year) over (partition by company, profession order by year) > 1 then 1
              else 0
           end as group_cnt
    from qualification
  ) t1
) t2
group by company, profession, group_nr
order by company, profession, group_nr

Возвращает следующий результат:

 company | profession |      years
---------+------------+------------------
 Google  | Programmer | {2000}
 Google  | Sales      | {2000,2001,2002}
 Google  | Sales      | {2004}
 Mozilla | Sales      | {2002}
(4 rows)

Это именно то, что вы хотели, если я не ошибаюсь.

11 голосов
/ 05 ноября 2011

Существует большое значение для ответа @ a_horse_with_no_name , как правильного решения, так и, как я уже сказал в комментарии, хорошего материала для изучения того, как использовать различные виды оконных функций в PostgreSQL.1003 *

И все же я не могу избавиться от ощущения, что подход, использованный в этом ответе, является слишком большим усилием для такой проблемы, как эта.По сути, вам нужен дополнительный критерий для группировки, прежде чем вы начнете агрегировать годы в массивах.У вас уже есть company и profession, теперь вам нужно только что-то, чтобы различать годы, принадлежащие разным последовательностям.

Это именно то, что дает вышеупомянутый ответ, и именно это, я думаю, можетбыть сделано более простым способом.Вот как:

WITH MarkedForGrouping AS (
  SELECT
    company,
    profession,
    year,
    year - ROW_NUMBER() OVER (
      PARTITION BY company, profession
      ORDER BY year
    ) AS seqID
  FROM atable
)
SELECT
  company,
  profession,
  array_agg(year) AS years
FROM MarkedForGrouping
GROUP BY
  company,
  profession,
  seqID
4 голосов
/ 04 ноября 2011

Процедурное решение с PL / pgSQL

Проблема довольно громоздкая для простого SQL с функциями агрегата / windows. Хотя зацикливание обычно медленнее, чем решения на основе множеств с простым SQL, процедурное решение с plpgsql может обойтись с однократным последовательным сканированием по таблице (неявный курсор цикла FOR) и должно быть 1006 * существенно быстрее в данном конкретном случае :

Тестовый стол:

CREATE TEMP TABLE tbl (company text, profession text, year int);
INSERT INTO tbl VALUES
 ('Google',  'Programmer', 2000)
,('Google',  'Sales',      2000)
,('Google',  'Sales',      2001)
,('Google',  'Sales',      2002)
,('Google',  'Sales',      2004)
,('Mozilla', 'Sales',      2002);

Функция:

CREATE OR REPLACE FUNCTION f_periods()
  RETURNS TABLE (company text, profession text, years int[]) AS
$func$
DECLARE
   r  tbl; -- use table type as row variable
   r0 tbl;
BEGIN

FOR r IN
   SELECT * FROM tbl t ORDER BY t.company, t.profession, t.year
LOOP
   IF ( r.company,  r.profession,  r.year)
   <> (r0.company, r0.profession, r0.year + 1) THEN -- not true for first row

      RETURN QUERY
      SELECT r0.company, r0.profession, years; -- output row

      years := ARRAY[r.year];     -- start new array
   ELSE
      years := years || r.year;   -- add to array - year can be NULL, too
   END IF;

   r0 := r;                       -- remember last row
END LOOP;

RETURN QUERY                      -- output last iteration
SELECT r0.company, r0.profession, years;

END
$func$ LANGUAGE plpgsql;

Звоните:

SELECT * FROM f_periods();

Создает запрошенный результат.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...