问题描述
我有一个相对简单的表结构和查询,但得到的执行计划似乎并不理想。尤其是执行时间让我怀疑。 这是我的表结构:
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 之间加入,看看会发生什么。正如我上面所说,它以错误的顺序连接表是很奇怪的,所以可能存在某个目前不明显的问题,但表现为索引在应该使用的时候无法使用。>