• 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 MySQL Bottlenecks: Tuning Queries for High-Performance Magento 2 Stores

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

Identifying Slow Queries with `pt-query-digest`

The first step in optimizing any MySQL database, especially one powering a demanding application like Magento 2, is to identify the queries that are consuming the most resources. The Percona Toolkit’s pt-query-digest is an indispensable tool for this task. It analyzes MySQL slow query logs and provides a ranked report of the most problematic queries.

Ensure your MySQL server is configured to log slow queries. This is typically done via the my.cnf (or my.ini) configuration file. Set slow_query_log = 1 and long_query_time to a reasonable value (e.g., 1 or 2 seconds for a high-performance environment). The log_queries_not_using_indexes option can also be invaluable.

Once the slow query log is populated, you can run pt-query-digest. A common invocation looks like this:

sudo pt-query-digest --review /var/log/mysql/mysql-slow.log --order percent_total_time --limit 10 > /tmp/slow_queries_report.txt

This command will:

  • --review /var/log/mysql/mysql-slow.log: Specifies the slow query log file to analyze.
  • --order percent_total_time: Sorts the results by the percentage of total query time consumed.
  • --limit 10: Limits the output to the top 10 queries.
  • > /tmp/slow_queries_report.txt: Redirects the output to a file for easier review.

The resulting report will detail query patterns, their frequency, total time spent, rows examined, and other critical metrics. Focus on queries that appear frequently and/or consume a significant amount of total time, especially those examining a large number of rows without using indexes.

Analyzing and Rewriting Inefficient SQL

After identifying problematic queries, the next step is to understand why they are slow and rewrite them for better performance. Magento 2, being a complex e-commerce platform, often generates queries that can be optimized, particularly those involving joins, subqueries, and large data sets.

Consider a common scenario: retrieving product data with associated categories and attributes. A naive query might look something like this:

