Postgresql 不使用索引

Postgresql doesn't use index

我有很大的 table 碎屑(大约 100M+ 行,100GB)。它只是作为文本存储的 json 的集合。它在列 run_id 上有索引,该索引具有大约 10K 个唯一值。所以每个 运行 都很小(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 scan 应该保留:

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

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

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

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

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

这更有可能使用索引扫描为 run_id 选择行,然后 "unnest" 为您选择数组元素。

但是第三个隐藏的问题是你实际想要得到的。如果您按原样 运行 最后一个查询,那么您将与第一个(未连接的)查询在同一条船上,没有限制,这意味着您可能不会进行索引扫描(并不是说那本身就很糟糕如果您正在阅读 table) 中的大量内容。

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

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

使用派生 table:

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

或者直接在select列表中使用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

然而,这与其他两个查询不同,因为它在 after"unnesting"json 数组中应用了限制,而不是在行数上从 crumbs table 返回(这是您的第一个查询所做的)。

数据建模建议:

        -- 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 文本字符串。