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 列で確認し、条件の列を関数で包まない。

関連記事:

お気軽にご相談ください

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