Piv*_*spo 5 postgresql performance hints plpgsql postgresql-9.4 postgresql-performance
我有一个非常繁忙的功能,我需要以最佳方式优化。此函数只是一个嵌套的 select 语句,遗留应用程序每秒请求数次。
索引已就位,但我注意到它仅在第一次执行函数后使用。我认为问题在于 Postgres 创建了一个通用的执行计划,因为它在大多数情况下是高度排他的,但有时可能没有那么好。
当我EXPLAIN ANALYZE
在第一次执行后进行测试时,查询运行得非常快,但应用程序会话仅调用该函数一次,然后终止。我需要第一次执行使用实际的优化计划。任何人都可以帮忙吗?
我们尝试弄乱管理连接池的连接器驱动程序来发出一个DISCARD TEMP
而不是DISCARD ALL
,因此它可以保持会话的缓存计划并且性能达到顶峰,但我不想在生产环境中这样做.
我们使用的是在 CentOS 6 上运行的 Postgres 9.4。我试过作为 SQL 函数运行,但没有帮助,它实际上作为 plpgsql 函数更快。下面是函数代码:
CREATE OR REPLACE FUNCTION public.ap_keepalive_geteqpid_veiid(
IN tcbserie bigint,
IN protocolo integer)
RETURNS TABLE(eqpid integer, veiid integer, tcbid integer, veiplaca character varying, veiproprietariocliid integer, tcbtppid integer, tcbversao character, veirpmparametro double precision, tcbconfiguracao bigint, tcbevtconfig integer, veibitsalertas integer, sluid integer, harid integer) AS
$BODY$
BEGIN
RETURN QUERY
SELECT teqp.eqpID,
teqp.eqpveiID AS veiID,
tcb.tcbID,
tvei.veiPlaca,
tvei.veiProprietariocliID,
tcb.tcbtppID,
tcb.tcbVersao,
tvei.veiRPMParametro,
COALESCE(COALESCE(NULLIF(tcb.tcbConfiguracao, 0), tcc.clcConfiguracaoBitsVeic), 0) AS tcbConfiguracao,
COALESCE(tcb.tcbevtConfig, 0) AS tcbevtConfig,
COALESCE(tvei.veiBitsAlertas, 0) AS veiBitsAlertas,
COALESCE(tvei.veisluID, 0) AS sluID,
COALESCE(tcb.tcbharID, 0) AS harID
FROM TabEquipamento teqp
INNER JOIN TabPacoteProduto tpp ON teqp.eqptppID = tpp.tppID
INNER JOIN TabComputadorBordo tcb ON teqp.eqptcbID = tcb.tcbID
INNER JOIN TabVeiculos tvei ON teqp.eqpveiID = tvei.veiID
LEFT JOIN TabCliente tcli ON tcli.cliid = tvei.veiProprietariocliID
LEFT JOIN TabClienteConfig tcc ON tcc.clcCliID = tcli.cliID
WHERE tcb.tcbserie = $1
AND teqp.eqpAtivo = 1
AND tpp.tppIDProtocolo = $2
AND tvei.veiBloqueioSinal = 0;
END
$BODY$
LANGUAGE plpgsql VOLATILE COST 100 ROWS 1;
Run Code Online (Sandbox Code Playgroud)
第一次执行中的执行计划:
"Function Scan on ap_keepalive_geteqpid_veiid (cost=0.25..0.26 rows=1 width=116) (actual time=3.268..3.268 rows=1 loops=1)"
"Planning time: 0.032 ms"
"Execution time: 3.288 ms"
Run Code Online (Sandbox Code Playgroud)
第二次执行:
"Function Scan on ap_keepalive_geteqpid_veiid (cost=0.25..0.26 rows=1 width=116) (actual time=0.401..0.402 rows=1 loops=1)"
"Planning time: 0.058 ms"
"Execution time: 0.423 ms"
Run Code Online (Sandbox Code Playgroud)
编辑:添加了具有意外结果的函数的自动解释输出(至少对我而言)。auto-explain 声称 postgres 只用了 0.230 毫秒就用所需的普通代码执行了该函数,但该函数本身用了 4.057 毫秒。我不知道这是否准确。
< 2015-12-14 18:10:02.314 BRST >LOG: duration: 0.234 ms plan:
Query Text: SELECT teqp.eqpID,
teqp.eqpveiID AS veiID,
tcb.tcbID,
tvei.veiPlaca,
tvei.veiProprietariocliID,
tcb.tcbtppID,
tcb.tcbVersao,
tvei.veiRPMParametro,
COALESCE(COALESCE(NULLIF(tcb.tcbConfiguracao, 0), tcc.clcConfiguracaoBitsVeic), 0) AS tcbConfiguracao,
COALESCE(tcb.tcbevtConfig, 0) AS tcbevtConfig,
COALESCE(tvei.veiBitsAlertas, 0) AS veiBitsAlertas,
COALESCE(tvei.veisluID, 0) AS sluID,
COALESCE(tcb.tcbharID, 0) AS harID
FROM TabComputadorBordo tcb
INNER JOIN TabEquipamento teqp ON teqp.eqptcbID = tcb.tcbID
INNER JOIN TabPacoteProduto tpp ON teqp.eqptppID = tpp.tppID
INNER JOIN TabVeiculos tvei ON teqp.eqpveiID = tvei.veiID
LEFT JOIN TabCliente tcli ON tcli.cliid = tvei.veiProprietariocliID
LEFT JOIN TabClienteConfig tcc ON tcc.clcCliID = tcli.cliID
WHERE tcb.tcbserie = $1
AND teqp.eqpAtivo = 1
AND tpp.tppIDProtocolo = $2
AND tvei.veiBloqueioSinal = 0
Nested Loop Left Join (cost=1.29..18.65 rows=1 width=75) (actual time=0.226..0.230 rows=1 loops=1)
Join Filter: (tcc.clccliid = tcli.cliid)
Rows Removed by Join Filter: 3
-> Nested Loop Left Join (cost=1.29..17.57 rows=1 width=75) (actual time=0.205..0.209 rows=1 loops=1)
-> Nested Loop (cost=1.01..17.26 rows=1 width=71) (actual time=0.200..0.203 rows=1 loops=1)
-> Nested Loop (cost=0.72..16.80 rows=1 width=43) (actual time=0.097..0.098 rows=1 loops=1)
-> Nested Loop (cost=0.58..16.63 rows=1 width=47) (actual time=0.079..0.080 rows=1 loops=1)
-> Index Scan using ix_tabcomputadorbordo_tcbserie on tabcomputadorbordo tcb (cost=0.29..8.31 rows=1 width=35) (actual time=0.046..0.046 rows=1 loops=1)
Index Cond: (tcbserie = $1)
-> Index Scan using ix_tabequipamento_eqptcbid_eqpativo_eqptppid_eqpveiid on tabequipamento teqp (cost=0.29..8.31 rows=1 width=16) (actual time=0.030..0.031 rows=1 loops=1)
Index Cond: ((eqptcbid = tcb.tcbid) AND (eqpativo = 1))
-> Index Only Scan using ix_tabpacoteproduto_tppidprotocolo on tabpacoteproduto tpp (cost=0.14..0.16 rows=1 width=4) (actual time=0.015..0.015 rows=1 loops=1)
Index Cond: ((tppidprotocolo = $2) AND (tppid = teqp.eqptppid))
Heap Fetches: 1
-> Index Scan using pk_tabveiculos on tabveiculos tvei (cost=0.29..0.45 rows=1 width=32) (actual time=0.100..0.101 rows=1 loops=1)
Index Cond: (veiid = teqp.eqpveiid)
Filter: (veibloqueiosinal = 0)
-> Index Only Scan using pk_tabcliente on tabcliente tcli (cost=0.28..0.30 rows=1 width=4) (actual time=0.004..0.005 rows=1 loops=1)
Index Cond: (cliid = tvei.veiproprietariocliid)
Heap Fetches: 1
-> Seq Scan on tabclienteconfig tcc (cost=0.00..1.03 rows=3 width=8) (actual time=0.014..0.015 rows=3 loops=1)
< 2015-12-14 18:10:02.314 BRST >CONTEXTO: função PL/pgSQL ap_keepalive_geteqpid_veiid(bigint,integer) linha 4 em RETURN QUERY
< 2015-12-14 18:10:02.314 BRST >LOG: duration: 4.057 ms plan:
Query Text: SELECT * FROM ap_keepalive_geteqpid_veiid (tcbSerie := 8259492, protocolo:= 422);
Run Code Online (Sandbox Code Playgroud)
我清理并简化了一些小细节。不过,这对于性能来说应该不会有太大改变。但添加的SET join_collapse_limit = 1
可能:
CREATE OR REPLACE FUNCTION public.ap_keepalive_geteqpid_veiid(tcbserie bigint, protocolo int)
RETURNS TABLE(eqpid int, veiid int, tcbid int
, veiplaca varchar, veiproprietariocliid int, tcbtppid int, tcbversao character, veirpmparametro double precision
, tcbconfiguracao bigint, tcbevtconfig int, veibitsalertas int, sluid int, harid int) AS
$func$
BEGIN
RETURN QUERY
SELECT eqp.eqpID
, eqp.eqpveiID AS veiID
, cb.tcbID
, vei.veiPlaca
, vei.veiProprietariocliID
, cb.tcbtppID
, cb.tcbVersao
, vei.veiRPMParametro
, CASE WHEN cb.tcbConfiguracao = 0 THEN COALESCE(cc.clcConfiguracaoBitsVeic, 0)
ELSE cb.tcbConfiguracao END -- AS tcbConfiguracao
, COALESCE(cb.tcbevtConfig, 0) -- AS tcbevtConfig
, COALESCE(vei.veiBitsAlertas, 0) -- AS veiBitsAlertas
, COALESCE(vei.veisluID, 0) -- AS sluID
, COALESCE(cb.tcbharID, 0) -- AS harID
FROM TabEquipamento eqp
JOIN TabVeiculos vei ON vei.veiID = eqp.eqpveiID
JOIN TabComputadorBordo cb ON cb.tcbID = eqp.eqptcbID
JOIN TabPacoteProduto pp ON pp.tppID = eqp.eqptppID
LEFT JOIN TabCliente cli ON cli.cliid = vei.veiProprietariocliID
LEFT JOIN TabClienteConfig cc ON cc.clcCliID = cli.cliID
WHERE eqp.eqpAtivo = 1
AND vei.veiBloqueioSinal = 0
AND cb.tcbserie = $1
AND pp.tppIDProtocolo = $2;
END
$func$ LANGUAGE plpgsql VOLATILE STRICT COST 10000 ROWS 1
SET join_collapse_limit = 1; -- see below!
Run Code Online (Sandbox Code Playgroud)
COALESCE
可以带多个参数,无需嵌套:
COALESCE(NULLIF(cb.tcbConfiguracao, 0), cc.clcConfiguracaoBitsVeic, 0) AS tcbConfiguracao
Run Code Online (Sandbox Code Playgroud)
我上面最终使用的表达式CASE
应该更快一点。
character
作为数据类型是可疑的。这与 相同char(1)
,我假设您已经意识到这一点。
在 PL/pgSQL 内部,同一查询中未引用的列别名仅用于文档。只有子句中的名称RETURNS TABLE
在函数外部可见。
COST 100
是用户定义函数的默认值,可能不适合您的情况。10000 可能是一个更好的估计,但是除非您将此函数嵌套在外部查询中,否则这几乎没有任何效果。
我删除了参数默认值(如所讨论的)并创建了 function STRICT
,因为查询无论如何都不会为任何 NULL 输入返回任何内容。
我简化了您的表别名并进行了更多格式化,以使其更易于阅读和使用。最后一点很大程度上是品味和风格的问题。
至于您的实际问题:PostgreSQL 中没有像其他 RDBMS 中那样的查询规划器(优化器)的(直接)提示。详细信息请参见 Postgres Wiki 中的“OptimizerHintsDiscussion”。
PL/pgSQL 在内部使用准备好的语句。它将使用每个会话的前几次调用的给定输入参数重新规划函数体内的查询。只有当它发现特定计划的性能不如通用计划时,它才会切换到通用计划并保留该计划,这可以节省一些开销。
细节:
但是,您可以调整一些设置。特别是,如果您知道最佳查询计划,则可以FROM
通过设置来强制 Postgres 按照给定的子句中的联接顺序,而不是尝试重新排序(这对于许多表来说可能会变得昂贵 - 并且您有 6 个表)这join_collapse_limit
。这将减少规划查询的成本。如果做得好,它将使前几个调用更快。如果你搞砸了,性能当然会受到影响。
你可以把一个SET LOCAL
作为第一个命令:
...
BEGIN
SET LOCAL join_collapse_limit = 1;
RETURN QUERY ...
Run Code Online (Sandbox Code Playgroud)
更好的是,像我上面那样将其声明为函数本身的属性。函数体内的效果SET LOCAL
将持续到交易结束,但是,根据文档:
该
SET
子句使指定的配置参数在进入函数时设置为指定值,然后在函数退出时恢复为其先前的值。
显然,您需要自己正确确定子句中的连接顺序FROM
。它必须适用于所有可能的参数组合。Postgres 不会尝试优化。(这STRICT
修饰符简化了一点,因为现在排除了 NULL 值。)
将具有最具选择性谓词的表放在前面。
警告:请注意,在升级到 Postgres 版本或数据库发生任何重大更改后,此类优化可能会从有益变为阻碍。
具有更多详细信息的相关答案:
我假设您知道还有其他一些可能的影响可能会使第一个调用比后续调用慢。就像填充缓存和其他东西一样: