【问题标题】:Postgresql doesn't use indexPostgresql 不使用索引
【发布时间】:2016-05-10 12:20:06
【问题描述】:

我有大表屑(大约 100M+ 行,100GB)。它只是存储为文本的 json 集合。它在具有大约 10K 唯一值的列 run_id 上具有索引。所以每次运行都很小(1K - 1M 行)。

简单查询:

explain analyze verbose select * from crumbs c 
where c.run_id='2016-04-26T19_02_01_015Z' limit 10

计划不错:

Limit  (cost=0.56..36.89 rows=10 width=2262) (actual time=1.978..2.016 rows=10 loops=1)
  Output: id, robot_id, run_id, content, created_at, updated_at, table_id, fork_id, log, err
  ->  Index Scan using index_crumbs_on_run_id on public.crumbs c  (cost=0.56..5533685.73 rows=1523397 width=2262) (actual time=1.975..1.996 rows=10 loops=1)
        Output: id, robot_id, run_id, content, created_at, updated_at, table_id, fork_id, log, err
        Index Cond: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text)
Planning time: 0.117 ms
Execution time: 2.048 ms

但是,如果我尝试查看存储在其中一列中的 json,它会想要进行全扫描:

explain verbose select x from crumbs c, 
lateral json_array_elements(c.content::json) x
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10

计划:

Limit  (cost=0.01..0.69 rows=10 width=32)
  Output: x.value
  ->  Nested Loop  (cost=0.01..10332878.67 rows=152343800 width=32)
        Output: x.value
        ->  Seq Scan on public.crumbs c  (cost=0.00..7286002.66 rows=1523438 width=895)
              Output: c.id, c.robot_id, c.run_id, c.content, c.created_at, c.updated_at, c.table_id, c.fork_id, c.log, c.err
              Filter: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text)
        ->  Function Scan on pg_catalog.json_array_elements x  (cost=0.01..1.01 rows=100 width=32)
              Output: x.value
              Function Call: json_array_elements((c.content)::json)

试过了:

analyze crumbs

但没什么区别。

更新 1 禁用对整个数据库的顺序扫描是可行的,但这不是我们的应用程序中的选项。在许多其他地方 seq 扫描应该保留:

set enable_seqscan=false;

计划:

Limit  (cost=0.57..1.14 rows=10 width=32) (actual time=0.120..0.294 rows=10 loops=1)
  Output: x.value
  ->  Nested Loop  (cost=0.57..8580698.45 rows=152343400 width=32) (actual time=0.118..0.273 rows=10 loops=1)
        Output: x.value
        ->  Index Scan using index_crumbs_on_run_id on public.crumbs c  (cost=0.56..5533830.45 rows=1523434 width=895) (actual time=0.087..0.107 rows=10 loops=1)
              Output: c.id, c.robot_id, c.run_id, c.content, c.created_at, c.updated_at, c.table_id, c.fork_id, c.log, c.err
              Index Cond: ((c.run_id)::text = '2016-04-26T19_02_01_015Z'::text)
        ->  Function Scan on pg_catalog.json_array_elements x  (cost=0.01..1.01 rows=100 width=32) (actual time=0.011..0.011 rows=1 loops=10)
              Output: x.value
              Function Call: json_array_elements((c.content)::json)
Planning time: 0.124 ms
Execution time: 0.337 ms

更新 2

架构是:

CREATE TABLE crumbs
(
  id serial NOT NULL,
  run_id character varying(255),
  content text,
  created_at timestamp without time zone,
  updated_at timestamp without time zone,
  CONSTRAINT crumbs_pkey PRIMARY KEY (id)
);

CREATE INDEX index_crumbs_on_run_id
  ON crumbs
  USING btree
  (run_id COLLATE pg_catalog."default");

更新 3

像这样重写查询:

select json_array_elements(c.content::json) x
from crumbs c
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10

得到正确的计划。仍然不清楚为什么第二次查询选择了错误的计划。

【问题讨论】:

  • ((run_id)::text = '2016-04-26T19_02_01_015Z'::text) run_id 在我看来就像一个时间戳。为什么将其存储为文本字段?另外:请添加表定义,包括索引。
  • 是的,run_id 是带有文本前缀的时间戳。我省略了有问题的前缀以避免引入不相关的复杂性。现在用解释分析详细更新输出。
  • 听起来像是泰勒为 jsonb 制作的情景
  • @e4c5 或者可能是 MongoDB? ;-)
  • @asgs 基准测试实际上表明带有 JSON 的 postgresql 9.5 优于 mongo :))

标签: json postgresql postgres-9.4


【解决方案1】:

重写查询以便应用限制首先然后针对函数的交叉连接应该使Postgres使用索引:

使用派生表:

select x 
from (
    select *
    from crumbs 
    where run_id='2016-04-26T19_02_01_015Z' 
    limit 10
) c 
  cross join lateral json_array_elements(c.content::json) x

或者使用 CTE:

with c as (
  select *
  from crumbs 
  where run_id='2016-04-26T19_02_01_015Z' 
  limit 10
)
select x
from c 
  cross join lateral json_array_elements(c.content::json) x

或者直接在选择列表中使用json_array_elements()

select json_array_elements(c.content::json) 
from crumbs c
where c.run_id='2016-04-26T19_02_01_015Z' 
limit 10

