PostgreSQL『cast from type to integer is not allowed』の原因と対処
- 作成日 2025.09.25
- 更新日 2025.10.06
- その他
「X という型から integer へのキャストは許可されない」趣旨のエラー(典型表示:cannot cast type X to integer)が出る状況を整理。発生しやすい型(boolean/jsonb/uuid/date/enum/bytea/array/ドメイン型)ごとの解決パターン、数値文字列の安全変換レシピ、マイグレーションやビューでの実戦例まで収録。
目次
- 1. エラーの意味と代表メッセージ
- 2. まず実行:型と中身の特定クエリ
- 3. 文字列→整数を安全に行う(正規表現ガード+前処理)
- 4. boolean → 整数は直接不可(CASEで写像)
- 5. json/jsonb → 整数は「テキスト抽出→キャスト」
- 6. date/time/timestamp → 整数は変換関数を使う
- 7. uuid/bytea/enum/array/ドメイン型の対処
- 8. ビュー/ETL向け:一括変換の定型(NULL安全・不正値隔離)
- 9. 生成列/制約で「常に整数化できる」設計へ
- 10. 最小再現→解消の通し例(NG→OK)
- 11. チェックリスト(上から順に)
- 12. まとめ:直接CASTに頼らず“意味の変換”を選ぶ
エラーの意味と代表メッセージ
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、二段変換)をテンプレ化して再利用
-
前の記事
PostgreSQL『ERROR: index row size exceeds maximum』の原因と対処 2025.09.24
-
次の記事
PostgreSQL『more than one row returned by a subquery used as an expression』の原因と対処 2025.09.26
コメントを書く