【问题标题】:Improve query efficiency for repetitive queries提高重复查询的查询效率
【发布时间】:2015-07-20 05:14:08
【问题描述】:

我正在编写一个 node.js 应用程序来启用对 PostgreSQL 数据库的搜索。为了在搜索框中启用 twitter 预输入,我必须从数据库中处理一组关键字以在页面加载之前初始化 Bloodhound。如下所示:

SELECT distinct handlerid from lotintro where char_length(lotid)=7;

所以对于一张大桌子(lotintro)来说,这是很昂贵的;这也是愚蠢的,因为查询结果很可能在一段时间内对于不同的网络访问者保持不变。

处理这个问题的正确方法是什么?我正在考虑几个选项:

1) 将查询放入存储过程中,并从 node.js 中调用:

   SELECT * from getallhandlerid()

这是否意味着查询将被编译并且数据库将自动返回相同的结果集,而实际运行的查询却不知道结果不会改变?

2) 或者,创建一个单独的表来存储不同的 handlerid 并使用每天运行的触发器更新表? (我知道理想情况下,触发器应该在每次插入/更新表时运行,但这太贵了)。

3) 按照建议创建部分索引。以下是收集的内容:

查询

SELECT distinct handlerid from lotintro where length(lotid) = 7;

索引

CREATE INDEX lotid7_idx ON lotintro (handlerid)
WHERE  length(lotid) = 7;

有索引,查询耗时250ms左右,试运行

explain (analyze on, TIMING OFF) SELECT distinct handlerid from lotintro where length(lotid) = 7

"HashAggregate  (cost=5542.64..5542.65 rows=1 width=6) (actual rows=151 loops=1)"
"  ->  Bitmap Heap Scan on lotintro  (cost=39.08..5537.50 rows=2056 width=6) (actual rows=298350 loops=1)"
"        Recheck Cond: (length(lotid) = 7)"
"        Rows Removed by Index Recheck: 55285"
"        ->  Bitmap Index Scan on lotid7_idx  (cost=0.00..38.57 rows=2056 width=0) (actual rows=298350 loops=1)"
"Total runtime: 243.686 ms"

无索引,查询耗时210ms左右,试运行

explain (analyze on, TIMING OFF) SELECT distinct handlerid from lotintro where length(lotid) = 7

"HashAggregate  (cost=19490.11..19490.12 rows=1 width=6) (actual rows=151 loops=1)"
"  ->  Seq Scan on lotintro  (cost=0.00..19484.97 rows=2056 width=6) (actual rows=298350 loops=1)"
"        Filter: (length(lotid) = 7)"
"        Rows Removed by Filter: 112915"
"Total runtime: 214.235 ms"

我在这里做错了什么?

4) 使用 alexius 建议的索引和查询:

create index on lotintro using btree(char_length(lotid), handlerid);

但这不是最佳解决方案。因为只有很少的不同值,您可以使用称为松散索引扫描的技巧,在您的情况下它应该工作得更快:

explain (analyze on, BUFFERS on, TIMING OFF)
WITH RECURSIVE t AS (
   (SELECT handlerid FROM lotintro WHERE char_length(lotid)=7 ORDER BY handlerid LIMIT 1)  -- parentheses required
   UNION ALL
   SELECT (SELECT handlerid FROM lotintro WHERE char_length(lotid)=7 AND handlerid > t.handlerid ORDER BY handlerid LIMIT 1)
   FROM t
   WHERE t.handlerid IS NOT NULL
   )
SELECT handlerid FROM t WHERE handlerid IS NOT NULL;

"CTE Scan on t  (cost=444.52..446.54 rows=100 width=32) (actual rows=151 loops=1)"
"  Filter: (handlerid IS NOT NULL)"
"  Rows Removed by Filter: 1"
"  Buffers: shared hit=608"
"  CTE t"
"    ->  Recursive Union  (cost=0.42..444.52 rows=101 width=32) (actual rows=152 loops=1)"
"          Buffers: shared hit=608"
"          ->  Limit  (cost=0.42..4.17 rows=1 width=6) (actual rows=1 loops=1)"
"                Buffers: shared hit=4"
"                ->  Index Scan using lotid_btree on lotintro lotintro_1  (cost=0.42..7704.41 rows=2056 width=6) (actual rows=1 loops=1)"
"                      Index Cond: (char_length(lotid) = 7)"
"                      Buffers: shared hit=4"
"          ->  WorkTable Scan on t t_1  (cost=0.00..43.83 rows=10 width=32) (actual rows=1 loops=152)"
"                Filter: (handlerid IS NOT NULL)"
"                Rows Removed by Filter: 0"
"                Buffers: shared hit=604"
"                SubPlan 1"
"                  ->  Limit  (cost=0.42..4.36 rows=1 width=6) (actual rows=1 loops=151)"
"                        Buffers: shared hit=604"
"                        ->  Index Scan using lotid_btree on lotintro  (cost=0.42..2698.13 rows=685 width=6) (actual rows=1 loops=151)"
"                              Index Cond: ((char_length(lotid) = 7) AND (handlerid > t_1.handlerid))"
"                              Buffers: shared hit=604"
"Planning time: 1.574 ms"
**"Execution time: 25.476 ms"**

