时间序列数据中罕见 SELECT 与频繁 INSERT 的性能

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 上。

jja*_*nes 6

在 上创建索引(item_id, event_time)

它将跳转到指定的 item_id,跳转到该 item_id 的指定 event_time,然后向后移动一个。不涉及排序。


Erw*_*ter 5

相互矛盾的解决方案

您需要像 @jjanes 提供的多列索引。在此过程中,您可以使(item_id, event_time)主键自动提供索引。

但这与@Michael 解释的写入性能相冲突:您将成本加倍,以50K of items ... updated every 1 to 5 minutes使偶尔的 SELECT查询更便宜。大约是1 mio。每小时行数。

分区

如果您没有更多冲突的要求,折衷方案可能是在当前分区还没有索引的情况下进行分区。通过这种方式,您可以获得最高的写入性能和(几乎)最高的读取性能

父表可以是movement_history当前分区movement_history_current。没有索引,只有一个约束允许排除约束。默认情况下可能是每日分区。但时间间隔可以是任意的,甚至不必是有规律的。我们可以使用它并在需要时启动一个新分区。

当您需要在所述查询中包含当前数据时,请执行以下操作:

  1. 要在一个事务中启动一个新分区:

    • 通过附加某项重命名当前分区。到名称,例如movement_history_20150110_20150115(或更具体)并调整 的约束event_time
    • 创建一个具有相同名称的新分区movement_history_current,并event_time对其不与上一个分区重叠且具有开放端的约束进行限制。
    • 根据您的访问模式,您可能必须处理并发写入访问......
  2. 将 PK 添加(item_id, event_time)到 hew 历史分区。不在同一笔交易中。在一个片段中创建索引比增量添加索引要便宜得多。

    2a. 要集成以下第二个查询的建议:

    REFRESH MATERIALIZED VIEW mv_last_movement 
    
    Run Code Online (Sandbox Code Playgroud)
  3. 运行查询。实际上,您可以随时运行查询。如果它包含当前分区或任何还没有索引的分区,则该分区的速度会较慢。

不时地归档最旧的分区。只需备份并删除表即可。不会过多干扰正在进行的操作,这就是分区的美妙之处。

首先阅读手册。对于继承分区有一些注意事项

您的第二个查询

您在编辑中添加的第二个查询对于性能来说是更大的问题。我说的是数量级:

select distinct on (item_id) * from movement_history
order by item_id, event_time desc;
Run Code Online (Sandbox Code Playgroud)

一旦开始插入 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 的约束。

  • 优化表格布局,避免因填充而损失空间