• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Eliminating PostgreSQL Bottlenecks: Tuning Queries for High-Performance Magento 2 Stores

Eliminating PostgreSQL Bottlenecks: Tuning Queries for High-Performance Magento 2 Stores

Identifying Slow Queries with `pg_stat_statements`

The first step in optimizing any PostgreSQL database, especially one powering a demanding application like Magento 2, is to identify the specific queries that are consuming the most resources. The `pg_stat_statements` module is an indispensable tool for this. It tracks execution statistics for all SQL statements executed by the server.

To enable `pg_stat_statements`, you need to add it to `shared_preload_libraries` in your `postgresql.conf` file and then restart PostgreSQL. After that, you can create the extension in your target database:

-- Connect to your Magento database
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Reload PostgreSQL configuration if you modified postgresql.conf
-- SELECT pg_reload_conf();

Once enabled, you can query `pg_stat_statements` to find the top offenders. A common approach is to look for queries with high cumulative execution time, a large number of calls, or a high average execution time. Here’s a query to identify the top 10 queries by total execution time:

SELECT
    (total_exec_time / 1000 / 60) AS total_minutes,
    (mean_exec_time / 1000) AS avg_ms,
    calls,
    rows,
    substring(query, 1, 60) AS query_snippet
FROM
    pg_stat_statements
ORDER BY
    total_exec_time DESC
LIMIT 10;

Pay close attention to the `query_snippet`. Magento 2 can generate complex queries, especially around product listings, category pages, and checkout. Look for repetitive patterns or queries that seem overly complex for their task.

Analyzing Query Plans with `EXPLAIN ANALYZE`

Once you’ve identified a slow query, the next crucial step is to understand *why* it’s slow. PostgreSQL’s `EXPLAIN ANALYZE` command provides detailed information about how the query planner executes a query, including actual run times and row counts for each step. This is far more informative than a static `EXPLAIN` plan.

Let’s assume you’ve identified a query that’s frequently appearing in your `pg_stat_statements` results. You would then run:

EXPLAIN ANALYZE YOUR_SLOW_QUERY_HERE;

For example, a common Magento query that might become slow involves fetching product data with multiple joins:

EXPLAIN ANALYZE
SELECT
    e.*,
    -- ... other columns and joins ...
FROM
    catalog_product_entity e
LEFT JOIN
    catalog_product_entity_varchar AS name_varchar ON e.entity_id = name_varchar.entity_id
    AND name_varchar.attribute_id = (SELECT attribute_id FROM eav_attribute WHERE attribute_code = 'name' AND entity_type_id = 4)
    AND name_varchar.store_id = 0
WHERE
    e.entity_id IN (100, 101, 102);

When analyzing the output of `EXPLAIN ANALYZE`, look for:

  • Sequential Scans (Seq Scan) on large tables where an index scan would be expected.
  • High costs associated with specific nodes (e.g., Nested Loop joins with many outer rows).
  • Discrepancies between estimated and actual row counts, indicating stale statistics.
  • Sort operations that could be avoided with appropriate indexing.
  • Bitmap Heap Scans that might be inefficient if the heap scan is large.

The output will show the execution plan, detailing each operation (e.g., Seq Scan, Index Scan, Hash Join, Nested Loop Join, Sort, Aggregate) and its associated cost, time, and row count. Focus on the operations that consume the most time.

Strategic Indexing for Magento 2 Performance

Indexing is paramount. Magento 2’s EAV (Entity-Attribute-Value) model, while flexible, can lead to performance issues if not properly indexed. `pg_stat_statements` and `EXPLAIN ANALYZE` will often point to missing or inefficient indexes.

Consider the example query from the previous section. If `name_varchar.attribute_id` and `name_varchar.store_id` are frequently used in `WHERE` clauses or join conditions, a composite index can dramatically improve performance.

-- Example: Index for fetching product names by attribute_id and store_id
CREATE INDEX IF NOT EXISTS idx_catalog_product_entity_varchar_attr_store
ON catalog_product_entity_varchar (attribute_id, store_id, entity_id);

Similarly, if you frequently query `catalog_product_entity` by `entity_id` within specific ranges or conditions, ensure there’s an index on `entity_id`. However, `entity_id` is typically the primary key and already indexed.

