PostgreSQL『cast from type to integer is not allowed』の原因と対処

  • 作成日 2025.09.25
  • 更新日 2025.10.06
  • その他
PostgreSQL『cast from type to integer is not allowed』の原因と対処

「X という型から integer へのキャストは許可されない」趣旨のエラー(典型表示:cannot cast type X to integer)が出る状況を整理。発生しやすい型(boolean/jsonb/uuid/date/enum/bytea/array/ドメイン型)ごとの解決パターン、数値文字列の安全変換レシピ、マイグレーションやビューでの実戦例まで収録。

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

ERROR:  cannot cast type boolean to integer
ERROR:  cannot cast type jsonb to integer
ERROR:  cannot cast type uuid to integer
ERROR:  cannot cast type bytea to integer
-- 似た仲間
ERROR:  invalid input syntax for type integer: "12,345円"

・「型自体が integer へ変換不可」か、「文字列→整数にできない内容」のどちらか
・まず“元の型”と“実際の中身”を特定する(pg_typeof、ts/debugなど)

まず実行:型と中身の特定クエリ

-- 問題の式や列の実型を見る
SELECT pg_typeof(col) AS actual_type, col FROM t LIMIT 5;

-- 文字列列の“中身”の傾向
SELECT
  COUNT(*) FILTER (WHERE col ~ '^-?\d+$') AS pure_int_like,
  COUNT(*) FILTER (WHERE col ~ '[^\d-]')  AS has_non_digit,
  COUNT(*) FILTER (WHERE btrim(col) = '') AS empty_str
FROM t;

・“文字列だけど整数にできるのか”と“そもそも文字列ではないのか”を切り分け

文字列→整数を安全に行う(正規表現ガード+前処理)

-- 千区切り・通貨記号・空白などを除去 → 数字だけになったらキャスト
SELECT CASE
         WHEN col IS NULL THEN NULL
         ELSE
           NULLIF(regexp_replace(col, '[^\d-]', '', 'g'), '')::int
       END AS col_int
FROM t;

-- 形式が一定なら to_number でも(数値→丸めて::int)
SELECT to_number(col, 'FM999G999G999')::int FROM t;

-- “確実に数字のときだけ”キャスト(try_cast相当)
SELECT CASE WHEN col ~ '^\s*-?\d+\s*$' THEN col::int END AS col_int FROM t;

・「何が来るか分からない入力」は“変換前に検査する”のが鉄則

boolean → 整数は直接不可(CASEで写像)

-- NG
SELECT true::int;  -- ERROR: cannot cast type boolean to integer

-- OK(CASEまたは bool::int を自作しない)
SELECT CASE WHEN flag THEN 1 ELSE 0 END AS flag_int FROM t;

-- 頻出なら生成列やビューで統一
ALTER TABLE t ADD COLUMN flag_i int GENERATED ALWAYS AS
  (CASE WHEN flag THEN 1 ELSE 0 END) STORED;

・PostgreSQLに既定の bool→int キャストはない(自作キャストは安全性の観点で推奨しない)

json/jsonb → 整数は「テキスト抽出→キャスト」

-- NG
SELECT payload::int FROM t;            -- cannot cast type jsonb to integer

-- OK(→> または ->> で“文字列”を取り出してから)
SELECT (payload->>'age')::int FROM t;

-- 配列の要素を整数化
SELECT (elem->>0)::int AS first_num
FROM t, LATERAL jsonb_array_elements_text(payload->'nums') AS elem(elem_text);

-- “数値型として格納している”なら cast 前に ::text を経由しない
SELECT (payload->'age')::int          -- これは不可。必ず ->> で文字列化してから。

・数値が“文字列で格納されている”前提なら →> で文字列抽出→::int

date/time/timestamp → 整数は変換関数を使う

-- Unix秒(整数)にしたい
SELECT EXTRACT(EPOCH FROM ts)::bigint AS ts_sec FROM t;

-- 日付を yyyymmdd の整数にしたい
SELECT to_char(d, 'YYYYMMDD')::int AS yyyymmdd FROM t;

-- 月数/年数など(整数)
SELECT EXTRACT(YEAR FROM d)::int AS yyyy, EXTRACT(MONTH FROM d)::int AS mm FROM t;

・「直接::int」は不可。EXTRACT / to_char など“意味のある尺度”へ変換してから整数化

uuid/bytea/enum/array/ドメイン型の対処

