Query plan caching and prepared statements

Maria Garcia Feb 2026
2 tabs
-- Prepared statements basics
-- PostgreSQL syntax
PREPARE get_user (INT) AS
SELECT id, username, email
FROM users
WHERE id = $1;

-- Execute prepared statement
EXECUTE get_user(123);
EXECUTE get_user(456);

-- Deallocate when done
DEALLOCATE get_user;

-- Named prepared statement
PREPARE user_search (VARCHAR) AS
SELECT id, username, email
FROM users
WHERE username LIKE $1 || '%'
ORDER BY username
LIMIT 20;

EXECUTE user_search('john');

-- Multiple parameters
PREPARE create_order (INT, DECIMAL, VARCHAR) AS
INSERT INTO orders (user_id, total, status)
VALUES ($1, $2, $3)
RETURNING id, created_at;

EXECUTE create_order(123, 99.99, 'pending');

-- View prepared statements
SELECT * FROM pg_prepared_statements;

/*
Application-level prepared statements (Node.js example):

const { Pool } = require('pg');
const pool = new Pool();

// Automatic prepared statement (pg driver)
async function getUser(id) {
  const result = await pool.query(
    'SELECT id, username, email FROM users WHERE id = $1',
    [id]
  );
  return result.rows[0];
}

// After 5 executions, pg driver automatically prepares
await getUser(123);
await getUser(456);
// ...

// Named prepared statement
const preparedQuery = {
  name: 'get-user',
  text: 'SELECT id, username, email FROM users WHERE id = $1',
};

const result = await pool.query(preparedQuery, [123]);

// Reuses prepared statement
const result2 = await pool.query(preparedQuery, [456]);

// Transaction with prepared statements
const client = await pool.connect();
try {
  await client.query('BEGIN');

  // Prepared within transaction
  const result = await client.query({
    name: 'insert-order',
    text: 'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id'
  }, [123, 99.99]);

  const orderId = result.rows[0].id;

  await client.query({
    name: 'insert-item',
    text: 'INSERT INTO order_items (order_id, product_id, quantity) VALUES ($1, $2, $3)'
  }, [orderId, 456, 2]);

  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}

// Connection pooling considerations
// Prepared statements are per-connection
// With connection pool, each connection may have different prepared statements

// Option 1: Use unnamed prepared statements (pg driver default)
await pool.query('SELECT * FROM users WHERE id = $1', [123]);

// Option 2: Prepare on each connection
client.on('connect', async (client) => {
  await client.query({
    name: 'common-query',
    text: 'SELECT * FROM users WHERE id = $1'
  });
});
*/

-- Plan caching behavior
-- PostgreSQL switches to generic plan after 5 custom plans

-- Custom plan (uses actual parameter values)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 123;
-- Uses index if selective

-- Generic plan (uses placeholder)
PREPARE orders_by_user (INT) AS
SELECT * FROM orders WHERE user_id = $1;

-- First 5 executions: custom plans
EXECUTE orders_by_user(123);
EXECUTE orders_by_user(456);
EXECUTE orders_by_user(789);
EXECUTE orders_by_user(101);
EXECUTE orders_by_user(102);

-- 6th execution: switches to generic plan if cheaper
EXECUTE orders_by_user(103);

-- Force custom plan every time
SET plan_cache_mode = 'force_custom_plan';

-- Force generic plan
SET plan_cache_mode = 'force_generic_plan';

-- Auto (default): Choose based on cost
SET plan_cache_mode = 'auto';

-- Monitor plan cache
SELECT
  name,
  statement,
  parameter_types,
  from_sql,
  generic_plans,
  custom_plans
FROM pg_prepared_statements;
2 files · sql Explain with highlit

Query plan caching improves performance by reusing execution plans. I use prepared statements to parse once, execute many times. PostgreSQL caches plans after 5 executions. Plan invalidation occurs when statistics change. Generic plans vs custom plans—PostgreSQL chooses based on cost. Prepared statements prevent SQL injection by separating code from data. Named parameters improve readability. Connection pooling requires proper prepared statement management. Plan cache monitoring helps identify inefficient plans. Understanding caching reduces database load, improves latency. Essential for high-performance applications.