我在PostgreSQL(9.5.1)中有以下查询:
select e.id, (select count(id) from imgitem ii where ii.tabid = e.id and ii.tab = 'esp') as imgs,
e.ano, e.mes, e.dia, cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||'-'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data,
pl.pltag, e.inpa, e.det, d.ano anodet, coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')' determinador, d.tax, coalesce(v.val,v.valf)||' '||vu.unit as altura,
coalesce(v1.val,v1.valf)||' '||vu1.unit as DAP, d.fam, tf.nome família, d.gen, tg.nome gênero, d.sp, ts.nome espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
from esp e
left join det d on e.det = d.id
left join tax tf on d.fam = tf.oldfam
left join tax tg on d.gen = tg.oldgen
left join tax ts on d.sp = ts.oldsp
left join tax ti on d.inf = ti.oldinf
left join loc l on e.loc = l.id
left join pess p on p.id = d.detby
left join var v on v.esp = e.id and v.key = 265
left join varunit vu on vu.id = v.unit
left join var v1 on v1.esp = e.id and v1.key = 264
left join varunit vu1 on vu1.id = v1.unit
left join pl on pl.id = e.pl
WHERE unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
从esp
表中的总计9250行检索1129行需要430毫秒。
如果我将搜索字词从%vicen%
更改为%vicent%
(添加't'),则需要431毫秒来检索相同的1129行。
按搜索栏排序,升序和降序,我看到所有1129行在两种情况下都具有完全相同的名称。
现在很奇怪:如果我将搜索字词从%vicent%
更改为%vicenti%
(添加'i'),现在需要难以置信的 24.4秒来检索相同的内容1129行!
搜索的字词始终位于第一个coalesce
,即coalesce(p.abrev,'')
。我希望查询运行得更慢或更快,具体取决于搜索字符串的大小,但不是那么多!任何人都知道发生了什么事?
EXPLAIN ANALYZE
的结果(此处超过30k字符限制):
%vicen%
:http://explain.depesz.com/s/2XF
%vicenti%
:http://explain.depesz.com/s/dEc6
答案 0 :(得分:8)
原因是这样的:
快速查询:
-> Hash Left Join (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)
慢查询:
-> Hash Left Join (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1) ... Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)
将搜索模式扩展为另一个字符会导致Postgres假设更少的匹配。 (通常,这是一个合理的估计。)Postgres显然没有足够精确的统计数据(没有,实际上,继续阅读)以期望获得相同数量的命中率。
这会导致切换到不同的查询计划,这对于 实际 点击次数rows=1129
来说甚至更不理想。
假设当前的Postgres 9.5尚未声明。
改善这种情况的一种方法是在谓词中的表达式上创建表达式索引。这使得Postgres收集实际表达式的统计信息,即使索引本身未用于查询,也可以帮助查询。如果没有索引,表达式根本就有 无统计数据 。如果做得好,索引可以用于查询,那就更好了。但是当前表达式存在 多个问题 :
<击> unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
击>
根据您对未公开表格定义的某些假设,考虑此更新后的查询:
SELECT e.id
, (SELECT count(*) FROM imgitem
WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
, e.ano, e.mes, e.dia
, e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
, pl.pltag, e.inpa, e.det, d.ano anodet
, format('%s (%s)', p.abrev, p.prenome) AS determinador
, d.tax
, coalesce(v.val,v.valf) || ' ' || vu.unit AS altura
, coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
, d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
, ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM pess p -- reorder!
JOIN det d ON d.detby = p.id -- INNER JOIN !
LEFT JOIN tax tf ON tf.oldfam = d.fam
LEFT JOIN tax tg ON tg.oldgen = d.gen
LEFT JOIN tax ts ON ts.oldsp = d.sp
LEFT JOIN tax ti ON ti.oldinf = d.inf -- unused, see @joop's comment
LEFT JOIN esp e ON e.det = d.id
LEFT JOIN loc l ON l.id = e.loc
LEFT JOIN var v ON v.esp = e.id AND v.key = 265
LEFT JOIN varunit vu ON vu.id = v.unit
LEFT JOIN var v1 ON v1.esp = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id = v1.unit
LEFT JOIN pl ON pl.id = e.pl
WHERE f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%') OR
f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');
为什么 f_unaccent()
?因为unaccent()
无法编入索引。阅读本文:
我使用了那里概述的功能允许以下(推荐!)多列功能三元组GIN 索引:
CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);
如果您不熟悉trigram索引,请先阅读:
可能:
请务必运行最新版本的Postgres(目前为9.5)。 GIN指数有了实质性的改进。您将对pg_trgm 1.2的改进感兴趣,计划与即将发布的Postgres 9.6一起发布:
预备语句是使用参数执行查询的常用方法(尤其是使用来自用户输入的文本)。 Postgres必须找到一个最适合任何给定参数的计划。将通配符作为常量添加到搜索词中,如下所示:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
('vicenti'
将替换为参数。)因此,Postgres知道我们正在处理既不左右也不左右的模式 - 这将允许不同的策略。相关答案详情如下:
或者可能为每个搜索词重新规划查询(可能在函数中使用动态SQL)。但要确保计划时间不会占用任何可能的性能提升。
WHERE
中的 pess
条件与 相矛盾。 Postgres被迫将其转换为LEFT JOIN
INNER JOIN
。更糟糕的是连接在连接树中来得晚了。而且由于Postgres无法重新排序您的联接(见下文),这可能会变得非常昂贵。将表移动到FROM
子句中的 第一个 位置,以尽早消除行。在LEFT JOIN
之后,根据定义,不会消除任何行。但是,有了这么多表,将乘以行的连接移动到最后是非常重要的。
您正在加入13个表格,其中12个表格与LEFT JOIN
一起留下12!
种可能的组合 - 或者11! * 2!
如果我们将LEFT JOIN
考虑在内是真的INNER JOIN
{1}}。对于Postgres而言, 很多都会为最佳查询计划评估所有可能的排列。阅读 join_collapse_limit
:
join_collapse_limit
的默认设置为 8 ,这意味着Postgres不会尝试重新排序FROM
子句中的表格表的顺序是 相关 。
解决此问题的一种方法是将性能关键部分拆分为CTE @joop commented。不要将join_collapse_limit
设置得更高或者涉及许多联接表的查询计划的时间会恶化。
关于名为data
的连锁日期:
<击> cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||'-'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data
击>
假设您使用三个数字列(年,月和日)构建,这些列定义为NOT NULL
,请改用:
e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
关于FM
模板模式修饰符:
但实际上,您应该将日期存储为数据类型date
以开始。
同样简化:
format('%s (%s)', p.abrev, p.prenome) AS determinador
不会让查询更快,但它更清晰。请参阅format()
。
首先,效果优化的所有常规建议均适用:
如果你能做到这一点,你应该会看到更快的查询所有模式。
答案 1 :(得分:2)
减少范围表大小的一种方法是将查询的一小部分压缩到CTE中,例如AS:
WITH zzz AS (
SELECT l.id, l.nome
, coalesce(v.val,v.valf)||' '||vu.unit as altura
, coalesce(v1.val,v1.valf)||' '||vu1.unit as DAP
FROM loc l
left join var v on v.esp = l.id and v.key = 265
left join varunit vu on vu.id = v.unit
left join var v1 on v1.esp = l.id and v1.key = 264
left join varunit vu1 on vu1.id = v1.unit
)
select e.id, (select count(id) from imgitem ii
where ii.tabid = e.id and ii.tab = 'esp'
) as imgs
, e.ano, e.mes, e.dia
, cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||'-'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data
, pl.pltag, e.inpa, e.det, d.ano anodet
, coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')' determinador
, d.tax
, zzz.altura as altura
, zzz.DAP as DAP
, d.fam, tf.nome família
, d.gen, tg.nome gênero
, d.sp , ts.nome espécie
, d.inf, e.loc
, zzz.nome AS localidade
, e.lat, e.lon
from esp e
left join det d on e.det = d.id -- these could possibly be
left join pess p on p.id = d.detby -- plain joins
--
left join tax tf on d.fam = tf.oldfam
left join tax tg on d.gen = tg.oldgen
left join tax ts on d.sp = ts.oldsp
-- ### commented out, since it is never referred
-- ### left join tax ti on d.inf = ti.oldinf
left join pl on pl.id = e.pl
left JOIN zzz ON zzz.id = e.loc
--
WHERE unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
;
[未经测试,因为我没有表格定义]