giip
SES Proposal
Database MigrationMedium移行サイジングロールバックストレージ承認

How to Plan a 3TB-Scale Database Migration

Published 2026-08-13 · Updated 2026-08-13 · Last Verified 2026-08-13

Answer

A plan for a 3TB-scale migration is built in this order: (1) measure the actual size per object, (2) decide between a full stop / initial load + CDC catch-up / dual writes based on the acceptable downtime, (3) run a trial with representative data to measure duration, (4) decide on a consistency-verification method, and (5) define the rollback conditions and the point of no return. Don't start with a rule-of-thumb duration figure — the only numbers that belong in the plan are measured values from a trial run in your own environment.

Applicability of This Document

ProductDatabase migration (MySQL family / SQL Server family, etc.)
Verified VersionsThe sizing SQL is verified on MySQL 5.7 / 8.0 and SQL Server 2012 and later. Check the migration tool version for each environment
EnvironmentOn-premises, AWS, Azure (and migrations between them)
Required PermissionsSizing requires metadata read privileges (`VIEW DATABASE STATE` on SQL Server). Retrieving a dump requires `SELECT` on the target schema
Execution ImpactNo impact from the sizing SQL. Retrieving a dump and a trial load put load on both the source and target
RestartNot required for measurement. Whether cutover involves downtime depends on the chosen method
Last Verified2026-08-13

Ready-to-Run Commands

Measure actual per-object size on the MySQL familyRead-only
対象
MySQL 5.7 / 8.0, Aurora MySQL 2.x / 3.x
権限
Read access to `information_schema`
変更作業
None (read-only)
Production実行
Possible
-- 対象: MySQL 5.7 / 8.0、Aurora MySQL 2.x / 3.x
-- 権限: information_schema の参照権限
-- 変更作業: なし(参照のみ)
-- Production 実行: 可能
SELECT
    TABLE_SCHEMA,
    TABLE_NAME,
    ENGINE,
    TABLE_ROWS                                              AS estimated_rows,
    ROUND(DATA_LENGTH        / 1024 / 1024 / 1024, 2)       AS data_gb,
    ROUND(INDEX_LENGTH       / 1024 / 1024 / 1024, 2)       AS index_gb,
    ROUND(DATA_FREE          / 1024 / 1024 / 1024, 2)       AS free_gb,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS total_gb
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
  AND TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC;

-- スキーマ単位の合計(全体像の把握用)
SELECT
    TABLE_SCHEMA,
    COUNT(*)                                                     AS table_count,
    ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS total_gb
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
  AND TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY TABLE_SCHEMA
ORDER BY total_gb DESC;

`TABLE_ROWS` is an estimate on InnoDB. Re-measure with `COUNT(*)` only for tables where you need the exact row count. A table with a high `index_gb` ratio is a good candidate for the "rebuild the index after loading" strategy described later.

Measure actual per-object size on SQL ServerRead-only
対象
SQL Server 2012 and later, Amazon RDS for SQL Server, Azure SQL Managed Instance
権限
`VIEW DATABASE STATE`
変更作業
None (read-only)
Production実行
Possible
-- 対象: SQL Server 2012 以降(RDS / Azure SQL Managed Instance を含む)
-- 権限: VIEW DATABASE STATE
-- 変更作業: なし(参照のみ)
-- Production 実行: 可能
SELECT
    SCHEMA_NAME(t.schema_id)                                            AS schema_name,
    t.name                                                              AS table_name,
    SUM(CASE WHEN ps.index_id IN (0, 1) THEN ps.row_count ELSE 0 END)   AS row_count,
    SUM(ps.reserved_page_count) * 8.0 / 1024 / 1024                     AS reserved_gb,
    SUM(ps.used_page_count)     * 8.0 / 1024 / 1024                     AS used_gb
FROM sys.dm_db_partition_stats AS ps
INNER JOIN sys.tables AS t
        ON t.object_id = ps.object_id
GROUP BY t.schema_id, t.name
ORDER BY SUM(ps.reserved_page_count) DESC;

This converts to GB assuming an 8KB page size. `reserved_gb` is reserved space and `used_gb` is actual usage. A table with a large gap between the two includes fragmentation or deleted space, so its size after migration can end up smaller than on the source (confirm this with a trial load rather than an estimate, too).

