当我使用Postgres 9.3(enable_seqscan
设置为off
)运行以下脚本时,我希望最终查询使用“forms_string”部分索引,而是使用“forms_int” “索引,没有意义。
当我使用具有JSON函数和索引的实际代码对更多类型进行测试时,对于每个查询,它始终似乎使用最后创建的索引。
添加更多不相关的行,以便与部分索引相关的行只占表中总行数的一小部分,从而导致“位图堆扫描”,但在此之后仍然会提到相同的错误索引。
知道我怎么能让它使用正确的索引吗?
CREATE EXTENSION IF NOT EXISTS plv8;
CREATE OR REPLACE FUNCTION
json_string(data json, key text) RETURNS TEXT AS $$
var ret = data,
keys = key.split('.'),
len = keys.length;
for (var i = 0; i < len; ++i) {
if (ret) {
ret = ret[keys[i]]
};
}
if (typeof ret === "undefined") {
ret = null;
} else if (ret) {
ret = ret.toString();
}
return ret;
$$ LANGUAGE plv8 IMMUTABLE STRICT;
CREATE OR REPLACE FUNCTION
json_int(data json, key text) RETURNS INT AS $$
var ret = data,
keys = key.split('.'),
len = keys.length;
for (var i = 0; i < len; ++i) {
if (ret) {
ret = ret[keys[i]]
}
}
if (typeof ret === "undefined") {
ret = null;
} else {
ret = parseInt(ret, 10);
if (isNaN(ret)) {
ret = null;
}
}
return ret;
$$ LANGUAGE plv8 IMMUTABLE STRICT;
CREATE TABLE form_types (
id SERIAL NOT NULL,
name VARCHAR(200),
PRIMARY KEY (id)
);
CREATE TABLE tenants (
id SERIAL NOT NULL,
name VARCHAR(200),
PRIMARY KEY (id)
);
CREATE TABLE forms (
id SERIAL NOT NULL,
tenant_id INTEGER,
type_id INTEGER,
data JSON,
PRIMARY KEY (id),
FOREIGN KEY(tenant_id) REFERENCES tenants (id),
FOREIGN KEY(type_id) REFERENCES form_types (id)
);
CREATE INDEX ix_forms_type_id ON forms (type_id);
CREATE INDEX ix_forms_tenant_id ON forms (tenant_id);
INSERT INTO tenants (name) VALUES ('mike'), ('bob');
INSERT INTO form_types (name) VALUES ('type 1'), ('type 2');
INSERT INTO forms (tenant_id, type_id, data) VALUES
(1, 1, '{"string": "unicorns", "int": 1}'),
(1, 1, '{"string": "pythons", "int": 2}'),
(1, 1, '{"string": "pythons", "int": 8}'),
(1, 1, '{"string": "penguins"}');
CREATE OR REPLACE VIEW foo AS
SELECT forms.id AS forms_id,
json_string(forms.data, 'string') AS "data.string",
json_int(forms.data, 'int') AS "data.int"
FROM forms
WHERE forms.tenant_id = 1 AND forms.type_id = 1;
CREATE INDEX "forms_string" ON forms (json_string(data, 'string'))
WHERE tenant_id = 1 AND type_id = 1;
CREATE INDEX "forms_int" ON forms (json_int(data, 'int'))
WHERE tenant_id = 1 AND type_id = 1;
EXPLAIN ANALYZE VERBOSE SELECT "data.string" from foo;
输出:
Index Scan using forms_int on public.forms
(cost=0.13..8.40 rows=1 width=32) (actual time=0.085..0.239 rows=20 loops=1)
Output: json_string(forms.data, 'string'::text)
Total runtime: 0.282 ms
没有enable_seqscan=off
:
Seq Scan on public.forms (cost=0.00..1.31 rows=1 width=32) (actual time=0.080..0.277 rows=28 loops=1)
Output: json_string(forms.data, 'string'::text)
Filter: ((forms.tenant_id = 1) AND (forms.type_id = 1))
Total runtime: 0.327 ms
\d forms
打印
Table "public.forms"
Column | Type | Modifiers
-----------+---------+----------------------------------------------------
id | integer | not null default nextval('forms_id_seq'::regclass)
tenant_id | integer |
type_id | integer |
data | json |
Indexes:
"forms_pkey" PRIMARY KEY, btree (id)
"forms_int" btree (json_int(data, 'int'::text)) WHERE tenant_id = 1 AND type_id = 1
"forms_string" btree (json_string(data, 'string'::text)) WHERE tenant_id = 1 AND type_id = 1
"ix_forms_tenant_id" btree (tenant_id)
"ix_forms_type_id" btree (type_id)
Foreign-key constraints:
"forms_tenant_id_fkey" FOREIGN KEY (tenant_id) REFERENCES tenants(id)
"forms_type_id_fkey" FOREIGN KEY (type_id) REFERENCES form_types(id)
答案 0 :(得分:3)
与您的计算机的实际性能相比,您的random_page_cost
看起来太高了。随机I / O比Pg认为的更快(成本更低),所以它选择了一个稍微不太理想的计划。
这就是为什么indexscan的成本估算为(cost=0.13..8.40 rows=1 width=32)
,而seqscan的成本估算在(cost=0.00..1.31 rows=1 width=32)
处略低。
降低random_page_cost
- 尝试SET random_page_cost = 2
然后重新开始运行。
要了解更多信息,请阅读有关PostgreSQL查询规划,参数和调优以及相关Wiki页面的文档。
PostgreSQL似乎正在forms_int
而不是forms_string
上选择索引扫描,因为它将是一个更紧凑,更小的索引,并且两个索引都与视图的搜索条件完全匹配:{{ 1}}。
如果您停用或删除tenant_id = 1 AND type_id = 1
,它可能会使用forms_int
并稍微放慢一点。
要理解的关键是,虽然索引确实包含了感兴趣的值,但PostgreSQL实际上并没有使用它。它正在扫描索引而没有索引条件,因为索引中的每个元组都匹配,以从堆中获取元组。然后它从那些堆元组中提取值并输出它们。
这可以通过常量上的表达式索引来证明:
forms_string
PostgreSQL很可能为查询选择此索引:
CREATE INDEX "forms_novalue" ON forms((true)) WHERE tenant_id = 1 AND type_id = 1;
所有索引的大小都相同,因为它们都非常小,符合最小分配:
regress=# EXPLAIN ANALYZE VERBOSE SELECT "data.string" from foo;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Index Scan using forms_novalue on public.forms (cost=0.13..13.21 rows=4 width=32) (actual time=0.190..0.310 rows=4 loops=1)
Output: json_string(forms.data, 'string'::text)
Total runtime: 0.346 ms
(3 rows)
但由于行宽较窄,novalue的统计数据会更具吸引力。
听起来你真正期望的是一个index- only 扫描,其中Pg从不接触表的堆,只使用索引中的元组。
我希望regress=# SELECT x.idxname, pg_relation_size(x.idxname) FROM (VALUES ('forms_novalue'),('forms_int'),('forms_string')) x(idxname);
idxname | pg_relation_size
---------------+------------------
forms_novalue | 16384
forms_int | 16384
forms_string | 16384
(3 rows)
可以满足此查询的要求,但无法让Pg为其选择仅索引扫描计划。
我不能立即清楚为什么Pg在这里不使用仅索引扫描,因为它应该是一个候选者,但它似乎无法计划一个。如果我强制forms_string
,它会选择一个较差的位图索引扫描计划,如果强制禁用enable_indexscan = off
,它将回退到最大成本估算seqscan。即使在感兴趣的表enable_bitmapscan
之后也是如此。
这意味着它不能被生成为查询规划器中的候选路径 - Pg不知道如何对此查询使用仅索引扫描,或者认为由于某种原因它不能这样做。
视图内省不是问题,因为扩展视图查询是相同的。
答案 1 :(得分:2)
您的表格中的数据不足。简而言之,当表格适合单个磁盘页面时,Postgres不会使用索引。永远。当你的表包含几百或几千行时,它会变得太大而不适合,然后你会看到Postgres在相关时开始使用索引扫描。
需要考虑的另一点是,在进行大量导入后,您需要analyze
表。如果没有关于实际数据的准确统计数据,Postgres最终可能会将某些索引扫描视为过于昂贵,而实际上它们并不便宜。
最后,有些情况 实质上,只要Postgres要重复访问大多数磁盘页面并以随机顺序访问大量行,它就会认真考虑访问大多数(位图索引)或所有(seq扫描)磁盘页面的成本一次顺序并过滤掉无效的行。如果你选择了足够的行,后者就会获胜。