我们可以在第一次执行 PL/pgSQL 函数时执行最佳计划而不是通用计划吗?

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)

Erw*_*ter 2

我清理并简化了一些小细节。不过,这对于性能来说应该不会有太大改变。但添加的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 版本或数据库发生任何重大更改后,此类优化可能会从有益变为阻碍。

具有更多详细信息的相关答案:

在旁边

我假设您知道还有其他一些可能的影响可能会使第一个调用比后续调用慢。就像填充缓存和其他东西一样:

  • 不要在函数内部设置`SET join_collapse_limit`,而是在函数的声明中设置它,例如`create or replacement function .... as $$ ... $$ SET join_collapse_limit = 1;`。这样,SET 就不会泄漏以影响函数返回后的查询。 (2认同)