如何提高我的 Postgres 选择语句的速度?
Posted
技术标签:
【中文标题】如何提高我的 Postgres 选择语句的速度?【英文标题】:How do I increase the speed of my Postgres select statement? 【发布时间】:2016-03-20 13:50:30 【问题描述】:我有以下表格:
CREATE TABLE views (
view_id bigint NOT NULL,
usr_id bigint,
ip inet,
referer_id bigint,
country_id integer,
validated smallint,
completed smallint,
value numeric
);
ALTER TABLE ONLY views
ADD CONSTRAINT "Views_pkey" PRIMARY KEY (view_id);
CREATE TABLE country (
country_id integer NOT NULL,
country character varying(2)
);
ALTER TABLE ONLY country
ADD CONSTRAINT country_pkey PRIMARY KEY (country_id);
CREATE TABLE file_id_view_id (
file_id bigint,
view_id bigint,
created_ts timestamp without time zone
);
CREATE TABLE file_owner (
file_id bigint NOT NULL,
owner_id bigint
);
ALTER TABLE ONLY file_owner
ADD CONSTRAINT owner_table_pkey PRIMARY KEY (file_id);
CREATE TABLE referer (
referer_id bigint NOT NULL,
referer character varying(255)
);
ALTER TABLE ONLY referer
ADD CONSTRAINT referer_pkey PRIMARY KEY (referer_id);
views
和 file_id_view_id
表有大约 340M 行每个。每小时它们都会增加 600K 行。
file_owner
表有 75K 行,每小时增加 100 行。
country
表有 233 行并且很少更改。
referer
表有 6494 行并且很少更改。
我的目标是能够执行如下查询:
SELECT Count(ft.*) AS total_views,
( Count(ft.*) - SUM(ft.valid) ) AS invalid_views,
SUM(ft.valid) AS valid_views,
SUM(ft.values) AS VALUES,
ft.day AS day,
( CASE
WHEN r.referer IS NULL THEN 'Unknown'
ELSE r.referer
END ) AS referer,
( CASE
WHEN c.country IS NULL THEN 'Unknown'
ELSE c.country
END ) AS country
FROM country c
right join (referer r
right join (SELECT v.validated AS valid,
v.value AS VALUES,
vf.day AS day,
vf.view_id AS view_id,
v.referer_id AS referer_id,
v.country_id AS country_id
FROM VIEWS v,
(SELECT view_id,
fivi.created_ts :: timestamp :: DATE AS
day
FROM file_id_view_id fivi
join (SELECT file_id
FROM file_owner
WHERE owner_id = 75
GROUP BY file_id) fo
ON ( fo.file_id = fivi.file_id )
WHERE ( fivi.created_ts BETWEEN
'2015-11-01' AND '2015-12-01' )
GROUP BY view_id,
day) vf
WHERE v.view_id = vf.view_id) ft
ON ( ft.referer_id = r.referer_id ))
ON ( ft.country_id = c.country_id )
GROUP BY day,
referer,
country;
生产:
total_views | invalid_views | valid_views | values | day | referer | country
------------+---------------+-------------+--------+------------+-----------------+---------
当使用EXPLAIN ANALYZE
运行此类查询时,会产生以下结果:
GroupAggregate (cost=38893491.99..40443007.61 rows=182295955 width=52) (actual time=183725.696..205882.889 rows=172 loops=1)
Group Key: ((fivi.created_ts)::date), r.referer, c.country
-> Sort (cost=38893491.99..38984639.97 rows=182295955 width=52) (actual time=183725.655..200899.098 rows=8390217 loops=1)
Sort Key: ((fivi.created_ts)::date), r.referer, c.country
Sort Method: external merge Disk: 420192kB
-> Hash Left Join (cost=16340128.88..24989809.75 rows=182295955 width=52) (actual time=23399.900..104337.332 rows=8390217 loops=1)
Hash Cond: (v.country_id = c.country_id)
-> Hash Left Join (cost=16340125.36..24800637.72 rows=182295955 width=49) (actual time=23399.782..102534.655 rows=8390217 loops=1)
Hash Cond: (v.referer_id = r.referer_id)
-> Merge Join (cost=16340033.52..24051874.62 rows=182295955 width=29) (actual time=23397.410..99955.000 rows=8390217 loops=1)
Merge Cond: (fivi.view_id = v.view_id)
-> Group (cost=16340033.41..16716038.36 rows=182295955 width=16) (actual time=23397.298..30454.444 rows=8390217 loops=1)
Group Key: fivi.view_id, ((fivi.created_ts)::date)
-> Sort (cost=16340033.41..16434985.73 rows=189904653 width=16) (actual time=23397.294..28165.729 rows=8390217 loops=1)
Sort Key: fivi.view_id, ((fivi.created_ts)::date)
Sort Method: external merge Disk: 180392kB
-> Nested Loop (cost=6530.43..8799350.01 rows=189904653 width=16) (actual time=63.123..15131.956 rows=8390217 loops=1)
-> HashAggregate (cost=6530.31..6659.62 rows=43104 width=8) (actual time=62.983..90.331 rows=43887 loops=1)
Group Key: file_owner.file_id
-> Bitmap Heap Scan on file_owner (cost=342.90..6508.76 rows=43104 width=8) (actual time=5.407..50.779 rows=43887 loops=1)
Recheck Cond: (owner_id = 75)
Heap Blocks: exact=5904
-> Bitmap Index Scan on owner_id_index (cost=0.00..340.74 rows=43104 width=0) (actual time=4.327..4.327 rows=45576 loops=1)
Index Cond: (owner_id = 75)
-> Index Scan using file_id_view_id_indexing on file_id_view_id fivi (cost=0.11..188.56 rows=4406 width=24) (actual time=0.122..0.306 rows=191 loops=43887)
Index Cond: (file_id = file_owner.file_id)
Filter: ((created_ts >= '2015-11-01 00:00:00'::timestamp without time zone) AND (created_ts <= '2015-12-01 00:00:00'::timestamp without time zone))
Rows Removed by Filter: 184
-> Index Scan using "Views_pkey" on views v (cost=0.11..5981433.17 rows=338958763 width=25) (actual time=0.088..46804.757 rows=213018702 loops=1)
-> Hash (cost=68.77..68.77 rows=6591 width=28) (actual time=2.344..2.344 rows=6495 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 410kB
-> Seq Scan on referer r (cost=0.00..68.77 rows=6591 width=28) (actual time=0.006..1.156 rows=6495 loops=1)
-> Hash (cost=2.70..2.70 rows=233 width=7) (actual time=0.078..0.078 rows=233 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 10kB
-> Seq Scan on country c (cost=0.00..2.70 rows=233 width=7) (actual time=0.005..0.042 rows=233 loops=1)
Planning time: 1.015 ms
Execution time: 206034.660 ms
(37 rows)
计划explain.depesz.com:http://explain.depesz.com/s/OiN
206s 运行时间。
一些注意事项,
Postgresql 版本 9.4
我已将配置调整如下:
-
shared_buffers = 30GB
work_mem = 32MB
random_page_cost = 2.0
cpu_tuple_cost = 0.0030
cpu_index_tuple_cost = 0.0010
cpu_operator_cost = 0.0005
有效缓存大小 = 52GB
目前存在以下索引:
-
CREATE INDEX country_index ON country USING btree (country);
CREATE INDEX created_ts_index ON file_id_view_id USING btree (created_ts);
使用 btree (created_ts, file_id) 在 file_id_view_id 上创建索引 file_id_created_ts_index;
使用 btree (file_id) 在 file_id_view_id 上创建索引 file_id_view_id_indexing;
使用 btree (file_id, owner_id) 在 file_owner 上创建索引 owner_id_file_id_index;
在 file_owner USING btree (owner_id) 上创建索引 owner_id_index;
CREATE INDEX referer_index ON referer USING btree (referer);
之前的查询使用的是保守地选择的所有者 ID,某些查询可能会导致 1/3 的 file_id_view_id 表正在与 视图 连接。
更改数据结构是最后的手段。在这个阶段,这样的改变肯定是出于严重的考虑。
如果需要,数据库可以被认为是只读的,正在写入的数据是按小时完成的,并且每次写入后都会为 Postgres 提供充足的喘息空间。在 600K 每小时写入的当前时刻,数据库在 1100 秒内返回(这是由于插入成本之外的其他原因)。如果可以提高读取速度,还有很大的空间可以添加额外的索引,读取速度是优先考虑的。
硬件规格为:
CPU:http://ark.intel.com/products/83356/Intel-Xeon-Processor-E5-2630-v3-20M-Cache-2_40-GHz
内存:128GB
存储:1.5TB PCIE SSD
如何优化我的数据库或查询,以便在合理的时间范围内从数据库中检索我需要的信息?
我可以做些什么来优化我当前的设计?
我相信 Postgres 和运行它的硬件有能力比现在表现得更好。
更新
我试过了:
-
分析表格,不影响性能。
增加work_mem,这导致速度增加到116s。
依靠 Postgres 的查询计划器避免子选择,这会对性能产生负面影响。
事先单独查找数据库,这似乎没有正面/负面影响。
有没有人有重组这么大的表的经验?可行吗?需要几天、几小时(当然是估计)?
我正在考虑对数据库进行反规范化,因为它实际上只会在此方法中被引用。我唯一关心的是 - 如果要从具有索引 owner_id 的表中调用 100M 行,它会足够快还是我仍然面临同样的性能问题?不愿走一条路,然后不得不回溯。
我正在研究的另一个解决方案是@ivan.panasuik 建议,将所有一天的数据分组到另一个表中,因为一旦一天过去了,该信息是不变的,不需要更改或更新。但是我不确定如何顺利实现这一点 - 我是否应该在插入暂停时对数据进行查询并尽快赶上这些日子?从那以后有一个触发器设置?
【问题讨论】:
估计值并不那么准确。您analyze
涉及的表格了吗?您还有两个在磁盘上完成的相当大的排序。您可以尝试针对该查询大幅增加 work_mem,例如set work_mem = '512MB'
甚至 set work_mem='1GB'
我的印象是 Postgres 会自动分析表格,我也应该手动分析吗?当您说 那个查询 时,您的意思是有一种特定的方法可以为单个查询设置 work_mem?
它应该自动执行此操作,但有时(例如在初始加载后)它启动速度不够快。我在运行 before 时显示的语句将更改当前会话的 work_mem
:postgresql.org/docs/current/static/sql-set.html
[除了missng统计]我没有看到任何FK约束,我想应该有一些。
@a_horse_with_no_name 我尝试了你的建议,work_mem='1GB'
似乎提高了速度,但不幸的是没有显着提高。
【参考方案1】:
您的数据库的速度通常不是您的硬件,而是您使用引擎本身的智能和功能的程度。
尽量避免子选择 - 特别是在处理大量数据时。这些通常不能由查询计划器优化。在大多数情况下,如果需要,您应该能够将简单的子选择转换为 JOIN 甚至单独的数据库查找。
对您的表进行分区 - PostgreSQL 本身并不这样做(在某种程度上),但如果您经常只访问最近的数据,您可以通过将存档数据移开来减少大量工作。
考虑一种数据仓库策略 - 当您处理大量数据时,您应该考虑以非规范化方式存储数据副本,这种方式可以非常快速地检索,因为已经处理了令人讨厌的 JOIN。我们使用 Redshift(PostgeSQL 的衍生产品)来执行此操作,因此我们在运行报告时不需要执行任何 JOIN。
【讨论】:
我喜欢你对我正在处理的一些东西的第三个建议......保持一个完全爆炸数据的临时缓存与有效存储的数据并行,直到一个合理的窗口过去。谢谢!【参考方案2】:-
删除 (Count(ft.*) - SUM(ft.valid) ) 作为 invalid_views,因为您已经有了这些值,您可以稍后在显示结果期间计算它
在 file_owner.file_id 上添加索引并检查查询中使用的每个字段是否都有索引(您在条件中使用的字段:where、group 等)
我没有对查询进行更多分析,但您似乎应该将查询拆分为几个更小(更快)的查询,并使用临时表或存储过程将其连接起来。
假设昨天的结果不会改变...您可以使用条件 day = today() 运行查询并避免按天分组。您可以将所有天的结果保存在一个单独的表格中。我发现它大部分时间都可以分组。
如果没有尝试和尝试,很难预测优化......所以一一尝试。祝你好运。
【讨论】:
以上是关于如何提高我的 Postgres 选择语句的速度?的主要内容,如果未能解决你的问题,请参考以下文章
提高查询速度:大 postgres 表中的简单 SELECT