2

我正在开发一个小型的临时复制框架(对于一家 reatil 公司),它根据特定的域级逻辑仅复制某些表的某些记录(大约 200 个)。

为了知道每个目标主机的每条记录的复制状态,我有一个repStatus 字符(NUMBER_OF_HOSTS)类型的列;其中主机始终代表相同的位置。

该列每个位置的值可以是0无操作)、1复制记录)、2复制记录)、3确认后重新发送)、A第一次错误)、B第二次错误).. 。 ETC。

例如:012A表示:

  • 不要向主机 1 发送任何内容
  • 将此记录发送给主机 2
  • 在主机 3 中正确接收记录
  • 从主机 3 收到错误

这看起来很容易和简单,并且具有“直截了当的阅读”:为了了解记录的状态,我只需阅读repStatus列。

但是,当应用程序必须查找要复制的目标记录时,这种方法似乎会导致性能问题。

所以我确信有更好的设计来解决这个问题。也许一个引用表、记录和主机的附加表可能是一个解决方案:

CREATE TABLE repStatus (tableID int, recordID int, targetHostID int, status int);

现在甚至可以将状态值标准化为新表。但是,200 个表 * 每个表约 500000 条记录可能是在单个表中以任何方式处理的大量行。

欢迎任何基于经验的替代方案。

4

2 回答 2

2

因此,您的典型查询是将所有记录复制到主机 x ... 对于此特定目标具有 repStatus 13。(做出假设,因为这不是问题。)

而且要复制的记录很少见,因为通常大多数记录都已经复制了,对吗?(更多假设。)

表达式的部分索引对您来说可能是一个非常快速的解决方案。

如果您只是保留向每一行添加文本字符串的设计,则可以为每个目标创建一个部分索引,如下所示:

CREATE INDEX tbl_rep_part1_idx ON tbl (tbl_id, substr(repstatus,1,1))
WHERE substr(repstatus,1,1) = '1' OR
      substr(repstatus,1,1) = '3';


CREATE INDEX tbl_rep_part2_idx ON tbl (tbl_id, substr(repstatus,2,1))
WHERE substr(repstatus,2,1) OR
      substr(repstatus,2,1);

...

由于每个索引的开销,所有部分索引的总和仅大于完整索引。在对表进行写操作时,只需更新受影响的部分索引。

将使这些查询非常快:

SELECT * FROM tbl WHERE substr(repstatus,1,1) = '1';

SELECT * FROM tbl WHERE substr(repstatus,1,1) = '1' OR
                        substr(repstatus,1,1) = '3';

索引中的添加tbl_id是可选的。我添加它是因为一个额外的 4 字节整数列可以利用空间,否则会因填充而丢失(索引大小不会增长)。如果您有使用它,请仅包含它(或另一个小列)。

与文本数组 + GIN 索引相比会发生什么?

只有当我的假设成立时,整个想法才适用。我在 text-array + GIN 索引上提出这条路线的原因是 3 倍:

  1. 小得多的列大小。比较:

    SELECT pg_column_size('{A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z}'::text[])  -- 232 byte
          ,pg_column_size('{A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z}'::"char"[])  -- 50 byte
          ,pg_column_size('ABCDEFGHIJKLMNOPQRSTUVWXYZ'::text);  -- 27 byte
    

    这对于 200 x 500k 行很重要。很多。

  2. 每个查询的索引更小,访问速度更快。
    虽然部分索引的总和将比单个完整的 GIN 索引多一些 -如果您想覆盖整个表,如果我们只需要覆盖极少数情况,情况就不是这样了。无论哪种方式,每个查询所需的索引都会小得多。考虑到它们的大小,我不希望索引被缓存。这更加强调了这一点

  3. 更便宜的写入操作。我希望对于小的部分索引来说,简单、小的 b 树索引的更新会大大加快,因为 GIN 在这方面的麻烦是众所周知的。不过,这必须得到验证。

于 2012-11-02T16:06:09.667 回答
2

好吧,我要做的第一件事就是将 icky 字符串解析放到任何地方,并将其替换为 PostgreSQL 本机类型。将复制状态存储在与当前解决方案类似的每条记录上:

CREATE TYPE replication_status AS ENUM (
  'no_action',
  'replicate_record',
  'record_replicated',
  'error_1',
  'error_2',
  'error_3'
  );
ALTER TABLE t ADD COLUMN rep_status_array replication_status[];

这会花费您更多的存储空间——枚举值是 4 个字节而不是 1,并且数组有一些开销。但是,通过向数据库教授您的概念而不是隐藏它们,您可以编写如下内容:

-- find all records that need to be replicated to host 4
SELECT * FROM t WHERE rep_status_array[4] = 'replicate_record';

-- find all records that contain any error status
SELECT * FROM t WHERE rep_status_array &&
  ARRAY['error_1', 'error_2', 'error_3']::replication_status[];

如果这对您的用例有帮助,您可以直接放置 GIN 索引rep_status_array,但最好查看您的查询并专门为您使用的内容创建索引:

CREATE INDEX t_replication_host_4_key ON t ((rep_status_array[4]));
CREATE INDEX t_replication_error_key ON t (id)
  WHERE rep_status_array && ARRAY['error_1', 'error_2', 'error_3']::replication_status[];

也就是说,给定 200 个表,我很想将其拆分为一个复制状态表——一行包含状态数组或每台主机一行,具体取决于其余复制逻辑的工作方式。我仍然会使用该枚举:

CREATE TABLE adhoc_replication (
  record_id bigint not null,
  table_oid oid not null,
  host_id integer not null,
  replication_status status not null default 'no_action',
  primary key (record_id,table_oid,host_id)
  );

PostgreSQL 在内部为每个表分配一个 OID (try SELECT *, tableoid FROM t LIMIT 1),它是单个数据库系统中方便的稳定数字标识符。换句话说,如果表被删除并重新创建,它会发生变化(例如,如果您转储和恢复数据库,可能会发生这种情况),出于同样的原因,开发和生产之间很可能会有所不同。如果您希望在添加或重命名表时使用这些情况来换取中断,请使用枚举而不是 OID。

将单个表用于所有复制将允许您轻松地重用触发器和查询等,从而将大多数复制逻辑与其复制的数据解耦。它还允许您通过引用单个索引来查询所有原始表中给定主机的状态,这可能很重要。

至于表大小,PostgreSQL 绝对可以处理同一张表中的 1000 万行。如果您使用专用的复制相关表,则始终可以按主机分区。(按表分区对我来说毫无意义;它似乎比在每个上游行上存储复制状态更糟糕。)分区的方式或是否合适完全取决于您打算向数据库提出什么样的问题,以及基表上发生了什么样的活动。(分区意味着维护许多较小的 blob 而不是几个大的 blob,并且可能会访问许多较小的 blob 以执行单个操作。)实际上是选择您希望磁盘何时进行搜索的问题。

于 2012-11-02T17:06:09.633 回答