在多列上选择 DISTINCT

Fab*_*oni 24 postgresql performance postgresql-9.4 distinct postgresql-performance

假设我们有一个包含四列(a,b,c,d)相同数据类型的表。

是否可以选择列中数据中的所有不同值并将它们作为单个列返回,或者我是否必须创建一个函数来实现这一点?

ype*_*eᵀᴹ 24

更新:用 100K 行(和 2 个单独的案例,一个有几个(25)个不同的值,另一个有很多(大约 25K 个值)在SQLfiddle 中测试了所有 5 个查询。

一个非常简单的查询是使用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 ;
Run Code Online (Sandbox Code Playgroud)

另一种方法是先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 ;
Run Code Online (Sandbox Code Playgroud)

其他答案使用数组函数或LATERAL语法提供了更多选项。Jack 的查询 ( 187 ms, 261 ms) 具有合理的性能,但 AndriyM 的查询似乎更有效 ( 125 ms, 155 ms)。它们都对表进行一次顺序扫描,并且不使用任何索引。

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


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

SQLfiddle


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

和杰克的改进:

-- Query 4b.  (104 ms, 128 ms)
select distinct unnest( array_agg(a)||
                        array_agg(b)||
                        array_agg(c)||
                        array_agg(d) )
from t ;
Run Code Online (Sandbox Code Playgroud)


And*_*y M 13

你可以使用 LATERAL,就像在这个查询中一样

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

LATERAL 关键字允许连接的右侧从左侧引用对象。在这种情况下,右侧是一个 VALUES 构造函数,它从要放入单个列的列值中构建一个单列子集。主查询仅引用新列,并对其应用 DISTINCT。


Jac*_*las 10

需要明确的是,我会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;
Run Code Online (Sandbox Code Playgroud)
| 取消嵌套 |
| :----- |
| 0 |
| 1 |
| 2 |
| 3 |
| 5 |
| 6 |
| 8 |
| 9 |

dbfiddle在这里


Erw*_*ter 7

最短的

SELECT DISTINCT n FROM observations, unnest(ARRAY[a,b,c,d]) n;
Run Code Online (Sandbox Code Playgroud)

Andriy 想法的一个不那么冗长的版本只是稍微长一点,但更优雅和更快。
对于许多不同的/很少的重复值:

SELECT DISTINCT n FROM observations, LATERAL (VALUES (a),(b),(c),(d)) t(n);
Run Code Online (Sandbox Code Playgroud)

最快的

每个涉及的列都有一个索引!
对于少数不同/许多重复值:

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;
Run Code Online (Sandbox Code Playgroud)

这是另一种 rCTE 变体,类似于已经发布的@ypercube,但我使用它ORDER BY 1 LIMIT 1来代替min(a)它通常更快一些。我也不需要额外的谓词来排除 NULL 值。
LATERAL不是相关的子查询,因为它更干净(不一定更快)。

在我对此技术的回答中详细解释:

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