使用多个表中的列提高 order by 的性能

Bre*_*ues 6 postgresql performance postgresql-8.4 recursive postgresql-performance

使用 PostgreSQL 8.4,我试图使用 order by 和两个表的索引列查询两个包含 100 万条记录的表,并且我正在失去性能(1 列需要 30 毫秒,两列需要 5 分钟)。例如:

select r.code, r.test_code, r.sample_code, s.barcode, s.registry_date
from requests r
inner join samples s on (s.code = r.sample_code)
order by s.barcode  asc , r.code asc
limit 21;
Run Code Online (Sandbox Code Playgroud)

表信息:

CREATE TABLE public.samples (
  code BIGINT NOT NULL,
  barcode VARCHAR(40) NOT NULL,
  registry_date TIMESTAMP WITH TIME ZONE NOT NULL,
  CONSTRAINT samples_pkey PRIMARY KEY(code)
);

CREATE INDEX idx_samp_barcode ON public.samples (barcode);
CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code);
CREATE INDEX idx_samp_barcode_code_desc ON public.samples (barcode DESC, code DESC);
CREATE INDEX idx_test_identifier_desc ON public.samples (barcode DESC);


CREATE TABLE public.requests (
  code BIGINT NOT NULL,
  test_code INTEGER NOT NULL,
  sample_code BIGINT NOT NULL,
  CONSTRAINT requests_pkey PRIMARY KEY(code),
  CONSTRAINT "Requests_S_fk" FOREIGN KEY (sample_code)
    REFERENCES public.samples(code)
    ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE,
  CONSTRAINT "Requests_T_fk" FOREIGN KEY (test_code)
    REFERENCES public.tests(code)
    ON DELETE NO ACTION ON UPDATE NO ACTION NOT DEFERRABLE
);

CREATE INDEX idx_req_test_code ON public.requests (test_code);
CREATE INDEX idx_req_test_code_desc ON public.requests (test_code DESC);
CREATE INDEX request_sample_code_index ON public.requests (sample_code);
CREATE INDEX requests_sample_code_desc_idx ON public.requests (sample_code DESC, code DESC);
CREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);
Run Code Online (Sandbox Code Playgroud)
  1. 每个表有 100 万行。
  2. 两个表中的所有行都是不同的。
  3. 如果我单独按任一列排序,执行时间约为 30 毫秒。
  4. 没有要求就没有样品。
  5. 一个样本可以有很多s.codesmae s.barcode

如何提高性能?

Erw*_*ter 5

问题

\n\n

这是一个比乍一看更复杂的问题。您按两列排序,每列来自不同的表,同时连接另外两列。这使得 Postgres 无法使用提供的索引,并且必须默认进行(非常)昂贵的顺序扫描。这是 dba.SE 上的一个相关案例:

\n\n\n\n

索引

\n\n

为了获得最佳性能,您需要两个索引,这两个索引都已存在:

\n\n
CREATE INDEX idx_samp_barcode_code ON public.samples (barcode, code);\nCREATE INDEX requests_sample_code_idx ON public.requests (sample_code, code);\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是你的普通查询无法使用它们,即使在 9.4 pg 中也是如此。

\n\n

询问

\n\n

Postgres 8.4 是一个相当大的障碍。但我想我找到了一种递归 CTE 的方法:

\n\n
WITH RECURSIVE cte AS (\n   ( -- all parentheses are required\n   SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date\n   FROM  (\n      SELECT s.barcode\n      FROM   samples s\n      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)\n      ORDER  BY s.barcode\n      LIMIT  1  -- get smallest barcode to start with\n      ) s0\n   JOIN   samples  s USING (barcode)  -- join all samples with same barcode\n   JOIN   requests r ON r.sample_code = s.code\n   ORDER  BY r.code  -- start with ordered list\n   )\n\n   UNION ALL\n   (\n   SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date\n   FROM  (\n      SELECT s.barcode\n      FROM   cte c\n      JOIN   samples s ON s.barcode > c.barcode \n      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)\n      ORDER  BY s.barcode\n      LIMIT  1  -- get next higher barcode\n      ) s0\n   JOIN   samples  s USING (barcode)  -- join all samples with same barcode\n   JOIN   requests r ON r.sample_code = s.code\n   ORDER  BY r.code  -- again, ordered list\n   )\n   )\nTABLE cte\nLIMIT 21;\n
Run Code Online (Sandbox Code Playgroud)\n\n

在第 9.4 页中测试并适用于我。它使用索引(部分用于 9.2+ pg 中的仅索引扫描)。

\n\n

