If you've been running a Magento 2 store for more than a year, your database is quietly growing. Log tables bloat into millions of rows. Changelog tables feed indexers that take longer every week. customer_visitor alone can accumulate gigabytes of data that nobody ever reads. The result? Slower admin pages, longer reindex times, bigger backups, and degraded query performance across the board.
This guide covers a practical, repeatable database maintenance strategy for Magento 2 stores — from routine cleanup to long-term monitoring and automation.
The Hidden Cost of Bloated Tables
Magento 2 ships with several tables that grow continuously but are rarely cleaned:
-
report_event— tracks product comparison and view events -
report_viewed_product_index— viewed product index -
report_compared_product_index— compared product index -
customer_log— customer login/logout history -
customer_visitor— visitor session tracking -
catalogsearch_result— search queries stored for analytics -
quote— abandoned carts (never converted) -
sales_order_grid— admin grid data (can grow very large)
A mid-sized store processing 200 orders/day with 5,000 daily visitors can accumulate 2-3 million rows per month across these tables. After a year, you're looking at 20-40 million rows of stale data that serves no operational purpose.
The impact is real:
- Reindex times increase because indexers read from changelog tables that keep growing
- Admin grids slow down because pagination queries scan bloated tables
- Backups take longer and consume more disk
- Database restore (discovery) during incidents takes minutes you don't have
Step 1: Audit Your Database Size
Before cleaning anything, measure what you have. Run this query to identify the largest tables:
SELECT table_name AS 'Table',
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)',
table_rows AS 'Rows'
FROM information_schema.tables
WHERE table_schema = 'your_magento_db'
ORDER BY (data_length + index_length) DESC
LIMIT 20;
Focus on tables larger than 500MB with row counts in the millions. The usual suspects will be customer_visitor, report_event, quote, and sales_order_grid.
Step 2: Magento's Built-in Log Cleaning
Magento 2 has a built-in mechanism for cleaning log tables, configured in Stores > Configuration > Advanced > System > Log Cleaning. You can also configure it via env.php or XML:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Log:etc/system.xsd">
<system>
<log>
<cleaning>
<enabled>1</enabled>
<time>03:00:00</time>
<frequency>D</frequency>
<log_save_days>14</log_save_days>
</cleaning>
</log>
</system>
</config>
Key settings:
-
log_save_days: Number of days to retain log entries. Default is 14, but for most stores, 7 days is sufficient. -
frequency:D(daily),W(weekly), orM(monthly). Daily is recommended. -
time: When to run the cleanup. Pick a low-traffic window.
This handles the core log tables (report_event, report_viewed_product_index, report_compared_product_index) automatically.
However, Magento's built-in cleaning does not cover customer_visitor, customer_log, or catalogsearch_result. You need a custom cleanup for those.
Step 3: Custom Cleanup for Visitor and Session Data
The customer_visitor table is one of the worst offenders for database bloat. Every visitor session — whether the visitor logs in or not — gets a row. For a store with 10,000 daily visitors, this adds 300,000 rows per month.
Create a cleanup mechanism using Magento's cron system:
<?php
// Module: Vendor/DatabaseCleanup/Cron/CleanVisitorLogs.php
namespace Vendor\DatabaseCleanup\Cron;
use Magento\Framework\App\ResourceConnection;
class CleanVisitorLogs
{
private const RETENTION_DAYS = 30;
private ResourceConnection $resource;
public function __construct(ResourceConnection $resource)
{
$this->resource = $resource;
}
public function execute(): void
{
$connection = $this->resource->getConnection();
$tableName = $this->resource->getTableName('customer_visitor');
$cutoffDate = date('Y-m-d H:i:s', strtotime('-' . self::RETENTION_DAYS . ' days'));
$connection->delete(
$tableName,
['created_at < ?' => $cutoffDate]
);
}
}
Register it in crontab.xml:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="vendor_databasecleanup_clean_visitor_logs" instance="Vendor\DatabaseCleanup\Cron\CleanVisitorLogs" method="execute">
<schedule>0 3 * * *</schedule>
</job>
</group>
</config>
Apply the same pattern to customer_log and catalogsearch_result. These tables serve no purpose beyond 30 days.
Step 4: Abandoned Quote Cleanup
The quote table stores every cart ever created, including abandoned carts that never converted. Over time, this table becomes massive and slows down checkout queries because new quotes are inserted alongside millions of abandoned ones.
Magento provides a built-in sales_clean_quotes cron job, but it's often disabled or set to retain too many days. Check your configuration:
bin/magento config:show sales/clean_quotes/enabled
# If disabled, enable it:
bin/magento config:set sales/clean_quotes/enabled 1
bin/magento config:set sales/clean_quotes/lifetime 30
This automatically removes quotes older than 30 days that haven't been converted to orders. Set the lifetime to match your business needs — 30 days is a good default for most stores.
Step 5: Changelog Table Management
Magento 2's MVIEW system uses _cl changelog tables to track changes for indexers. Every INSERT, UPDATE, or DELETE on a tracked table writes to the changelog. When indexers run, they process the changelog and truncate it — but if indexers fail or run too infrequently, changelog tables accumulate millions of rows.
Monitor changelog table sizes:
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = 'your_magento_db'
AND table_name LIKE '%_cl'
ORDER BY table_rows DESC;
Healthy changelog tables should have few rows — typically in the hundreds or low thousands. If you see millions of rows, your indexers are failing or running too infrequently.
Actions:
- Check indexer status:
bin/magento indexer:status— all should be "Ready" - If any indexer is "Reindex required" repeatedly, investigate the root cause (memory limits, deadlocks, long-running queries)
- Run
bin/magento indexer:reindex <indexer_name>to force a full reindex and clear the changelog - If a changelog is stuck and you've already reindexed, you can safely truncate it:
TRUNCATE TABLE catalog_product_entity_cl;
Step 6: Table Optimization (OPTIMIZE TABLE)
MySQL's InnoDB engine doesn't reclaim disk space after DELETE operations — it marks pages as free internally. Over time, this leads to fragmentation where the physical file size significantly exceeds the actual data size.
Run OPTIMIZE TABLE on heavily deleted tables to reclaim space:
OPTIMIZE TABLE customer_visitor;
OPTIMIZE TABLE report_event;
OPTIMIZE TABLE quote;
Important considerations:
-
OPTIMIZE TABLElocks the table during execution. For large tables, this can take minutes. Run it during maintenance windows. - It requires free disk space equal to the table size (MySQL creates a copy of the table).
- Use it sparingly — monthly or quarterly is sufficient for most tables.
- For very large tables where downtime isn't acceptable, use
pt-online-schema-changefrom Percona Toolkit:
pt-online-schema-change --alter "ENGINE=InnoDB" \
D=your_magento_db,t=quote \
--execute
Step 7: Monitor Database Health Continuously
Set up monitoring to catch bloat before it becomes a problem. A simple approach is a weekly cron that logs table sizes:
#!/bin/bash
# db-health-check.sh
DB_NAME="your_magento_db"
mysql -e "
SELECT NOW() as checked_at, table_name,
ROUND((data_length + index_length) / 1024 / 1024, 2) as size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = '${DB_NAME}'
ORDER BY (data_length + index_length) DESC
LIMIT 20;
" >> /var/log/magento-db-health.log
Schedule it weekly:
0 6 * * 1 /path/to/db-health-check.sh
Alert thresholds:
- Any log table > 1GB → cleanup needed
- Any changelog table > 100,000 rows → indexer issue
- Database total size growth > 20% month-over-month → investigate
Step 8: Automated Maintenance Script
For a complete automated routine, create a script that combines all cleanup operations:
#!/bin/bash
# magento-db-maintenance.sh
# Run weekly during low-traffic window
DB_NAME=$(grep 'dbname' app/etc/env.php | head -1 | sed "s/.*'dbname' => '//;s/'.*//")
DB_USER=$(grep 'username' app/etc/env.php | head -1 | sed "s/.*'username' => '//;s/'.*//")
DB_PASS=$(grep 'password' app/etc/env.php | head -1 | sed "s/.*'password' => '//;s/'.*//")
MYSQL_CMD="mysql -u${DB_USER} -p${DB_PASS} ${DB_NAME}"
echo "[$(date)] Starting database maintenance..."
# 1. Clean old customer visitors (45 days)
echo "Cleaning customer_visitor..."
$MYSQL_CMD -e "DELETE FROM customer_visitor WHERE created_at < DATE_SUB(NOW(), INTERVAL 45 DAY);"
# 2. Clean catalogsearch_result (30 days)
echo "Cleaning catalogsearch_result..."
$MYSQL_CMD -e "DELETE FROM catalogsearch_result WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);"
# 3. Optimize the cleaned tables
echo "Optimizing tables..."
$MYSQL_CMD -e "OPTIMIZE TABLE customer_visitor;" 2>/dev/null
$MYSQL_CMD -e "OPTIMIZE TABLE report_event;" 2>/dev/null
$MYSQL_CMD -e "OPTIMIZE TABLE catalogsearch_result;" 2>/dev/null
# 4. Check for stale changelogs
echo "Changelog table sizes:"
$MYSQL_CMD -e "SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE '%_cl' ORDER BY table_rows DESC;"
echo "[$(date)] Maintenance complete."
Schedule it through Magento's cron or system crontab:
0 4 * * 0 cd /var/www/magento && ./scripts/magento-db-maintenance.sh >> /var/log/magento-db-maintenance.log 2>&1
Step 9: Impact on Backups and Recovery
A bloated database doesn't just slow down day-to-day operations — it directly impacts your disaster recovery capability. Consider:
- A 15GB database backup takes roughly 2-3 minutes to compress and transfer. A 50GB database takes 10+ minutes.
- During an incident, every minute of restore time matters.
- If 60% of your database is stale log data, you're spending 60% more time on backups and restores than necessary.
After implementing a cleanup routine, track your backup size weekly. A healthy database should grow proportionally to your order volume, not your visitor traffic. If backup growth outpaces order growth, you have a bloat problem.
Step 10: Never Delete Sales Data
One important note: never clean up sales_order, sales_order_grid, sales_invoice, sales_shipment, or other transactional sales tables as part of routine maintenance. These are legal records that must be retained per your local regulations (typically 7-10 years for tax/accounting purposes).
If these tables grow too large for comfortable operation, that's a signal to implement table partitioning by date rather than deletion. Partitioning splits the physical storage by month or year, keeping queries fast on recent data while preserving historical records.
Conclusion
Database maintenance isn't glamorous, but it's one of the highest-ROI performance tasks for Magento 2 stores. A clean database means faster admin pages, quicker reindex times, smaller backups, and more predictable performance under load.
Start with an audit, configure the built-in log cleaning, add custom cleanup for visitor tables, automate it with cron, and monitor table sizes monthly. Your future self — and your on-call engineer — will thank you.
The key is consistency. A weekly cleanup that removes stale data is far more effective than a quarterly scramble to deal with a 50GB database that nobody saw coming.
Top comments (0)