DEV Community

Cover image for PostgreSQL plan_cache_mode
Franck Pachot for YugabyteDB

Posted on

PostgreSQL plan_cache_mode

Here is a small test case to demonstrate plan_cache_mode, a PostgreSQL 12 feature available in YugabyteDB 2.25

                                                                                            yugabyte=> \c
psql (16.2, server 15.2-YB-2.25.0.0-b0)
You are now connected to database "yugabyte" as user "yugabyte".

yugabyte=> drop table if exists t; 
DROP TABLE

yugabyte=> deallocate all;
show plan_cache_mode;
DEALLOCATE ALL

yugabyte=> show plan_cache_mode;
 plan_cache_mode
-----------------
 auto
(1 row)

Enter fullscreen mode Exit fullscreen mode

I create a small table and a prepared query:

yugabyte=> create table t 
           as select generate_series(1,100000) val
;
SELECT 100000

yugabyte=> prepare query(int) 
           as select * from t where val=$1
;
PREPARE

yugabyte=> explain (analyze) execute query(42);

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=56.087..56.092 rows=1 loops=1)
   Storage Filter: (val = 42)
 Planning Time: 5.451 ms
 Execution Time: 56.162 ms
 Peak Memory Usage: 48 kB
(5 rows)

Enter fullscreen mode Exit fullscreen mode

The execution plan shows that the parameter $1 was replaced with the value 42 before query planning.

I execute the same prepared statement five more times:


yugabyte=> \watch count=5
                            Tue 14 Jan 2025 01:45:56 PM GMT (every 2s)

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=88.165..88.170 rows=1 loops=1)
   Storage Filter: (val = 42)
 Planning Time: 0.091 ms
 Execution Time: 88.220 ms
 Peak Memory Usage: 24 kB
(5 rows)

                            Tue 14 Jan 2025 01:45:58 PM GMT (every 2s)

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=22.839..22.845 rows=1 loops=1)
   Storage Filter: (val = 42)
 Planning Time: 0.128 ms
 Execution Time: 22.900 ms
 Peak Memory Usage: 24 kB
(5 rows)

                            Tue 14 Jan 2025 01:46:00 PM GMT (every 2s)

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=36.821..36.826 rows=1 loops=1)
   Storage Filter: (val = 42)
 Planning Time: 0.133 ms
 Execution Time: 36.878 ms
 Peak Memory Usage: 24 kB
(5 rows)

                            Tue 14 Jan 2025 01:46:02 PM GMT (every 2s)

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=22.879..22.885 rows=1 loops=1)
   Storage Filter: (val = 42)
 Planning Time: 0.093 ms
 Execution Time: 22.935 ms
 Peak Memory Usage: 24 kB
(5 rows)

                            Tue 14 Jan 2025 01:46:04 PM GMT (every 2s)

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=22.278..22.283 rows=1 loops=1)
   Storage Filter: (val = $1)
 Planning Time: 0.107 ms
 Execution Time: 22.331 ms
 Peak Memory Usage: 24 kB
(5 rows)

Enter fullscreen mode Exit fullscreen mode

After five executions, the query planner evaluates an execution plan that can be used for any value (val = $1 instead of val = 42). SQL Server would call that disabling parameter sniffing, and Oracle would call that disabling bind peeking. The plan may not be the best one for a specific value, but it will be stable and avoid spending more planning time in future executions:

yugabyte=> explain (analyze) execute query(42);

                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=22.857..22.862 rows=1 loops=1)
   Storage Filter: (val = $1)
 Planning Time: 0.012 ms
 Execution Time: 22.919 ms
 Peak Memory Usage: 24 kB
(5 rows)
Enter fullscreen mode Exit fullscreen mode

The difference in planning time is small for a simple query without indexes, but it matters for more complex queries.

For the demonstration, I'll create an index that is faster but works only for some values, and then cannot be used by a generic plan:

yugabyte=> create index on t(val) where val<50;
CREATE INDEX
Enter fullscreen mode Exit fullscreen mode

My prepared statement still use the generic plan:

yugabyte=> explain (analyze) execute query(42);
                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4) (actual time=22.647..22.653 rows=1 loops=1)
   Storage Filter: (val = $1)
 Planning Time: 11.583 ms
 Execution Time: 22.719 ms
 Peak Memory Usage: 24 kB
(5 rows)
Enter fullscreen mode Exit fullscreen mode

With plan_cache_mode I can force a custom or generic plan rather than relying on the automatic mechanism:

yugabyte=> set plan_cache_mode = force_custom_plan;
SET

yugabyte=> explain execute query(42);
                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

yugabyte=> set plan_cache_mode = force_generic_plan;
SET

yugabyte=> explain execute query(42);
                      QUERY PLAN
------------------------------------------------------
 Seq Scan on t  (cost=180.00..1098.09 rows=5 width=4)
   Storage Filter: (val = $1)
(2 rows)

Enter fullscreen mode Exit fullscreen mode

Given the cost of the custom plan for this value, it is better to use a custom plan rather than switching to a generic one. When using the auto plan cache mode, the query planner evaluates a generic plan after five executions but can decide to keep the custom plan if the cost is lower so that the estimated total execution (planning plus execution) remains cheaper than the execution of the generic plan:

yugabyte=> set plan_cache_mode = auto;
SET

yugabyte=> explain execute query(42);
                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

yugabyte=> \watch count=10 0.01
               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

               Tue 14 Jan 2025 01:56:29 PM GMT (every 0.01s)

                                 QUERY PLAN
----------------------------------------------------------------------------
 Index Only Scan using t_val_idx on t  (cost=180.00..555.06 rows=5 width=4)
   Index Cond: (val = 42)
(2 rows)

Enter fullscreen mode Exit fullscreen mode

The query planner never switched to the generic plan because of its higher estimated cost. I made this example with a partial index, it also happens when the custom plan allows partition pruning.

A final note: the five executions are hardcoded in PostgreSQL, but YugabyteDB made it configurable:

           List of configuration parameters
                  Parameter                   | Value
----------------------------------------------+-------
 plan_cache_mode                              | auto
 yb_planner_custom_plan_for_partition_pruning | on
 yb_test_planner_custom_plan_threshold        | 5
(3 rows)
Enter fullscreen mode Exit fullscreen mode

It has a test in its name because the main reason is regression tests, where you want to see the behavior without executing it multiple times.

You may see a generic plan later with the JDBC driver because the five executions kick in only when the statement is prepared servrr-side, and pgJDBC doesn't do it immediately. It waits for the fith client-side execution before a server-side prepared statement is created, so it's finally the tenth execution that can switch to a generic plan. The client-side uses the same value, five, but it is configurable with the prepareThreshold JDBC property.

Top comments (0)