【问题标题】:Postgresql table with one ID column, sorted index, with duplicate primary key具有一个 ID 列、排序索引、具有重复主键的 Postgresql 表
【发布时间】:2013-06-20 07:37:12
【问题描述】:

我想使用 PostgreSQL 表作为文档的一种工作队列。每个文档都有一个 ID,并存储在另一个带有许多附加列的普通表中。但是这个问题是关于为工作队列创建表的。

我想为这个队列创建一个没有 OID 的表,只有一列:文档的 ID 为整数。如果此工作队列表中存在文档的 ID,则表示具有该 ID 的文档是脏的,必须进行一些处理。 额外的表应避免 VACUUM 和死元组问题以及如果主文档表中的每个文档条目上只有一个脏位就会出现的事务死锁。

我系统的许多部分会将文档标记为脏,因此会将要处理的 ID 插入到该表中。这些插入将用于一个事务中的多个 ID。我不想使用任何类型的嵌套事务,并且似乎没有任何类型的 INSERT IF NOT EXISTS 命令。我宁愿在表中有重复的 ID。因此,该表中唯一的列必须可以重复。

处理工作队列的进程将删除所有进程 ID,因此会处理重复项。 (顺便说一句:下一步还有另一个队列,所以关于竞争条件,这个想法应该是干净的并且没有问题)

但我也希望按顺序处理文档:始终应首先处理 ID 较小的文档。

因此,我希望在 ID 列(工作队列表中的唯一列)上有一个帮助 LIMIT 和 ORDER BY 的索引。 理想情况下,我只有一列,这应该是主键。但是主键不能有重复,所以我好像做不到。

没有索引,ORDER BY 和 LIMIT 会很慢。

我可以在该列上添加一个普通的二级索引。但我担心 PostgreSQL 会在磁盘上添加第二个文件(PostgreSQL 会为每个额外的索引执行此操作)并为该表使用双倍的磁盘操作。

最好的做法是什么? 添加一个带有随机内容(如 OID)的虚拟列,以使主键不会抱怨重复?我必须在我的队列表中浪费那个空间吗?

或者添加第二个索引是无害的,它会成为直接在主元组btree中的主索引吗?


我应该删除上面的所有内容并保留以下内容吗?最初的问题让人分心,并且包含太多不相关的信息。

我想在 PostgreSQL 中创建一个具有以下属性的表:

  • 一列有一个整数
  • 允许重复
  • 列上的高效 ORDER BY+LIMIT
  • INSERT 不应对该表或任何类型的唯一索引执行任何查询。 INSERT 应该只找到该表的主文件/主 btree 的最佳页面,并将行插入到其他行之间,按 ID 排序。
  • INSERT 将成批发生,并且不能失败,除非磁盘已满等。
  • 此表不应有额外的 btree 文件,因此没有二级索引
  • 行不应占用太多空间,例如没有 OID

我想不出能解决所有这些问题的解决方案。

我唯一的解决方案是在最后一个要点上妥协:添加一个涵盖整数的 PRIMARY KEY 以及一个虚拟列,如 OID、时间戳或序列。

另一种解决方案是使用假设的 INSERT IF NOT EXISTS,或者嵌套事务或带有 WHERE 的特殊 INSERT。所有这些解决方案都会在插入时添加对 btree 的查询。 它们也可能导致死锁。