第 8.4 页已经有了递归 CTE。其余的是基本 SQL(甚至TABLE在 pg 8.4 中可用(并且可以用 替换SELECT * FROM)。我希望没有任何限制来破坏聚会,我不再安装 pg 8.4 了。

\n\n

解释

\n\n

评论部分:

\n\n
-- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果可能存在根本没有请求的样本(您在评论中排除了这种情况),则需要。

\n\n

该查询依赖于递归 CTE 的记录行为

\n\n
\n

提示:递归查询评估算法以广度优先搜索顺序生成输出 。

\n
\n\n

大胆强调我的。关于这一点,也是如此

\n\n
\n

这是有效的,因为 PostgreSQL 的实现仅评估父查询实际获取的查询行数WITH。不建议在生产中使用此技巧,因为其他系统的工作方式可能不同。另外,如果您让外部查询对递归查询的结果进行排序或将它们连接到其他表,则通常不会起作用。

\n
\n\n

大胆强调我的。ORDER BY因此,只有当您不在外部查询中添加另一个时,它才能保证在 PostgreSQL 中工作。

\n\n

LIMIT对于外部查询中(相对)较小的情况来说,这很快。对于手头的情况来说,应该是很快的。该查询对于大型LIMIT.

\n\n

PL/pgSQL 函数

\n\n

我能想到的另一个选择是使用 plpgsql 的程序解决方案。应该更快一点,特别是对于重复调用:

\n\n
CREATE OR REPLACE FUNCTION f_demo(_limit int)\n  RETURNS TABLE (code int8, test_code int, sample_code int8\n               , barcode varchar, registry_date timestamptz) AS\n$func$\nDECLARE\n   _barcode text;\n   _n       int;\n   _rest    int := _limit;  -- init with _limit parameter\nBEGIN\n   FOR _barcode IN\n      SELECT DISTINCT s.barcode\n      FROM   samples s\n      -- WHERE  EXISTS (SELECT 1 FROM requests WHERE r.sample_code = s.code)\n      ORDER  BY s.barcode\n\n   LOOP\n      RETURN QUERY\n      SELECT r.code, r.test_code, r.sample_code, s.barcode, s.registry_date\n      FROM   samples  s\n      JOIN   requests r ON r.sample_code = s.code\n      WHERE  s.barcode = _barcode\n      ORDER  BY r.code\n      LIMIT  _limit;\n\n      GET DIAGNOSTICS _n = ROW_COUNT;\n      _rest := _rest - _n;\n      EXIT WHEN _rest < 1;\n   END LOOP;\nEND\n$func$ LANGUAGE plpgsql;\n
Run Code Online (Sandbox Code Playgroud)\n\n

称呼:

\n\n
SELECT * FROM f_demo(21); \n
Run Code Online (Sandbox Code Playgroud)\n\n

MATERIALIZED VIEW

\n\n

LargeOFFSET具有与 Large 类似的效果LIMIT。虽然所有跳过的行不会添加到 I/O,但仍然需要计算它们以确定偏移后的第一行。如果需要,请使用MATERIALIZED VIEW带有添加行号和索引的 a 。

\n\n

Postgres 9.3+ 有一个内置功能,您必须使用过时的软件手动编写解决方案。不过,事情并没有那么复杂。基本上是一个由预定义SELECT语句或VIEW类似语句填充的快照表。基本上,将表创建为:

\n\n
CREATE TABLE req_sample AS\nSELECT row_number() OVER (ORDER BY s.barcode, r.code) AS rn\n     , r.code, r.test_code, r.sample_code, s.barcode, s.registry_date\nFROM   requests r\nJOIN   samples s on (s.code = r.sample_code)\nORDER  by s.barcode, r.code;\n\nALTER TABLE req_sample ADD CONSTRAINT req_sample_rk PRIMARY KEY (code);\nCREATE INDEX foo ON  req_sample (rn);\n
Run Code Online (Sandbox Code Playgroud)\n\n

将该 SELECT 保存为函数、视图或纯查询文本。在一笔交易中刷新(昂贵):

\n\n
BEGIN;\n-- drop PK & index\nTRUNCATE req_sample;\nINSERT INTO req_sample SELECT ... ;\n-- add PK & index\nCOMMIT;\n
Run Code Online (Sandbox Code Playgroud)\n\n

冗余barcode

\n\n

如果底层表发生很大变化并且您需要“当前”结果,则 MV 会变得更加昂贵。还有一种更便宜的选择:将冗余存储barcoderequests表中。您可以将其作为第二列包含在( )的 FK 约束中,并使其避免过时/冲突的数据。然后您可以使用in上的索引。samples"Requests_S_fk"ON UPDATE CASCADE(barcode, code)requests

\n\n

对于最后两个选项,您可以使用直接适用的索引来简化分页,并使用WHERE上一页中的值的子句替换OFFSET. 请考虑use-the-index-luke.com 上有关“以 PostgreSQL 方式完成分页”的演示。

\n