PostgreSQL - 设计数据库以避免频繁更新大型数据集

Fla*_*vio 6 postgresql performance database-design

我们正在尝试使用这些数字和查询要求来优化数据库的性能:

  1. 200-400k 网段由唯一ID标识
  2. 每个网段都有一个状态,其动态属性数量有限(即平均速度)。动态属性的单个状态可以存储在 8 个字节中
  3. 段状态每 3 分钟改变一次,H24,7/7
  4. 是否可以查询特定日期范围内的一组段(有时是所有段)的状态或仅查询实际情况。
  5. 可以请求空间查询以查找特定日期(通常是“现在”)“我周围”的所有段

有了这些必要条件,我们就得出了这个解决方案(有一个很大的缺点,解释如下)。

[A] TABLE main_segments_history( 
      id_segment integer NOT NULL,
      day date NOT NULL,    
      day_slices bigint[],
      CONSTRAINT main_segments_history_pk PRIMARY KEY (id_segment,day)
)

[B] TABLE current_segment_release_state(
      id_segment integer NOT NULL,
      release_date timestamptz,
      ... all other attributes ...
      CONSTRAINT currsegm_release_state_pk PRIMARY KEY (id_segment,release_date)
)
Run Code Online (Sandbox Code Playgroud)

解释[A]表:

  1. 它在字段“day”上使用 partition_manager ( pg_partman)进行分区。每个分区是一个月
  2. day_slices 数组是一个 480 个元素的一维数组,表示全天每个 3 分钟切片的粒度

解释[B]表:

  1. 它只是每个段的当前发布状态

有一个后端进程详细说明了网络。
它每 3 分钟插入或更新每个段的状态。
换句话说,此过程将在一天开始时插入新行,并将每 3 分钟更新一次内部数组。

该解决方案的优点

  1. 每个月分区表的行数有限
  2. 加入线段(即几何)的静态数据时性能良好
  3. 当前版本的小冗余非常好响应实时请求
  4. 空间更安全:每个分区存储约 12 GB的数据(1 个月)-

缺点

  1. 约束_排除。在查询日期范围时,需要使用 PostgreSQL 的该功能/参数。即在跨多个分区的预编译查询中使用常量值。例子:

    约束排除 OK(仅在 2017 年 2 月和 3 月搜索):

    SELECT * FROM main_segments_history 
             WHERE day BETWEEN '2017-01-01' AND '2017-02-03'
    
    Run Code Online (Sandbox Code Playgroud)

    约束排除 KO(将搜索所有分区表):

    SELECT * FROM main_segments_history m 
             JOIN sometable s ON s.id=m.id_segment 
             WHERE day BETWEEN s.day_from AND s.day_to
    
    Run Code Online (Sandbox Code Playgroud)
  2. UPDATE 是邪恶的

由于性能问题,我们关闭了 autovacuum,每晚执行一批。
考虑到通过这种方式,我们每天几乎有 90M 到 190M 的更新,这也是 postgreSQL 完全重写的行数(你当然知道 UPDATE 会标记行已删除,然后插入新行)。

此外,UPDATE 是一项非常耗时的操作,通常会导致写入延迟。

我们首先研究了使用 LINK-TO-DATA 设计的可能性,例如使用一个表作为段 id 的容器,其中包含一个 data_id 到段状态的大表,但是当我们只计算每个行的行数时我们就放弃了要处理的月份:〜2.880.000.000每天〜3GB的空间。不太好。

你有什么想法?你有什么解决方案来优化这个系统吗?

joa*_*olo 2

避免更新表的唯一方法就是执行更新。鉴于您只是记录数据,我建议您使用每日表,在其中存储一天的数据。您可能每天有 190 M(较小的)插入,而不是每天 190 M 更新;有多少段就有多少更新。

 CREATE TABLE main_segments_history
 ( 
       id_segment integer NOT NULL,
       day date NOT NULL,    
       day_slices bigint[],
       CONSTRAINT main_segments_history_pk PRIMARY KEY (id_segment, day)
 ) ;

 CREATE TABLE dayly_segments
 (
      id_segment integer NOT NULL,
      day date NOT NULL,
      id_slice integer NOT NULL,
      slice bigint,
      PRIMARY KEY (id_segment, day, id_slice)
 ) ;
Run Code Online (Sandbox Code Playgroud)

这将模拟一天的数据(以及 200 个分段;而不是您可能拥有的数百万个分段):

 INSERT INTO 
     dayly_segments
     (id_segment, day, id_slice, slice)
 SELECT
     id_segment, '2017-01-01', id_slice, (random()*1e7)::bigint
 FROM
     generate_series (1, 200) AS s1(id_segment)
     CROSS JOIN generate_series (1, 20*24) AS s2(id_slice) ;
Run Code Online (Sandbox Code Playgroud)

我假设当您运行进程时,您有一段时间活动较少vacuuming。这时,您可以将数据从dayly_segments表中移动到 中main_segments_history,并将所有数据放入一个数组中。这样,您就永远不会更新main_segments_history

基本上,这就是您要做的:

 -- Move segments from dayly_segments to main_segment_history
 INSERT INTO
     main_segments_history
     (id_segment, day, day_slices)
 SELECT
     id_segment, day, 
     (SELECT array_agg(slice) 
      FROM (SELECT slice 
              FROM dayly_segments s1 
             WHERE s1.id_segment = s0.id_segment AND s1.day = s0.day 
          ORDER BY id_slice) AS s2)
 FROM
     (SELECT DISTINCT 
         id_segment, day 
     FROM
         dayly_segments s0
     WHERE 
         day = '2017-01-01'
     ) AS s0 ;

 -- Delete them from original
 DELETE FROM
     dayly_segments
 WHERE
     day = '2017-01-01' ;

 -- At this point, you should also...
 VACUUM dayly_segments ;
Run Code Online (Sandbox Code Playgroud)

dbfiddle在这里


假设:

  1. 你永远不会“错过”任何一个$current_value。也就是说,您的阵列中没有漏洞
  2. id_slice可以分配任何递增序列的结果。您也可以使用一个值,而不是整数值time