SELECCIONE DISTINCT en varias columnas

23

Supongamos que tenemos una tabla con cuatro columnas (a,b,c,d)del mismo tipo de datos.

¿Es posible seleccionar todos los valores distintos dentro de los datos en las columnas y devolverlos como una sola columna o tengo que crear una función para lograr esto?

Fabrizio Mazzoni
fuente
77
Quieres decir SELECT a FROM tablename UNION SELECT b FROM tablename UNION SELECT c FROM tablename UNION SELECT d FROM tablename ;?
ypercubeᵀᴹ
Sí. Eso sería suficiente, pero tendría que ejecutar 4 consultas. ¿No sería un cuello de botella de rendimiento?
Fabrizio Mazzoni
66
Esa es una consulta, no 4.
ypercubeᵀᴹ
1
Puedo ver varias formas de escribir la consulta que pueden tener un rendimiento diferente, dependiendo de los índices disponibles, etc. Pero no puedo imaginar cómo ayudaría una función
ypercubeᵀᴹ
1
OKAY. Probándolo conUNION
Fabrizio Mazzoni

Respuestas:

24

Actualización: Probé las 5 consultas en SQLfiddle con 100K filas (y 2 casos separados, uno con pocos (25) valores distintos y otro con lotes (alrededor de 25K valores).

Una consulta muy simple sería usar UNION DISTINCT. Creo que sería más eficiente si hubiera un índice separado en cada una de las cuatro columnas. Sería eficiente con un índice separado en cada una de las cuatro columnas, si Postgres hubiera implementado la optimización Loose Index Scan , que no lo ha hecho. Por lo tanto, esta consulta no será eficiente ya que requiere 4 escaneos de la tabla (y no se utiliza ningún índice):

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

Otro sería primero UNION ALLy luego usar DISTINCT. Esto también requerirá 4 escaneos de tabla (y sin uso de índices). No es una mala eficiencia cuando los valores son pocos, y con más valores se convierte en el más rápido en mi (no extensa) prueba:

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

Las otras respuestas han proporcionado más opciones utilizando funciones de matriz o la LATERALsintaxis. La consulta de Jack ( 187 ms, 261 ms) tiene un rendimiento razonable, pero la consulta de AndriyM parece más eficiente ( 125 ms, 155 ms). Ambos realizan una exploración secuencial de la tabla y no utilizan ningún índice.

En realidad, los resultados de la consulta de Jack son un poco mejores que los mostrados anteriormente (si eliminamos el order by) y pueden mejorarse aún más eliminando los 4 internos distincty dejando solo el externo.


Finalmente, si, y solo si , los valores distintos de las 4 columnas son relativamente pocos, puede usar el WITH RECURSIVEhack / optimización descrito en la página anterior de Análisis de índice suelto y usar los 4 índices, ¡con un resultado notablemente rápido! Probado con las mismas filas de 100K y aproximadamente 25 valores distintos distribuidos en las 4 columnas (¡se ejecuta en solo 2 ms!), Mientras que con 25K valores distintos es el más lento con 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, cuando los valores distintos son pocos, la consulta recursiva es la ganadora absoluta, mientras que con muchos valores, mi segunda, las consultas de Jack (versión mejorada a continuación) y AndriyM son las de mejor desempeño.


Adiciones tardías, una variación en la primera consulta que, a pesar de las operaciones adicionales, funciona mucho mejor que la primera y solo un poco peor que la segunda:

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

y Jack ha mejorado:

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

Puede usar LATERAL, como en esta consulta :

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

La palabra clave LATERAL permite que el lado derecho de la unión haga referencia a objetos desde el lado izquierdo. En este caso, el lado derecho es un constructor VALUES que construye un subconjunto de una sola columna a partir de los valores de columna que desea colocar en una sola columna. La consulta principal simplemente hace referencia a la nueva columna y también le aplica DISTINCT.

Andriy M
fuente
10

Para ser claros, lo usaría unioncomo sugiere ypercube , pero también es posible con matrices:

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

dbfiddle aquí

Jack Douglas
fuente
7

Más corto

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

Una versión menos detallada de la idea de Andriy es solo un poco más larga, pero más elegante y más rápida.
Para muchos valores duplicados distintos / pocos :

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

Lo más rápido

¡Con un índice en cada columna involucrada!
Para algunos valores duplicados distintos / muchos :

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 es otra variante de rCTE, similar a la que @ypercube ya publicó , pero la uso en ORDER BY 1 LIMIT 1lugar de la min(a)cual suele ser un poco más rápida. Tampoco necesito un predicado adicional para excluir valores NULL.
Y en LATERALlugar de una subconsulta correlacionada, porque es más limpia (no necesariamente más rápida).

Explicación detallada en mi respuesta para esta técnica:

Actualicé el Fiddle de SQL de ypercube y agregué el mío a la lista de reproducción.

Erwin Brandstetter
fuente
¿Se puede probar EXPLAIN (ANALYZE, TIMING OFF)para verificar el mejor rendimiento general? (Lo mejor de 5 para excluir los efectos de almacenamiento en caché.)
Erwin Brandstetter
Interesante. Pensé que una combinación de comas sería equivalente a una CROSS JOIN en todos los aspectos, es decir, en términos de rendimiento también. ¿Es la diferencia específica de usar LATERAL?
Andriy M
O tal vez lo entendí mal. Cuando dijo "más rápido" sobre la versión menos detallada de mi sugerencia, ¿quiso decir más rápido que el mío o más rápido que el SELECCIONAR DISTINTO con desacuerdo?
Andriy M
1
@AndriyM: La coma es equivalente (excepto que la sintaxis explícita `CROSS JOIN` se une más fuerte al resolver la secuencia de unión). Sí, quiero decir que tu idea VALUES ...es más rápida que unnest(ARRAY[...]). LATERALestá implícito para las funciones de devolución de conjuntos en la FROMlista.
Erwin Brandstetter
¡Gracias por las mejoras! Probé la variante order / limit-1 pero no hubo ninguna diferencia notable. Usar LATERAL es bastante bueno, evitar las múltiples comprobaciones NO ES NULO, genial. Debería sugerir esta variante a los chicos de Postgres, que se agregará en la página Loose-Index-Scan.
ypercubeᵀᴹ
3

Puede, pero mientras escribía y probaba la función me sentí mal. Es un desperdicio de recursos.
Solo use una unión y más selectos. Única ventaja (si lo es), un solo escaneo desde la tabla principal.

En sql fiddle, debe cambiar el separador de $ a otra cosa, 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();
usuario_0
fuente
En realidad tienes razón, ya que una función aún usaría una unión. En cualquier caso +1 por el esfuerzo.
Fabrizio Mazzoni
2
¿Por qué haces esta magia de matriz y cursor? La solución de @ypercube hace el trabajo y es muy fácil de incluir en una función de lenguaje SQL.
dezso
Lo siento, no pude hacer que tu función compilara. Probablemente hice algo tonto. Si logra que funcione aquí , proporcione un enlace y actualizaré mi respuesta con los resultados, para que podamos comparar con las otras respuestas.
ypercubeᵀᴹ
@ypercube La solución editada debe funcionar. Recuerde cambiar el separador en violín. Probé en mi base de datos local con la creación de tablas y funciona bien.
usuario_0