ВЫБЕРИТЕ DISTINCT на нескольких столбцах

23

Предположим, у нас есть таблица с четырьмя столбцами (a,b,c,d)одного типа данных.

Можно ли выбрать все отдельные значения в данных в столбцах и вернуть их в виде одного столбца, или мне нужно создать функцию для достижения этой цели?

Фабрицио Маззони
источник
7
Вы имеете в виду SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Да. Это сделало бы, но я должен был выполнить 4 запроса. Не будет ли это узким местом в производительности?
Фабрицио Маззони
6
Это один запрос, а не 4.
ypercubeᵀᴹ
1
Я вижу несколько способов написания запроса, который может иметь различную производительность, в зависимости от доступных индексов и т. Д. Но я не могу представить, как функция может помочь
ypercubeᵀᴹ
1
ХОРОШО. Давать ему идти сUNION
Фабрицио Маццони

Ответы:

24

Обновление: протестированы все 5 запросов в SQLfiddle с 100K строк (и 2 отдельных случая, один с несколькими (25) различными значениями и другой с партиями (около 25K значений).

Очень простой запрос будет использовать UNION DISTINCT. Я думаю, что было бы наиболее эффективно, если бы для каждого из четырех столбцов был отдельный индекс. Было бы эффективно использовать отдельный индекс для каждого из четырех столбцов, если бы в Postgres была реализована оптимизация Loose Index Scan , чего у него нет. Таким образом, этот запрос не будет эффективным, поскольку он требует 4 сканирования таблицы (и индекс не используется):

-- Query 1. (334 ms, 368ms) 
SELECT a AS abcd FROM tablename 
UNION                           -- means UNION DISTINCT
SELECT b FROM tablename 
UNION 
SELECT c FROM tablename 
UNION 
SELECT d FROM tablename ;

Другой будет сначала, UNION ALLа затем использовать DISTINCT. Это также потребует 4 сканирования таблицы (и не использовать индексы). Неплохая эффективность, когда значений немного, и с большим количеством значений становится самым быстрым в моем (не обширном) тесте:

-- Query 2. (87 ms, 117 ms)
SELECT DISTINCT a AS abcd
FROM
  ( SELECT a FROM tablename 
    UNION ALL 
    SELECT b FROM tablename 
    UNION ALL
    SELECT c FROM tablename 
    UNION ALL
    SELECT d FROM tablename 
  ) AS x ;

Другие ответы предоставили больше опций с использованием функций массива или LATERALсинтаксиса. Джек query ( 187 ms, 261 ms) имеет разумную производительность, но запрос AndriyM кажется более эффективным ( 125 ms, 155 ms). Оба они выполняют одно последовательное сканирование таблицы и не используют какой-либо индекс.

На самом деле результаты запросов Джека немного лучше, чем показано выше (если мы удалим их order by), и их можно улучшить, удалив 4 внутренних distinctи оставив только внешний.


Наконец, если - и только если - отдельные значения 4 столбцов относительно немного, вы можете использовать метод WITH RECURSIVEвзлома / оптимизации, описанный на приведенной выше странице Loose Index Scan, и использовать все 4 индекса, с удивительно быстрым результатом! Протестировано с теми же строками 100K и примерно 25 различными значениями, распределенными по 4 столбцам (работает всего за 2 мс!), В то время как с 25K различными значениями это самое медленное с 368 мс:

-- Query 3.  (2 ms, 368ms)
WITH RECURSIVE 
    da AS (
       SELECT min(a) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(a) FROM observations
               WHERE  a > s.n)
       FROM   da AS s  WHERE s.n IS NOT NULL  ),
    db AS (
       SELECT min(b) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(b) FROM observations
               WHERE  b > s.n)
       FROM   db AS s  WHERE s.n IS NOT NULL  ),
   dc AS (
       SELECT min(c) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(c) FROM observations
               WHERE  c > s.n)
       FROM   dc AS s  WHERE s.n IS NOT NULL  ),
   dd AS (
       SELECT min(d) AS n  FROM observations
       UNION ALL
       SELECT (SELECT min(d) FROM observations
               WHERE  d > s.n)
       FROM   db AS s  WHERE s.n IS NOT NULL  )
