优化大表最新行的查询性能

use*_*300 3 sql postgresql indexing performance postgresql-performance

我有一张大桌子:

CREATE TABLE "orders" (
"id" serial NOT NULL,
"person_id" int4,
"created" int4,
CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
);
Run Code Online (Sandbox Code Playgroud)

90% 的请求与 过去 2-3 天的订单有关person_id,例如:

select * from orders
where person_id = 1
and created >= extract(epoch from current_timestamp)::int - 60 * 60 * 24 * 3;
Run Code Online (Sandbox Code Playgroud)

我怎样才能提高性能?

我了解分区,但是现有行呢?看起来我需要INHERITS每 2-3 天手动创建一次表。

Erw*_*ter 6

带有伪条件的部分多列索引会有所帮助(很多)。需要不时重新创建以保持性能。(person_id, created)IMMUTABLE

请注意,如果您的表不是很大,您可以很大程度上简化并使用普通的多列索引。
或者考虑Postgres 12 或更高版本中的表分区(该功能最终成熟)。

原始函数提供 3 天或更多天前的恒定时间点(在您的情况下由 unix 纪元表示):

CREATE OR REPLACE FUNCTION f_orders_idx_start()
  RETURNS int LANGUAGE sql IMMUTABLE PARALLEL SAFE COST 1 AS
'SELECT 1387497600';
Run Code Online (Sandbox Code Playgroud)

PARALLEL SAFE仅适用于 Postgres 10 或更高版本。
1387497600是以下结果:

SELECT extract(epoch from now())::integer - 259200;
-- 259200 being the result of 60 * 60 * 24 * 3
Run Code Online (Sandbox Code Playgroud)

您的部分索引基于此伪IMMUTABLE条件:

CREATE INDEX orders_created_recent_idx ON orders (person_id, created)
WHERE created >= f_orders_idx_start();
Run Code Online (Sandbox Code Playgroud)

您的查询基于相同的条件:

SELECT *
FROM   orders
WHERE  person_id = 1
AND    created >= f_orders_idx_start()  -- match partial idx condition
AND    created >= extract(epoch from now())::integer - 259200;  -- actual condition
Run Code Online (Sandbox Code Playgroud)

该行AND created >= f_orders_idx_start()看起来多余,但有助于说服 Postgres 使用部分索引。

不时重新创建函数和索引的函数。可能每晚都有一个 cron 作业:

CREATE OR REPLACE FUNCTION f_orders_reindex_partial()
  RETURNS void AS
$func$
DECLARE
   -- 3 days back, starting at 00:00
   _start int := extract(epoch from now()::date -3)::int;
BEGIN       
   IF _start = f_orders_idx_start() THEN
      -- do nothing, nothing changes.
   ELSE
      DROP INDEX IF EXISTS orders_created_recent_idx;
      -- Recreate IMMUTABLE function
      EXECUTE format('
         CREATE OR REPLACE FUNCTION f_orders_idx_start()
           RETURNS int LANGUAGE sql IMMUTABLE PARALLEL SAFE COST 1 AS
         $$SELECT %s $$'
       , _start
      );
      -- Recreate partial index
      CREATE INDEX orders_created_recent_idx ON orders (person_id, created)
      WHERE created >= f_orders_idx_start();
   END IF;    
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

然后,要重新设置索引基数,请调用(理想情况下只有很少或没有并发负载):

SELECT f_orders_reindex_partial();  -- that's all
Run Code Online (Sandbox Code Playgroud)

如果您无法承受由于并发负载而删除并重新创建索引的代价,请考虑REINDEX CONCURRENTLY使用 Postgres 12 或更高版本。这非常简单:

REINDEX INDEX orders_created_recent_idx;
Run Code Online (Sandbox Code Playgroud)

即使您从未调用此函数,所有查询都会继续工作。随着部分索引的增长,性能会随着时间慢慢恶化。

我正在成功地使用这个机制来处理几个大表和类似的要求。非常快。

对于 Postgres 9.2 或更高版本,如果您的表只有很少的小列,并且表写入量不大,那么将其设为覆盖索引可能是值得的:

CREATE INDEX orders_created_recent_idx ON orders (person_id, created, id)
WHERE created >= f_orders_idx_start();
Run Code Online (Sandbox Code Playgroud)

在 Postgres 11 或更高版本中,您可能想改用INCLUDE

CREATE INDEX orders_created_recent_idx ON orders (person_id, created) INCLUDE (id)
WHERE created >= f_orders_idx_start();
Run Code Online (Sandbox Code Playgroud)