Key areas for indexing in Magento 2:

  • EAV tables (`catalog_product_entity_varchar`, `catalog_product_entity_int`, `catalog_product_entity_decimal`, etc.): Indexes on `attribute_id`, `store_id`, and `entity_id` are critical for filtering and joining.
  • `url_rewrite` table: Often a bottleneck for product and category page loading. Indexes on `request_path`, `target_path`, `entity_type`, and `store_id` are beneficial.
  • `quote` and `sales_order` tables: For checkout and order history performance. Indexes on `customer_id`, `store_id`, and `created_at` can be useful.
  • `inventory_stock_item`: For real-time stock checks.

When creating indexes, always consider the query patterns. A composite index `(col1, col2)` is most effective when `col1` is used in the `WHERE` clause. Use `pg_stat_user_indexes` to monitor index usage and identify unused or redundant indexes.

SELECT
    schemaname,
    relname,
    indexrelname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM
    pg_stat_user_indexes
WHERE
    schemaname = 'public' -- Or your Magento schema
ORDER BY
    idx_scan ASC, idx_tup_fetch ASC
LIMIT 20;

Indexes add overhead to write operations (INSERT, UPDATE, DELETE). Therefore, avoid over-indexing. Regularly review and prune indexes that are not being used.

Database Configuration Tuning (`postgresql.conf`)

Beyond query tuning and indexing, PostgreSQL’s configuration parameters in `postgresql.conf` play a vital role in performance. For a high-traffic Magento store, these are some of the most impactful parameters to adjust:

`shared_buffers`: This is arguably the most important parameter. It determines how much memory PostgreSQL dedicates to caching data blocks. A common recommendation is 25% of system RAM, but for very large databases or systems with ample RAM, it can be increased further. However, avoid setting it too high, as it can lead to memory contention with the OS cache.

# Example: For a server with 64GB RAM
shared_buffers = 16GB

`work_mem`: This parameter controls the amount of memory that can be used for internal sort operations and hash tables before spilling to disk. If your `EXPLAIN ANALYZE` output shows significant disk-based sorts, increasing `work_mem` can help. Be cautious, as this is allocated per operation, so a high value combined with many concurrent complex queries can exhaust memory.

# Example: Increase for complex reporting queries
work_mem = 64MB

`maintenance_work_mem`: Used for maintenance operations like `VACUUM`, `CREATE INDEX`, and `ALTER TABLE ADD FOREIGN KEY`. Increasing this can speed up these operations, which are crucial for database health and index creation.

# Example: Speed up index creation and vacuuming
maintenance_work_mem = 512MB

`effective_cache_size`: This parameter informs the query planner about the total memory available for disk caching by both PostgreSQL (`shared_buffers`) and the operating system. Setting this to a value around 50-75% of total system RAM helps the planner make better decisions about using indexes.

# Example: For a server with 64GB RAM
effective_cache_size = 48GB

`random_page_cost`: This parameter influences the planner’s choice between sequential scans and index scans. The default is 4.0. If your storage is fast (e.g., SSDs), reducing this value (e.g., to 1.1 or 1.5) can encourage the planner to use indexes more aggressively, which is often beneficial for Magento’s selective queries.

# Example: For SSD storage
random_page_cost = 1.5

`max_connections`: Ensure this is set appropriately for your application’s needs, but avoid setting it excessively high, as each connection consumes memory. Consider using a connection pooler like PgBouncer for large-scale applications.

After modifying `postgresql.conf`, remember to reload the configuration:

pg_ctl reload -D /path/to/your/data/directory

Connection Pooling with PgBouncer

Magento 2, especially with multiple cron jobs, third-party integrations, and frontend requests, can open a large number of database connections. Each connection has an overhead in terms of memory and CPU. For busy Magento installations, a connection pooler like PgBouncer is essential.

PgBouncer sits between your application and PostgreSQL, managing a pool of persistent connections to the database. Applications connect to PgBouncer, which then assigns an available connection from its pool. This significantly reduces the overhead of establishing new connections.

Installation and Configuration:

Install PgBouncer using your distribution’s package manager (e.g., `apt install pgbouncer` or `yum install pgbouncer`). The primary configuration file is `pgbouncer.ini`.

[databases]
magento_db = host=your_pg_host port=5432 dbname=magento_database

[pgbouncer]
; Listen on localhost, port 6432
listen_addr = 127.0.0.1
listen_port = 6432