SELECT
    e.entity_id,
    e.sku,
    e.type_id,
    e.created_at,
    e.updated_at,
    e.attribute_set_id,
    e.parent_id,
    e.created_in,
    e.row_id,
    e.has_options,
    e.required_options,
    e.image,
    e.small_image,
    e.thumbnail,
    e.url_key,
    e.name,
    e.short_description,
    e.description,
    e.meta_title,
    e.meta_keyword,
    e.meta_description,
    e.custom_layout_update,
    e.custom_xml_layout_update,
    e.page_layout,
    e.options_container,
    e.enable_google_checkout,
    e.is_imported,
    e.price,
    e.special_price,
    e.special_from_date,
    e.special_to_date,
    e.cost,
    e.status,
    e.visibility,
    e.tax_class_id,
    e.weight,
    e.manufacturer,
    e.color,
    e.size,
    e.material,
    e.gender,
    e.age,
    e.news_from_date,
    e.news_to_date,
    e.country_of_manufacture,
    e.custom_design,
    e.custom_design_from,
    e.custom_design_to,
    e.custom_layout,
    e.custom_options_read_only,
    e.options_title,
    e.msrp_display_actual_price,
    e.msrp_enabled,
    e.msrp_price,
    e.msrp_value,
    e.price_view,
    e.shipment_type,
    e.tier_price,
    e.media_gallery,
    e.tier_price_changed,
    e.old_price,
    e.is_salable,
    e.required_options_display,
    e.required_options_label,
    e.required_options_type,
    e.required_options_value,
    e.required_options_sku,
    e.required_options_qty,
    e.required_options_price,
    e.required_options_weight,
    e.required_options_image,
    e.required_options_small_image,
    e.required_options_thumbnail,
    e.required_options_url_key,
    e.required_options_name,
    e.required_options_short_description,
    e.required_options_description,
    e.required_options_meta_title,
    e.required_options_meta_keyword,
    e.required_options_meta_description,
    e.required_options_custom_layout_update,
    e.required_options_custom_xml_layout_update,
    e.required_options_page_layout,
    e.required_options_options_container,
    e.required_options_enable_google_checkout,
    e.required_options_is_imported,
    e.required_options_price,
    e.required_options_special_price,
    e.required_options_special_from_date,
    e.required_options_special_to_date,
    e.required_options_cost,
    e.required_options_status,
    e.required_options_visibility,
    e.required_options_tax_class_id,
    e.required_options_weight,
    e.required_options_manufacturer,
    e.required_options_color,
    e.required_options_size,
    e.required_options_material,
    e.required_options_gender,
    e.required_options_age,
    e.required_options_news_from_date,
    e.required_options_news_to_date,
    e.required_options_country_of_manufacture,
    e.required_options_custom_design,
    e.required_options_custom_design_from,
    e.required_options_custom_design_to,
    e.required_options_custom_layout,
    e.required_options_custom_options_read_only,
    e.required_options_options_title,
    e.required_options_msrp_display_actual_price,
    e.required_options_msrp_enabled,
    e.required_options_msrp_price,
    e.required_options_msrp_value,
    e.required_options_price_view,
    e.required_options_shipment_type,
    e.required_options_tier_price,
    e.required_options_media_gallery,
    e.required_options_tier_price_changed,
    e.required_options_old_price,
    e.required_options_is_salable,
    e.required_options_required_options_display,
    e.required_options_required_options_label,
    e.required_options_required_options_type,
    e.required_options_required_options_value,
    e.required_options_required_options_sku,
    e.required_options_required_options_qty,
    e.required_options_required_options_price,
    e.required_options_required_options_weight,
    e.required_options_required_options_image,
    e.required_options_required_options_small_image,
    e.required_options_required_options_thumbnail,
    e.required_options_required_options_url_key,
    e.required_options_required_options_name,
    e.required_options_required_options_short_description,
    e.required_options_required_options_description,
    e.required_options_required_options_meta_title,
    e.required_options_required_options_meta_keyword,
    e.required_options_required_options_meta_description,
    e.required_options_required_options_custom_layout_update,
    e.required_options_required_options_custom_xml_layout_update,
    e.required_options_required_options_page_layout,
    e.required_options_required_options_options_container,
    e.required_options_required_options_enable_google_checkout,
    e.required_options_required_options_is_imported,
    e.required_options_required_options_price,
    e.required_options_required_options_special_price,
    e.required_options_required_options_special_from_date,
    e.required_options_required_options_special_to_date,
    e.required_options_required_options_cost,
    e.required_options_required_options_status,
    e.required_options_required_options_visibility,
    e.required_options_required_options_tax_class_id,
    e.required_options_required_options_weight,
    e.required_options_required_options_manufacturer,
    e.required_options_required_options_color,
    e.required_options_required_options_size,
    e.required_options_required_options_material,
    e.required_options_required_options_gender,
    e.required_options_required_options_age,
    e.required_options_required_options_news_from_date,
    e.required_options_required_options_news_to_date,
    e.required_options_required_options_country_of_manufacture,
    e.required_options_required_options_custom_design,
    e.required_options_required_options_custom_design_from,
    e.required_options_required_options_custom_design_to,
    e.required_options_required_options_custom_layout,
    e.required_options_required_options_custom_options_read_only,
    e.required_options_required_options_options_title,
    e.required_options_required_options_msrp_display_actual_price,
    e.required_options_required_options_msrp_enabled,
    e.required_options_required_options_msrp_price,
    e.required_options_required_options_msrp_value,
    e.required_options_required_options_price_view,
    e.required_options_required_options_shipment_type,
    e.required_options_required_options_tier_price,
    e.required_options_required_options_media_gallery,
    e.required_options_required_options_tier_price_changed,
    e.required_options_required_options_old_price,
    e.required_options_required_options_is_salable,
    e.required_options_required_options_required_options_display,
    e.required_options_required_options_required_options_label,
    e.required_options_required_options_required_options_type,
    e.required_options_required_options_required_options_value,
    e.required_options_required_options_required_options_sku,
    e.required_options_required_options_required_options_qty,
    e.required_options_required_options_required_options_price,
    e.required_options_required_options_required_options_weight,
    e.required_options_required_options_required_options_image,
    e.required_options_required_options_required_options_small_image,
    e.required_options_required_options_required_options_thumbnail,
    e.required_options_required_options_required_options_url_key,
    e.required_options_required_options_required_options_name,
    e.required_options_required_options_required_options_short_description,
    e.required_options_required_options_required_options_description,
    e.required_options_required_options_required_options_meta_title,
    e.required_options_required_options_required_options_meta_keyword,
    e.required_options_required_options_required_options_meta_description,
    e.required_options_required_options_required_options_custom_layout_update,
    e.required_options_required_options_required_options_custom_xml_layout_update,
    e.required_options_required_options_required_options_page_layout,
    e.required_options_required_options_required_options_options_container,
    e.required_options_required_options_required_options_enable_google_checkout,
    e.required_options_required_options_required_options_is_imported,
    e.required_options_required_options_required_options_price,
    e.required_options_required_options_required_options_special_price,
    e.required_options_required_options_required_options_special_from_date,
    e.required_options_required_options_required_options_special_to_date,
    e.required_options_required_options_required_options_cost,
    e.required_options_required_options_required_options_status,
    e.required_options_required_options_required_options_visibility,
    e.required_options_required_options_required_options_tax_class_id,
    e.required_options_required_options_required_options_weight,
    e.required_options_required_options_required_options_manufacturer,
    e.required_options_required_options_required_options_color,
    e.required_options_required_options_required_options_size,
    e.required_options_required_options_required_options_material,
    e.required_options_required_options_required_options_gender,
    e.required_options_required_options_required_options_age,
    e.required_options_required_options_required_options_news_from_date,
    e.required_options_required_options_required_options_news_to_date,
    e.required_options_required_options_required_options_country_of_manufacture,
    e.required_options_required_options_required_options_custom_design,
    e.required_options_required_options_required_options_custom_design_from,
    e.required_options_required_options_required_options_custom_design_to,
    e.required_options_required_options_required_options_custom_layout,
    e.required_options_required_options_required_options_custom_options_read_only,
    e.required_options_required_options_required_options_options_title,
    e.required_options_required_options_required_options_msrp_display_actual_price,
    e.required_options_required_options_required_options_msrp_enabled,
    e.required_options_required_options_required_options_msrp_price,
    e.required_options_required_options_required_options_msrp_value,
    e.required_options_required_options_required_options_price_view,
    e.required_options_required_options_required_options_shipment_type,
    e.required_options_required_options_required_options_tier_price,
    e.required_options_required_options_required_options_media_gallery,
    e.required_options_required_options_required_options_tier_price_changed,
    e.required_options_required_options_required_options_old_price,
    e.required_options_required_options_required_options_is_salable,
    c.category_id,
    c.path AS category_path,
    c.level AS category_level,
    c.position AS category_position,
    c.children_count AS category_children_count,
    ca.value AS category_name
