SELECT DISTINCT em várias colunas

23

Supondo que tenhamos uma tabela com quatro colunas (a,b,c,d)do mesmo tipo de dados.

É possível selecionar todos os valores distintos nos dados nas colunas e retorná-los como uma única coluna ou tenho que criar uma função para conseguir isso?

Fabrizio Mazzoni
fonte
7
Você quer dizer SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Sim. Isso faria, mas eu teria que executar 4 consultas. Não seria um gargalo de desempenho?
Fabrizio Mazzoni
6
Essa é uma consulta, não 4.
ypercubeᵀᴹ
1
Eu posso ver várias maneiras de escrever a consulta que pode ter um desempenho diferente, dependendo índices disponíveis, etc. Mas eu não posso imaginar como uma função ajudaria
ypercubeᵀᴹ
1
ESTÁ BEM. Dando uma chance comUNION
Fabrizio Mazzoni

Respostas:

24

Atualização: foram testadas todas as 5 consultas no SQLfiddle com 100K linhas (e 2 casos separados, um com poucos (25) valores distintos e outro com lotes (valores em torno de 25K).

Uma consulta muito simples seria usar UNION DISTINCT. Eu acho que seria mais eficiente se houvesse um índice separado em cada uma das quatro colunas . Seria eficiente com um índice separado em cada uma das quatro colunas, se o Postgres tivesse implementado a otimização de Loose Index Scan , o que não existe. Portanto, essa consulta não será eficiente, pois requer 4 varreduras da tabela (e nenhum índice é usado):

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

Outro seria primeiro UNION ALLe depois usar DISTINCT. Isso também exigirá 4 varreduras de tabela (e nenhum uso de índices). Não é uma eficiência ruim quando os valores são poucos e, com mais valores, se torna o mais rápido no meu (não extenso) teste:

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

As outras respostas forneceram mais opções usando funções de matriz ou a LATERALsintaxe. A consulta de Jack ( 187 ms, 261 ms) tem um desempenho razoável, mas a consulta de AndriyM parece mais eficiente ( 125 ms, 155 ms). Ambos fazem uma varredura seqüencial da tabela e não usam nenhum índice.

Na verdade, os resultados da consulta de Jack são um pouco melhores do que os mostrados acima (se removermos o order by) e podem ser melhorados removendo os 4 internos distincte deixando apenas o externo.


Finalmente, se - e somente se - os valores distintos das 4 colunas forem relativamente poucos, você poderá usar o WITH RECURSIVEhack / otimização descrito na página Loose Index Scan acima e usar todos os 4 índices, com resultados notavelmente rápidos! Testado com as mesmas 100K linhas e aproximadamente 25 valores distintos espalhados pelas 4 colunas (é executado em apenas 2 ms!), Enquanto que com 25K valores distintos, é o mais lento com 368 ms:

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


Para resumir, quando os valores distintos são poucos, a consulta recursiva é a vencedora absoluta, enquanto com muitos valores, a minha segunda, as consultas de Jack (versão melhorada abaixo) e AndriyM são as melhores.


Adições tardias, uma variação na 1ª consulta que, apesar das operações extra distintas, tem um desempenho muito melhor que a 1ª original e apenas um pouco pior que a 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 ;

e Jack melhorou:

-- Query 4b.  (104 ms, 128 ms)
select distinct unnest( array_agg(a)||
                        array_agg(b)||
                        array_agg(c)||
                        array_agg(d) )
from t ;
ypercubeᵀᴹ
fonte
12

Você pode usar LATERAL, como nesta consulta :

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

A palavra-chave LATERAL permite que o lado direito da junção faça referência a objetos do lado esquerdo. Nesse caso, o lado direito é um construtor VALUES que cria um subconjunto de coluna única a partir dos valores da coluna que você deseja colocar em uma única coluna. A consulta principal simplesmente faz referência à nova coluna, também aplicando DISTINCT a ela.

Andriy M
fonte
10

Para ser claro, eu usaria unioncomo o ypercube sugere , mas também é possível com matrizes:

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

dbfiddle aqui

Jack Douglas
fonte
7

Mais curto

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

Uma versão menos detalhada da idéia de Andriy é apenas um pouco mais longa, mas mais elegante e mais rápida.
Para muitos valores distintos / poucos duplicados:

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

O mais rápido

Com um índice em cada coluna envolvida!
Para alguns valores distintos / muitos duplicados:

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;

Esta é outra variante do rCTE, semelhante à que o @ypercube já postou , mas eu uso em ORDER BY 1 LIMIT 1vez da min(a)qual normalmente é um pouco mais rápida. Também não preciso de nenhum predicado adicional para excluir valores NULL.
E em LATERALvez de uma subconsulta correlacionada, porque é mais limpa (não necessariamente mais rápida).

Explicação detalhada em minha resposta a seguir para esta técnica:

Atualizei o SQL Fiddle do ypercube e adicionei o meu à lista de reprodução.

Erwin Brandstetter
fonte
Você pode testar EXPLAIN (ANALYZE, TIMING OFF)para verificar o melhor desempenho geral? (Melhor de 5 para excluir efeitos cache.)
Erwin Brandstetter
Interessante. Eu pensei que uma junção de vírgula seria equivalente a uma CROSS JOIN em todos os aspectos, ou seja, em termos de desempenho também. A diferença é específica no uso do LATERAL?
Andriy M
Ou talvez eu tenha entendido errado. Quando você disse "mais rápido" sobre a versão menos detalhada da minha sugestão, você quis dizer mais rápido que o meu ou mais rápido que o SELECT DISTINCT com desinência?
Andriy M
1
@AndriyM: A vírgula é equivalente (exceto que a sintaxe explícita `CROSS JOIN` se liga mais forte ao resolver a sequência de junção). Sim, quero dizer que sua ideia VALUES ...é mais rápida que unnest(ARRAY[...]). LATERALestá implícito para funções de retorno de conjunto na FROMlista.
Erwin Brandstetter
Thnx para as melhorias! Eu tentei a variante de pedido / limite-1, mas não havia nenhuma diferença perceptível. Usando LATERAL, é bem legal, evitando as várias verificações NÃO É NULL, ótimo. Você deve sugerir essa variante para o pessoal do Postgres, para ser adicionada na página Loose-Index-Scan.
ypercubeᵀᴹ
3

Você pode, mas, ao escrever e testar a função, senti-me errado. É um desperdício de recursos.
Apenas use uma união e mais opções. Única vantagem (se for): uma única verificação da tabela principal.

No sql fiddle, você precisa alterar o separador de $ para outra coisa, como /

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
fonte
Você está realmente certo, pois uma função ainda usaria uma união. Em qualquer caso, +1 para o esforço.
Fabrizio Mazzoni
2
Por que você está fazendo essa mágica de matriz e cursor? A solução do @ ypercube faz o trabalho e é muito fácil envolver uma função da linguagem SQL.
Dez28
Desculpe, não consegui fazer sua função compilar. Eu provavelmente fiz algo bobo. Se você conseguir que ele funcione aqui , forneça-me um link e atualizarei minha resposta com os resultados, para que possamos comparar com as outras respostas.
ypercubeᵀᴹ
@ypercube A solução editada deve funcionar. Lembre-se de mudar o separador no violino. Eu testei no meu banco de dados local com a criação de tabela e funciona bem.
user_0