========= 关于 db 的更多信息 ============================

dataloggerDB=# \d lotintro 表“public.lotintro”

    Column    |            Type             |  Modifiers
 --------------+-----------------------------+--------------
  lotstartdt   | timestamp without time zone | not null
  lotid        | text                        | not null
  ftc          | text                        | not null
  deviceid     | text                        | not null
  packageid    | text                        | not null
  testprogname | text                        | not null
  testprogdir  | text                        | not null
  testgrade    | text                        | not null
  testgroup    | text                        | not null
  temperature  | smallint                    | not null
  testerid     | text                        | not null
  handlerid    | text                        | not null
  numofsite    | text                        | not null
  masknum      | text                        |
  soaktime     | text                        |
  xamsqty      | smallint                    |
  scd          | text                        |
  speedgrade   | text                        |
  loginid      | text                        |
  operatorid   | text                        | not null
  loadboardid  | text                        | not null
  checksum     | text                        |
  lotenddt     | timestamp without time zone | not null
  totaltest    | integer                     | default (-1)
  totalpass    | integer                     | default (-1)
  earnhour     | real                        | default 0
  avetesttime  | real                        | default 0
  Indexes:
  "pkey_lotintro" PRIMARY KEY, btree (lotstartdt, testerid)
  "lotid7_idx" btree (handlerid) WHERE length(lotid) = 7
your version of Postgres,         [PostgreSQL 9.2]
cardinalities (how many rows?),   [411K rows for table lotintro]
percentage for length(lotid) = 7. [298350/411000=  73%]

============= 将所有内容移植到 PG 9.4 之后 =====================

带索引:

explain (analyze on, BUFFERS on, TIMING OFF) SELECT distinct handlerid from lotintro where length(lotid) = 7

"HashAggregate  (cost=5542.78..5542.79 rows=1 width=6) (actual rows=151 loops=1)"
"  Group Key: handlerid"
"  Buffers: shared hit=14242"
"  ->  Bitmap Heap Scan on lotintro  (cost=39.22..5537.64 rows=2056 width=6) (actual rows=298350 loops=1)"
"        Recheck Cond: (length(lotid) = 7)"
"        Heap Blocks: exact=13313"
"        Buffers: shared hit=14242"
"        ->  Bitmap Index Scan on lotid7_idx  (cost=0.00..38.70 rows=2056 width=0) (actual rows=298350 loops=1)"
"              Buffers: shared hit=929"
"Planning time: 0.256 ms"
"Execution time: 154.657 ms"

无索引:

explain (analyze on, BUFFERS on, TIMING OFF) SELECT distinct handlerid from lotintro where length(lotid) = 7

"HashAggregate  (cost=19490.11..19490.12 rows=1 width=6) (actual rows=151 loops=1)"
"  Group Key: handlerid"
"  Buffers: shared hit=13316"
"  ->  Seq Scan on lotintro  (cost=0.00..19484.97 rows=2056 width=6) (actual rows=298350 loops=1)"
"        Filter: (length(lotid) = 7)"
"        Rows Removed by Filter: 112915"
"        Buffers: shared hit=13316"
"Planning time: 0.168 ms"
"Execution time: 176.466 ms"

【问题讨论】:

  • "将自动返回相同的结果集而无需实际运行查询" - 不会。查询将每次运行。 2) 可以通过使用物化视图来实现 - 可能是更好的方法
  • 谢谢! @a_horse_with_no_name
  • 我刚刚找到了一篇很好的文章。 pgcon.org/2008/schedule/events/69.en.html
  • "查询成本大约 200 毫秒" - 查询成本不是以毫秒为单位的(实际上它根本没有单位)。使用索引的第一个查询的成本为 5542,第二个(不使用索引)的成本为 19490 - 高出 3.5 倍 - 因此索引使用效率更高。如果您想获得查询的实际运行时间(以毫秒为单位),您需要使用explain (analyze, timing)
  • @a_horse_with_no_name:要获得最准确的性能比较,EXPLAIN (ANALYZE, TIMING OFF) 应该是最好的(只是内部细节没有时间安排)。

标签: postgresql stored-procedures triggers twitter-typeahead bloodhound


【解决方案1】:

您需要为WHERE 子句中使用的确切表达式编制索引:http://www.postgresql.org/docs/9.4/static/indexes-expressional.html

CREATE INDEX char_length_lotid_idx ON lotintro (char_length(lotid));

您还可以按照您的建议创建STABLEIMMUTABLE 函数来包装此查询:http://www.postgresql.org/docs/9.4/static/sql-createfunction.html

