PostgreSQL『duplicate column name』エラーの原因と対処

PostgreSQL『duplicate column name』エラーの原因と対処

CREATE/ALTER/INSERT/UPDATE/VIEW/CTASなどで列名が重複したと判断されると失敗する。典型メッセージ、発生条件、切り分けSQL、確実に直すための具体的修正例(IF NOT EXISTS/別名付与/DDLの冪等化/ORM設定見直し)をまとめた。再発防止チェックリストと再現~解消の通し手順付き。

エラーの意味と代表メッセージ

・テーブルやビューの「列名」は一意である必要がある
・代表的なメッセージ

ERROR:  column "a" specified more than once
ERROR:  column "a" of relation "t" already exists
ERROR:  column name specified more than once

発生条件の早見表

・CREATE TABLE定義内で同一名の列を二重に定義
・ALTER TABLE … ADD COLUMN が既存列に衝突
・INSERT/UPDATE/COPYで列リストに同じ列名を重複指定
・CREATE VIEW / MATERIALIZED VIEW / CREATE TABLE AS / SELECT INTO の出力列名が重複
・SELECT * と JOIN(特にON句)で同名列が複数出て、そのままテーブル/ビュー化
・ALTER TABLE … RENAME COLUMN の新名称が既存列と衝突
・ORM/マイグレーションで同じDDLが再実行され、既存列とぶつかる

まず確認:情報スキーマで対象テーブルの列を洗い出す

-- テーブルの現行列一覧(大小文字・クォートは注意)
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 't'
ORDER BY ordinal_position;

CREATE/ALTER TABLEでの重複:正しい修正パターン

・NG(CREATE TABLEで同名列を2回)

CREATE TABLE t (
  id bigint,
  id text   -- ERROR: column "id" specified more than once
);

・OK(列名を変える/設計を見直す)

CREATE TABLE t (
  id bigint,
  id_text text
);

・NG(既存列にADD COLUMN)

ALTER TABLE t ADD COLUMN id bigint;
-- ERROR: column "id" of relation "t" already exists

・OK(冪等化)

ALTER TABLE t ADD COLUMN IF NOT EXISTS id bigint;

INSERT/UPDATE/COPYの列リスト重複

・NG(同じ列を2回指定)

INSERT INTO t (id, id, name) VALUES (1, 2, 'x');
-- ERROR: column "id" specified more than once

・OK(1回だけ指定)

INSERT INTO t (id, name) VALUES (1, 'x');

・UPDATEでも同様

UPDATE t SET id = 1, id = 2;
-- ERROR: column "id" specified more than once

・COPYでも列リストの重複は不可

ビュー/CTAS/SELECT INTOでの重複(SELECTの結果列名がぶつかる)

・NG(同じ別名)

CREATE VIEW v AS
SELECT 1 AS a, 2 AS a;
-- ERROR: column "a" specified more than once

・OK(別名を分ける)

CREATE VIEW v AS
SELECT 1 AS a1, 2 AS a2;

・CTAS/SELECT INTOでも同様

SELECT a.id AS id, b.id AS id INTO t2 FROM a JOIN b ON a.id=b.id;
-- ERROR(重複)

SELECT * と JOINの落とし穴(ONとUSINGの違い)

・ON句でJOIN + SELECT * は、両テーブルの同名列が「2本」出る
→ それをCREATE VIEW/CTAS/SELECT INTOに流すと重複で失敗

-- 回避:明示的に別名を振る
CREATE VIEW v AS
SELECT a.id AS a_id, b.id AS b_id, a.*, b.*
FROM a JOIN b ON a.id = b.id;

・USING/NATURALは同名列を1本に「畳み込む」挙動(SELECT *の結果では重複しない)
→ ただし読みやすさ優先で明示別名にする方が安全

ALTER TABLE RENAMEで既存列と衝突

・NG

ALTER TABLE t RENAME COLUMN name TO id;
-- ERROR: column "id" of relation "t" already exists

・OK

ALTER TABLE t RENAME COLUMN name TO full_name;

大小文字・引用符の落とし穴(”UserID” と userid)

・未引用識別子は小文字化される(UserID → userid)
・引用符で囲んだ列名は大小文字を保持する(”UserID” は別物)
・プロジェクト全体で「基本は未引用・小文字」の方針に揃えると衝突/混乱を避けやすい

マイグレーション/ORMの二重実行対策(冪等にする)

・ADD COLUMN/ADD CONSTRAINT は IF NOT EXISTS を使える箇所は極力利用
・スキーマバージョンテーブル(schema_migrations等)で適用済みを確実に記録
・手動実行と自動マイグレーションの二重適用を禁止

ジョイン対象の「衝突候補列名」を事前に洗い出すSQL

-- 2テーブル間で同名の列を列挙(public.a と public.b の例)
SELECT a.attname AS dup_column
FROM pg_attribute a
JOIN pg_attribute b
  ON a.attname = b.attname
WHERE a.attrelid = 'public.a'::regclass
  AND b.attrelid = 'public.b'::regclass
  AND a.attnum > 0 AND b.attnum > 0
ORDER BY a.attname;

・出力に出てきた列は別名を割り当てる方針にする

安全に「列を統合/置換」する手順(実データありのケース)

-- 1) 新しい列を作成
ALTER TABLE t ADD COLUMN id_new bigint;

-- 2) データ移行
UPDATE t SET id_new = id;

-- 3) 参照側(ビュー/コード)を id_new に切替

-- 4) 古い列を削除 or 退避名にリネーム(メンテ時間に合わせて)
ALTER TABLE t DROP COLUMN id;
ALTER TABLE t RENAME COLUMN id_new TO id;

再発防止チェックリスト

・DDLはIF NOT EXISTS/IF EXISTSで冪等化
・SELECT * + JOIN をそのままテーブル/ビュー化しない(必ず別名設計)
・リネーム時は既存列との衝突を情報スキーマで事前チェック
・ORMの自動DDLは差分の正当性をレビュー
・識別子ポリシー(小文字・未引用)を統一

再現→解消の通し例(CTASでの重複)

-- 準備
DROP TABLE IF EXISTS a, b;
CREATE TABLE a(id int, name text);
CREATE TABLE b(id int, note text);
INSERT INTO a VALUES (1,'alice');
INSERT INTO b VALUES (1,'memo');

-- NG:SELECT * + JOIN + CTAS(id が2本出る)
CREATE TABLE ab AS
SELECT * FROM a JOIN b ON a.id = b.id;
-- ERROR: column "id" specified more than once

-- 解消:出力列に別名を付けてCTAS
CREATE TABLE ab AS
SELECT a.id AS a_id, b.id AS b_id, a.name, b.note
FROM a JOIN b ON a.id = b.id;

-- 確認
SELECT * FROM ab;