-- uuid → int は不可。必要なら“ハッシュ等の代理キー”に変換してから
SELECT hashtextextended(id::text, 0) AS id_hash_int8 FROM u;  -- int8(bigint)推奨

-- bytea はバイナリ。整数を取り出すなら get_byte/substring などで“数値化”
SELECT get_byte(data, 0)::int AS first_byte FROM files;

-- enum は整数に直接キャスト不可。“序数”が必要なら enum_range + array_position
SELECT array_position(enum_range(NULL::myenum), val) - 1 AS ordinal FROM e;

-- array は“要素”を取り出してから
SELECT (arr[1])::int FROM t;  -- または UNNEST(arr)::int

-- ドメイン型は“基底型へ戻してから”
CREATE DOMAIN posint AS int CHECK (VALUE > 0);
-- posint → int は OK(基底が int)。別基底なら (val::basetype)::int の二段。

・「型の意味を壊さず整数にする観点」で“どの整数にしたいか”を明確に

ビュー/ETL向け:一括変換の定型(NULL安全・不正値隔離)

-- 変換失敗候補を先に隔離(監査用)
CREATE MATERIALIZED VIEW bad_rows AS
SELECT id, raw_value
FROM src
WHERE raw_value IS NOT NULL
  AND NOT (raw_value ~ '^\s*-?\d+\s*$');

-- 正常系は安全キャストで流す
CREATE VIEW cleaned AS
SELECT id, CASE WHEN raw_value ~ '^\s*-?\d+\s*$' THEN raw_value::int END AS value_int
FROM src;

・“失敗データは落とさず溜める”パターンは本番運用で有効

生成列/制約で「常に整数化できる」設計へ

-- 生成列で正規化
ALTER TABLE orders
  ADD COLUMN amount_i int GENERATED ALWAYS AS
    (NULLIF(regexp_replace(amount_txt, '[^\d-]', '', 'g'), '')::int) STORED;

-- 新規データが整数化可能かを保証するチェック
ALTER TABLE orders
  ADD CONSTRAINT amount_txt_int_check
  CHECK (amount_txt IS NULL OR amount_txt ~ '^\s*-?\d+\s*$');

・“後で困らない”ために入口で保証する

最小再現→解消の通し例(NG→OK)

DROP TABLE IF EXISTS demo;
CREATE TABLE demo(
  id    serial PRIMARY KEY,
  b     boolean,
  j     jsonb,
  ts    timestamp,
  u     uuid,
  s     text
);

INSERT INTO demo(b,j,ts,u,s) VALUES
(true,  '{"age":"19"}', now(), gen_random_uuid(), '12,345円'),
(false, '{"age":25}',   now(), gen_random_uuid(), '  42  '),
(true,  '{"age":"x"}',  now(), gen_random_uuid(), '');

-- NG:直接キャスト
-- SELECT b::int, j::int, u::int, ts::int, s::int FROM demo;  -- すべて失敗可能

-- OK:型別の正しい変換
SELECT
  CASE WHEN b THEN 1 ELSE 0 END                 AS b_i,
  NULLIF(j->>'age','')::int                     AS age_i,         -- 文字列数値/NULLならOK
  EXTRACT(EPOCH FROM ts)::bigint                AS ts_sec,
  hashtextextended(u::text, 0)                  AS u_hash_i8,
  NULLIF(regexp_replace(s, '[^\d-]', '', 'g'),'')::int AS s_i
FROM demo;

・「何を整数として扱いたいのか」を明示し、適切な関数で“意味のある整数”に変換

チェックリスト(上から順に)

□ pg_typeof() で“元の型”を把握したか
□ 文字列→整数は正規表現ガードや前処理を入れているか
□ boolean/jsonb/uuid/date/enum/bytea/array/ドメイン型は“専用の変換手段”を使っているか
□ 変換できない値(空文字・単位付き・区切り付き)を事前に除去/隔離しているか
□ ビュー/生成列/制約で“常に整数化可能”を設計で担保できるか

まとめ:直接CASTに頼らず“意味の変換”を選ぶ

・失敗の本質=「その型をそのまま整数にする意味がない/規定がない」
・整数にしたい“意味”を明確化し、対応する関数/式(EXTRACT/->>/CASE/hash)を使う
・安全変換の定型(正規表現ガード、NULLIF、二段変換)をテンプレ化して再利用