Take a trial dump of a representative table and measure duration and output sizeMedium
対象
MySQL 5.7 / 8.0, Aurora MySQL (mysqldump / mydumper)
権限
`SELECT` on the target schema
変更作業
None (the source is read-only), but it adds read load
Production実行
Carry out during a window that can tolerate the load
# 対象: MySQL 5.7 / 8.0、Aurora MySQL(mysqldump / mydumper)
# 権限: 対象スキーマへの SELECT
# 変更作業: なし(移行元は参照のみ)。ただし読み取り負荷がかかる
# Production 実行: 負荷を許容できる時間帯で実施すること

# 1) 単一テーブルで所要時間を実測する(--no-tablespaces は MySQL 8.0 で
#    PROCESS 権限を要求されるのを避けるため)
time mysqldump \
  --single-transaction \
  --quick \
  --no-tablespaces \
  --host example-rds-endpoint \
  --user sample_user \
  --password \
  SampleDB sample_table \
  | gzip > /var/tmp/sample_table.sql.gz

# 2) 並列ダンプで実測する場合(資格情報はコマンドラインに書かず設定ファイルで渡す)
mydumper \
  --defaults-file /etc/mysql/sample-migration.cnf \
  --database SampleDB \
  --threads 8 \
  --rows 500000 \
  --compress \
  --outputdir /var/tmp/dump-sample

# 3) 出力サイズを確認し、テーブルサイズとの比率を記録する
du -sh /var/tmp/dump-sample

"How many minutes this table took and how many GB the output was" is the only number obtained here that belongs in the plan. When extrapolating a single table's result to the whole, the ratio changes for tables with different row width, index count, or BLOB presence, so measure several representative tables with different characteristics. `--single-transaction` is an option for getting a consistent snapshot; concurrent DDL breaks that consistency.

Verify consistency after migrationLow
対象
MySQL 5.7 / 8.0, Aurora MySQL (both source and target)
権限
`SELECT` on the target table
変更作業
None (read-only), but reading every row adds load
Production実行
Carry out during downtime or a low-load window
-- 対象: MySQL 5.7 / 8.0、Aurora MySQL(移行元・移行先の双方で同じSQLを実行する)
-- 権限: 対象テーブルへの SELECT
-- 変更作業: なし(参照のみ)。ただし全行を読むため I/O 負荷がかかる
-- Production 実行: 停止中または低負荷時間帯に実施すること

-- 1) 行数の一致を確認する(最も軽い検証。ここが合わなければ先へ進まない)
SELECT 'sample_table' AS table_name, COUNT(*) AS row_count
FROM SampleDB.sample_table;

-- 2) 主要列の値までを含めた突き合わせ(同一エンジン間の比較に使う)
SELECT
    COUNT(*)                                                   AS row_count,
    SUM(CRC32(CONCAT_WS('|', id, name, DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s')))) AS crc_sum
FROM SampleDB.sample_table;

-- 3) テーブル単位のチェックサム(全行を読む。大きな表では時間がかかる)
CHECKSUM TABLE SampleDB.sample_table EXTENDED;

Use step 2 when the source and target run the same engine family. Since it won't match unless NULL handling, floating-point representation, and date formatting line up, it explicitly normalizes values with `CONCAT_WS`. In a cross-engine migration, the representation of values itself changes, so focus mainly on row-count matching and application-level verification (comparing the results of representative screens and batch jobs).

Check the freeze scope for the migration (inventory of DDL and batch jobs)Read-only
対象
MySQL 5.7 / 8.0, Aurora MySQL
権限
Read access to `information_schema`
変更作業
None (read-only)
Production実行
Possible
-- 対象: MySQL 5.7 / 8.0、Aurora MySQL
-- 権限: information_schema の参照権限
-- 変更作業: なし(参照のみ)
-- Production 実行: 可能

-- 1) 直近に定義が変わったテーブル(凍結期間中に変更が入っていないかの確認に使う)
SELECT TABLE_SCHEMA, TABLE_NAME, CREATE_TIME, UPDATE_TIME
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
  AND TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
ORDER BY CREATE_TIME DESC;

-- 2) 二次インデックスの一覧(ロード後に作り直す対象の洗い出し)
SELECT
    TABLE_SCHEMA,
    TABLE_NAME,
    INDEX_NAME,
    GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS index_columns,
    MAX(NON_UNIQUE)                                 AS non_unique
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
  AND INDEX_NAME <> 'PRIMARY'
GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME
ORDER BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME;

The result of step 2 is itself the work list if you choose the strategy of dropping secondary indexes before loading and rebuilding them afterward. Generating and storing the rebuild DDL in advance means you don't have to recall the definitions on cutover day. Since a unique index (`non_unique = 0`) also serves to detect duplicate data, arrange a separate guarantee against duplicates if you drop one.

How to Read the Results

ColumnMeaningWhat to Check
TABLE_SCHEMA / TABLE_NAMETarget schema and tableUse this list to make the line between in-scope and out-of-scope explicit
estimated_rowsEstimated row count (an approximation based on statistics on InnoDB)Re-pull the exact figure with `COUNT(*)` if it will serve as a verification baseline
data_gbSize of the data portionThe main contributor to transfer volume. Pick trial-run targets in descending order
index_gbSize of the index portionThe higher this ratio, the bigger the payoff from rebuilding indexes after loading
free_gb / the gap between reserved and usedSpace reserved but unusedOften resolved on the target. The required capacity on the target can end up smaller than the reserved amount on the source
total_gbTotal of data plus indexesSum per schema to pin down the overall "actual size"
index_columns (secondary index list)Columns making up the indexBecomes the work list for what to recreate after loading
UPDATE_TIMEWhen the table definition or data was last updated (engine-dependent)Use this to check for unexpected changes during the freeze period

When to Use This

  • Asked "how many hours will it take" but unable to give a number backed by evidence
  • A schedule has been set without ever rehearsing the migration
  • Discussion of the cutover method has started before the acceptable downtime has been decided
  • The initial load finished, but catching up on the difference never finishes
  • Making the target's capacity equal to the source's ran out of space during the load
  • It's undecided who decides to roll back, and when

Possible Causes (Most Likely First)

  1. 01

    Planning around a total figure like "3TB" without measuring actual size

    Even with the same total, the work involved is completely different between a single giant table and thousands of mid-sized tables. Pin down the per-object size and count first.

  2. 02

    Estimating duration from a generic rule of thumb

    Transfer speed varies by orders of magnitude depending on network bandwidth, storage throughput, parallelism, presence of indexes, and compression. Applying someone else's figures to your own environment makes the plan itself unworkable.

  3. 03

    The acceptable downtime was not decided first

    The cutover method should be decided based on acceptable downtime. If a long outage is acceptable, a full stop is the simplest and most reliable. Choosing a method without working backward from this leads to rework later.

  4. 04

    Loading with indexes still attached

    A bulk load with secondary indexes kept in place triggers an index update on every row insert. Since duration differs from rebuilding them all at once after loading, compare both with a trial run.

  5. 05

    Reaching cutover day without deciding the verification method

    "It looks like it's working" is not verification. How far to go with row counts, value comparison, and application-level checks needs to be agreed on in advance.

  6. 06

    No point of no return has been defined

    Once writes to the new environment begin, a simple rollback is no longer possible. Spell out in the procedure exactly which point, once passed, leaves only forward progress as an option.

Diagnostic Steps

  1. 1

    Measure actual size and count per object

    Read-only

    Run the sizing SQL above to pin down the number of tables, the size of the largest table, and what share of the total the top 10 represent.

  2. 2

    Finalize the acceptable downtime with stakeholders

    Read-only

    Before any technical discussion, decide how many hours the business can tolerate being down. Nothing else can be decided until this is set.

  3. 3

    Run trials on representative tables with different characteristics

    Medium

    Measure duration on several tables selected for different traits: large, wide rows, containing BLOBs, many indexes, and so on.

  4. 4

    Confirm the target's actual capacity with a trial load

    Medium

    It will not equal the source's usage. It can shrink from resolved fragmentation or grow from storage structure differences.

  5. 5

    Measure the rate at which differences accumulate

    Read-only

    If using a CDC approach, the volume of changes generated during the initial load determines how long catch-up takes. Measure the daily row-change count.

  6. 6

    Run the rehearsal at least once with a production-equivalent procedure

    Medium

    Gaps in the runbook are found only in a rehearsal. This is also where you pin down the measured duration.

