PostgreSQLの「row is too big: size exceeds maximum」エラーをINSERT時に修正する

intermediate🐘 PostgreSQL2026-06-03| PostgreSQL 12~16(Linux、macOS、Windows)

Error Message

ERROR: row is too big: size 8208, maximum size 8160
#postgresql#row-size#toast#storage#large-object

TL;DRPostgreSQLの行は8KBのページに収まる必要があります。ヘッダーのオーバーヘッドを差し引くと、1行あたり正確に8160バイトしか使えません。可変長カラムをEXTENDEDストレージに切り替えれば、PostgreSQLが自動的に値を圧縮してTOASTテーブルに退避してくれます:

ALTER TABLE your_table ALTER COLUMN large_column SET STORAGE EXTENDED;

-- 新しいストラテジを既存の行に適用するため、行を書き直す
UPDATE your_table SET large_column = large_column WHERE id IS NOT NULL;

それでも制限に引っかかる場合は、テーブルを正規化するか、BLOBをデータベースの外に出す必要があります。

なぜこのエラーが発生するのかPostgreSQLのすべての行は8KBのページに格納されます。TOAST(The Oversized-Attribute Storage Technique)はその回避策です。単一カラムの値がおよそ2KBを超えると、PostgreSQLはその値を圧縮したり、別のTOASTテーブルに移動したりします。ただし、TOASTはカラム単位で発動するため、行単位では発動しません。

つまり、各カラムに300バイトのJSONを持つ30カラムのテーブルでは、個々のカラムに対してTOASTが発動することはありません。しかし30 × 300 = 9000バイトとなり、8160バイトの制限を大きく超えてしまいます。そのため、すべてのINSERTが次のエラーで失敗します:

ERROR: row is too big: size 8208, maximum size 8160

もう一つの落とし穴はPLAINストレージです。特定の固定幅配列型や、明示的にPLAINに設定されたカラムは、圧縮もアウトオブライン化もできません。そのバイト数は削減不可能です。

まず診断する何かを変更する前に、どのカラムがTOASTをブロックしているかを確認しましょう:

SELECT
    a.attname AS column_name,
    t.typname AS data_type,
    CASE a.attstorage
        WHEN 'p' THEN 'PLAIN (TOASTなし)'
        WHEN 'm' THEN 'MAIN (インライン圧縮、最終手段としてTOAST)'
        WHEN 'x' THEN 'EXTENDED (圧縮+アウトオブライン)'
        WHEN 'e' THEN 'EXTERNAL (アウトオブライン、圧縮なし)'
    END AS storage_strategy
FROM pg_attribute a
JOIN pg_type t ON a.atttypid = t.oid
WHERE a.attrelid = 'your_table'::regclass
  AND a.attnum > 0
  AND NOT a.attisdropped
ORDER BY a.attnum;

次に実際のサイズを計測します:

SELECT pg_size_pretty(AVG(pg_column_size(t.*))::bigint) AS avg_row_size
FROM your_table t;

-- 最も大きな原因を特定する
SELECT id, pg_column_size(t.*) AS row_bytes
FROM your_table t
ORDER BY row_bytes DESC
LIMIT 10;

MAINまたはPLAINストレージになっている可変長カラム(text、varchar、jsonb、bytea)が対処の起点です。

修正1 — カラムのストレージストラテジを変更する多くの場合、ALTER文一つで解決できます。MAINカラムをEXTENDEDに切り替えると、PostgreSQLはまず値を圧縮し、それでも大きすぎる場合はアウトオブラインに移動します:

-- text、varchar、jsonb、xmlカラムの場合
ALTER TABLE events ALTER COLUMN notes SET STORAGE EXTENDED;
ALTER TABLE events ALTER COLUMN payload SET STORAGE EXTENDED;

-- 圧縮オーバーヘッドなしでアウトオブラインにしたいbyteaの場合
ALTER TABLE events ALTER COLUMN raw_blob SET STORAGE EXTERNAL;

注意点として、新しいストラテジはALTER実行に書き込まれた行にのみ適用されます。既存の行は明示的に書き直すまで古いディスク上のフォーマットを維持します:

-- 軽量な書き直し(各行を一時的にロック)
UPDATE events SET notes = notes;

-- またはゼロダウンタイムの書き直しにpg_repackを使用(pg_repack拡張が必要)
pg_repack -t events -d your_database

修正2 — 複数カラムをJSONBに統合するカラム数の多いテーブルが典型的な原因です。15〜30個のtextカラムがあり、その多くは特定の行でNULLになっています。任意のカラムを一つのJSONBカラムにまとめることで、行の幅とスキーマの複雑さの両方が削減できます。JSONBはデフォルトでEXTENDEDストレージのため、圧縮は自動的に行われます:

-- オーバーフローするフィールドを保持するJSONBカラムを追加
ALTER TABLE events ADD COLUMN metadata JSONB;

-- 既存の行をマイグレーション
UPDATE events
SET metadata = jsonb_build_object(
    'extra_field_1', extra_field_1,
    'extra_field_2', extra_field_2,
    'extra_field_3', extra_field_3
);

-- 確認後、古いカラムを削除
ALTER TABLE events
    DROP COLUMN extra_field_1,
    DROP COLUMN extra_field_2,
    DROP COLUMN extra_field_3;

個別のB-treeインデックスが不要で、単一クエリで一緒に読み出されることが多いフィールドに最適です。

修正3 — 1対1の子テーブルで正規化するスキーマが固定されている場合(ORM、外部制約、DDLを管理するレガシーアプリなど)は、テーブルの垂直分割が回避策です。サイズが大きく、アクセス頻度の低いカラムを子テーブルに移動し、必要な時にJOINして取得します:

CREATE TABLE events_content (
    event_id BIGINT PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE,
    description TEXT,
    raw_payload BYTEA,
    audit_log TEXT
);

INSERT INTO events_content (event_id, description, raw_payload, audit_log)
SELECT id, description, raw_payload, audit_log FROM events;

ALTER TABLE events
    DROP COLUMN description,
    DROP COLUMN raw_payload,
    DROP COLUMN audit_log;

-- 完全なレコードが必要な時はJOINする
SELECT e.*, ec.description, ec.raw_payload
FROM events e
LEFT JOIN events_content ec ON ec.event_id = e.id
WHERE e.id = 123;

修正4 — BLOBをデータベースの外に移動する画像、PDF、ZIPアーカイブなどのバイナリファイルはPostgreSQLの行に格納すべきではありません。500KBのPDF一つだけで8160バイトの制限を軽く超えてしまいます。ファイルはS3、CDN、またはローカルディスクに保存し、テーブルには参照情報だけを保持しましょう:

-- パス/URL参照カラムを追加
ALTER TABLE documents ADD COLUMN file_url TEXT;

-- アプリケーション層:ファイルをS3にアップロードし、URLを書き込む
UPDATE documents
    SET file_url = 'https://your-bucket.s3.amazonaws.com/docs/doc_123.pdf'
WHERE id = 123;

-- マイグレーション完了後、インラインBLOBカラムを削除
ALTER TABLE documents DROP COLUMN file_blob;

動作確認失敗したINSERTを再試行する前に、以下のクエリで修正が正しく適用されたことを確認してください:

-- テーブル全体での最大行サイズ
SELECT MAX(pg_column_size(t.*)) AS max_row_bytes
FROM your_table t;
-- 8160未満であること

-- TOASTテーブルにアウトオブラインデータがあることを確認
SELECT
    c.relname AS toast_table,
    pg_size_pretty(pg_total_relation_size(c.oid)) AS toast_size
FROM pg_class c
JOIN pg_class parent ON parent.reltoastrelid = c.oid
WHERE parent.relname = 'your_table';

-- 最初に失敗したINSERTを再試行
INSERT INTO your_table (col1, col2, large_col) VALUES ('...', '...', '...');
-- ERROR: row is too big が出ずに成功するはず

ストレージストラテジ一覧ストラテジ圧縮アウトオブライン最適な用途PLAINなしなし固定サイズ型(int、float)MAINあり(優先)あり(最終手段)可能な限りインラインに保ちたいカラムEXTENDEDあり(優先)ありtext、varchar、jsonb、xml(デフォルト)EXTERNALなしありランダムアクセスが重要なbyteaTEXT、VARCHAR、BYTEA、JSONBはすでにデフォルトでEXTENDEDになっています。診断クエリでこれらの型にMAINが表示された場合、誰かが明示的に変更しています。EXTENDEDに戻すだけで解決することがほとんどです。

Related Error Notes