你最后的建议也是可行的,你要找的是MATERIALIZED VIEWShttp://www.postgresql.org/docs/9.4/static/sql-creatematerializedview.html 这会阻止您编写自定义触发器来更新非规范化表。

【讨论】:

  • 不客气 :)。我想知道哪种解决方案最适合您以及您正在处理的数据量。现在查询持续多长时间?并带有索引?
  • 嗨@Clément Prévost,表lotintro 有411k 行,不同的handlerid 大约有150 行。一个普通的 SQL 查询或存储过程大约需要 220 毫秒。我最初使用不支持 MV 的 PostgreSQL 9.2。我现在将同一张表移植到 PostgreSQL 9.4,是的,MV 按预期工作,而且几乎是即时的(
【解决方案2】:

由于 3/4 的行满足您的条件 (length(lotid) = 7) 索引本身不会有太大帮助。由于仅扫描索引,您可能会使用此索引获得更好的性能:

create index on lotintro using btree(char_length(lotid), handlerid);

但这不是最佳解决方案。因为只有几个不同的值,您可以使用称为 loose index scan 的技巧,在您的情况下它应该工作得更快:

WITH RECURSIVE t AS (
   (SELECT handlerid FROM lotintro WHERE char_length(lotid)=7 ORDER BY handlerid LIMIT 1)  -- parentheses required
   UNION ALL
   SELECT (SELECT handlerid FROM lotintro WHERE char_length(lotid)=7 AND handlerid > t.handlerid ORDER BY handlerid LIMIT 1)
   FROM t
   WHERE t.handlerid IS NOT NULL
   )
SELECT handlerid FROM t WHERE handlerid IS NOT NULL;

对于这个查询,您还需要创建我上面提到的索引。

【讨论】:

  • 嗨@alexius,这确实表现得更好,尽管我需要更多时间来消化你在这里尝试什么。我也会将您的答案添加到问题陈述中。谢谢!
【解决方案3】:

1)

不,函数不会以任何方式保留结果的快照。如果您定义函数STABLE(这将是正确的),则有一些 的性能优化潜力。 Per documentation:

STABLE 函数不能修改数据库并保证 在给定相同参数的情况下,为 a 中的所有行返回相同的结果 单个语句

IMMUTABLE 会在此处错误并可能导致错误。

因此,这可以极大地使同一语句中的多个调用受益 - 但这不适合您的用例...

而 plpgsql 函数的工作方式类似于 prepared statements 在同一个 session 内为您提供类似的奖励:

2)

试试MATERIALIZED VIEW。无论有没有 MV(或其他一些缓存技术),partial index 对于您的特殊情况都是最有效的:

CREATE INDEX lotid7_idx ON lotintro (handlerid)
WHERE  length(lotid) = 7;

记住在应该使用索引的查询中包含索引条件,即使这看起来是多余的:

但是,正如您提供的那样:

长度百分比(lotid)= 7。[298350/411000= 73%]

该索引只有在您可以从中获得仅索引扫描时才会有所帮助,因为该条件几乎没有选择性。由于表的行非常宽,因此仅索引扫描可以大大加快。

松散索引扫描

另外,rows=298350 被折叠成rows=151,所以松散的索引扫描是有代价的,正如我在这里解释的那样:

或者在Postgres Wiki - 这实际上是基于这篇文章。

WITH RECURSIVE t AS (
   (SELECT handlerid FROM lotintro
    WHERE  length(lotid) = 7
    ORDER  BY 1 LIMIT 1)

   UNION ALL
   SELECT (SELECT handlerid FROM lotintro
           WHERE  length(lotid) = 7
           AND    handlerid > t.handlerid
           ORDER  BY 1 LIMIT 1)
   FROM  t
   WHERE t.handlerid IS NOT NULL
   )
SELECT handlerid FROM t
WHERE  handlerid IS NOT NULL;

结合我建议的部分索引,这会更快。由于部分索引只有大约一半大小并且更新频率较低(取决于访问模式),因此总体上更便宜。

如果您将表保持为真空状态以允许仅索引扫描,则速度会更快。如果你有很多写入,你可以为这个表设置更积极的存储参数:

最后,您可以使用基于此查询的物化视图更快地完成此操作。

【讨论】:

  • 嗨,@Erwin Brandstetter,我在这里尝试部分索引。添加索引后似乎变得更糟了。我尝试运行'EXPLAIN',它确认查询实际上正在使用索引。这里有什么问题? -- 我更新了问题陈述中的细节。谢谢!
  • @sqr:你能用EXPLAIN (ANALYZE, TIMING OFF)重复你的测试吗? (最好的 5 个。)
  • 多方正在这张票的几个地方进行编辑,我最终在您对“松散索引扫描”进行编辑之前看到了 Alex 的回答。正如我现在所看到的,您的回答建议了所有可能的解决方案并提供了完整的解释,我会将您的回复标记为答案。谢谢。 -- 我想我可以早点标记多个答案。