跪拜 Guibai
← All articles
Backend · Interview · Java

10 Billion Rows into MySQL: From 231 Days to 3.5 Hours

By Rain的Java大神之路 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

MySQL bulk-load performance has hard ceilings, and the gap between a multi-month import and an afternoon job comes down to a handful of concrete techniques and parameter settings. The article's strongest point is its reminder to question whether MySQL is even the right store for the data, which is the architectural decision that matters more than any tuning knob.

Summary

A practical guide breaks down how to load 10 billion rows into MySQL in hours instead of months. The core math: row-by-row INSERT at ~1000 rows/s means 231 days, while sharding to 1000 tables and using LOAD DATA INFILE with 16–32 concurrent shard writers gets the job done in under 3.5 hours. The article walks through six steps — calculate scale, shard, batch, tune, parallelize, wrap up — with concrete code for JDBC batch (with the critical `rewriteBatchedStatements=true`), MyISAM staging, and shadow-table switchover.

The piece also explains why the "20 million rows per table" rule comes from B+ tree page math (16KB pages, 1KB rows, 8B primary keys), warns against common pitfalls like fake batches and UUID primary keys, and stresses that recovery, idempotency, reconciliation, and zero-downtime cuts are as important as raw speed. It ends with a blunt reminder: 10 billion rows of logs or time-series data shouldn't be in MySQL at all — ClickHouse, Doris, HBase, or TiDB may be the right tool.

Takeaways
— Row-by-row INSERT with auto-commit runs at 500–1000 rows/s, so 10 billion rows takes 115–231 days.
— JDBC Batch with `rewriteBatchedStatements=true` cuts network round-trips from N to 1; without it, `addBatch()` is a fake batch.
— `LOAD DATA INFILE` is 3–5x faster than batched INSERT and 10–50x faster than row-by-row, because the server reads the file directly and skips SQL parsing and per-row transactions.
— Sharding to 500–2000 tables keeps each table under the ~20 million-row threshold where a three-level B+ tree holds; beyond that, queries incur an extra disk IO.
— The "20 million per table" rule is derived from 16KB pages: 1170 non-leaf pointers per node × 1170 × 16 rows per leaf ≈ 21.9 million rows.
— Import-time parameters like `unique_checks=0`, `innodb_flush_log_at_trx_commit=2`, and `sync_binlog=0` trade crash durability for throughput and must be reverted after the import.
— Load data first, then build secondary indexes; inserting while maintaining indexes causes random IO and page splits, which is 30%+ slower.
— Parallelize at the shard level (one file, one connection, one transaction per shard), with 16–32 threads per machine; more concurrency can cause innodb_thread_concurrency contention.
— Use idempotent writes (`ON DUPLICATE KEY UPDATE` or `INSERT IGNORE`), a watermark table for resumable checkpoints, and shadow tables with atomic `RENAME` for zero-downtime switchover.
— For 10 billion rows of logs, time-series, or cold data, MySQL is often the wrong choice; ClickHouse, Doris, HBase, Elasticsearch, or TiDB may be more appropriate.
Conclusions

The article turns a common interview question into a funnel for engineering judgment: the first test isn't SQL knowledge but whether the candidate asks clarifying questions about data source, freshness, and idempotency requirements before writing any code.

The B+ tree derivation for the "20 million" rule is the kind of back-of-envelope math that separates practitioners who understand systems from those who memorize answers; quoting the number without the derivation signals a surface-level understanding.

The strongest architectural statement is the closing cold-water note — 10 billion rows of analytical or log data shouldn't go into MySQL at all. This reframes the entire exercise: the interviewer's question may itself be the trap, and the best answer challenges the premise.

The emphasis on re-runnable, idempotent, checkpointed imports reflects a production mindset where failure is expected; the article treats crash recovery and reconciliation as first-class concerns, not afterthoughts.

Concepts & terms
rewriteBatchedStatements
A MySQL JDBC driver parameter that rewrites a batch of N INSERT statements into a single multi-value INSERT, reducing network round-trips from N to 1. Without it, `addBatch()` sends individual statements and offers little speedup.
LOAD DATA INFILE
A MySQL statement that imports data directly from a file on the server (or client with LOCAL), bypassing SQL parsing, the optimizer, and per-row transaction handling. It's typically 3–5x faster than batched INSERT and 10–50x faster than row-by-row.
Shadow table
A technique for zero-downtime table replacement: load data into a copy of the table (the shadow), build indexes, validate, then atomically swap it into place with a `RENAME TABLE` statement. The old table can be kept as a rollback target.
Watermark table
A checkpoint table that records the progress of each shard import (offset, status, batch ID). On crash or retry, the import resumes from the saved offset instead of restarting from scratch.
MyISAM staging
An offline import trick: load data into a MyISAM table (which has no transactions and offers faster writes), then run `ALTER TABLE ... ENGINE=InnoDB` to convert it to InnoDB in one ordered rebuild. Only safe for re-runnable, offline scenarios.
B+ tree page math
A rough capacity calculation for InnoDB: with 16KB pages, 1KB rows, and 8B primary keys + 6B pointers, a three-level B+ tree holds ~21.9 million rows (1170 × 1170 × 16). Exceeding that depth adds a disk IO per query.
From the discussion
Featured comments
liyunde 1 likes

The approach is correct, but the conclusion is somewhat outdated; a single table with 1B rows is actually fine, with minimal performance impact; 20M is just old theory, purely theoretical, not accounting for actual configurations, only of reference value; 10B typically goes to a data warehouse; offline analysis methods are more common; in large-memory scenarios, these are not issues at all, loading the entire table into memory poses no bottleneck; the solution is closely tied to use-case cost.

Rain的Java大神之路

Thanks for the valuable addition! I fully agree that 'the solution is closely tied to use-case cost.' 20M is a theoretical reference, not a hard standard. The '20 million per table' figure is a theoretical reference based on 16KB pages and 1KB rows, not a hard red line. In a large-memory environment, the IO overhead of the four-level B+ tree is largely shielded by the buffer pool, so a single table's capacity can be greatly increased. The article mostly explains the origin of this experience value for interview purposes, and I also agree that for billion-row cold data, a data warehouse should be the priority.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