2026年8月13日
2026年8月13日
MySQLのパーティショニングでテーブルを分割する方法|RANGE分割の手順と主キー・外部キーの制約
MySQLのパーティショニングは、1つのテーブルを内部的に複数の区画(パーティション)に分けて保存する機能です。うまく使うと、日付で絞り込むクエリが対象の区画だけを読むようになり、古いデータの削除も DROP PARTITION で一瞬で済みます。一方で、「パーティションキーをすべての一意キーに含める」「InnoDBでは外部キーを使えない」といった強い制約があり、どのテーブルにも効く高速化策ではありません。この記事では、向き不向きの判断から、日付によるRANGE分割の作成、月ごとの区画の追加・削除、効いているかの確認方法までを説明します。
パーティショニングが向いているテーブル
効果が出やすいのは、次の条件がそろうテーブルです。
- アクセスログ、操作履歴、計測データなど、行数が数千万行以上に増え続ける
- 検索のほとんどが「直近1か月」のように、日付などの決まった列で範囲を絞る
- 「90日より古いデータは削除する」といった保管期間のルールがある
逆に、数百万行程度のテーブルや、絞り込み条件がばらばらなテーブルでは、まず適切なインデックスを作るほうが効果的です。パーティショニングは、パーティションキーで絞り込まないクエリに対しては全区画を順に調べることになり、かえって遅くなる場合もあります。
WordPressのコアテーブル(wp_posts や wp_postmeta など)は、主キーが ID や meta_id だけで構成され、検索条件も多様なため、パーティショニングには向きません。対象として考えられるのは、プラグインや独自開発で作ったログ系のテーブルです。
パーティショニングの種類
MySQLには主に次の分け方があります。
RANGE:値の範囲で分ける。日付や連番で期間ごとに区切る用途の基本形RANGE COLUMNS:RANGEの拡張で、DATE・DATETIME・文字列の列をそのまま範囲指定に使えるLIST/LIST COLUMNS:値の一覧で分ける。地域コードやステータスごとに分ける場合HASH/KEY:値から計算したハッシュで、指定した数の区画に均等に振り分ける
ログの保管期間を管理したいなら、ほぼ RANGE か RANGE COLUMNS 一択です。通常の RANGE は区切りの式が整数を返す必要があるため、DATETIME列なら TO_DAYS(created_at) のような関数を通します。RANGE COLUMNS なら列をそのまま書けて読みやすくなります。なお、TIMESTAMP型の列はCOLUMNS系の分割に使えないため、RANGE と UNIX_TIMESTAMP() の組み合わせを使います。
作る前に知っておく制約
設計に直接影響する制約から確認します。後から気づくと、テーブル構造やアプリケーション側の作り直しが必要になるためです。
パーティションキーはすべての一意キーに含める
パーティショニングに使う列は、主キーを含むすべての一意キー(PRIMARY KEY と UNIQUE)に含まれている必要があります。MySQLは一意性を区画ごとにしか確かめられないため、区画をまたいだ重複を防げないからです。主キーが id だけのテーブルを created_at で分割しようとすると、次のエラーになります。
ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function
対処として、主キーを (id, created_at) の複合キーにするのが一般的です。ただしこれにより、データベースとしては「id だけで一意」という保証がなくなります。AUTO_INCREMENTで採番している限り値は重複しませんが、手動で id を指定して挿入するような処理がある場合は注意が必要です。メールアドレスの一意制約のように、パーティションキーを含められない一意キーがあるテーブルは、そもそも分割に向きません。
InnoDBのパーティションテーブルでは外部キーを使えない
InnoDBのパーティションテーブルは、外部キーを持つことも、他のテーブルの外部キーから参照されることもできません。親子関係の整合性をデータベースの外部キー制約に任せている設計では、その確認をアプリケーション側で行う形に変える必要があります。
その他の主な制約
- MySQL 8.0以降でパーティショニングを使えるストレージエンジンはInnoDBとNDBだけで、MyISAMのテーブルは分割できません
- FULLTEXTインデックスはパーティションテーブルでは使えません
- 1テーブルあたりの区画数の上限は、サブパーティションを含めて8192です。ただし実用上は、区画が多すぎるとテーブルを開く処理やメモリ使用量の負担が増えるため、月単位で数年分程度にとどめるのが無難です
RANGE COLUMNSでテーブルを作る
月ごとに区切ったアクセスログのテーブルの例です。
CREATE TABLE access_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
user_id BIGINT UNSIGNED NULL,
path VARCHAR(255) NOT NULL,
status SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (id, created_at),
KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE COLUMNS(created_at) (
PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
VALUES LESS THAN は「この値未満」を表し、上から順に大きくなるよう並べる必要があります。最初の p202608 には2026年9月1日より前のすべての行が入ります。最後の pmax は、どの区画にも当てはまらない未来の日付の受け皿です。これがないと、用意した範囲を超える日付の行を挿入したときに「Table has no partition for value」というエラーになり、ログの書き込みが止まります。
区画名を p202609 のように年月で付けておくと、後の追加・削除の作業で取り違えを防げます。
既存テーブルを分割する場合
既存のテーブルにも ALTER TABLE ... PARTITION BY で後から分割を設定できます。
ALTER TABLE access_log
DROP PRIMARY KEY, ADD PRIMARY KEY (id, created_at);
ALTER TABLE access_log
PARTITION BY RANGE COLUMNS(created_at) (
PARTITION p202608 VALUES LESS THAN ('2026-09-01'),
PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
どちらの操作もテーブル全体の作り直しになり、大きなテーブルでは数十分から数時間かかります。その間、テーブルへの書き込みは待たされるため、本番では書き込みを止められる時間帯に行うか、オンラインでスキーマを変更するツールの利用を検討してください。実行前には mysqldump などでバックアップを取り、同じデータ量の検証環境で所要時間を測っておくと、作業計画が立てやすくなります。
区画の追加と古いデータの削除
運用では、毎月「翌月分の区画を足す」「保管期間を過ぎた区画を消す」の2つを繰り返します。
新しい区画は、pmax を分割する形で追加します。pmax にまだ行が入っていなければ、処理は短時間で終わります。
ALTER TABLE access_log REORGANIZE PARTITION pmax INTO (
PARTITION p202611 VALUES LESS THAN ('2026-12-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
pmax に大量の行がたまってからだと、その行を移し替える処理が発生して時間がかかります。月末を待たず、少なくとも1〜2か月先の区画まで用意しておく運用にすると安全です。
古いデータは、区画ごと削除します。
ALTER TABLE access_log DROP PARTITION p202608;
DELETE FROM access_log WHERE created_at < '2026-09-01' で数千万行を消すと、長時間のロックや大量のUndoログ、レプリケーションの遅延を招きますが、DROP PARTITION は区画のファイルを捨てるだけなので短時間で終わります。ただし、取り消しはできません。区画を消さずに中身だけ空にしたい場合は ALTER TABLE access_log TRUNCATE PARTITION p202608; を使います。
削除前にデータを残したい場合は、EXCHANGE PARTITION で区画の中身を同じ構造の通常テーブルと入れ替え、そのテーブルをダンプして保管する方法があります。
CREATE TABLE access_log_202608 LIKE access_log;
ALTER TABLE access_log_202608 REMOVE PARTITIONING;
ALTER TABLE access_log EXCHANGE PARTITION p202608 WITH TABLE access_log_202608;
この3つのSQLをcronやイベントスケジューラーで毎月実行する仕組みにしておくと、区画の追加忘れによる書き込み停止を防げます。
効いているかを確認する
クエリが必要な区画だけを読んでいるか(パーティションプルーニング)は、EXPLAIN の partitions 列で確認できます。
EXPLAIN SELECT COUNT(*) FROM access_log
WHERE created_at >= '2026-10-01' AND created_at < '2026-11-01';
partitions に p202610 だけが表示されていれば、絞り込みが効いています。WHERE DATE(created_at) = '2026-10-15' のように列を関数で包むと、区画を絞れずにすべての区画が表示されることがあります。日付は範囲指定で書くのが基本です。
また、主キーが (id, created_at) になったことで、WHERE id = 123 のように id だけで検索するクエリは、すべての区画の主キーを順に調べる形になります。アプリケーションに id だけの検索が多い場合は、created_at も条件に加えられないかを見直してください。
各区画のおおよその行数は、INFORMATION_SCHEMA.PARTITIONS で確認できます。
SELECT PARTITION_NAME, TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'access_log';
TABLE_ROWS は統計情報に基づく推定値ですが、pmax に行が入り始めていないか、区画ごとの偏りがないかを見るには十分です。(2026年10月現在、ここで紹介した構文はMySQL 8.0以降、8.4 LTSでも同じように使えます。)
まとめ
- パーティショニングは、行数が多く、日付などで範囲を絞る検索と保管期間の管理があるテーブル向け。まずはインデックスで解決できないかを考える
- パーティションキーは主キーを含むすべての一意キーに含める必要があり、InnoDBでは外部キーを使えない
- 日付で分けるなら
RANGE COLUMNSが読みやすく、MAXVALUEの受け皿区画を用意しておく - 区画の追加は
REORGANIZE PARTITION、古いデータの削除はDROP PARTITIONで行い、先回りして自動化する EXPLAINのpartitions列で、クエリが必要な区画だけを読んでいるかを確かめる