SELECT n 
FROM 
( TABLE da  UNION 
  TABLE db  UNION 
  TABLE dc  UNION 
  TABLE dd
) AS x 
WHERE n IS NOT NULL ;

SQLfiddle


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


Позднее добавление - вариант первого запроса, который, несмотря на дополнительные отчетливые операции, работает намного лучше, чем исходный первый, и лишь немного хуже второго:

-- Query 1b.  (85 ms, 149 ms)
SELECT DISTINCT a AS n FROM observations 
UNION 
SELECT DISTINCT b FROM observations 
UNION 
SELECT DISTINCT c FROM observations 
UNION 
SELECT DISTINCT d FROM observations ;

и Джек улучшился:

-- Query 4b.  (104 ms, 128 ms)
select distinct unnest( array_agg(a)||
                        array_agg(b)||
                        array_agg(c)||
                        array_agg(d) )
from t ;
ypercubeᵀᴹ
источник
12

Вы можете использовать LATERAL, как в этом запросе :

SELECT DISTINCT
  x.n
FROM
  atable
  CROSS JOIN LATERAL (
    VALUES (a), (b), (c), (d)
  ) AS x (n)
;

Ключевое слово LATERAL позволяет правой стороне объединения ссылаться на объекты с левой стороны. В этом случае правая сторона - это конструктор VALUES, который строит подмножество из одного столбца из значений столбцов, которые вы хотите поместить в один столбец. Основной запрос просто ссылается на новый столбец, также применяя к нему DISTINCT.

Андрей М
источник
10

Чтобы было ясно, я бы использовал, unionкак подсказывает ypercube , но это также возможно с массивами:

select distinct unnest( array_agg(distinct a)||
                        array_agg(distinct b)||
                        array_agg(distinct c)||
                        array_agg(distinct d) )
from t
order by 1;
| Гнездо |
| : ----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle здесь

Джек Дуглас
источник
7

самый короткий

SELECT DISTINCT n FROM observations, unnest(ARRAY[a,b,c,d]) n;

Менее многословная версия идеи Андрея лишь немного длиннее, но более элегантна и быстрее.
Для многих различных / нескольких повторяющихся значений:

SELECT DISTINCT n FROM observations, LATERAL (VALUES (a),(b),(c),(d)) t(n);

Самый быстрый

С индексом на каждый задействованный столбец!
Для несколько различных / много повторяющихся значений:

WITH RECURSIVE
  ta AS (
   (SELECT a FROM observations ORDER BY a LIMIT 1)  -- parentheses required!
   UNION ALL
   SELECT o.a FROM ta t
    , LATERAL (SELECT a FROM observations WHERE a > t.a ORDER BY a LIMIT 1) o
   )
, tb AS (
   (SELECT b FROM observations ORDER BY b LIMIT 1)
   UNION ALL
   SELECT o.b FROM tb t
    , LATERAL (SELECT b FROM observations WHERE b > t.b ORDER BY b LIMIT 1) o
   )
, tc AS (
   (SELECT c FROM observations ORDER BY c LIMIT 1)
   UNION ALL
   SELECT o.c FROM tc t
    , LATERAL (SELECT c FROM observations WHERE c > t.c ORDER BY c LIMIT 1) o
   )
, td AS (
   (SELECT d FROM observations ORDER BY d LIMIT 1)
   UNION ALL
   SELECT o.d FROM td t
    , LATERAL (SELECT d FROM observations WHERE d > t.d ORDER BY d LIMIT 1) o
   )
SELECT a
FROM  (
       TABLE ta
 UNION TABLE tb
 UNION TABLE tc
 UNION TABLE td
 ) sub;

Это еще один вариант rCTE, похожий на уже опубликованный @ypercube , но ORDER BY 1 LIMIT 1вместо min(a)него я обычно использую его немного быстрее. Мне также не нужен дополнительный предикат для исключения значений NULL.
И LATERALвместо коррелированного подзапроса, потому что он чище (не обязательно быстрее).

Подробное объяснение в моем кратком ответе на эту технику:

Я обновил SQL Fiddle в ypercube и добавил мой в список воспроизведения.