Solutions

Low-risk action that can be done immediately

  • Finish the inventory of what is in scope

    Read-only

    Separate tables to migrate, tables not to migrate, and tables to delete before migration. Deciding what not to migrate alone often shrinks the total significantly.

  • Narrow cutover method candidates from the acceptable downtime

    Read-only

    A full stop if a long outage is tolerable, initial load + CDC catch-up if only a short one is, and dual writes if none is.

Change requiring advance review

  • Measure duration with a trial run

    Medium

    Build up the total from measured values on representative tables. Put only measured values, not rules of thumb, into the plan.

  • Decide how to handle indexes and constraints

    Medium

    Compare rebuilding secondary indexes and some constraints after loading through measurement, and decide whether to adopt it. Generate and store the rebuild DDL in advance.

  • Document the pass criteria for verification

    Read-only

    Decide upfront "what needs to be true to call the cutover complete" — row-count matches, checksum matches on key tables, matching results on representative screens/batch jobs, and so on.

  • Create a freeze and notification plan

    Read-only

    Decide when to stop schema changes, batch jobs, and data entry, who to notify, and how to handle exception requests.

Change involving a restart or service impact

  • Carry out the initial load

    High

    This puts read load on the source and write load on the target. Build margin into the measured duration for unexpected re-runs.

  • Set up change data synchronization (CDC) to catch up

    High

    If the rate of new differences exceeds the catch-up rate, it will never catch up. Continuously check whether the lag is shrinking.

Work requiring expert review

  • Carry out the cutover

    Expert Review Required

    Execute in the order: stop writes → confirm the difference has fully caught up → verify → switch the connection target → strengthen monitoring, with the decision-maker and pass criteria for each stage set beforehand.

  • Decide on a rollback

    Expert Review Required

    Decide in advance how far to roll back if pass criteria are not met (just the connection target, or the data too). Once past the point of no return, only moving forward remains an option.

!Warnings

  • This article does not give a generic duration figure, because it varies by orders of magnitude with network, storage performance, parallelism, and index configuration. The only numbers that belong in the plan are measured values from a trial run in your own environment.
  • When extrapolating a single table's measured value to the whole, the ratio changes for tables that differ in row width, index count, or BLOB presence. Measure several representative tables with different characteristics.
  • The target's required capacity will not equal the source's usage. It can shrink from resolved fragmentation or grow from a different storage structure. Confirm this with a trial load.
  • When loading with secondary indexes dropped, also dropping a unique index can let duplicate data in. Arrange a separate guarantee against duplicates.
  • `CHECKSUM TABLE` and computing a checksum over all rows reads the entire table. Do not run this against a large table while it is live in production.
  • A migration plan with no defined point of no return leaves you unable to make a call when something goes wrong. Decide the conditions, procedure, and decision-maker for a rollback before the schedule.
  • A schema change during the freeze period puts already-loaded data out of sync with the target's definition. Document what is in scope for the freeze and how exceptions are handled.

Differences by Version and Environment

Cutover method 1: full stopStop the service, run the dump/load/verification, then resume. The simplest and easiest to verify, but the duration directly becomes the downtime. Most reliable when a long outage is acceptable.
Cutover method 2: initial load + CDC catch-upLoad the initial data while live, catch up on the difference via CDC, then cut over with a short stop. Downtime can be shortened, but it adds separate work — configuring, monitoring, and error-handling CDC. If new changes outpace catch-up, it never finishes.
Cutover method 3: dual writesThe application writes to both old and new systems, runs them in parallel for a while, then cuts over. Downtime can be nearly eliminated, but it requires application changes, and designing how to handle inconsistency between the two systems is the hardest part.
Engine differences in the sizing SQLMySQL's `TABLE_ROWS` and `DATA_FREE` and SQL Server's `reserved_page_count` and `used_page_count` each mean something different. Don't compare them directly across engines.

