ВИБІРТЕ ДИСТИНЦТ у кількох стовпцях


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синтаксис. Запит Джека ( 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-й, Джек (покращена версія нижче) та запити АндріяМ - найкращі виконавці.


Пізні доповнення, варіація 1-го запиту, яка, незважаючи на надзвичайно чіткі операції, працює набагато краще, ніж початковий 1-й і лише трохи гірше, ніж 2-й:

-- 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 ;

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?
Андрій М

А може, я неправильно зрозумів. Коли ви сказали "швидше" про менш багатослівну версію моєї пропозиції, ви мали на увазі швидше, ніж моя, або швидше, ніж ВИБІР ВІДТРИМУВАТИ з unnest?
Андрій М

1
@AndriyM: Кома є еквівалентом ( за винятком того, що явне `CROSS JOIN` синтаксис пов'язує сильніше при вирішенні приєднатися до послідовності). Так, я маю на увазі, що ваша ідея VALUES ...швидше, ніж unnest(ARRAY[...]). LATERALнеявна для функцій повернення набору в FROMсписку.
Ервін Брандстеттер

Thnx для поліпшень! Я спробував варіант замовлення / ліміт-1, але помітної різниці не було. Використання LATERAL є досить крутим, уникаючи декількох НЕ НУЛЬНИХ чеків, чудово. Ви повинні запропонувати цей варіант хлопцям Postgres, які потрібно додати на сторінці Loose-Index-Scan.
ypercubeᵀᴹ

3

Можна, але, як я писав і тестував функцію, я почував себе неправильним. Це відходи ресурсів.
Просто будь ласка, використовуйте союз і більше виберіть. Єдина перевага (якщо вона є), одне сканування з головної таблиці.

У sql скрипці вам потрібно змінити роздільник з $ на щось інше, наприклад /

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();

Ви насправді маєте рацію, оскільки функція все-таки використовує союз. У будь-якому випадку +1 за зусилля.
Фабріціо Маццоні

2
Чому ви займаєтеся цим масивом і магією курсору? Рішення @ ypercube спрацьовує, і дуже легко перетворитися на функцію мови SQL.
dezso

На жаль, не вдалося змусити вашу функцію компілювати. Я, певно, зробив щось дурне. Якщо вам вдасться, щоб він працював тут , надайте мені посилання, і я оновлю свою відповідь результатами, щоб ми могли порівняти з іншими відповідями.
ypercubeᵀᴹ

@ypercube Відредаговане рішення повинно працювати. Не забудьте змінити роздільник у скрипці. Я перевірив свій локальний db зі створенням таблиць і працює чудово.
користувач_0
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.