FROM
    catalog_product_entity AS e
LEFT JOIN
    catalog_category_product_index AS ccpi ON e.entity_id = ccpi.product_id
LEFT JOIN
    catalog_category_entity AS c ON ccpi.category_id = c.entity_id
LEFT JOIN
    catalog_category_entity_varchar AS ca ON c.entity_id = ca.entity_id AND ca.attribute_id = (SELECT attribute_id FROM eav_attribute WHERE entity_type_id = (SELECT entity_type_id FROM eav_entity_type WHERE entity_type_code = 'catalog_category') AND attribute_code = 'name') AND ca.store_id = 1
WHERE
    e.entity_id = 12345;

This query is problematic for several reasons:

  • Excessive Column Selection: It selects a vast number of columns, many of which might not be needed for the specific context. This increases I/O and network traffic.
  • Complex Joins: Multiple joins, especially with index tables like catalog_category_product_index, can be resource-intensive.
  • Subquery for Attribute Name: The subquery to fetch the category name is inefficient. It’s executed for every row, and its performance depends heavily on the eav_attribute and eav_entity_type tables.
  • Lack of Specificity: If this query is used in a loop or for multiple products, the repeated execution of such a broad query becomes a significant bottleneck.

A more optimized approach would involve:

  • Selecting Only Necessary Columns: Identify the exact fields required by the application logic and select only those.
  • Denormalization or Pre-computation: For frequently accessed, derived data (like category names), consider denormalizing or pre-computing values into dedicated tables or using Magento’s EAV indexing mechanisms more effectively.
  • Optimizing Joins: Ensure appropriate indexes are in place on join columns.
  • Using `EXPLAIN`: Always use EXPLAIN on your queries to understand the execution plan and identify missing indexes or inefficient join strategies.

Here’s a refactored example, assuming we only need the product SKU, name, and its primary category name:

SELECT
    e.sku,
    e.name,
    c.name AS category_name
FROM
    catalog_product_entity AS e
INNER JOIN
    catalog_category_product_index AS ccpi ON e.entity_id = ccpi.product_id AND ccpi.is_main_category = 1 -- Assuming 'is_main_category' flag exists or can be derived
INNER JOIN
    catalog_category_entity_varchar AS c ON ccpi.category_id = c.entity_id
    AND c.attribute_id = (SELECT attribute_id FROM eav_attribute WHERE attribute_code = 'name' AND entity_type_id = (SELECT entity_type_id FROM eav_entity_type WHERE entity_type_code = 'catalog_category'))
    AND c.store_id = 1
WHERE
    e.entity_id = 12345;

This revised query is more targeted. The `INNER JOIN` on `catalog_category_product_index` is crucial, and if Magento provides a way to identify the “main” category (e.g., via a flag or by position), that should be leveraged to avoid fetching all categories. The subquery for the attribute ID is still present but is less of a bottleneck if the `eav_attribute` and `eav_entity_type` tables are well-indexed.