但是,这与其他两个查询有所不同,因为它应用了限制“取消嵌套”json 数组,而不是从 crumbs 表返回的行数(这就是您的第一个查询正在执行)。

【讨论】:

  • 您对一般不鼓励将 SRF 放在 select 子句中是否有任何参考?
  • @yieldsfalsehood:这已在邮件列表中多次提及。阻止它的一个原因 - 据我所知 - 是如果您选择其他列以及函数的行为没有明确定义。但我现在找不到。
  • 感谢您的重写。他们工作,但我仍然在神奇的土地 - 似乎相同的查询,但计划不同,因此我们永远无法确定是否会在某个时候选择错误的计划。
  • @TomasVitulskis:你可以永远确定是否选择了“正确的”执行计划。这是基于成本的优化器的缺点。从表中添加或删除行可以使优化器选择不同的计划。更改表中的值可以使优化器选择不同的计划 - 通常它通常会正确(并且更改计划一件好事)。但没有任何软件是完美的。
  • @yieldsfalsehood:我发现了一个邮件列表线程:postgresql.nabble.com/…"这种行为被广泛反对......长期计划是在 FROM 中实现 LATERAL,然后在全部目标列表" 和this "在一个目标列表中缺乏任何明显合理的方法来处理多个 SRF,这正是该功能被视为不受欢迎的原因"
【解决方案2】:

您遇到了三个不同的问题。首先,第一个查询中的limit 10 使计划器倾向于索引扫描,否则要获得与run_id 匹配的所有行将非常昂贵。为了比较起见,您可能希望查看删除限制后第一个(未连接的)查询计划的样子。我的猜测是规划器切换到表扫描。

其次,横向连接是不必要的,并且会甩掉规划器。您可以像这样在 select 子句中扩展内容数组的元素:

select json_array_elements(content::json)
from crumbs
where run_id = '2016-04-26T19_02_01_015Z'
;

这更有可能使用索引扫描为 run_id 挑选行,然后为您“取消嵌套”数组元素。

但第三个隐藏的问题是你真正想要得到的。如果您按原样运行最后一个查询,那么您将与第一个(未连接的)查询在同一条船上,没有限制,这意味着您可能不会获得索引扫描(如果您是读取这么大一块表)。

您是否只需要该运行中所有内容数组的前几个任意数组元素?如果是这样,那么在这里添加限制条款应该是故事的结尾。如果您想要此特定运行的所有数组元素,那么您可能只需要接受表扫描,尽管没有横向连接,您可能处于比原始查询更好的情况。

【讨论】:

  • 删除限制在所有情况下都会得到正确的计划(使用索引,而不是完全扫描)。正如您所猜测的那样,限制的目的是获取任意元素以预览完整作业。你的重写解决了这个问题。但是,当有便宜的计划可用时,为什么 PostgreSQL 选择非常昂贵的计划(扫描 100GB)仍然是个谜。
【解决方案3】:

数据建模建议:

        -- Suggest replacing the column run_id (low cardinality, and rather fat)
        -- by a reference to a domain table, like:
        -- ------------------------------------------------------------------
CREATE TABLE runs
        ( run_seq serial NOT NULL PRIMARY KEY
        , run_id character varying UNIQUE
        );

        -- Grab all the distinct values occuring in crumbs.run_id
        -- -------------------------------------------------------
INSERT INTO runs (run_id)
SELECT DISTINCT run_id FROM crumbs;

        -- Add an FK column
        -- -----------------
ALTER TABLE crumbs
        ADD COLUMN run_seq integer REFERENCES runs(run_seq)
        ;

UPDATE crumbs c
SET run_seq = r.run_seq
FROM runs r
WHERE r.run_id = c.run_id
        ;
VACUUM ANALYZE runs;

        -- Drop old column and set new column to not nullable
        -- ---------------------------------------------------
ALTER TABLE crumbs
        DROP COLUMN run_id
        ;
ALTER TABLE crumbs
        ALTER COLUMN run_seq SET NOT NULL
        ;

        -- Recreate the supporting index for the FK
        -- adding id to support index-only lookups
        -- (and enforce uniqueness)
        -- -------------------------------------
CREATE UNIQUE INDEX index_crumbs_run_seq_id ON crumbs (run_seq,id)
        ;

        -- Refresh statistics
        -- ------------------
VACUUM ANALYZE crumbs; -- this may take some time ...

-- and then: join the runs table to your original crumbs table
-- -----------------------------------------------------------
-- explain analyze 
SELECT x FROM crumbs c
JOIN runs r ON r.run_seq = c.run_seq
        , lateral json_array_elements(c.content::json) x
WHERE r.run_id='2016-04-26T19_02_01_015Z'
LIMIT 10
        ;

或者:使用其他回答者的建议进行类似的加入。


但可能更好:用实际时间戳替换丑陋的 run_id 文本字符串。

【讨论】:

  • 谢谢!我们有跑步桌。以额外的存储而不是整数为代价拥有人类可读的键只是一种偏好。
  • 您抱怨expensive plan,但不想修复昂贵的数据建模错误?
猜你喜欢
  • 2012-05-21
  • 2014-11-19
  • 1970-01-01
  • 2020-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多