If This Doesn't Resolve It

  • Check the network path and its actual effective bandwidth

    Measure the actual effective bandwidth of the path between source and target (dedicated line, VPN, internet) by actually transferring data. Catalog figures are not enough to plan with.

  • Check the target's temporary and log space

    During loading, the transaction log and temp space grow much larger than normal. Sizing based only on data space will fall short.

  • Check the impact on the source while loading

    Read load can slow down the service side. Consider options such as reducing parallelism, splitting across time windows, or reading from a replica.

  • Check how character set, collation, and time zone are handled

    If value representation changes across the migration, verification will show mismatches. Decide the standard before migrating.

  • Include migrating privileges, connection info, and monitoring settings in the plan

    Moving the data doesn't automatically move users, privileges, monitoring, or backup settings. Include time for this work in the schedule too.

  • Decide on stakeholder notification and a point of contact

    Notify people in advance of the freeze period, cutover time, scope of impact, and who to contact. Most of the confusion on the day can be prevented here.

Basis and Limitations of This Document

General technical explanation

A general approach to migration planning based on the public specifications of MySQL's `information_schema.TABLES` and SQL Server's `sys.dm_db_partition_stats`, and the documented options of `mysqldump` / `mydumper`. Figures such as duration, transfer speed, and capacity ratios are intentionally omitted because they depend heavily on the environment. It does not include any specific customer's migration case.

FAQ

How long does a 3TB migration take?

This article does not give a figure. Duration varies by orders of magnitude with the network's actual effective bandwidth, source and target storage performance, parallelism, index configuration, and compression. Run a trial on representative tables and build up the total from those measured values — applying someone else's figures to your own environment makes the plan unworkable.

Which cutover method should I choose?

Decide based on acceptable downtime. A full stop is simplest and easiest to verify if a long outage is acceptable; initial load + CDC catch-up if only a short one is; dual writes if almost none is. The latter two trade downtime for extra work — operating CDC or modifying the application.

Can this be run in production?

The sizing and definition-inventory SQL is read-only and can be run in production. A trial dump and checksum computation add read load, so pick an appropriate time window. Treat the initial load and cutover as planned operations carried out under a pre-agreed procedure.

Should indexes be built before or after loading?

This can't be decided in the abstract — compare both with a trial run. Rebuilding secondary indexes after loading is often favorable, but the rebuild itself takes time, so judge by the total time. If you drop a unique index, arrange a separate guarantee against duplicate data.

How much verification is enough?

The standard is whatever you agreed in advance counts as "complete." At minimum, check that row counts match across every target table, then compare values on key tables, and confirm that results from representative screens/batch jobs match the pre-migration state. A full value comparison takes time, so define the scope and target before running it.

Until when is a rollback possible?

Until writes to the new environment begin. After that, data that exists only in the new environment is created, so simply reverting to the old environment means losing it. Document this boundary as the point of no return in the procedure, and decide who makes the call to cross it.

Questions This Document Covers

  • How to estimate the time a large-scale database migration will take
  • How to choose a cutover method for a database migration
  • How to verify data consistency after a migration
  • How to estimate the target's storage capacity

What the Risk Labels Mean

  • Read-onlyDoes not change data or settings.
  • LowImpact is limited, but permissions and load should be checked.
  • MediumMay affect performance, locking, or cost.
  • HighMay cause an outage, data loss, or require recovery work.
  • Expert Review RequiredRequires a separate review before applying to production.

GIIP's Scope of Support

The migration plan itself can be built up through repeated measurement and review. What's easy to miss in practice is the weeks after cutover. GIIP saves key metrics collected before cutover (representative query execution time, error rate, storage usage trend) as a baseline, and tracks the same metrics side by side after cutover to catch any drift. Only items that show a difference are isolated for action, with day-to-day comparison and initial investigation handled by an AI agent, while the decision to roll back or change the configuration is made by a person.

Author & Technical Review

GIIP Production Operations Team

About 30 years of experience in designing, migrating, and operating large-scale web services, SQL Server, Oracle, AWS, and Azure. Experience includes 12 sets of x12large-class AWS RDS for SQL Server environments, an Oracle environment with roughly 120,000 tables, and migrating roughly 3TB from TiDB to Aurora MySQL. AI agents and human experts currently continue to monitor and operate multiple cloud databases and about 30 web services.

Related Knowledge

Related Services

Request a plan review for a 3TB-class migration

If you need to repeat the same checks across multiple environments, we can discuss your entire operations setup.

Request a plan review for a 3TB-class migration

← Back to Knowledge Base