为什么在SELECT COUNT(DISTINCT)中不使用索引?

3

这是我在PostgreSQL 10.9中所做的事情(xVARCHAR(100)):

最初的回答:

SELECT COUNT(DISTINCT x) FROM t

这张表有超过150万条记录,并且它有一个索引:

CREA­TE INDE­X idx_1 ON t­ USIN­G btre­e (x)

这个请求需要超过7秒的时间。根据EXPLAIN,情况如下:

最初的回答:

Aggr­egat­e (cos­t=23­67597..­2367­5.97­ rows­=1 widt­h=8)­
->­; Seq Scan­ on t (cos­t=0.­00..­2293­0.97­ rows­=148­9990­ widt­h=23­)

出了什么问题?为什么没有使用索引?

最初的回答:

3个回答

3

这取决于两个因素:

  1. 表是否有“宽行”
  2. 表是否已执行了vacuum操作

无论如何,查询都必须扫描整个索引或整个表,因为在PostgreSQL中没有索引跳过扫描

PostgreSQL可以扫描索引或表。

  • 如果表最近没有执行过vacuum操作,则索引扫描将始终需要访问表来确定行是否可见。在这种情况下,顺序扫描始终更快。

  • 如果表最近已经执行了vacuum操作,并且可见性映射大多数块标记为“全部可见”,则可能会获得仅索引扫描

    • 如果表行很窄,则不太可能获得仅索引扫描,因为此时读取索引的代价不会比读取表(顺序读取更快)更便宜。

    • 对于具有宽行的表,您将获得仅索引扫描。


2

问题在于,虽然您在列t上有一个B树索引,但它不一定能帮助找到不同计数。假设索引在概念上类似于这样:

Original Answer翻译成“最初的回答”。

1 - 1 - 2 - 2 - 2 - 2 - 4 - 4 - 9

如果您只需要最小值和最大值,理论上可以使用索引,因为第一个和最后一个值包含了这些信息,不需要扫描。但是,要找到所有不同的值,就需要进行索引扫描。请注意,即使有索引,也没有真正帮助Postgres获取答案,因为它仍然需要访问t列中的每个值。 COUNT是一个聚合函数,往往不适合使用索引(与MINMAX不同,它们可以使用索引)。

0

给初学者们:

这个问题有一个有趣的解决方案。

感谢这两个问题Q1Q2,以及敏锐的提问者和特别是天才回答者Erwin Brandstetter,我们可以使用递归CTE来实现部分索引跳过扫描

至于你的例子,这里是SQL:

WITH RECURSIVE mid_cte AS (
    (   -- parentheses required
        SELECT x
        FROM   t
        ORDER  BY 1
        LIMIT  1
    )
    UNION ALL
    SELECT n.*
    FROM   mid_cte o
    CROSS JOIN LATERAL (
        SELECT next_t.x
        FROM   t next_t
        WHERE  next_t.x > o.x
        ORDER  BY 1
        LIMIT  1
    ) n
)
SELECT MIN(x),MAX(x),COUNT(DISTINCT x)
FROM   mid_cte

但是有一些事情我们必须注意: 这个索引跳过扫描仿真的性能深度依赖于索引列的基数,而表扫描只依赖于表大小。 以下是我的实验和结果:

第一部分。1k基数--2.67秒表扫描 vs 0.03秒模拟索引跳过扫描

-- all test table t has 1.5million rows and run on a ssd.
-- ################# 1) 1k cardinality of column x #################
DROP TABLE IF EXISTS t;
CREATE TABLE t AS SELECT MD5(LEFT (RANDOM()::TEXT,5)) AS x FROM GENERATE_SERIES(1,1500000);
CREATE INDEX t_x_idx ON t USING btree(x);

-- table scan
SELECT MIN(x),MAX(x),COUNT(DISTINCT x) FROM t;
/*
### result set ###
min max count
... ... 1,161  
### use time ###
2.67s
*/

-- emulate index skip scan
WITH RECURSIVE mid_cte AS (
    (   -- parentheses required
        SELECT x
        FROM   t
        ORDER  BY 1
        LIMIT  1
    )
    UNION ALL
    SELECT n.*
    FROM   mid_cte o
    CROSS JOIN LATERAL (
        SELECT next_t.x
        FROM   t next_t
        WHERE  next_t.x > o.x
        ORDER  BY 1
        LIMIT  1
    ) n
)
SELECT MIN(x),MAX(x),COUNT(DISTINCT x)
FROM   mid_cte
;
/*
### result set ###
min max count
... ... 1,161
### use time ###
0.03s
*/

第二部分。10k基数--3.44秒表扫描 vs 0.09秒模拟索引跳过扫描

-- ################# 2) 10k cardinality of column x #################
-- other SQLs omitted here. the other SQLs are the same as above.
......
CREATE TABLE t AS SELECT MD5(LEFT (RANDOM()::TEXT,6)) AS x FROM GENERATE_SERIES(1,1500000);
......

-- table scan
/*
### result set ###
min max count
... ... 10,146  
### use time ###
3.44s
*/

-- emulate index skip scan
;
/*
### result set ###
min max count
... ... 10,146  
### use time ###
0.09s
*/

第三方。100k 基数 -- 4.27 秒表扫描 vs 0.89 秒模拟索引跳过扫描

-- ################# 3) 100k cardinality of column x #################
-- other SQLs omitted here. the other SQLs are the same as above.
......
CREATE TABLE t AS SELECT MD5(LEFT (RANDOM()::TEXT,7)) AS x FROM GENERATE_SERIES(1,1500000);
......

-- table scan
/*
### result set ###
min max count
... ... 100,134  
### use time ###
4.27s
*/

-- emulate index skip scan
;
/*
### result set ###
min max count
... ... 100,134  
### use time ###
0.89s
*/

第四部分。776k基数(总行数的50%)-- 5.16秒表扫描 vs 10.38秒模拟索引跳过扫描

-- ################# 4) 776k cardinality of column x #################
-- other SQLs omitted here. the other SQLs are the same as above.
......
CREATE TABLE t AS SELECT MD5(LEFT (RANDOM()::TEXT,8)) AS x FROM GENERATE_SERIES(1,1500000);
......

-- table scan
/*
### result set ###
min max count
... ... 776,864  
### use time ###
5.16s
*/

-- emulate index skip scan
;
/*
### result set ###
min max count
... ... 776,864  
### use time ###
10.38s
*/

当基数增加时,表扫描时间的增加速度比模拟索引跳过扫描要慢得多。因此,在何时使用此方法是重要的,并且必须考虑到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接