一次(保证)更改后索引对“状态”字段的影响

Wes*_*ork 3 postgresql index queue event query-performance

介绍

我有一个 PostgreSQL 表设置作为队列/事件源。

我非常希望保留事件的“顺序”(即使在处理队列项之后)作为 e2e 测试的来源。

我开始遇到查询性能下降的问题(可能是因为表膨胀),并且我不知道如何有效地查询不断变化的键上的表。

初始设置

Postgres:v15

表DDL

CREATE TABLE eventsource.events (
    id serial4 NOT NULL,
    message jsonb NOT NULL,
    status varchar(50) NOT NULL,
    createdOn timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT events_pkey PRIMARY KEY (id)
);
CREATE INDEX ON eventsource.events (createdOn)
Run Code Online (Sandbox Code Playgroud)

抓取查询(伪代码)

BEGIN;  -- Start transaction

SELECT message, status
FROM eventsource.events ee
WHERE status = 'PENDING'
ORDER BY ee.createdOn ASC
FOR UPDATE SKIP LOCKED
LIMIT 10;  -- Get the OLDEST 10 events that are pending
-- I found that having a batch of work items was more performant than taking 1 at a time.

...
-- The application then uses the entries as tickets for doing work as in "I am working on these 10 items, no one else touch"
...
UPDATE ONLY eventsource.events SET status = 'DONE' WHERE id = $id_1
UPDATE ONLY eventsource.events SET status = 'DONE' WHERE id = $id_2
UPDATE ONLY eventsource.events SET status = 'FAIL' WHERE id = $id_3
UPDATE ONLY eventsource.events SET status = 'DONE' WHERE id = $id_n
...
END; -- finish transaction
Run Code Online (Sandbox Code Playgroud)

粗工大纲

多个工作人员从队列中取出一批工作项目,然后对它们进行操作并报告其状态。我希望尽可能少的重叠。

排队草图的粗略工作

评估

查看执行计划时,查询似乎必须遍历整个表才能获取处于“PENDING”状态的记录。

我想这可能是因为一ORDER BY ee.createdOn ASC开始的原因。但在查看执行计划后,我发现查询正在遍历整个表来搜索status,然后才对其进行排序。

试图

我看到了部分索引,希望它可以减少查询的搜索空间。

CREATE INDEX ON eventsource.events (status)
WHERE status = 'PENDING'
Run Code Online (Sandbox Code Playgroud)

但我想我让事情变得更糟了......

记录以“PENDING”状态插入,然后几乎立即更改为“DONE”(或“FAIL”),因为应用程序正在消耗队列。我认为这可能每次都会破坏索引,然后在更新字段后从头开始重新创建它status(可能非常昂贵)。

问题

更新部分索引的键/谓词有什么影响(如果很重要)我如何有效地根据变化的键过滤大表?

指数法

我的索引方法合理吗?

我的第一个想法是索引,但也许分区更适合这里?
如果分区键更改会发生什么?
它和破坏索引一样具有破坏性吗?

指数类型

我知道默认索引类型是 B 树,在这种情况下 HASH 索引(或其他索引)会更好吗?

在幕后,更改 HASH 索引的索引键是否会导致破坏/重新创建索引表,就像使用 B 树一样?

索引创建

我不确定部分索引的键与谓词的效果是什么。索引之间的有效区别是什么:

CREATE INDEX ON eventsource.events (status)
WHERE status = 'PENDING'
Run Code Online (Sandbox Code Playgroud)

CREATE INDEX ON eventsource.events (createdOn)
WHERE status = 'PENDING'
Run Code Online (Sandbox Code Playgroud)

我在这里使用它是createdOn因为它在我的抓取查询中,但我认为id也可以工作。

将索引键移动到不同的字段会影响索引创建/重新创建吗?在本例中,我将其从status字段(会发生变化)移至字段createdOn,而字段不会发生变化。我不太明白这意味着什么
我对Postgres文档对这种类型的部分索引有点不清楚。

Erw*_*ter 5

不要使用timestamp( without time zone)

你的整个设置很容易失败:

CREATE TABLE eventsource.events (
    ...
    createdOn timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -- !
    ..
Run Code Online (Sandbox Code Playgroud)

CURRENT_TIMESTAMP(又名now())返回timestamptz,而不是timestamp

如果偶然、意外或恶意意图,任何会话设置了不同的值timezone,然后根据列默认值插入一行,您将得到不同的(错误的)本地时间,从而破坏排序顺序。而且你很难找出原因。不要这样做。尤其是没有这样的默认列。(LOCALTIMESTAMP遇到同样的问题:也取决于当前的timezone设置。)

有关的:

更好的表定义

CREATE TABLE eventsource.event (
  event_id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY
, message     jsonb NOT NULL
, status      text NOT NULL CHECK (status = ANY ('{PENDING,DONE,FAIL}'::text[]))  -- more?
, created_on  timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP  -- !!!
);
Run Code Online (Sandbox Code Playgroud)

如果可能,请使用合法的小写标识符。看:

使用text并添加CHECK约束来强制执行合法状态。

IDENTITYserial比现代 Postgres更可取。看:

最重要的是timestamptz按照顶部的说明使用。所有其他要点仅是建议。

更好的指数

使用部分索引,正如Charlieface已经建议的那样:

CREATE INDEX ON eventsource.event (created)
WHERE status = 'PENDING';
Run Code Online (Sandbox Code Playgroud)

对于您的用例来说,它的尺寸要小得多,并且提供排序的行。小索引的维护成本也较低。然而,会有大量的流失,因此索引会很快膨胀。看:

考虑autovacuum对表进行激进的设置。喜欢:

ALTER TABLE eventsource.event SET (autovacuum_vacuum_scale_factor = 0.03);
Run Code Online (Sandbox Code Playgroud)

的全局默认autovacuum_vacuum_scale_factor值为0.2。意思是,autovacuum在 20% 的表行 + autovacuum_vacuum_threshold(默认为 50)更改后触发。如果桌子很大,那么对于您的目的来说可能太懒了。在增加的维护成本和提高的查询性能之间找到平衡。

(created_on)为了其他目的,您可能需要也可能不需要完整的索引。

更好的方法

假设:

  • 当前的 Postgres 15。
  • 可以存在并发写入(和/或并发锁)。
  • 您想要处理尚未处理的最旧的行。(并且没有被另一个会话同时处理。)
  • 大多数情况下,申请过程都会成功。
BEGIN;  -- !!!

UPDATE eventsource.event
SET    status = 'DONE' 
WHERE  event_id = (
         SELECT event_id
         FROM   eventsource.event
         WHERE  status = 'PENDING'
         ORDER  BY created_on
         LIMIT  1
         FOR    UPDATE SKIP LOCKED  -- !!!
         )
RETURNING *;  -- or just what you need!

-- The application then processes the entries returned by the query and will then update them

-- ONLY in case of a failure !!!
-- Else just skip this:
UPDATE eventsource.event
SET    status = 'FAIL' 
WHERE  event_id = $id_3;  -- your failed ID

COMMIT;
Run Code Online (Sandbox Code Playgroud)

这种方法在并发写入负载下可靠地工作,并且永远不会阻塞。它在每个会话中仅锁定一行,从而最大限度地减少出现复杂情况的可能性。它立即锁定并更新行,这比稍后锁定和更新要快。在极少数发生失败的情况下,您需要进行第二次更新。但相比之下,这很便宜。

如果您需要(或只是想)锁定和处理多行,则可以以类似的方式工作。看: