<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>テーブル &#8211; SRA OSS Tech Blog</title>
	<atom:link href="https://www.sraoss.co.jp/tech-blog/tag/c0ae3018e19920e77093165bb51f822a/feed/index.xml" rel="self" type="application/rss+xml" />
	<link>https://www.sraoss.co.jp/tech-blog/</link>
	<description>OSS専門の技術ブログ。PostgreSQL、Zabbixを中心に10種類以上のOSSの解説、アップデート情報、運用ヒントを提供。</description>
	<lastBuildDate>Mon, 09 Nov 2026 03:56:52 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://www.sraoss.co.jp/tech-blog/wp-content/uploads/2018/07/blog-icon-150x150.png</url>
	<title>テーブル &#8211; SRA OSS Tech Blog</title>
	<link>https://www.sraoss.co.jp/tech-blog/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>PostgreSQLのデータ型を「なんとなく」で選ばない</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/postgresql-data-type-selection/</link>
		
		<dc:creator><![CDATA[正野 裕大 (Yuta Masano)]]></dc:creator>
		<pubDate>Thu, 01 Oct 2026 05:25:11 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[データ型]]></category>
		<category><![CDATA[テーブル]]></category>
		<category><![CDATA[テーブル設計]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=20890</guid>

					<description><![CDATA[PostgreSQLでテーブルを作るとき、保存する値が決まっていても、データ型の選択に迷うことがあります。注文コードには、長さを指定できるvarchar(n)を使うのか、textでよいのか。金額にはnumericを使うの ...]]></description>
										<content:encoded><![CDATA[<p>PostgreSQLでテーブルを作るとき、保存する値が決まっていても、データ型の選択に迷うことがあります。注文コードには、長さを指定できる<code>varchar(n)</code>を使うのか、<code>text</code>でよいのか。金額には<code>numeric</code>を使うのか、円単位なら整数型でよいのか。日時や外部APIのレスポンスにも、複数の保存方法があります。</p>
<p><span id="more-20890"></span></p>
<p>テストデータを少し入れるだけなら、どの型でも問題なく動くように見えるかもしれません。しかし、注文コードの形式が変わったり、金額を集計したり、海外拠点の時刻を扱ったり、決済レスポンスを検索したりすると、型の違いが表面化します。型を選ぶときは、値を保存できるかだけでなく、保存後にどう使うかまで考えます。</p>
<p>この記事は、SQLの基本を理解し、これからテーブルを設計する方に向けた入門向け記事です。架空の注文管理テーブル<code>orders</code>を例に、文字列、数値、日付時刻、JSONの型を用途別に検討します。型選びに関わる性質と判断理由に絞って説明します。</p>
<p>例では、注文コード、単価、数量、注文日時などを保存します。まずテーブルを定義し、各列の型を用途に照らして確認します。</p>
<p>この最初の定義には、後で見直す候補として<code>double precision</code>と<code>json</code>をあえて含めています。以降の章で、金額の正確さやJSON検索の必要性に応じて、型を変更する条件を確認します。</p>
<pre><code>CREATE TABLE orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY -- 注文を一意に識別するID。値はPostgreSQLが自動で採番する。
  , order_code varchar(16) NOT NULL                  -- 注文コード。最大16文字の文字列を必ず入れる。
  , customer_memo text                               -- 顧客からのメモ。業務上の長さ制限を設けず、入力は任意。
  , unit_price numeric(12, 2) NOT NULL               -- 商品1個あたりの単価。全体で12桁、小数点以下2桁。
  , quantity integer NOT NULL                        -- 注文数量。整数値を必ず入れる。
  , total_amount double precision NOT NULL           -- 注文金額の合計。必ず入れる。
  , ordered_at timestamp without time zone NOT NULL  -- 注文日時。必ず入れる。
  , payment_payload json                             -- 決済サービスなどから返されたJSON形式のデータ。
);
</code></pre>
<h2>1. 文字列型は上限と条件の表し方で選ぶ</h2>
<h3>1.1. <code>char(n)</code>を選ぶ場面は限られる</h3>
<p>注文コードのように形式が決まった文字列には、固定長の<code>char(n)</code>も候補に見えます。ただし、<code>char(n)</code>は入力が指定した長さに満たない場合、末尾に空白を補います。また、<code>char</code>同士の比較では末尾の空白を無視します。そのため、末尾の空白を値の一部として区別したい列には向きません。</p>
<pre><code>CREATE TEMP TABLE order_code_char (
  code char(5) NOT NULL
);

INSERT INTO order_code_char VALUES ('hi');

SELECT code, char_length(code), octet_length(code), code = 'hi   ', code = 'hi'
FROM order_code_char;
 code  | char_length | octet_length | ?column? | ?column?
-------+-------------+--------------+----------+----------
 hi    |           2 |            5 | t        | t
(1 row)
</code></pre>
<p>この例では、<code>'hi'</code>の末尾に3つの空白を補って<code>char(5)</code>の幅で保存しています。<code>char_length(code)</code>は末尾の空白を数えないため2を返しますが、<code>octet_length(code)</code>は、このASCII文字列では5を返します。空白を含む<code>'hi '</code>との比較も、含まない<code>'hi'</code>との比較も<code>true</code>です。固定長を指定しても、入力された文字列をそのまま区別して保存できるわけではありません。</p>
<p>性能面でも、<code>char(n)</code>を選ぶ利点はありません。公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-character.html">8.3. 文字型</a>」によると、PostgreSQLの<code>char(n)</code>には固有の性能上の利点がありません。空白埋めで保存領域が増えるため、<code>char(n)</code>、<code>varchar(n)</code>、<code>text</code>の中では通常最も遅い型です。新規設計では<code>char(n)</code>を避け、最大長を制限するなら<code>varchar(n)</code>や<code>CHECK</code>制約を検討します。</p>
<h3>1.2. <code>varchar(n)</code>は文字数の上限を型に記せる</h3>
<p><code>orders.order_code</code>には、最大16文字を保存できる<code>varchar(16)</code>を指定しています。<code>varchar(16)</code>は、<code>char(n)</code>のように後ろに空白文字を補わず、指定した文字列をそのまま保存します。初期の注文コードが次の形式なら15文字なので、後ろに空白文字が付くことなく保存できます。</p>
<pre><code>CREATE TEMP TABLE orders_code_varchar (
  order_code varchar(16) NOT NULL
);

INSERT INTO orders_code_varchar VALUES ('OD-000000000001');
</code></pre>
<p>発番側の仕様変更で注文コードに日付が加わると、上限を超える場合があります。次の値は18文字なので、同じ列には保存できません。</p>
<pre><code>INSERT INTO orders_code_varchar VALUES ('OD-20260801-000001');
ERROR:  value too long for type character varying(16)
</code></pre>
<p>これは<code>varchar(n)</code>の長さ制限によるエラーです。公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-character.html">8.3. 文字型</a>」では、<code>character varying(n)</code>、つまり<code>varchar(n)</code>は最大<code>n</code>文字を保存できる型と説明されています。通常の挿入では、超過分がすべて空白の場合を除き、上限を超える入力はエラーになります。ここでいう<code>n</code>はバイト数ではなく文字数です。</p>
<p><code>varchar(n)</code>を使うときは、現在のデータが収まるかに加えて、上限の根拠を確認します。注文コードの仕様が変わったら、発番側の変更に合わせてデータベース側の上限も見直します。</p>
<h3>1.3. 長さの上限は型か制約で表す</h3>
<p>注文コードの上限が業務仕様として「最大18文字」と決まっているなら、<code>varchar(18)</code>を使えます。型定義から最大長が分かり、上限を超える入力はデータベースが拒否します。上限を変える場合は、列の型定義を変更します。</p>
<p>文字列型と業務上の条件を分けて定義するなら、<code>text</code>と<code>CHECK</code>制約を組み合わせる方法もあります。<code>CHECK</code>制約は、条件式が<code>FALSE</code>になる値を拒否します。条件式が<code>NULL</code>になる値は通過するため、<code>NULL</code>も拒否する場合は<code>NOT NULL</code>制約を併用します。次の定義では、<code>text</code>型の注文コードを<code>OD-</code>で始まる18文字以下の値に制限しています。</p>
<pre><code>CREATE TEMP TABLE orders_code_text (
  order_code text NOT NULL,
  CONSTRAINT orders_code_format_check CHECK (
    order_code LIKE 'OD-%'
    AND char_length(order_code) &lt;= 18
  )
);
</code></pre>
<p><code>CHECK</code>制約では、長さと接頭辞の条件を同時に定義できます。上限を変えるときは、列の型を維持して制約を変更します。長さだけを型に記すなら<code>varchar(n)</code>、長さを含む業務条件をまとめて管理するなら<code>text</code>と<code>CHECK</code>制約、というように条件の表し方で選べます（<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS">CHECK制約</a>）。</p>
<p>この使い分けは、短い文字列には<code>varchar(n)</code>、長い文字列には<code>text</code>という性能上の区別ではありません。公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-character.html">8.3. 文字型</a>」によると、長さ制限の検査や空白埋めの負担を除けば、文字列型の間に性能差はありません。<code>orders.customer_memo</code>のように業務上の長さ制限がない列には、<code>text</code>を使えます。</p>
<h3>1.4. 保存できる文字列でもB-treeインデックスに入らない場合がある</h3>
<p>長い文字列を保存できることと、その値全体をインデックスのキーにできることは別です。たとえば<code>orders.customer_memo</code>を<code>text</code>で保存できても、その列にB-treeインデックスを作成できるとは限りません。B-treeは等価比較や範囲検索などに使うインデックスですが、1件のインデックスエントリにはサイズ上限があります。</p>
<p>PostgreSQLは、テーブルやインデックスをページ（データを格納する単位）に分けて管理します。標準的なページサイズは8 KiBです。公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/btree.html">B-treeインデックス</a>」によると、1つのインデックスエントリは、1ページのおよそ3分の1（8 KiBのページでは約2.7 KiB）を超えられません。TOASTによる圧縮が適用される場合は、圧縮後のサイズが制限の対象です。そのため、テーブルには保存できても、値全体をキーにすると<code>INSERT</code>や<code>CREATE INDEX</code>がエラーで失敗します。値を切り詰めて登録することはありません。</p>
<p>たとえば、圧縮されにくい文字列を保存してからB-treeインデックスを作成すると、次のようなエラーが発生します。エラーメッセージの<code>buffer page</code>は、上述のページを指しています。</p>
<pre><code>CREATE TEMP TABLE long_memo (
  memo text
);

INSERT INTO long_memo
SELECT string_agg(md5(random()::text), '')
FROM generate_series(1, 200);

CREATE INDEX ON long_memo (memo);
ERROR:  index row size 6416 exceeds btree version 4 maximum 2704 for index "long_memo_memo_idx"
DETAIL:  Index row references tuple (0,1) in relation "long_memo".
HINT:  Values larger than 1/3 of a buffer page cannot be indexed.
Consider a function index of an MD5 hash of the value, or use full text indexing.
</code></pre>
<p>この例では、文字列自体は<code>text</code>列に保存できますが、値全体をB-treeのキーにできないため、<code>CREATE INDEX</code>が失敗しています。既にインデックスがある場合は、同じ値を<code>INSERT</code>した時点でインデックスへの追加に失敗します。<code>INSERT</code>文はエラーで取り消されるため、テーブルに行だけが残ることもありません。この単一列の<code>text</code>インデックスでは、サイズ制限を回避するために値を途中で切り詰めて登録することはありません。切り詰めた部分だけを使って、異なる値を同一視する動作もありません。</p>
<p>この制限は<code>text</code>だけでなく、大きな<code>varchar(n)</code>にも当てはまります。長い文字列を検索するときは、検索方法に合ったインデックスを選びます。単語単位の検索には全文検索とGINインデックス、完全一致検索にはハッシュ値を使った式インデックスなどが候補です。ハッシュ値には衝突の可能性があるため、利用者はハッシュ値の条件に加えて元の値との比較条件も検索に含めます。PostgreSQLは、指定された元の値との比較条件を評価し、ハッシュ値が一致した候補から検索対象を絞り込みます。</p>
<h2>2. 数値型は必要な正確さと値の範囲で選ぶ</h2>
<h3>2.1. <code>double precision</code>は2進数で表せる値しか正確に扱えない</h3>
<p>金額の計算や比較を10進数の規則どおりに行うなら、<code>double precision</code>は使いません。冒頭の<code>orders.total_amount</code>は、必要な桁数を指定した<code>numeric</code>に変更します。一方、測定値や統計計算のように近似値を許容し、計算速度や表現できる値の範囲を優先する用途では、<code>double precision</code>を使えます。</p>
<p><code>double precision</code>は小数を扱えますが、2進数で有限桁に表せない値は近似値になります。税率や割引率を使って金額を計算し、小数点以下まで正確に扱う場合には、この性質が問題になります。</p>
<p><code>numeric</code>と<code>double precision</code>の違いは、次の計算で確認できます。どちらも<code>0.1 + 0.2</code>が<code>0.3</code>と等しいかを比較しています。</p>
<pre><code>SELECT 0.1::numeric + 0.2::numeric = 0.3::numeric AS numeric_equal;
 numeric_equal
---------------
 t
(1 row)

SELECT 0.1::double precision
     + 0.2::double precision = 0.3::double precision AS double_equal;
 double_equal
--------------
 f
(1 row)
</code></pre>
<p><code>numeric</code>では<code>true</code>、<code>double precision</code>では<code>false</code>になります。<code>0.1</code>は2進数で有限桁に表せないため、<code>double precision</code>では近似値として扱われます。一方、<code>numeric</code>は10進数の値を正確に扱うため、この計算では期待どおりの比較結果になります（<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-numeric.html#DATATYPE-FLOAT">浮動小数点数データ型</a>）。</p>
<p>整数だけなら、<code>double precision</code>でも−2の53乗から2の53乗までの整数をすべて正確に保存できます。正数の上限は約9,000兆です。この範囲の金額を1円単位で保存するだけなら、浮動小数点表現による誤差は生じません。また、<code>0.5</code>のように2進数で正確に表せる小数もあります。ただし、税率や割引率を使った計算には、<code>0.1</code>のように近似値になる小数が含まれることがあります。整数を正確に保存できても、その後の計算まで正確とは限りません。</p>
<p><code>0.1</code>のように2進数で有限桁に表せない小数が計算に含まれると、その値は近似になります。そのため、金額を10進数の規則で計算・比較し、丸めを管理したい要件には<code>double precision</code>は向きません。<code>orders.total_amount</code>には<code>numeric</code>を使います。</p>
<h3>2.2. 金額には<code>numeric</code>、個数には整数型を使う</h3>
<p>10進数として正確に扱う金額には、必要な桁数を決めて<code>numeric</code>を使います。<code>numeric(precision, scale)</code>の<code>precision</code>は全体の桁数、<code>scale</code>は小数点以下の桁数です。<code>orders.unit_price</code>の<code>numeric(12, 2)</code>は、全体で12桁、小数点以下2桁を指定しています。指定より多い小数桁の入力は、まず2桁に丸められます。丸めた後の整数部が10桁を超える値は保存できません。<code>orders.total_amount</code>にも同じ考え方を使えますが、合計額に必要な桁数は単価とは分けて確認します。</p>
<p>金額を常に円単位の整数として扱うなら、<code>integer</code>や<code>bigint</code>に保存する設計も選べます。この場合も、税率や割引率の計算で小数が出たとき、どの時点で丸めるかを決めます。<code>orders.quantity</code>のような個数や回数には整数型を使い、想定する値の範囲に応じて<code>integer</code>か<code>bigint</code>を選びます。</p>
<p>近似値を許容できる値には、<code>double precision</code>を検討します。測定値や統計計算の結果など、10進数としての厳密な一致より計算速度や表現できる値の範囲を優先する用途です。小数を含むかどうかだけでなく、どの程度の正確さが要るかで<code>numeric</code>と使い分けます。</p>
<h3>2.3. 新規設計では<code>money</code>型より<code>numeric</code>を優先する</h3>
<p>金額を保存する型には<code>money</code>もありますが、新規設計では<code>numeric</code>を優先します。<code>money</code>の表示形式や小数部の精度は、データベースの<code>lc_monetary</code>設定に依存します。設定が異なるデータベースへダンプをリストアすると、ダンプに含まれる<code>money</code>の文字列表現をリストア先の<code>lc_monetary</code>で解釈できず、読み込みに失敗することがあります。読み込みに成功しても、表示形式や小数部の扱いが変わる可能性があります。また、<code>money</code>を整数で割った結果は、表現できない小数部を0方向へ切り捨てます。負数の場合も、絶対値が小さくなる方向です。そのため、<code>money</code>はデータベースの設定と、演算に組み合わせる型の規則を意識して使う必要があります（<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-money.html">通貨型</a>）。</p>
<p><code>money</code>は既存スキーマとの互換性など、明確な理由がある場合に検討します。通貨記号を含む表示が必要なら、<code>numeric</code>の値を<code>to_char</code>で整形できます（<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/functions-formatting.html">データ型書式設定関数</a>）。たとえば、<code>to_char(unit_price, 'L999G999G999G990D00')</code>で通貨記号、桁区切り、小数点以下の表示形式を指定できます。<code>orders</code>の金額列では、保存した<code>numeric</code>の値を計算や比較に使い、画面に表示するときだけ<code>to_char</code>で表示用の文字列に変換します。</p>
<h2>3. 日付時刻型は保存したい時刻の意味で選ぶ</h2>
<p><code>orders.ordered_at</code>の型を選ぶには、注文日時として何を記録するのかを決めます。受信した表記をそのまま残すのか、特定の地域の日付と時刻を記録するのか、注文が起きた一瞬を記録するのかで選択が変わります。ここでは文字列型、<code>timestamp without time zone</code>、<code>timestamp with time zone</code>を比較します。</p>
<h3>3.1. 受信した表記を残すなら文字列型を選べる</h3>
<p>受信した表記をそのまま残す必要があるなら、文字列型を選べます。UTCとの時差を表すオフセットやタイムゾーン名、部分的な日時、仕様外の値も、監査記録として保存できます。PostgreSQLや、JDBCやODBCなどのデータベースドライバに日時として解釈・変換させず、アプリケーション側だけで解釈する方針にも合います。</p>
<p>ただし、文字列型では日時としての妥当性は保証されません。データベースで日時の範囲検索や演算をするなら、日付時刻型を使います。検索と受信原文の保存の両方が必要なら、検索用の日付時刻型列と原文用の文字列列を分けます。</p>
<p>文字列の並び順を日時順として使うには、入力形式と照合順序（文字列比較の規則）を統一します。たとえば日付を<code>YYYY-MM-DD</code>にそろえ、桁数、精度、表記規則を固定すれば、文字列の順序を日付順として利用できます。ただし、異なるタイムゾーンの日時を同じ列に保存すると、表記上の順序と実際の時系列が一致しない場合があります。形式と照合順序を統一するだけで目的を満たせるか確認が必要です。</p>
<h3>3.2. <code>timestamp without time zone</code>は時差を変換しない</h3>
<p><code>orders.ordered_at</code>に指定した<code>timestamp without time zone</code>は、日付と時刻を保存しますが、タイムゾーン情報は持ちません。タイムゾーン付きの文字列を入力しても、時差の部分は無視されます。次の例では、同じ入力を<code>timestamp with time zone</code>にも保存し、セッションのタイムゾーンを変えて表示を比較します。</p>
<pre><code>SET TIME ZONE 'Asia/Tokyo';

CREATE TEMP TABLE orders_time (
  ts_without timestamp without time zone,
  ts_with timestamp with time zone
);

INSERT INTO orders_time VALUES
('2026-07-01 09:00:00+09', '2026-07-01 09:00:00+09');

SELECT ts_without, ts_with FROM orders_time;
     ts_without      |        ts_with
---------------------+------------------------
 2026-07-01 09:00:00 | 2026-07-01 09:00:00+09
(1 row)

SET TIME ZONE 'UTC';

SELECT ts_without, ts_with FROM orders_time;
     ts_without      |        ts_with
---------------------+------------------------
 2026-07-01 09:00:00 | 2026-07-01 00:00:00+00
(1 row)
</code></pre>
<p><code>ts_with</code>は、<code>Asia/Tokyo</code>では<code>09:00:00+09</code>、UTCでは<code>00:00:00+00</code>と表示されます。一方、<code>ts_without</code>はどちらでも<code>09:00:00</code>です。<code>timestamp without time zone</code>では、入力に含まれる<code>+09</code>を使った変換も、表示先のタイムゾーンに合わせた変換も行われません。</p>
<p><code>timestamp without time zone</code>が適するのは、特定の地域の日付と時刻そのものを扱う場合です。たとえば、店舗の営業開始日時やシフト開始日時なら、<code>timestamp without time zone</code>を検討できます。その場合は、どの地域の日時かを店舗や拠点の情報と一緒に管理します。注文が実際に起きた時点を複数の地域から比較する用途には、次の<code>timestamp with time zone</code>が適しています。</p>
<h3>3.3. <code>timestamp with time zone</code>は同じ一瞬を保存する</h3>
<p><code>timestamp with time zone</code>は、タイムゾーンを考慮して日時を扱います。明示的なタイムゾーンを含む入力はUTCに変換して保存し、出力時にはセッションの<code>TimeZone</code>に合わせて表示します。入力時のタイムゾーン名や<code>+09</code>という指定自体は保存しません。</p>
<p>注文受付時刻、決済完了時刻、ログ発生時刻のように、同じ一瞬を複数の地域から比較するなら、<code>timestamp with time zone</code>が適しています。<code>orders.ordered_at</code>をこの用途に使うなら、<code>timestamp without time zone</code>から変更する候補になります。前節の例で表示が変わったのは、保存した一瞬はそのままに、表示先のタイムゾーンに合わせて変換したためです。</p>
<p>ただし、タイムゾーンのない入力を<code>timestamp with time zone</code>に渡すと、セッションの<code>TimeZone</code>で解釈されます。アプリケーションの接続設定、データベースドライバによる日付時刻型への変換、取得後の表示設定が一致しないと、意図しない時刻として扱われる可能性があります。入力にタイムゾーンを含めるか、セッションの<code>TimeZone</code>を固定するかを決め、取得後の表示先も明確にします。</p>
<p>保存したい値ごとの選択肢をまとめます。日付時刻型の仕様は、公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-datetime.html">日付/時刻データ型</a>」と「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-datetime.html#DATATYPE-DATETIME-INPUT-TIME-STAMPS">日付/時刻の入力</a>」を参照してください。</p>
<table>
<thead>
<tr>
<th>表したいもの</th>
<th>選択肢</th>
</tr>
</thead>
<tbody>
<tr>
<td>注文受付、決済完了、ログ発生のような同じ一瞬</td>
<td><code>timestamp with time zone</code></td>
</tr>
<tr>
<td>特定の地域における、ある日の営業開始日時やシフト開始日時</td>
<td><code>timestamp without time zone</code></td>
</tr>
<tr>
<td>受信した表記、部分的な日時、仕様外の日時をそのまま保存</td>
<td><code>text</code>または<code>varchar</code></td>
</tr>
<tr>
<td>日付時刻型の値と受信原文の両方が必要</td>
<td>日付時刻型列と原文用文字列列を分ける</td>
</tr>
</tbody>
</table>
<h2>4. JSON型は原文の保持と検索の必要性で選ぶ</h2>
<h3>4.1. <code>text</code>、<code>json</code>、<code>jsonb</code>は保存後の用途が異なる</h3>
<p><code>orders.payment_payload</code>には、決済サービスから返されたJSON形式のデータを保存します。保存先には<code>text</code>、<code>json</code>、<code>jsonb</code>があり、受信内容をどこまでそのまま残すか、保存後にどう検索するかで選びます。</p>
<p><code>text</code>はJSONとして検証しないため、JSONとして不正な文字列も保存できます。NUL文字だけは保存できません。これは、PostgreSQLの<code>text</code>型自体がNUL文字を保存できないためです。実装上のサイズ上限は約1GBです。JSONとしての妥当性を検証せず、受信内容を残すことが目的なら候補です。</p>
<p><code>json</code>は入力がJSONとして正しいことを検証し、入力されたテキストを保存します。空白、キーの順序、重複キーも保持します。冒頭の定義のように<code>payment_payload</code>を<code>json</code>にすると、JSONとして検証しながら受信時の表記を残せます。</p>
<p><code>jsonb</code>は入力を解析し、分解した形式で保存します。そのため、保存後の検索や比較に向いています。入力時に変換が必要ですが、GINインデックスも利用できます。一方、空白やキーの順序は保持されず、重複キーは最後の値だけが残ります。次のSQLで、重複キーの扱いを比較できます。</p>
<pre><code>SELECT '{"a": 1, "a": 2, "active": false}'::json;
               json
-----------------------------------
 {"a": 1, "a": 2, "active": false}
(1 row)

SELECT '{"a": 1, "a": 2, "active": false}'::jsonb;
            jsonb
---------------------------
 {"a": 2, "active": false}
(1 row)
</code></pre>
<p><code>json</code>では2つの<code>a</code>が入力どおりに残りますが、<code>jsonb</code>では最後の値である<code>2</code>だけが残ります。受信時の表記を保持するなら<code>json</code>、JSONとしての検証もせず文字列を残すなら<code>text</code>、JSONの内容を検索・比較するなら<code>jsonb</code>を選びます。</p>
<h3>4.2. JSONのキーや値を検索するなら<code>jsonb</code>を使う</h3>
<p>決済レスポンスのトップレベルに<code>error_code</code>キーがある注文を調べるなら、<code>orders.payment_payload</code>を<code>jsonb</code>にする方法があります。冒頭のテーブル定義どおり列が<code>json</code>型の場合、次の検索を実行するとエラーが発生します。<code>?</code>演算子は<code>jsonb</code>型に対して定義されているためです。</p>
<pre><code>SELECT id
FROM orders
WHERE payment_payload ? 'error_code';
ERROR:  operator does not exist: json ? unknown
LINE 3: WHERE payment_payload ? 'error_code';
                              ^
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.
</code></pre>
<p>列を<code>jsonb</code>に変更すると、同じ条件でキーの有無を調べられます。</p>
<pre><code>ALTER TABLE orders
  ALTER COLUMN payment_payload TYPE jsonb
  USING payment_payload::jsonb;

SELECT id
FROM orders
WHERE payment_payload ? 'error_code';
 id
----
(0 rows)
</code></pre>
<p><code>json</code>や<code>text</code>で保存し、検索のたびに<code>jsonb</code>へ変換する方法もあります。ただし、変換が必要になり、インデックスも検索式に合わせて定義しなければなりません。</p>
<p>検索量が増えたら、<code>jsonb</code>列へのGINインデックスも検討します。GINは、JSONのキーや値などを使った検索に利用できるインデックスです。標準のGIN演算子クラスでは、キーの存在、包含、JSONPathによる検索などを扱えます。詳しくは公式文書の「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-json.html">JSONデータ型</a>」と「<a href="https://www.sraoss.co.jp/PostgreSQL/Manual/document/18/datatype-json.html#JSON-INDEXING">jsonbインデックス</a>」を参照してください。</p>
<h2>5. まとめ: 列の用途から型を決める</h2>
<p><code>orders</code>の各列を見直すと、値の形式だけでは型を決められないことが分かります。注文コードには長さの上限、金額には計算の正確さ、注文日時にはタイムゾーンの扱い、決済レスポンスには原文の保持と検索方法を考える必要があります。</p>
<p>文字列では、最大長を型に記すなら<code>varchar(n)</code>、業務条件を制約で管理するなら<code>text</code>と<code>CHECK</code>制約を検討します。<code>char(n)</code>は空白埋めと比較規則に注意が必要で、性能上の利点もないため、新規設計で選ぶ場面は限られます。また、長い文字列を保存できても、値全体をB-treeインデックスのキーにできるとは限りません。</p>
<p>数値では、10進数の正確さが必要なら<code>numeric</code>、整数で足りるなら値の範囲に応じて<code>integer</code>か<code>bigint</code>を選びます。<code>double precision</code>は近似値を許容できる用途に使い、金額では丸める時点と規則も決めます。<code>money</code>は設定や演算規則に依存するため、新規設計で使う場合は注意が必要です。</p>
<p>日付時刻では、同じ一瞬を扱うなら<code>timestamp with time zone</code>、特定の地域の日付と時刻を扱うなら<code>timestamp without time zone</code>を検討します。受信した表記を残すために文字列型を選ぶこともできますが、その場合は日時としての妥当性や比較方法を別途管理します。</p>
<p>JSONでは、受信文字列を検証せずに残すなら<code>text</code>、JSONとして検証して入力テキストを保持するなら<code>json</code>、内容を検索・比較するなら<code>jsonb</code>が候補です。決済レスポンスを記録として残すのか、内容を使って注文を検索するのかで、適する型が変わります。</p>
<p>テーブルを設計するときは、列ごとに保存する値と用途を確かめます。拒否する入力、必要な計算、検索条件が明確になれば、型を選ぶ理由も定まります。</p>
<p>&nbsp;</p>
<div>
<a href="https://www.sraoss.co.jp/prod_serv/support/pgsql-mainte/?utm_source=sraoss-techblog&utm_medium=referral&utm_campaign=under-article-pgsql" rel="noopener" style="color:initial;" target="_blank" class="no-ext"></p>
<div style="background-color:#DAEFF6; padding:20px; border-radius:8px; text-align:center;">
SRA OSSでは、PostgreSQL/Pgpool-II のサポートを提供しております。<br />
詳しくは<strong style="color:#2581c4;">弊社ホームページ</strong>をご覧ください。
</div>
<p></a>
</div>

]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
