我正在开发一个小型ad-hoc复制框架(用于reatil公司),它只复制某些表(约200个)的某些记录,具体取决于特定的域级逻辑。
为了了解每个目标主机的每条记录的复制状态,我有一个 repStatus 字符(NUMBER_OF_HOSTS)类型列;主机总是代表相同的位置。
此列的每个位置的值可以是 0 (无操作), 1 (复制记录 >), 2 (记录复制), 3 (确认后重新发送), A (第一次出错), B (第二次出错)......等等。
例如: 012A 表示:
这看起来非常简单,并且有一个“直接阅读”:为了了解记录的状态,我只需阅读 repStatus 列。 / p>
但是,看起来这种方法会导致performance problems when the application has to look for the target records to replicate。
所以我确信有一个更好的设计可以在性能方面解决这个问题。也许引用表,记录和主机的附加表可以是一个解决方案:
CREATE TABLE repStatus (tableID int, recordID int, targetHostID int, status int);
现在,状态值甚至可以归一化为新表格。但是,每个表有200个表* ~500000个记录,可能是单个表中任意处理的相当大的行数。
欢迎任何基于经验的替代方案。
答案 0 :(得分:2)
因此,您的典型查询是将所有记录复制到主机x ...对于此特定目标,具有repStatus 1 或 3 。 (做出假设,因为那不在问题中。)
要复制的记录是罕见的案例,因为通常情况下,大多数记录已经被复制了,对吗? (更多假设。)
表达式上的部分索引可能是一个非常快速的解决方案。
如果您只是保持为每行添加文本字符串的设计,您可以为这些行创建每个目标的部分索引:
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字节的额外整数列可以占用空间,否则会丢失到填充(索引大小不会增长)。如果你有用它,只包括它(或另一个小列)。
如果我的假设成立,整个想法才适用。我在text-array + GIN索引上提出这条路线的原因是3倍:
很多较小的列大小。比较:
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行。很多。
每个查询的索引小得多,访问速度也快 虽然部分索引的总和将略多于一个完整的GIN索引 - 如果你想要覆盖整个表,但情况并非如此如果我们只需要涵盖罕见的情况。无论哪种方式,每个查询所需的索引将多更小。考虑到它们的大小,我不希望缓存索引。这更加重视这一点
写入操作更便宜。我希望简单的小型b树索引的更新对于小的部分索引来说要快得多,因为GIN在这方面有问题。不过,这必须经过验证。
答案 1 :(得分: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[];
如果这有助于您的使用案例,您可以在rep_status_array
上放置一个GIN索引,但最好查看您的查询并专门为您使用的内容创建索引:
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万行。如果您使用专用的复制相关表,则每个主机始终可以partition。 (按表分区对我来说没什么意义;它似乎比在每个上游行存储复制状态更糟糕。)分区的方式或者它是否合适完全取决于您打算询问数据库的问题类型,以及基表上发生了什么样的活动。 (分区意味着维护许多较小的blob而不是几个较大的blob,并且可能访问许多较小的blob来执行单个操作。)这实际上是选择何时希望磁盘发生的问题。