SELECT 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个查询。这不是性能瓶颈吗?
Fabrizio Mazzoni

6
那是一个查询,而不是4。
ypercubeᵀᴹ15年

1
我可以看到几种方式来写可能有不同的表现查询,这取决于可用的索引,等等。但我不能想象一个功能将如何帮助
ypercubeᵀᴹ

1
好。与给它一展身手UNION
法布里奇奥崇义

Answers:


24

更新:测试了SQLfiddle中的所有5个查询,其中包含10万行(和2个单独的案例),其中一个具有很少(25)个不同的值,另一个具有很多(大约25K个值)。

一个非常简单的查询将是使用UNION DISTINCT我认为,如果四列中的每一列都有一个单独的索引,这将是最有效的。如果Postgres实施了松散索引扫描优化,那么四列中的每一列都有一个单独的索引将是有效的。因此,此查询将效率不高,因为它需要对表进行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语法提供了更多选项。Jack的查询(187 ms, 261 ms)具有合理的性能,但是AndriyM的查询似乎更有效(125 ms, 155 ms)。它们都对表进行一次顺序扫描,并且不使用任何索引。

实际上,杰克的查询结果比上面显示的要好一些(如果我们删除了order by),可以通过删除内部的4个distinct而仅保留外部的4个来进一步改善。


最后,如果- 并且仅当 -4列的不同值相对较少,您可以使用WITH RECURSIVE上面的“松散索引扫描”页面中描述的hack /优化,并使用所有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 ;

SQL小提琴


总而言之,当不同的值很少时,递归查询绝对是赢家,而值很多,我的第二个查询(杰克(下面是改进版本)和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 ;

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;

安德里(Andriy)想法的较为冗长的版本仅稍长一些,但更为优雅和快捷。
对于许多不同的/ 很少的重复值:

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 1min(a)通常不是更快。我也不需要其他谓词来排除NULL值。
LATERAL不是相关的子查询,因为它更干净(不一定更快)。

在我对此技术的解答中的详细说明:

我更新了ypercube的SQL Fiddle,并将我的添加到了播放列表中。


您可以测试EXPLAIN (ANALYZE, TIMING OFF)以验证最佳整体性能吗?(最好的方法是5个,以排除缓存影响。)
Erwin Brandstetter

有趣。我认为逗号联接在各个方面都等同于交叉联接,即在性能方面也是如此。区别是特定于使用LATERAL吗?
Andriy M

也许我误会了。当您对我的建议的较不冗长的版本说“更快”时,您的意思是比我的快还是比没有嵌套的SELECT DISTINCT快?
Andriy M

1
@AndriyM:逗号等效的(除了解析连接序列时,显式的`CROSS JOIN`语法绑定更强)。是的,我的意思是您的想法VALUES ...比更快unnest(ARRAY[...])LATERAL对于FROM列表中的返回集合函数是隐式的。
Erwin Brandstetter,2015年

感谢您的改进!我尝试了order / limit-1变体,但没有明显的区别。使用LATERAL很酷,避免了多次IS NOT NULL检查,很好。您应该向Postgres专家推荐这种变体,将其添加到“宽松索引扫描”页面中。
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的努力。
Fabrizio Mazzoni

2
为什么要执行此数组和游标魔术?@ypercube的解决方案可以完成工作,并且很容易包装成SQL语言函数。
dezso

抱歉,我无法编译您的函数。我可能做了一些愚蠢的事情。如果您设法在这里工作,请提供一个链接,我将用结果更新答案,以便我们可以与其他答案进行比较。
ypercubeᵀᴹ

@ypercube编辑过的解决方案必须有效。切记在小提琴中更改分隔符。我使用表创建在本地数据库上进行了测试,并且工作正常。
user_0 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.