如何避免数据碎片在" INSERT一次,更新一次"表?

时间:2017-05-09 18:14:47

标签: sql postgresql mvcc

我有大量的表" INSERT一次",然后是只读的表。 ie:在记录的初始INSERT之后,永远不会有任何UPDATEDELETE语句。因此,磁盘上表的数据碎片很少。

我现在考虑在每个表中添加needs_action布尔字段。此字段只会更新一次,并且会在慢速/定期的基础上完成。作为MVCC的结果,当VACUUMUPDATE之后出现(时间更慢)时,表格变得非常碎片化,因为它清除了最初插入的元组,随后它们被新的回填插入。

简而言之:添加此单曲"始终更新一次"字段将表格从设计上最小化分割,到设计高度分散。

是否有一些有效实现此单needs_action记录标记的方法可以避免产生的表碎片?

< 现在有一些背景/补充信息...... >

到目前为止考虑的一些选项......

冒着使这个问题变得庞大(因此被忽视?)的风险,下面是目前已经考虑过的一些选项:

  1. 只需将列添加到每个表格中,执行UPDATE并且不要担心产生的碎片,直到它确实被证明是一个问题。

    • 我意识到这里的过早优化,但是随着一些表变大(> 1M,甚至> 1B),我宁愿预先设计好。
  2. 创建一个独立的跟踪表(对于每个表),只包含A)来自主表的PK,以及B)needs_action标志。使用主表中的AFTER INSERT触发器在跟踪表中创建记录

    • 这将仅保留" INSERT"主表上的最小碎片级别...以增加(重大?)前期写入开销为代价
    • 将跟踪表放在单独的模式中也可以巧妙地将功能与核心表分开
  3. 强制needs_action字段为HOT更新以避免元组复制

    • WHERE needs_action = TRUE上需要一个索引似乎排除了这个选项,但也许有另一种方法可以快速找到它们?
  4. 使用表填充因子(50?)为不可避免的UPDATE

    留出空间
    • 例如:将fillfactor设置为50以留出UPDATE的空间,因此将其保留在同一页面中
    • 但是......只有一个UPDATE,这似乎会使表包装分数永远保持在50%并占用两倍的存储空间?我还没有100%理解这个选项......还在学习。
  5. 在主表记录中查找特殊/魔法字段/位,可以在没有MVCC影响的情况下进行翻转。

    • 这似乎不存在于postgres中。即使它确实如此,也需要将其编入索引(或者使用类似于WHERE needs_action = TRUE部分索引的其他快速查找机制)
    • 能够选择性地抑制特定列上的MVCC操作似乎在这里会很好(虽然肯定充满了危险)
  6. 在postgres之外存储needs_action (例如:作为<table_name>:needs_copying redis中的PK列表)以避免因mvcc而导致的碎片。

    • 我担心保持这种原子性。也许在redis_fdw触发器中使用AFTER INSERT(或其他一些fdw?)可以保持原子状态?我需要了解更多有关fdw功能的信息......看起来我能找到的所有fdw都是只读的。
  7. 运行带有背景碎片整理/压缩的精美视图,如this fantastic article

    中所述
    • 对于所有桌子来说,似乎有点太多了。
  8. 只需跟踪需要在postgres表中复制的ID / PK

    • 只需将需要操作的存储ID作为快速惰性表的记录(例如:无PK),并在操作完成时DELETE记录
    • 类似于RPUSH离线redis列表(但肯定是 ACID)
    • 这似乎是目前最好的选择。
  9. 还有其他选择吗?

    有关推动此问题的具体用例的更多信息......

    我对如何避免这种碎片的一般情况感兴趣,但在目前的用例中还有更多内容:

    1. 读取性能比所有表上的写入性能更重要(但避免疯狂的慢速写入显然是可取的)
    2. 有些表会达到数百万行。有些可能会达到数十亿行。
    3. SELECT查询将跨越广泛的表格范围(不仅仅是最近的数据),范围从单个结果记录到100k +
    4. 表设计可以从头开始......无需担心现有数据
    5. PostgreSQL 9.6

2 个答案:

答案 0 :(得分:1)

您可以尝试使用继承的表。此方法不直接支持表的PK,但可以通过触发器解决。

CREATE TABLE data_parent (a int8, updated bool); 
CREATE TABLE data_inserted (CHECK (NOT updated)) INHERITS (data_parent);
CREATE TABLE data_updated (CHECK (updated)) INHERITS (data_parent);



CREATE FUNCTION d_insert () RETURNS TRIGGER AS $$
BEGIN
    NEW.updated = false;
    INSERT INTO data_inserted VALUES (NEW.*);

    RETURN NULL;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER d_insert BEFORE INSERT ON data_parent FOR EACH ROW EXECUTE PROCEDURE d_insert();


CREATE FUNCTION d_update () RETURNS TRIGGER AS $$
BEGIN
    NEW.updated = true;
    INSERT INTO data_updated VALUES (NEW.*);
    DELETE FROM data_inserted WHERE (data_inserted.*) IN (OLD);

    RETURN NULL;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER d_update BEFORE INSERT ON data_inserted FOR EACH ROW EXECUTE PROCEDURE d_update();


-- GRANT on d_insert to regular user
-- REVOKE insert / update to regular user on data_inserted/updated


INSERT INTO data_parent (a) VALUES (1);


SELECT * FROM ONLY data_parent;
SELECT * FROM ONLY data_inserted;
SELECT * FROM ONLY data_updated;



INSERT 0 0
 a | updated 
---+---------
(0 rows)

 a | updated 
---+---------
 1 | f
(1 row)

 a | updated 
---+---------
(0 rows)


UPDATE data_parent SET updated = true;

SELECT * FROM ONLY data_parent;
SELECT * FROM ONLY data_inserted;
SELECT * FROM ONLY data_updated;


UPDATE 0
 a | updated 
---+---------
(0 rows)

 a | updated 
---+---------
(0 rows)

 a | updated 
---+---------
 1 | t
(1 row)

答案 1 :(得分:1)

我只想将fillfactor降低到默认值100以下。

根据行的大小,使用80或90之类的值,这样一些新行仍然适合该块。更新后,旧行可以被“修剪”并通过下一个事务进行碎片整理,以便可以重用该空间。

50的值似乎太低了。没错,这会为同时更新的块中的所有行留出空间,但这不是你的用例,对吗?