Indexing Strategies for Magento 2 Tables

Proper indexing is paramount for database performance. Magento 2’s EAV (Entity-Attribute-Value) model, while flexible, can lead to complex table structures and performance challenges if not indexed correctly. Beyond the default indexes, specific strategies can significantly improve query times.

Common Bottleneck Tables and Indexes:

  • catalog_product_entity: Primary key on entity_id is standard. Consider indexes on frequently queried attributes if they are stored directly in this table (though most are in EAV tables).
  • catalog_product_entity_varchar, catalog_product_entity_int, catalog_product_entity_decimal, catalog_product_entity_text, catalog_product_entity_datetime: These are the core EAV tables. The composite index (entity_type_id, attribute_id, store_id, value) is critical for attribute lookups. However, querying by value directly can be slow if the value column is large (e.g., for text or decimal).
  • catalog_category_entity_varchar: Similar to product EAV tables, indexes on (entity_id, attribute_id, store_id) are essential.
  • catalog_category_product_index: This table is heavily used for category-product relationships. Indexes on (category_id, product_id) and (product_id, category_id) are vital. If you frequently query for a product’s main category, consider adding an index that includes a flag or ordering column if available.
  • quote and sales_order tables: These tables can grow very large. Indexes on customer_id, created_at, and status fields are often beneficial for reporting and cleanup tasks.
  • inventory_stock_1 (and similar for other stock IDs): Indexes on product_id and stock_id are crucial for inventory checks.

Adding Custom Indexes:

When pt-query-digest or EXPLAIN reveals queries that are performing full table scans or inefficient index usage, custom indexes might be necessary. For example, if you frequently filter products by a specific custom attribute (e.g., `my_custom_attribute_varchar`):

-- First, find the attribute_id and entity_type_id for your custom attribute
SELECT attribute_id, entity_type_id
FROM eav_attribute
WHERE attribute_code = 'my_custom_attribute_varchar';

-- Assuming attribute_id = 123 and entity_type_id = 4 (for catalog_product)
-- Add a composite index to the relevant EAV table
ALTER TABLE catalog_product_entity_varchar
ADD INDEX idx_custom_attribute_varchar (entity_type_id, attribute_id, store_id, value(255)); -- Adjust value length as needed

Important Considerations for EAV Indexes:

  • Index Selectivity: Indexes on low-cardinality columns (columns with few unique values) are less effective.
  • Index Size: Over-indexing can degrade write performance and consume excessive disk space.
  • Attribute Type: The optimal index strategy differs for varchar, int, decimal, and text attributes. For large text fields, consider indexing only a prefix of the value.
  • Magento Version and Configuration: Newer Magento versions might have improved indexing strategies. Ensure your Magento configuration (e.g., flat catalog settings) is aligned with your indexing approach.

Leveraging MySQL Configuration Tuning

Beyond query and index optimization, fine-tuning MySQL server configuration parameters is crucial for maximizing performance. These settings directly impact how MySQL utilizes system resources like memory and CPU.

Key Parameters for Magento 2:

[mysqld]
# General Settings
innodb_buffer_pool_size = 4G       # Crucial for InnoDB performance. Aim for 70-80% of available RAM on a dedicated DB server.
innodb_log_file_size = 512M        # Larger log files can improve write performance, but increase recovery time.
innodb_log_buffer_size = 16M       # Buffer for transactions before writing to log file.
innodb_flush_log_at_trx_commit = 1 # ACID compliance. Set to 2 for slightly better performance at minor risk.
innodb_flush_method = O_DIRECT     # Avoid double buffering by the OS. Requires careful OS tuning.
innodb_io_capacity = 2000          # Adjust based on your storage subsystem's IOPS.
innodb_io_capacity_max = 4000      # Higher value for SSDs.

# Query Cache (Deprecated in MySQL 5.7, Removed in 8.0 - Use with caution if on older versions)
# query_cache_type = 1
# query_cache_size = 256M
# query_cache_limit = 1M

# Connection Handling
max_connections = 300              # Adjust based on application load and server capacity.
thread_cache_size = 16             # Cache threads for reuse.

# Sorting and Joins
sort_buffer_size = 2M              # Per-thread buffer for sorts.
join_buffer_size = 2M              # Per-thread buffer for joins.
read_rnd_buffer_size = 1M          # Per-thread buffer for reading rows after a sort.

# Temporary Tables
tmp_table_size = 64M               # Max size for in-memory temporary tables.
max_heap_table_size = 64M          # Max size for in-memory temporary tables.

# Character Set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

# Other
table_open_cache = 2000
table_definition_cache = 1000
slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1

