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 列で、クエリが必要な区画だけを読んでいるかを確かめる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