(也在这里发布:https://dba.stackexchange.com/q/45126/7788

【问题讨论】:

  • Many parts of the system would insert IDs to process into that table. Therefore duplicates must be possible. 恕我直言,您的想法在这里是错误的:如果 id 不是唯一的,则不是 id。可能{datasource,document_id} 可以作为候选键?
  • 什么样的队列?先进先出? 5行具有相同的ID是什么意思?
  • 非常感谢您提出的非常好的问题!我已经编辑了帖子以使其更清晰。对此感到抱歉。每个文档都有一个包含列和唯一 ID 的适当表格。这里的这个问题是关于一个额外的工作队列表。它本身不是一个队列,而只是一个外包的脏表。具有相同 ID 的 5 行与具有相同 ID 的 1 或 10 行具有相同的含义:它们意味着具有该 ID 的文档是脏的。
  • 一点都不清楚。一个文档可以在队列中多次出现吗? (似乎是的。)但是那是什么意思呢?该文档是否由其他几个进程处理?
  • 为什么你认为没有INSERT IF NOT EXISTS?不使用这些确切的关键字(特别是 IF),但使用 INSERT .. SELECT .. WHERE 编写此功能很容易

标签: postgresql indexing sql-order-by primary-key


【解决方案1】:

你说

我系统的许多部分会将文档标记为脏,因此 将要处理的 ID 插入到该表中。因此必须重复 可能。

具有相同 ID 的 5 行与具有相同 ID 的 1 或 10 行含义相同 相同的 ID:它们意味着具有该 ID 的文档是脏的。

你不需要重复。如果此表的唯一目的是识别脏文档,则包含文档 ID 号的单行就足够了。没有令人信服的理由允许重复。

如果您需要跟踪哪个进程插入了该行,或者在插入行时对行进行排序,则每个 ID 号只有一行是不够,但单列不足以 首先。所以我确信主键约束或唯一约束对你来说很好。

其他进程必须忽略重复键错误,但这很简单。无论如何,这些进程都必须捕获错误——除了重复键之外,还有很多事情会阻止插入语句成功。


允许重复的实现。 . .

create table dirty_documents (
  document_id integer not null
);

create index on dirty_documents (document_id);

在该表中插入 100k ID 号以进行测试。这必然需要更新索引。 (Duh。)包括一堆重复。

insert into dirty_documents 
select generate_series(1,100000);

insert into dirty_documents
select generate_series(1, 100);

insert into dirty_documents
select generate_series(1, 50);

insert into dirty_documents
select generate_series(88000, 93245);

insert into dirty_documents
select generate_series(83000, 87245);

在我的桌面上用了不到一秒钟,这没什么特别的,它正在运行三个不同的数据库服务器、两个 Web 服务器,并播放一张 Rammstein CD。

选择第一个脏文档ID号进行清理。

select min(document_id) 
from dirty_documents; 

document_id
--
1

只用了 0.136 毫秒。现在让我们删除文档 ID 为 1 的每一行。

delete from dirty_documents
where document_id = 1; 

耗时 0.272 毫秒。

让我们重新开始。

drop table dirty_documents;
create table dirty_documents (
  document_id integer primary key
);

insert into dirty_documents 
select generate_series(1,100000); 

耗时 500 毫秒。让我们再次找到第一个。

select min(document_id) 
from dirty_documents; 

耗时 0.054 毫秒。这大约是使用允许重复的表所用时间的一半。

delete from dirty_documents
where document_id = 1;

还花了 0.054 毫秒。这比另一张桌子快大约 50 倍。

让我们重新开始,尝试一个未索引的表。

drop table dirty_documents;
create table dirty_documents (
  document_id integer not null
);

insert into dirty_documents 
select generate_series(1,100000);

insert into dirty_documents
select generate_series(1, 100);

insert into dirty_documents
select generate_series(1, 50);

insert into dirty_documents
select generate_series(88000, 93245);

insert into dirty_documents
select generate_series(83000, 87245);

获取第一个文档。

select min(document_id) 
from dirty_documents; 

耗时 32.5 毫秒。删除那些文件。 . .

delete from dirty_documents
where document_id = 1;

耗时 12 毫秒。

所有这些都花了我 12 分钟。 (我使用了秒表。)如果您想了解性能如何,请构建表格并编写测试。

【讨论】:

  • 我不想跟踪哪个进程将其标记为脏。它就像一面旗帜。捕获重复的键错误有多简单?我不想为每个 INSERT 启动一个嵌套事务。我想要将行插入表的原始性能,我不希望 PostgreSQL 检查或更新任何索引!问题是 PostgreSQL 是否不能以某种方式检查重复项(如 PRIMARY KEY 所做的那样),但无论如何都将其用作磁盘上的主要顺序以将数据存储在主 betreff 中(如 PRIMARY KEY 所做的那样)
  • 一方面,你说,“因此我想要一个索引……”另一方面,你说“我不想让 PostgreSQL 检查或更新任何索引”。你想要哪一个?您可以在 any 列上创建索引:CREATE INDEX ON table_name (column_name); 但如果这样做,PostgreSQL 将必须在插入行时更新索引,并且在删除行时必须更新索引行。
  • 实际上两者都可以:PostgreSQL 将所有行存储在 btree 中。我希望该 btree 按 ID 排序。我相信主 KEY 正是这样做的,但会强制对我进行重复检查。所以我希望对主 btree 进行排序,以便 ORDER BY+LIMIT 快速。
  • “没有过早的优化。” 如果您还没有编写和测试过任何代码,就会出现这种情况。差不多就是这样的定义。
  • @Christian 似乎想要的是一个索引组织表。目前 PostgreSQL 不支持此功能。我不相信这实际上是这里真正潜在问题的正确答案,但这大致就是您所描述的。
【解决方案2】:

字里行间,我认为您正在尝试实施工作排队系统。

停下来。现在。

工作排队很困难。关系 DBMS 中的工作排队非常困难。人们提出的大多数“聪明”解决方案最终都会在他们没有意识到的情况下序列化锁上的工作,或者他们在并发操作中存在令人讨厌的错误。

使用现有的消息/任务排队系统。 ZeroMQ、RabbitMQ、PGQ 等等等等等等。有很多可供选择,它们具有(a)工作和(b)高效的显着优势。您很可能需要运行外部辅助进程或服务器,但关系数据库模型的局限性往往使这成为必要。

您似乎设想的方案,尽我所能猜测,听起来它在故障处理、插入/删除竞争等方面会遇到无可救药的并发问题。真的,不要 /em> 尝试自己设计,尤其是当您对潜在的并发性和性能问题没有真正掌握时。

【讨论】:

  • 这个队列只有一个消费者,所以在接收端不应该有太多的并发问题。此外,接收方不会收到要处理的数据的真正“工作”,只处理脏文档。我非常感谢您得到我想要的(索引组织表),并且您在 dba stackexchange 上链接的问题表明您确实了解并发事务的详细信息(实际发生的情况)。我认为您总体上是对的,但是添加其中一个新系统会在复杂性、RAM 使用和 fsync-churn 方面给整个系统增加很多复杂性。
  • @Christian 即使在一个只有一个消费者(甚至只有一个生产者)的系统中,你的方法仍然存在并发问题;特别是我担心当文档再次被弄脏时你会丢失脏标志,就像某些东西完成处理它一样。不过,可以肯定的是,这一切都是手摇和不明确的。我认为您自己尝试这样做是错误的。使用经过测试且有效的现有解决方案。
  • 竞争条件问题很明显并且会造成很多麻烦,但是从问题来看:“(顺便说一句:下一步还有另一个队列,所以关于竞争条件,这个想法应该是干净的没问题)”(第 1 步中的比赛是无害的)。我承认 PostgreSQL 不太适合这种用法,我应该使用 MQ 解决方案,但恐怕我现在负担不起,也许在以后的一轮系统更改中。我也不知道如何将插入队列与真实数据库中的其他更新同步,他们肯定不能共享一个事务/WAL/fsync。
  • @CraigRinger:排队系统是否支持“任意id号任意次数,最低id号出”的模式?
  • @MikeSherrill'Catcall' 我发现这有点不明确。如果作者的意思是“FIFO”,那么是的。如果他们真的意味着“最低 ID 优先”,那么对于我合作过的任何人,我都不能肯定地说……但是“最低 ID 优先”又是一个有点奇怪的策略。
猜你喜欢
  • 2019-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-04
  • 2018-07-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多