Tuning Methodology:

  • Start with Defaults and Incremental Changes: Don’t blindly apply settings. Understand what each parameter does.
  • Monitor Key Metrics: Use tools like SHOW GLOBAL STATUS;, SHOW ENGINE INNODB STATUS;, and performance monitoring systems (e.g., Prometheus with mysqld_exporter) to track buffer pool hit rate, I/O activity, temporary table usage, and connection counts.
  • `innodb_buffer_pool_size` is King: This is the most critical parameter for InnoDB. Ensure it’s large enough to hold your working dataset. A low hit rate indicates the buffer pool is too small.
  • `innodb_log_file_size` and `innodb_flush_log_at_trx_commit`: These impact write performance. Setting innodb_flush_log_at_trx_commit = 2 can offer a significant boost for write-heavy workloads, but be aware of the slight risk of data loss in case of a crash.
  • Per-Thread Buffers: Parameters like sort_buffer_size and join_buffer_size are allocated per connection. Setting them too high can exhaust memory quickly. Tune them based on observed query patterns and available RAM.
  • Temporary Tables: Monitor Created_tmp_disk_tables vs. Created_tmp_tables in SHOW GLOBAL STATUS;. If many temporary tables are created on disk, increasing tmp_table_size and max_heap_table_size might help, but often indicates a need for query optimization or better indexing.
  • Replication Lag: If using replication, monitor replica lag closely. High write loads and inefficient configurations can exacerbate lag.

After making changes, restart the MySQL server and monitor performance closely. Use pt-query-digest again to see if the problematic queries have improved or disappeared.

Advanced Techniques: Partitioning and Read Replicas

For extremely large Magento 2 installations or those experiencing significant read/write contention, advanced strategies like table partitioning and the use of read replicas become essential.

Table Partitioning

Partitioning involves dividing large tables into smaller, more manageable pieces based on defined rules (e.g., date ranges, hash values). This can improve query performance by allowing MySQL to scan only relevant partitions, and it simplifies data management (e.g., archiving old data).

Common candidates for partitioning in Magento 2 include:

  • sales_order: Partitioning by date (e.g., month or year) can significantly speed up historical order lookups and archiving.
  • quote: Similar to orders, partitioning by creation date can help manage the large number of abandoned carts.
  • report_event: For sites with extensive event logging.

Example: Partitioning sales_order by year:

-- Ensure the table is not partitioned already
SHOW CREATE TABLE sales_order;

-- If not partitioned, create partitions (example for 2023 and 2024)
-- This requires careful planning and downtime.
-- It's often easier to create a new partitioned table and migrate data.

-- Example for creating partitions on a new table or during a maintenance window:
-- This is a simplified example; actual implementation requires careful consideration of date formats and ranges.
CREATE TABLE sales_order_partitioned LIKE sales_order;

ALTER TABLE sales_order_partitioned
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- After creating the partitioned table, you would need to migrate data from the original table.
-- This is a complex operation and requires thorough testing.
-- INSERT INTO sales_order_partitioned SELECT * FROM sales_order WHERE YEAR(created_at) = 2023;
-- INSERT INTO sales_order_partitioned SELECT * FROM sales_order WHERE YEAR(created_at) = 2024;
-- ... and so on.

-- Once data is migrated, rename tables and update application references.
-- RENAME TABLE sales_order TO sales_order_old, sales_order_partitioned TO sales_order;

Considerations for Partitioning:

  • Downtime: Creating partitions on existing large tables often requires significant downtime.
  • Query Rewrite: Queries must be written to take advantage of partitioning (e.g., filtering by the partitioning key). Magento’s ORM might need adjustments.
  • Management Overhead: New partitions need to be added periodically.
  • Performance Impact: While beneficial for specific queries, partitioning can sometimes add overhead to queries that don’t filter by the partition key.

Read Replicas

Read replicas allow you to offload read-heavy operations from your primary MySQL server. This is particularly effective for Magento 2, which often has a high ratio of read operations (browsing products, categories) to write operations (placing orders, updating inventory).

Setup Overview:

# On the Master Server (Primary DB)
# Enable binary logging
log_bin = mysql-bin
server-id = 1

# On the Replica Server(s)
server-id = 2 # Must be unique
read_only = 1 # Prevent accidental writes to the replica
relay_log = mysql-relay-bin
# ... other replication-related settings ...

# After configuring both, start replication:
# On the Master:
# CREATE USER 'repl_user'@'%' IDENTIFIED BY 'password';
# GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'%';
# FLUSH PRIVILEGES;
# SHOW MASTER STATUS; -- Note

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

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala