Cod*_*007 3 postgresql performance partitioning index-tuning postgresql-performance
我有一个简单的时间序列表
movement_history (
data_id serial,
item_id character varying (8),
event_time timestamp without timezone,
location_id character varying (7),
area_id character varying (2)
);
Run Code Online (Sandbox Code Playgroud)
我的前端开发人员告诉我,如果他想知道某个项目在给定时间戳的位置,那么成本太高了,因为他必须对表格进行排序。他希望我为下一个事件添加另一个时间戳字段,这样他就不必进行排序。然而,这将使我插入新动作的代码成本增加一倍以上,因为我需要查询该项目的前一个条目,更新该条目,然后插入新数据。
我的插入当然远远超过他的查询频率。而且我从未见过包含下一个事件时间条目的时间序列表。他告诉我我的表坏了,因为他不频繁的查询需要排序。有什么建议?
我不知道他在使用什么查询,但我会这样做:
select * from movement_history
where event_time <= '1-15-2015'::timestamp
and item_id = 'H665AYG3'
order by event_time desc limit 1;
Run Code Online (Sandbox Code Playgroud)
我们目前有大约 15,000 个项目,它们最多每天输入一次。然而,我们很快就会有 50K 的项目,其传感器数据每 1 到 5 分钟更新一次。
我没有看到他的查询经常执行,但是另一个获取托盘当前状态的查询将会执行。
select distinct on (item_id) *
from movement_history
order by item_id, event_time desc;
Run Code Online (Sandbox Code Playgroud)
该服务器当前运行的是 9.3,但如果需要,它也可以运行在 9.4 上。
在 上创建索引(item_id, event_time)
。
它将跳转到指定的 item_id,跳转到该 item_id 的指定 event_time,然后向后移动一个。不涉及排序。
您需要像 @jjanes 提供的多列索引。在此过程中,您可以使(item_id, event_time)
主键自动提供索引。
但这与@Michael 解释的写入性能相冲突:您将成本加倍,以50K of items ... updated every 1 to 5 minutes
使偶尔的 SELECT
查询更便宜。大约是1 mio。每小时行数。
如果您没有更多冲突的要求,折衷方案可能是在当前分区还没有索引的情况下进行分区。通过这种方式,您可以获得最高的写入性能和(几乎)最高的读取性能。
父表可以是movement_history
当前分区movement_history_current
。没有索引,只有一个约束允许排除约束。默认情况下可能是每日分区。但时间间隔可以是任意的,甚至不必是有规律的。我们可以使用它并在需要时启动一个新分区。
当您需要在所述查询中包含当前数据时,请执行以下操作:
要在一个事务中启动一个新分区:
movement_history_20150110_20150115
(或更具体)并调整 的约束event_time
。movement_history_current
,并event_time
对其不与上一个分区重叠且具有开放端的约束进行限制。将 PK 添加(item_id, event_time)
到 hew 历史分区。不在同一笔交易中。在一个片段中创建索引比增量添加索引要便宜得多。
2a. 要集成以下第二个查询的建议:
REFRESH MATERIALIZED VIEW mv_last_movement
Run Code Online (Sandbox Code Playgroud)运行查询。实际上,您可以随时运行查询。如果它包含当前分区或任何还没有索引的分区,则该分区的速度会较慢。
不时地归档最旧的分区。只需备份并删除表即可。不会过多干扰正在进行的操作,这就是分区的美妙之处。
您在编辑中添加的第二个查询对于性能来说是更大的问题。我说的是数量级:
Run Code Online (Sandbox Code Playgroud)select distinct on (item_id) * from movement_history order by item_id, event_time desc;
一旦开始插入 1 mio。每小时行数,此查询的性能将很快恶化。您正在处理每个项目的许多行,DISTINCT ON
只适合每个项目的几行。详细解释DISTINCT ON
和更快的替代方案:
我仍然建议像我的第一个答案中那样进行分区。但以合理的间隔强制执行新分区,因此当前分区不会变得太大。
此外,创建一个“物化视图”,跟踪每个项目的最新状态。它不是标准,MATERIALIZED VIEW
因为定义查询具有自引用。我命名它mv_last_movement
,它具有与 相同的行类型movement_history
。
每当新分区启动时刷新(见上文)。
假设存在一个item
表:
CREATE TABLE item (
item_id varchar(8) PRIMARY KEY -- should really be a serial
-- more columns?
);
Run Code Online (Sandbox Code Playgroud)
如果您没有,请创建它。或者使用上面链接的答案中概述的替代递归 CTE 技术。
初始化mv_last_movement
一次:
CREATE TABLE mv_last_movement AS
SELECT m.*
FROM item i
, LATERAL (
SELECT *
FROM movement_history_current -- current partition
WHERE item_id = i.item_id -- lateral reference
ORDER BY event_time DESC
LIMIT 1
) m;
ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);
Run Code Online (Sandbox Code Playgroud)
然后,刷新(在单个事务中!):
BEGIN;
CREATE TABLE mv_last_movement2 AS
SELECT m.*
FROM item i
, LATERAL (
( -- parentheses required
SELECT *
FROM movement_history_current -- current partition
WHERE item_id = i.item_id -- lateral reference
ORDER BY event_time DESC
LIMIT 1 -- applied to this SELECT, not strictly needed but cheaper
)
UNION ALL -- if not found, fall back to latest previous state
SELECT *
FROM mv_last_movement -- your materialized view
WHERE item_id = i.item_id -- lateral reference
LIMIT 1 -- applied to whole UNION query
) m;
DROP TABLE mv_last_movement;
ALTER TABLE mv_last_movement2 RENAME mv_last_movement;
ALTER TABLE mv_last_movement ADD PRIMARY KEY (item_id);
COMMIT;
Run Code Online (Sandbox Code Playgroud)
或者类似的。更多详细信息请参见此处:
上面完全相同的查询(粗体强调)也替换了顶部引用的原始查询。
这样您就不必检查没有当前行的项目的整个历史记录,这将是非常昂贵的。
为什么UNION ALL ... LIMIT 1
?
varchar
对于 PK / FK 列效率很低,特别是对于每小时 1 mio 行的大表。请integer
改用钥匙。
始终对日期和时间戳文字使用 ISO 格式,或者您的查询取决于区域设置:'2015-15-01'
而不是.'1-15-2015'
添加NOT NULL
列不能为 NULL 的约束。
优化表格布局,避免因填充而损失空间
归档时间: |
|
查看次数: |
694 次 |
最近记录: |