web-dev-qa-db-ja.com

複数の列に対するSELECT DISTINCT

同じデータ型の4つの列(a,b,c,d)を持つテーブルがあるとします。

列のデータ内のすべての個別の値を選択して単一の列として返すことは可能ですか、またはこれを実現する関数を作成する必要がありますか?

23

Update:5つのクエリすべてをSQLfiddleで100K行(および2つの個別のケース、1つは少数(25)の個別の値とロットのある別の値(約25Kの値)。

非常に単純なクエリは、UNION DISTINCTを使用することです。 4つの列のそれぞれに個別のインデックスがある場合、それが最も効率的だと思います Postgresが Loose Index Scan 最適化を実装している場合、4列のそれぞれに個別のインデックスを使用すると効率的ですが、実装していません。したがって、このクエリはテーブルを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 ;

もう1つは、最初に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)。どちらもテーブルの順次スキャンを1回実行し、インデックスを使用しません。

実際、Jackのクエリ結果は上に示したものより少し良く(order byを削除した場合)、4つの内部distinctを削除して外部のものだけを残すことでさらに改善できます。


最後に、-およびif-4つの列の個別の値が比較的少ない場合、上記のルーズインデックススキャンで説明されているWITH RECURSIVE hack/optimizationを使用できます。ページし、4つのインデックスすべてを使用して、非常に高速な結果を実現します同じ10万行と約25の異なる値でテストすると、4列に分散します(わずか2ミリ秒で実行されます)。一方、25千の異なる値では、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のクエリが最高のパフォーマンスを発揮します。


後の追加は、最初のクエリのバリエーションであり、余分な個別の操作にもかかわらず、元の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 ;
24
ypercubeᵀᴹ

このクエリ のようにLATERALを使用できます。

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

LATERALキーワードを使用すると、結合の右側で左側からオブジェクトを参照できます。この場合、右側はVALUESコンストラクターであり、単一列に入れたい列値から単一列サブセットを構築します。メインクエリは単に新しい列を参照し、それにDISTINCTも適用します。

12
Andriy M

明確にするために、私はunionypercubeが示唆する として使用しますが、配列でも可能です。

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 ---(ここ

最短

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

Andriyのアイデア のより簡潔なバージョンは、わずかに長くなりますが、よりエレガントで高速です。
many異なる/few重複する値の場合:

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

最速

関連する各列にインデックスが付いています!
few異なる/many重複する値の場合:

_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が既に投稿されている に似ていますが、min(a)の代わりに_ORDER BY 1 LIMIT 1_を使用しています。 NULL値を除外するための追加の述語も必要ありません。
さらに、相関サブクエリの代わりにLATERAL

この手法に関する私の回答の詳細な説明:

Ypercubeの SQL Fiddle を更新して、プレイリストにマイニングを追加しました。

7

できますが、関数を書いてテストしたところ、間違っていると感じました。資源の浪費です。
ただ組合とより多くの選択を使用してください。メリットのみ(ある場合)、メインテーブルからの1回のスキャン。

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