如何优化三个表的简单连接的执行计划

问题描述

我有一个相对简单的表结构和查询,但得到的执行计划似乎并不理想。尤其是执行时间让我怀疑。 这是我的表结构:

CREATE TABLE t_c (
        c_id uuid DEFAULT (md5(((random())::text || (clock_timestamp())::text)))::uuid NOT NULL,cn character varying NOT NULL,cs character varying,cp character varying
);

CREATE TABLE t_t (
        t_id uuid DEFAULT (md5(((random())::text || (clock_timestamp())::text)))::uuid NOT NULL,tp character varying,tn character varying,ts bigint NOT NULL,tt character varying,tii character varying(256) NOT NULL
);
 
CREATE TABLE t_t_c (
        t_id uuid NOT NULL,c_id uuid NOT NULL,mc_id uuid NOT NULL
);

ALTER TABLE ONLY t_c ADD CONSTRAINT t_c_pkey PRIMARY KEY (c_id);
ALTER TABLE ONLY t_t ADD CONSTRAINT t_t_pkey PRIMARY KEY (t_id);

CREATE INDEX t_c_cn_idx ON t_c USING btree (cn);
CREATE INDEX t_t_tii_idx ON t_t USING btree (tii);

ALTER TABLE ONLY t_c ADD CONSTRAINT t_c_unique UNIQUE(cn,cs,cp);

CREATE UNIQUE INDEX idx_cid_tid ON t_t_c USING btree (c_id,t_id);
CREATE UNIQUE INDEX idx_tid_cid ON t_t_c USING btree (t_id,c_id);
ALTER TABLE ONLY t_t_c ADD CONSTRAINT t_t_c_cid_fkey FOREIGN KEY (c_id) REFERENCES t_c(c_id);
ALTER TABLE ONLY t_t_c ADD CONSTRAINT t_t_c_mcid_fkey FOREIGN KEY (mc_id) REFERENCES t_c(c_id);
ALTER TABLE ONLY t_t_c ADD CONSTRAINT t_t_c_tid_fkey FOREIGN KEY (t_id) REFERENCES t_t(t_id);

t_c 和 t_t 都有大约 200 万行,t_t_c 大约有 20 亿行。

这是我要运行的查询

explain analyze select t_t.tii from t_t_c ttc join t_t tt on tt.t_id=ttc.t_id
JOIN t_c c on c.c_id=ttc.c_id
where c.cn = 'xxx'
group by t_t.tii

结果:

Group  (cost=5118006.20..5119624.81 rows=718 width=8) (actual time=231430.737..233032.234 rows=712 loops=1)
  Group Key: t.tii
  ->  Gather Merge  (cost=5118006.20..5119621.22 rows=1436 width=8) (actual time=231430.730..233497.475 rows=937 loops=1)
        Workers Planned: 2
        Workers Launched: 2
        ->  Group  (cost=5117006.18..5118455.45 rows=718 width=8) (actual time=231244.223..232715.561 rows=312 loops=3)
              Group Key: t.tii
              ->  Sort  (cost=5117006.18..5117730.81 rows=289854 width=8) (actual time=231244.213..231965.197 rows=295104 loops=3)
                    Sort Key: t.tii
                    Sort Method: quicksort  Memory: 25821kB
                    Worker 0:  Sort Method: quicksort  Memory: 27140kB
                    Worker 1:  Sort Method: quicksort  Memory: 25404kB
                    ->  Parallel Hash Join  (cost=212629.56..5090709.22 rows=289854 width=8) (actual time=3618.432..229889.447 rows=295104 loops=3)
                          Hash Cond: (ttc.t_id = tt.t_id)
                          ->  nested Loop  (cost=0.70..4877319.49 rows=289854 width=16) (actual time=1.869..224573.547 rows=295104 loops=3)
                                ->  Parallel Seq Scan on t_c c  (cost=0.00..54571.92 rows=408 width=16) (actual time=0.443..220.151 rows=310 loops=3)
                                      Filter: ((cn)::text = 'xxx'::text)
                                      Rows Removed by Filter: 652230
                                ->  Index Only Scan using idx_cid_tid on t_t_c ttc  (cost=0.70..11740.18 rows=8028 width=32) (actual time=0.884..719.911 rows=952 loops=930)
                                      Index Cond: (c_id = c.c_id)
                                      Heap Fetches: 885317
                          ->  Parallel Hash  (cost=201875.05..201875.05 rows=860305 width=24) (actual time=3599.908..3599.911 rows=692137 loops=3)
                                Buckets: 2097152  Batches: 1  Memory Usage: 130208kB
                                ->  Parallel Seq Scan on t_t t  (cost=0.00..201875.05 rows=860305 width=24) (actual time=0.057..1950.674 rows=692137 loops=3)

                            

