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 扫描,尽管没有横向连接你可能处于比原始查询更好的情况。
重写查询以应用限制 first 和 then 针对函数的交叉连接应该使 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
文本字符串。
我有很大的 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 扫描,尽管没有横向连接你可能处于比原始查询更好的情况。
重写查询以应用限制 first 和 then 针对函数的交叉连接应该使 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
文本字符串。