Database tuning for Super Speedy Imports
Importing 100k+ rows hits MySQL / MariaDB differently than serving web traffic. The same buffers that need to be small-per-connection for 200 concurrent web requests need to be large for one big sequential JOIN + DISTINCT. SSI splits the problem into two halves:
- Per-connection buffers that ONLY help the import worker — applied at
SET SESSIONscope so concurrent web connections never see the bump. These are opt-in per import via a checkbox. - Server-wide settings that can’t be set per-connection — left to the admin to configure in
my.cnf/50-server.cnf. This doc gives a recommended baseline for a host that serves both web traffic and runs SSI imports.
The opt-in lives in Additional Options → Optimise DB session settings for slow stages.
Table of Contents
Part 1 — Session-scoped settings SSI applies
Set only when enable_db_tuning is checked, only for stages that register tuning, and only for the lifetime of the import worker’s MySQL connection. Zero impact on any other connection.
Currently registered for: upsert-relationships. Other heavy stages can be added via the ssi_session_tuning_for_stage filter (see Part 3).
| Setting | MariaDB / MySQL default | SSI sets to | Why |
|---|---|---|---|
tmp_table_size |
16M | 512M | The per-taxonomy INSERT … SELECT DISTINCT JOIN materialises ~(post_id, term_taxonomy_id) pairs at ~16 bytes each. At 500k+ batch rows that’s ~10MB — right at the spill-to-disk threshold. Disk spill turns a seconds-long in-RAM op into a minutes-long I/O-bound one. |
max_heap_table_size |
16M | 512M | MySQL takes MIN(tmp_table_size, max_heap_table_size) for in-memory temp tables. Both must be raised together. |
sort_buffer_size |
2M (MariaDB) / 256K (MySQL) | 8M | DISTINCT and GROUP BY use this when sorting. |
join_buffer_size |
256K | 32M | For BNL (Block Nested Loop) fallback when an index can’t fully drive the join. With the 2.47 covering indexes this is rarely the limiting factor, but the buffer is allocated per-join so keeping it generous protects worst-case plans. |
read_buffer_size |
128K | 4M | Sequential reads from the batch table during the join. |
read_rnd_buffer_size |
256K | 16M | Index-driven random reads back into the post/term tables. |
Peak RAM cost: ~600MB on the single import worker connection during the tuned stage. Released when the stage completes its queries (the SESSION variable persists for the connection’s lifetime, but the buffers themselves are allocated per-query on-demand).
Why this is opt-in and not “always on”
Some users run SSI on shared hosting with 1-2GB total RAM. A 600MB-per-connection bump is fine for one worker but disastrous if accidentally applied globally to a 100-connection web pool. Opt-in default-OFF protects those users; the recommendation is turn it ON for any import of 100k+ rows on a host with 4GB+ RAM.
Why it’s safe even with the checkbox ON
SET SESSION only affects the connection that issued it. SSI imports run in their own PHP processes (CLI workers via proc_open, or one PHP-FPM process for AJAX-driven imports), each holding their own MySQL connection. Web traffic, cron jobs, other plugins, your own DB clients — none of them see the bumped values.
The session variables ALSO persist only for that connection’s lifetime. When the import process exits, the connection closes and every byte the buffers held is released back to the OS.
Part 2 — Server-wide settings (admin configures in my.cnf)
These cannot be set at session scope. They affect every connection — but unlike the SSI session bumps, they’re either one-time allocations (buffer pool) or per-query costs that scale with the workload, not 100-fold worst-case multipliers. Pick a baseline below that matches your server class and put it in your config.
Where to put these: typically /etc/mysql/mariadb.conf.d/50-server.cnf (MariaDB) or /etc/mysql/mysql.conf.d/mysqld.cnf (MySQL) under [mysqld]. Restart the daemon to apply (most of these can also be set at runtime via SET GLOBAL ... but won’t survive a restart unless also written to config).
Check your current values:
SHOW GLOBAL VARIABLES WHERE Variable_name IN (
'innodb_buffer_pool_size',
'innodb_buffer_pool_instances',
'innodb_log_file_size',
'innodb_log_buffer_size',
'innodb_flush_log_at_trx_commit',
'innodb_flush_method',
'innodb_io_capacity',
'innodb_io_capacity_max',
'innodb_autoinc_lock_mode',
'max_connections',
'thread_cache_size',
'table_open_cache',
'table_definition_cache',
'max_allowed_packet',
'query_cache_type',
'query_cache_size'
);
InnoDB core
| Setting | Default | Recommended (web + import mix) | Notes |
|---|---|---|---|
innodb_buffer_pool_size |
128M | 50–70% of total RAM on dedicated DB host. On a single-box LAMP setup (Apache + PHP-FPM + MariaDB), 25–40% of RAM — leave room for PHP workers and the OS file cache. e.g. 16GB box → 6G buffer pool. | The single biggest tunable. Holds InnoDB data + indexes in RAM. If your dataset fits, queries effectively become RAM-speed. SSI’s upsert-relationships benefits hugely when the entire wp_term_relationships, wp_term_taxonomy, and the per-import batch + hierarchies tables all fit. |
innodb_buffer_pool_instances |
8 (MariaDB 10.5+) / 1 (older) | 8 when pool ≥ 1G; 1 when pool < 1G | More instances = less contention on the buffer pool mutex under concurrent traffic. |
innodb_log_file_size |
96M (MariaDB 10.5+) / smaller older | 512M–1G | The redo log. Larger files = fewer fsyncs during heavy writes (SSI inserts/updates). Costs disk space and slightly longer crash recovery. For an import-heavy server, the speedup is worth it. |
innodb_log_buffer_size |
16M | 64M–128M | Buffers redo records in memory before flushing. Larger = fewer flushes during multi-MB transactions, which SSI generates constantly. |
innodb_flush_log_at_trx_commit |
1 (full ACID — fsync at every commit) | 1 for normal operation; 2 during import-only periods if you want max speed | At 1, every commit fsyncs to disk — durable but slow. At 2, the OS buffers the log writes and only loses ~1 second of writes on a crash. SSI’s parallel workers commit constantly; flipping to 2 can 2-3× import throughput. Set as SET GLOBAL before the import, set back after. Don’t leave at 2 permanently on a production WordPress site. |
innodb_flush_method |
fsync (Linux) |
O_DIRECT on SSD/NVMe |
Bypasses the OS page cache for InnoDB data files (the buffer pool already caches at the DB level — double-caching wastes RAM). No-op on HDD; significant memory saving on SSD. |
innodb_io_capacity |
200 | SSD: 1000–2000. NVMe: 5000–10000 | The default assumes spinning rust. SSDs can do 10× more IOPS; tell InnoDB so its background flushing keeps up with write bursts. |
innodb_io_capacity_max |
2000 | SSD: 4000. NVMe: 20000 | Burst capacity ceiling. Set 2× of innodb_io_capacity. |
innodb_autoinc_lock_mode |
2 in MySQL 8 / MariaDB 10.3+ | 2 | “Interleaved” — lets multiple parallel INSERTs allocate auto-increment IDs without serialising. Critical for SSI’s 8 parallel load-csv workers all inserting into the batch table concurrently. If your version still shows 1, your parallel inserts are bottlenecked by a single table-level auto-inc lock. Verify with SHOW GLOBAL VARIABLES LIKE 'innodb_autoinc_lock_mode';. |
innodb_file_per_table |
ON (MariaDB 10+ / MySQL 5.6+) | ON | One .ibd file per table — lets you reclaim space when a table is dropped. SSI creates per-import tables and drops them; with this OFF the shared tablespace grows forever. |
Connections
| Setting | Default | Recommended | Notes |
|---|---|---|---|
max_connections |
151 | 200–500 | SSI imports use up to 9 connections (1 main + 8 workers by default) on top of your normal web traffic. Bump enough that a running import never starves web connections. |
thread_cache_size |
0 (MySQL) / 200 (MariaDB 10.5+) | max_connections / 4 |
Caches threads instead of creating + destroying them per connection. Reduces connection-setup latency on the WordPress side. |
table_open_cache |
2000 | 4000–8000 | Each open table costs a file descriptor + memory. WP installs with many plugins (postmeta variants, custom tables, multisite) easily exceed 2000 distinct tables × connections × 2 (per-connection cache). |
table_definition_cache |
400 | 2000–4000 | Same as above for table definitions (.frm parsed metadata). |
Per-statement limits
| Setting | Default | Recommended | Notes |
|---|---|---|---|
max_allowed_packet |
4M (MariaDB) / 64M (MySQL 8) | 64M–256M | Caps a single SQL statement’s wire size. SSI’s bulk INSERTs and large serialized postmeta blobs hit this. If you see “Got a packet bigger than ‘max_allowed_packet’ bytes” during an import, this is why. |
Query cache (legacy)
| Setting | Default | Recommended | Notes |
|---|---|---|---|
query_cache_type |
OFF (MariaDB 10.3+) | OFF | The query cache serialises invalidation on every write. On a mixed workload — WP frontend reads + import writes hammering the same tables — invalidation contention can cost more than the cache saves. Removed entirely from MySQL 8. |
query_cache_size |
0 (often) / 1M | 0 | If query_cache_type = OFF, set this to 0 to free the memory. |
Part 3 — Overriding / extending SSI’s defaults
Apply tuning to other stages
add_filter('ssi_session_tuning_for_stage', function($tuning, $stage_name, $template) {
if ($stage_name === 'import-taxonomies') {
$tuning['tmp_table_size'] = 268435456; // 256M
$tuning['max_heap_table_size'] = 268435456;
$tuning['sort_buffer_size'] = 4194304; // 4M
}
return $tuning;
}, 20, 3); // priority 20 > SSI's default 10, so this runs after
Change SSI’s per-stage values
Same filter, higher priority — array_merge semantics mean your keys overwrite the bundled defaults:
add_filter('ssi_session_tuning_for_stage', function($tuning, $stage_name, $template) {
if ($stage_name === 'upsert-relationships') {
$tuning['tmp_table_size'] = 1073741824; // 1G instead of 512M
$tuning['max_heap_table_size'] = 1073741824;
}
return $tuning;
}, 20, 3);
Turn tuning off without unchecking the box
add_filter('ssi_session_tuning_for_stage', '__return_empty_array', 999, 3);
Inspect what SSI is applying
When tuning runs, the CLI prints a line like:
DB session tuning applied: tmp_table_size=512.0M, max_heap_table_size=512.0M, sort_buffer_size=8.0M, join_buffer_size=32.0M, read_buffer_size=4.0M, read_rnd_buffer_size=16.0M
If you don’t see this, either (a) the checkbox is off, or (b) the stage doesn’t have any registered tuning.
In --show-sql mode, the SET SESSION statements are printed but not applied, so you can preview without committing.
Part 4 — Per-size reference configs
Three tiers covering the range of host sizes SSI is realistically run on. Each one is complete — paste the [mysqld] block into your config, set the SSI-side options below it, restart MariaDB. Each tier accounts for everything competing for RAM during an import: the SSI PHP workers (~150-300MB each), the orchestrator PHP process, GridPane / cPanel / Plesk agents, the web stack, the OS itself.
The RAM budget below each tier shows where it goes — if your server runs unusually heavy / light workloads alongside, slide the buffer-pool size up or down accordingly.
4GB host — small / hobbyist / GridPane “Bronze” sized box
This tier is the floor. Imports work but you’re trading wall-clock time for memory headroom. Anything smaller than 4GB will swap-thrash. For imports >500k rows, an 8GB box pays for itself in the first run.
[mysqld]
# InnoDB — sized for the available RAM, not the workload
innodb_buffer_pool_size = 768M
innodb_buffer_pool_instances = 1 # 1 instance is correct below 1GB pool
innodb_log_file_size = 256M
innodb_log_buffer_size = 32M
innodb_flush_log_at_trx_commit = 1 # set to 2 ONLY during import; revert to 1 after
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_autoinc_lock_mode = 2
innodb_file_per_table = ON
# Connections — small box doesn't need many
max_connections = 50
thread_cache_size = 16
table_open_cache = 2000
table_definition_cache = 1000
# Per-statement
max_allowed_packet = 128M
# Query cache off
query_cache_type = OFF
query_cache_size = 0
SSI-side:
– Performance Options → Workers per stage: 2 (NOT 8)
– Additional Options → “Optimise DB session settings for slow stages”: OFF
Why off: the per-connection buffers SSI bumps add ~600MB during upsert-relationships. That plus the 768M buffer pool won’t fit alongside the PHP workers on a 4GB box.
RAM budget (during peak import): – MariaDB (buffer pool + connection overhead + daemon): ~1.0GB – 2 SSI PHP workers × ~200MB: 400MB – Orchestrator PHP + WP-CLI: 200MB – Web stack (nginx + PHP-FPM): 400MB – OS + GridPane agents + cron: 500MB – Total: ~2.5GB used, ~1.5GB headroom for the OS file cache
Will hit GridPane MEM_RESTART threshold? No — MariaDB stays under 1.2GB so the 1953MB GridPane trigger is safe.
8GB host — sweet spot for serious imports
The point where everything fits comfortably. Recommended size for production imports of 100k–1M rows.
[mysqld]
# InnoDB
innodb_buffer_pool_size = 2G
innodb_buffer_pool_instances = 4
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_autoinc_lock_mode = 2
innodb_file_per_table = ON
# Connections
max_connections = 100
thread_cache_size = 32
table_open_cache = 3000
table_definition_cache = 1500
# Per-statement
max_allowed_packet = 128M
# Query cache off
query_cache_type = OFF
query_cache_size = 0
SSI-side: – Performance Options → Workers per stage: 4 – Additional Options → “Optimise DB session settings for slow stages”: ON The 600MB session-tuning bump fits — 2GB buffer pool + 600MB during the tuned stage = 2.6GB MariaDB peak, leaves plenty of room for PHP workers.
RAM budget (during peak import): – MariaDB (buffer pool + session-tuning bump + overhead): ~2.8GB – 4 SSI PHP workers × ~200MB: 800MB – Orchestrator PHP: 200MB – Web stack: 600MB – OS + agents: 600MB – Total: ~5.0GB used, ~3.0GB headroom for OS file cache (which speeds reads further)
Will hit GridPane MEM_RESTART threshold? Yes — MariaDB will exceed 1953MB during upsert-relationships. Either raise the GridPane threshold to 4GB+ or disable the MEM_RESTART monitor on this server. See the GridPane section below.
16GB host — production-grade import server
For imports >1M rows or where wall-clock matters. The original tier the KB was sized for.
[mysqld]
# InnoDB
innodb_buffer_pool_size = 6G
innodb_buffer_pool_instances = 8
innodb_log_file_size = 1G
innodb_log_buffer_size = 128M
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_autoinc_lock_mode = 2
innodb_file_per_table = ON
# Connections
max_connections = 300
thread_cache_size = 75
table_open_cache = 4000
table_definition_cache = 2000
# Per-statement
max_allowed_packet = 128M
# Query cache off
query_cache_type = OFF
query_cache_size = 0
SSI-side: – Performance Options → Workers per stage: 8 – Additional Options → “Optimise DB session settings for slow stages”: ON
RAM budget (during peak import): – MariaDB (buffer pool + session-tuning + overhead): ~7.0GB – 8 SSI PHP workers × ~250MB: 2.0GB – Orchestrator PHP: 300MB – Web stack: 1.0GB – OS + agents: 700MB – Total: ~11GB used, ~5GB headroom for OS file cache (massive speedup for repeat reads)
Will hit GridPane MEM_RESTART threshold? Definitely — buffer pool alone exceeds it. Disable the monitor or raise the threshold to 8GB+. See the GridPane section below.
Sizing rule of thumb
If you don’t fit cleanly into one of the three tiers above, use this allocation:
| Component | Share of total RAM |
|---|---|
| MariaDB buffer pool | ~50% (single-purpose import server) or ~30% (mixed web + import) |
| MariaDB connection / log buffers | reserve another ~10% |
| SSI PHP workers (~250MB each, planned worker count) | calculate explicitly |
| Web stack + OS + agents | reserve 1.0–1.5GB minimum |
| OS file cache headroom | ≥20% of total RAM unused — InnoDB benefits from this even with O_DIRECT |
If your calculated “MariaDB-able” RAM is less than 512MB, you’re on the wrong server class — get more RAM. SSI’s smallest aux-table CREATE alone needs ~200MB to be comfortable during a 100k-row import.
GridPane-specific note
GridPane bundles a MySQL memory monitor (/usr/local/bin/gpworker) with hardcoded thresholds:
- MEM_HIGH warning: MySQL using ≥1757MB for 20 minutes
- MEM_RESTART action: MySQL using ≥1953MB for 40 minutes →
systemctl restart mariadb(kills any running import)
These thresholds are tuned for cookie-cutter WordPress hosts where MySQL shouldn’t grow large. They’re incompatible with the 8GB and 16GB tiers above — the recommended innodb_buffer_pool_size alone exceeds the threshold.
For an import server, either:
- Disable the MySQL memory monitor in GridPane’s UI / CLI (preferred — survives
gpupdate). - Raise the thresholds in
/usr/local/bin/gpworker— works but eachgpupdatemay overwrite it (the cron self-update runs every ~15-30 mins). - Pause the gpworker cron entry for the duration of the import:
bash sudo crontab -l | sed 's|^.*gpworker.*$|# &|' | sudo crontab - # … run import … sudo crontab -e # uncomment the gpworker lines when done
Without one of these, the GridPane monitor will kill MariaDB mid-import on any tier above 4GB.
Same applies to similar memory-watchdog features in cPanel / Plesk / RunCloud — check your control panel for “MySQL auto-restart on high memory” toggles before launching a big import.