总执行时间约为 4 分钟。尤其是hash join嵌套循环 需要很长时间。 有什么要优化的,也许添加一个索引? 我也不确定 uuid 是 t_id、c_id 的最佳数据类型,它们是主键/外键。也许整数数据类型可以提高性能

Postgres 版本是 11.6

非常感谢

基督徒

编辑: 使用 EXISTS() 的修改后的查询导致不同的执行计划,但执行时间几乎相同,可能要好 10%:

Gather  (cost=5043832.59..5251301.23 rows=30951 width=8) (actual time=210773.943..217715.805 rows=853778 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Hash Semi Join  (cost=5042832.59..5247206.13 rows=12896 width=8) (actual time=210762.956..217221.553 rows=284593 loops=3)
        Hash Cond: (tt.t_id = ttc.t_id)
        ->  Parallel Seq Scan on t_t tt  (cost=0.00..201875.05 rows=860305 width=24) (actual time=0.393..4337.062 rows=698756 loops=3)
        ->  Parallel Hash  (cost=5039175.26..5039175.26 rows=292587 width=16) (actual time=210754.705..210754.707 rows=297660 loops=3)
              Buckets: 1048576  Batches: 1  Memory Usage: 50144kB
              ->  nested Loop  (cost=0.70..5039175.26 rows=292587 width=16) (actual time=1.127..209827.976 rows=297660 loops=3)
                    ->  Parallel Seq Scan on t_c c  (cost=0.00..55789.33 rows=417 width=16) (actual time=0.961..394.992 rows=314 loops=3)
                          Filter: ((cn)::text = 'xxx'::text)
                          Rows Removed by Filter: 667056
                    ->  Index Only Scan using idx_cid_tid on t_t_c ttc  (cost=0.70..11869.46 rows=8111 width=32) (actual time=2.031..662.330 rows=947 loops=943)
                          Index Cond: (c_id = c.c_id)
                          Heap Fetches: 892980
Planning Time: 0.475 ms
Execution Time: 219290.828 ms

解决方法

堆获取:885317

这可能是您几乎所有时间都去的地方。清空 t_t_c 以便索引只扫描有效。

如果这不起作用,则打开 track_io_timing 并显示查询的 EXPLAIN (ANALYZE,BUFFERS)

,

如果您不从中选择任何列,请避免在主查询中加入表。相反,将它们向下推入 EXISTS(correlated subquery)

简单的重写,避免(愚蠢的)GROUP BY


-- EXPLAIN ANALYZE 

SELECT /* distinct ? */ tt.tii
FROM t_t tt
WHERE EXISTS (
        SELECT *
        FROM t_t_c ttc
        JOIN t_c c on c.c_id = ttc.c_id
        WHERE c.cn = 'xxx'
        AND tt.t_id = ttc.t_id
        );
,

也许重新排列表格会有所帮助,尽管我对此表示怀疑。这将使查询更具可读性。

select t_t.tii from 
t_c c
JOIN t_t_c ttc USING (c_id)
JOIN t_t tt USING (t_id)
where c.cn = 'xxx'
group by t_t.tii

从您所做的测试来看,cn 是一个非常有选择性的列,并且“SELECT * FROM t_c WHERE cn=...”很快。 postgres 以错误的顺序加入真的很奇怪。因此,您可以强制 postgres 以此开始并稍后加入其他表:

WITH cids AS MATERIALIZED( SELECT c_id FROM c WHERE cn=... )
SELECT DISTINCT t_t.tii from 
cids
JOIN t_t_c ttc USING (c_id)
JOIN t_t tt USING (t_id)

注意我问过你 cn 最常见值的最大行数,以避免这个 MATERIALIZED CTE 很大的可能性。但是 30k 行就可以了。

你也可以这样做:

SELECT DISTINCT t_t.tii from t_t
WHERE t_id IN (SELECT t_id FROM t_t_c WHERE c_id IN (
    SELECT c_id FROM t_c WHERE cn=...
))

如果多个 c_id 在 t_t_c 中具有相同的 c_id,这可能会更快,因为 IN() 会删除重复项。如果不是这种情况,那么删除重复项将是浪费时间,所以它可能会更慢。我对结果很感兴趣。

如果上面的查询坚持不使用链接表 t_t_c 上的多列索引,他们肯定应该使用,那么这应该提供一个关于为什么原始查询有如此糟糕计划的线索,问题就变成了,为什么它不使用索引吗?也许是整理问题,或者类型问题,什么的。尝试在 t_c 和 t_t_c 之间加入,看看会发生什么。正如我上面所说,它以错误的顺序连接表是很奇怪的,所以可能存在某个目前不明显的问题,但表现为索引在应该使用的时候无法使用。>

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...