Эрвин Брандштеттер
источник
Можете ли вы проверить, EXPLAIN (ANALYZE, TIMING OFF)чтобы проверить лучшую общую производительность? (Лучшее из 5, чтобы исключить эффекты кэширования.)
Эрвин Брандштеттер,
Интересный. Я думал, что соединение через запятую будет эквивалентно CROSS JOIN во всех отношениях, то есть с точки зрения производительности. Различия специфичны для использования LATERAL?
Андрей М
Или, может быть, я неправильно понял. Когда вы сказали «быстрее» о менее подробной версии моего предложения, вы имели в виду быстрее, чем моя или быстрее, чем SELECT DISTINCT с unnest?
Андрей М
1
@AndriyM: Запятая является эквивалентом ( за исключением того, что явное `CROSS JOIN` синтаксис связывает сильнее при решении присоединиться к последовательности). Да, я имею в виду, что ваша идея VALUES ...быстрее unnest(ARRAY[...]). LATERALнеявно для функций, возвращающих множество в FROMсписке.
Эрвин Брандстеттер
Спасибо за улучшения! Я попробовал вариант order / limit-1, но заметной разницы не было. Использование LATERAL довольно круто, избегать многократных проверок IS NOT NULL, отлично. Вам следует предложить этот вариант ребятам из Postgres, чтобы добавить его на страницу Loose-Index-Scan.
ypercubeᵀᴹ
3

Вы можете, но, как я написал и проверил функцию, я чувствовал себя неправильно. Это трата ресурсов.
Просто, пожалуйста, используйте союз и выберите больше. Единственное преимущество (если оно есть), одно сканирование из основной таблицы.

В sql fiddle вам нужно изменить разделитель с $ на что-то другое, например /

CREATE TABLE observations (
    id         serial
  , a int not null
  , b int not null
  , c int not null
  , d int not null
  , created_at timestamp
  , foo        text
);

INSERT INTO observations (a, b, c, d, created_at, foo)
SELECT (random() * 20)::int        AS a          -- few values for a,b,c,d
     , (15 + random() * 10)::int 
     , (10 + random() * 10)::int 
     , ( 5 + random() * 20)::int 
     , '2014-01-01 0:0'::timestamp 
       + interval '1s' * g         AS created_at -- ascending (probably like in real life)
     , 'aöguihaophgaduigha' || g   AS foo        -- random ballast
FROM generate_series (1, 10) g;               -- 10k rows

CREATE INDEX observations_a_idx ON observations (a);
CREATE INDEX observations_b_idx ON observations (b);
CREATE INDEX observations_c_idx ON observations (c);
CREATE INDEX observations_d_idx ON observations (d);

CREATE OR REPLACE FUNCTION fn_readuniqu()
  RETURNS SETOF text AS $$
DECLARE
    a_array     text[];
    b_array     text[];
    c_array     text[];
    d_array     text[];
    r       text;
BEGIN

    SELECT INTO a_array, b_array, c_array, d_array array_agg(a), array_agg(b), array_agg(c), array_agg(d)
    FROM observations;

    FOR r IN
        SELECT DISTINCT x
        FROM
        (
            SELECT unnest(a_array) AS x
            UNION
            SELECT unnest(b_array) AS x
            UNION
            SELECT unnest(c_array) AS x
            UNION
            SELECT unnest(d_array) AS x
        ) AS a

    LOOP
        RETURN NEXT r;
    END LOOP;

END;
$$
  LANGUAGE plpgsql STABLE
  COST 100
  ROWS 1000;

SELECT * FROM fn_readuniqu();
user_0
источник
Вы на самом деле правы, так как функция все равно будет использовать объединение. В любом случае +1 за усилие.
Фабрицио Маззони
2
Почему вы делаете этот массив и курсор курсора? Решение @ ypercube делает свою работу, и его очень легко обернуть в функцию языка SQL.
Дезсо
Извините, я не могу сделать вашу функцию для компиляции. Я наверное сделал что-то глупое. Если вам удастся заставить его работать здесь , предоставьте мне ссылку, и я обновлю свой ответ результатами, чтобы мы могли сравнить его с другими ответами.
ypercubeᵀᴹ
@ypercube Отредактированное решение должно работать. Не забудьте изменить разделитель в скрипке. Я проверил на моей локальной БД с созданием таблицы и работает нормально.
user_0