; Use 'session' pooling for most applications. 'transaction' pooling can be faster but has caveats.
; 'statement' pooling is generally not recommended for complex apps like Magento.
pool_mode = session

; Maximum number of connections to the PostgreSQL server.
; Should be related to max_connections in postgresql.conf and your workload.
max_client_conn = 2000

; Maximum number of clients that can connect to PgBouncer.
max_db_connections = 100

; Authentication method. 'md5' is common.
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

; Log file location
logfile = /var/log/pgbouncer/pgbouncer.log

; Database connection timeout
connect_timeout = 5

; Server connection timeout
server_connect_timeout = 5

; Enable the stats database for monitoring
stats_period = 60
stats_file = /var/log/pgbouncer/stats.log
admin_users = postgres
stats_users = postgres

You’ll also need a `userlist.txt` file for authentication:

"postgres" "md5_hashed_password_for_postgres"
"magento_user" "md5_hashed_password_for_magento_user"

Ensure your Magento 2 database connection details in `app/etc/env.php` are updated to point to PgBouncer’s host and port (e.g., `localhost` and `6432` if running on the same server).

return [
    'db' => [
        'connection' => [
            'default' => [
                'host' => '127.0.0.1',
                'port' => '6432', // PgBouncer port
                'dbname' => 'magento_database',
                'username' => 'magento_user',
                'password' => 'your_magento_user_password',
                'model' => 'mysql4',
                'initStatements' => 'SET NAMES utf8;',
                'options' => [
                    PDO::ATTR_PERSISTENT => true,
                    PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
                ],
            ],
            // ... other connections
        ],
        // ... other db settings
    ],
    // ...
];

Start and enable the PgBouncer service. Monitor its logs and the PostgreSQL connection count to ensure it’s effectively managing connections.

Regular Maintenance: VACUUM and ANALYZE

PostgreSQL’s MVCC (Multi-Version Concurrency Control) mechanism means that UPDATEs and DELETEs don’t immediately remove old row versions. These dead tuples can accumulate, leading to bloated tables and indexes, and slower query performance. `VACUUM` reclaims this space, and `ANALYZE` updates table statistics, which are crucial for the query planner.

While PostgreSQL has an autovacuum daemon, its aggressiveness and tuning can be critical for a busy Magento store. You might need to tune autovacuum parameters or schedule manual `VACUUM FULL` (which locks tables but reclaims more space) or `VACUUM ANALYZE` operations during low-traffic periods.

Autovacuum Tuning:

autovacuum = on
log_autovacuum_min_duration = 10s ; Log autovacuum actions taking longer than 10 seconds
autovacuum_max_workers = 4
autovacuum_naptime = 15s
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
autovacuum_vacuum_scale_factor = 0.1 ; Vacuum when 10% of table is dead tuples
autovacuum_analyze_scale_factor = 0.05 ; Analyze when 5% of table is new/changed rows

For very large tables, the default `scale_factor` might be too low, leading to excessive autovacuum activity. Conversely, for smaller, frequently updated tables, a higher `threshold` might be needed. Monitor `pg_stat_user_tables` to see when `last_autovacuum` and `last_autoanalyze` were last run.

SELECT
    relname,
    n_live_tup,
    n_dead_tup,
    last_autovacuum,
    last_autoanalyze,
    vacuum_count,
    analyze_count
FROM
    pg_stat_user_tables
WHERE
    n_dead_tup > 1000 -- Example: show tables with at least 1000 dead tuples
ORDER BY
    n_dead_tup DESC;

Regularly running `VACUUM ANALYZE` on critical tables, especially after large data imports or purges, is a good practice. This ensures the query planner has the most up-to-date statistics for optimal query plan generation.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • How to Optimize Largest Contentful Paint (LCP) and Interaction to Next Paint (INP) in Large-Scale WooCommerce Enterprise Sites
  • Server Monitoring Best Practices: Keeping Your Laravel App and Elasticsearch Clusters Alive on Linode
  • Resolving thread pools deadlock during concurrent ActiveRecord transaction processing Under Peak Event Traffic on OVH
  • Eliminating PostgreSQL Bottlenecks: Tuning Queries for High-Performance Laravel Stores
  • The Ultimate DevOps Playbook: Tuning Nginx, Gunicorn/FPM, and DynamoDB on OVH for Magento 2

Copyright © 2026 · Vinay Vengala