2026年7月30日
2026年7月30日
MySQLのパーティションテーブルのメリットと設定方法(RANGE COLUMNSで月別に分割)
MySQL のパーティショニングは、1つのテーブルを内部で複数の「パーティション」に分けて保存する機能です。アプリケーションからは普通のテーブルと同じように SELECT や INSERT ができ、MySQL が条件に合うパーティションだけを読みます。
ただし、どんなテーブルでも速くなるわけではなく、主キーの設計にも制約があります。この記事では、2026年10月時点の MySQL 8.4 LTS(Docker の公式イメージ)と MariaDB 11 で実際に SQL を実行して確認した内容をもとに、効く場面・制約・作成と運用の手順を説明します。
パーティションテーブルのメリット
パーティショニングが役に立つのは、主に次の2つです。
| メリット | 内容 |
|---|---|
| パーティションの刈り込み(pruning) | WHERE の条件から、読む必要のないパーティションを最初から除外する。日付の範囲で絞る検索で効く |
| 古いデータを一瞬で消せる | DELETE で何百万行も消す代わりに、ALTER TABLE ... DROP PARTITION でパーティションごと削除できる。大量の行削除による負荷やバイナリログの肥大を避けられる |
アクセスログ、操作履歴、計測データのように、日付の列があり、新しい行が増え続け、古い行は一定期間で捨てるテーブルに向いています。
逆に、次のような期待は外れます。
- 分割の列を条件に使わない検索は速くならない。 全パーティションを読むため、適切なインデックスがある普通のテーブルより遅くなることもあります。
- サーバーの台数が増えるわけではない。 すべてのパーティションは同じ MySQL サーバー上にあります。負荷分散が目的なら、レプリケーションなど別の方法が必要です。
- 一部のパーティションだけ壊れても残りが動く、という耐障害性の機能ではありません。
検索を速くしたいだけなら、まずインデックスと EXPLAIN で解決できないかを確認するのが先です。
先に知っておく制約
MySQL 8.4 の公式マニュアルと、実際の実行結果で確認した主な制約です。
| 制約 | 内容 |
|---|---|
| 主キー・一意キー | テーブルのすべての主キーと一意キーに、分割に使う列を含める必要がある |
| 外部キー | InnoDB のパーティションテーブルは外部キーを持てず、外部キーから参照もされない |
| 全文検索 | FULLTEXT インデックスを作れない |
| ストレージエンジン | MySQL 8.0 以降は InnoDB(と NDB)だけ。MyISAM のテーブルは分割できない |
| パーティション数 | 1テーブルあたり最大 8192(サブパーティションを含む) |
| 分割の式 | 整数を返す式か、RANGE COLUMNS / LIST COLUMNS で列を直接指定する |
主キーに分割の列が入っていないと、作成時に次のエラーになります。
CREATE TABLE bad (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
PRIMARY KEY (id)
)
PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function ...
WordPressのコアのテーブルは分割しない
wp_posts などの WordPress のコアのテーブルは、パーティショニングを前提に作られていません。たとえば wp_posts を post_date で分けるには主キーを (ID, post_date) に変える必要があり、そうすると ID だけの一意性をデータベースが保証しなくなります。プラグインが外部キーや FULLTEXT インデックスを追加している場合は、分割そのものができません。
パーティショニングを使うのは、プラグインや自作のアプリケーションが持つ、ログのような専用テーブルにとどめるのが安全です。
ステップ1: 月別に分割したテーブルを作る
日付・日時の列で分けるときは、RANGE COLUMNS を使うと値をそのまま書けて読みやすくなります。次はアクセスログを月ごとに分ける例です。
CREATE TABLE access_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
url VARCHAR(2048) NOT NULL,
status SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (id, created_at),
KEY idx_created_at (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 ('2026-09-01')は「2026年9月1日 0時より前」なので、p202608には8月までの行が入ります(最初のパーティションには、それより古い行もすべて入ります)。pmaxは、まだパーティションを用意していない将来の日付の受け皿です。これが無いと、範囲外の日付をINSERTしたときにエラーになります。- 主キーを
(id, created_at)にしているのは、上の制約のためです。AUTO_INCREMENTの列が主キーの先頭にあれば、採番は普通どおり動きます。
どのパーティションに何行あるかは INFORMATION_SCHEMA.PARTITIONS で確認できます。TABLE_ROWS は推定値なので、正確に見たいときは先に ANALYZE TABLE access_log; を実行します。
SELECT PARTITION_NAME, TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'access_log'
ORDER BY PARTITION_ORDINAL_POSITION;
既存のテーブルを分割する場合
既存のテーブルも 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 p202609 VALUES LESS THAN ('2026-10-01'),
PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
この変更ではテーブル全体が作り直されるため、行数に応じた時間と、テーブルと同じくらいの空きディスクが必要です。本番で行う前に、バックアップを取り、同じ規模のデータで所要時間を測ってください。分割をやめるときは ALTER TABLE access_log REMOVE PARTITIONING; で普通のテーブルに戻せます。
ステップ2: 刈り込みが効いているかEXPLAINで確かめる
MySQL 8.0 以降は、普通の EXPLAIN の結果に partitions 列が表示されます(EXPLAIN PARTITIONS という書き方は MySQL 8.0 で廃止され、8.4 では構文エラーになります)。
EXPLAIN SELECT COUNT(*) FROM access_log
WHERE created_at >= '2026-09-01' AND created_at < '2026-10-01';
-- partitions: p202609
条件の列を関数で包むと、刈り込みが効かなくなります。
EXPLAIN SELECT COUNT(*) FROM access_log WHERE DATE(created_at) = '2026-09-10';
-- partitions: p202608,p202609,p202610,pmax
「9月10日の1日分」なら、created_at >= '2026-09-10' AND created_at < '2026-09-11' のように、列をそのまま範囲で比べる書き方にします。
MariaDB では、通常の EXPLAIN には partitions 列が出ません。EXPLAIN PARTITIONS SELECT ... を使います。
ステップ3: 毎月のパーティション追加と古いデータの削除
来月のパーティションを用意する
MAXVALUE のパーティションがあるテーブルには、ADD PARTITION でその後ろに追加できません(RANGE の追加は最後尾にしかできず、MAXVALUE より後ろは無いため)。
ALTER TABLE access_log ADD PARTITION (PARTITION p202611 VALUES LESS THAN ('2026-12-01'));
-- ERROR 1493 (HY000): VALUES LESS THAN value must be strictly increasing for each partition
代わりに REORGANIZE PARTITION で pmax を2つに分けます。pmax に行がほとんど入っていないうちに行えば、すぐに終わります。
ALTER TABLE access_log REORGANIZE PARTITION pmax INTO (
PARTITION p202611 VALUES LESS THAN ('2026-12-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
pmax に大量の行が入ってから分けると、その行を移し替えるぶん時間がかかります。月末より前に、翌月分を用意しておくのが運用のこつです。
古い月のデータを消す
-- 2026年8月以前の行を、パーティションごと削除する
ALTER TABLE access_log DROP PARTITION p202608;
-- パーティションは残し、中の行だけを空にする
ALTER TABLE access_log TRUNCATE PARTITION p202609;
DROP PARTITION はそのパーティションのデータも削除します。何行消えたかは表示されないので、先に SELECT COUNT(*) ... WHERE created_at < '2026-09-01' で件数を確認し、必要ならバックアップを取ってから実行してください。実行にはテーブルに対する ALTER と DROP の権限が必要です。
定期実行する
上の2つの ALTER TABLE を、毎月決まった日に cron(mysql コマンドで SQL ファイルを流す)や MySQL のイベントスケジューラーで実行します。パーティション名と日付は実行する月によって変わるので、スクリプトで組み立てるか、少し先の月の分まで前もって作っておきます。いずれの方法でも、実行後に INFORMATION_SCHEMA.PARTITIONS を確認し、pmax に行が増え続けていないかを定期的に見てください。
まとめ
- パーティショニングが効くのは、日付で絞る検索と、古いデータの一括削除が中心のテーブル。分割の列を条件に使わない検索は速くならない。
- 主キー・一意キーに分割の列を含める必要があり、外部キーと
FULLTEXTは使えない。WordPress のコアのテーブルには使わない。 - 日付で分けるなら
RANGE COLUMNS、MAXVALUEのパーティションはREORGANIZE PARTITIONで分けて翌月分を作る。 - 刈り込みは
EXPLAINのpartitions列で確認し、条件の列を関数で包まない。
関連記事: