<?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>正野 裕大 (Yuta Masano) &#8211; SRA OSS Tech Blog</title>
	<atom:link href="https://www.sraoss.co.jp/tech-blog/author/masano/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>Wed, 04 Nov 2026 06:58:17 +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>正野 裕大 (Yuta Masano) &#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>
		<item>
		<title>PostgreSQL であなたの UPDATE が終わらない理由 〜トランザクション、ロック、MVCC を実験で理解する〜</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/postgresql-update-lock-wait/</link>
		
		<dc:creator><![CDATA[正野 裕大 (Yuta Masano)]]></dc:creator>
		<pubDate>Tue, 30 Jun 2026 05:21:12 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[MVCC]]></category>
		<category><![CDATA[pg_blocking_pids()]]></category>
		<category><![CDATA[トランザクション]]></category>
		<category><![CDATA[ロック]]></category>
		<category><![CDATA[監視ビュー]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=20842</guid>

					<description><![CDATA[アプリケーションから次のようなSQLを実行したのに、なかなか応答が返ってこないことがあります。 UPDATE accounts SET balance = balance - 100 WHERE id = 1; こういう ...]]></description>
										<content:encoded><![CDATA[<p>アプリケーションから次のようなSQLを実行したのに、なかなか応答が返ってこないことがあります。</p>
<pre><code>UPDATE accounts SET balance = balance - 100 WHERE id = 1;
</code></pre>
<p>こういうときに原因の一つとして早めに確認しておきたいものがロック待ちです。ロック待ちとは、あるトランザクションが更新中の行などを、別のトランザクションが同時に変更しようとして、先のトランザクションが終わるまで待っている状態です。PostgreSQLはデータの矛盾を防ぐため、同じ行を複数の処理が同時に勝手に書き換えられないようにします。そのため、後から実行した<code>UPDATE</code>は、必要なロックを取得できるまで待つことがあります。</p>
<p>本記事では、同じ行への<code>UPDATE</code>によってロック待ちが起きる状況を作り、どこを確認すれば原因を切り分けられるのかを説明します。</p>
<p><span id="more-20842"></span></p>
<p>対象読者は、PostgreSQLを使い始めたアプリケーション開発者や運用初心者です。SQLは書けるものの、トランザクション、ロック、MVCC、監視ビューの関係をまだ整理できていない人を想定しています。</p>
<h3>実験の前に</h3>
<p>この実験では、<code>psql</code>のセッションを3つ使います。</p>
<ul>
<li>セッションA:先に行を更新して、ロックを保持する</li>
<li>セッションB:同じ行を更新しようとして待つ</li>
<li>セッションC: PostgreSQLの状態を確認する</li>
</ul>
<p>ここで、実験に入る前にトランザクションについて簡単に整理します。</p>
<p>トランザクションとは、複数のSQL操作をひとまとまりの処理として扱う仕組みです。たとえば、口座Aから100円を引き、口座Bに100円を足す処理を考えます。この2つの更新は、片方だけの成功は許されません。引き落としだけ成功して、入金が失敗すると、データの整合性が崩れるためです。そこで、PostgreSQLでは、関連するSQLを1つのトランザクションとして扱えます。</p>
<p><code>BEGIN</code>を実行すると、トランザクションが始まります。</p>
<pre><code>BEGIN;
</code></pre>
<p>トランザクション内で実行した変更は、<code>COMMIT</code>するまで確定しません。</p>
<pre><code>COMMIT;
</code></pre>
<p><code>COMMIT</code>は、トランザクション内で行った変更を確定する操作です。<code>COMMIT</code> すると、他のセッションからもその変更が見えるようになります。一方、変更を取り消したい場合は<code>ROLLBACK</code>を実行します。</p>
<pre><code>ROLLBACK;
</code></pre>
<p><code>ROLLBACK</code>は、トランザクション内で行った変更を取り消す操作です。<code>ROLLBACK</code>すると、そのトランザクションで実行した更新はなかったことになります。</p>
<p>本記事のロック待ちを理解するうえで重要なのは、次の点です。トランザクション中に<code>UPDATE</code>文の実行が終わっていても、<code>COMMIT</code>または<code>ROLLBACK</code>を実行するまで、トランザクションは終わりません。そして、トランザクションが終わるまで、更新に関係するロックが保持されることがあります。つまり、次のような状態が問題になります。</p>
<pre><code>BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- COMMITもROLLBACKもしていない
</code></pre>
<p>この状態では、<code>UPDATE</code> 自体は終わっています。しかし、トランザクションはまだ終わっていません。そのため、別のセッションが同じ行を更新しようとすると、先のトランザクションが終わるまで待つことがあります。</p>
<p>本記事では実験を通して、この「トランザクションが終わっていないために、別の更新が待たされる状態」を意図的に作ります。つまり、単に <code>UPDATE</code>を実行するだけでなく、<code>BEGIN</code>でトランザクションを開始し、あえて <code>COMMIT</code>せずに残す操作が重要になります。</p>
<p>以降の実験では、セッションAがトランザクションを開いたまま行を更新し、セッションBが同じ行を更新しようとして待つ、という流れを何度か使います。</p>
<h2>実験の準備</h2>
<p>まず、検証用のテーブルを作ります。ここでは、口座残高を表す<code>accounts</code>テーブルを使います。</p>
<pre><code>-- accountsテーブルを作成
CREATE TABLE accounts (
  id integer PRIMARY KEY
  , owner_name text NOT NULL
  , balance integer NOT NULL
);

-- データを2件登録
INSERT INTO accounts (id, owner_name, balance)
VALUES
  (1, 'alice', 1000),
  (2, 'bob', 2000);
</code></pre>
<p>初期状態を確認します。</p>
<pre><code>SELECT * FROM accounts ORDER BY id;
 id | owner_name | balance
----+------------+---------
  1 | alice      |    1000
  2 | bob        |    2000
(2 rows)
</code></pre>
<p>以降は各実験を始める前に、セッションAとセッションBで次を実行しておくと安全です。</p>
<pre><code>-- 各実験の前にROLLBACKを実行してトランザクションを終了しておく
-- Session A/B
ROLLBACK;
</code></pre>
<p>すでにトランザクションが終わっている場合は、警告が出ることがあります。その場合でも、この実験では問題ありません。</p>
<p>合わせて、セッションAと セッションBのPIDも確認しておきます。</p>
<pre><code>-- セッションA
SELECT pg_backend_pid();
 pg_backend_pid
----------------
             52
(1 row)


-- セッションB
SELECT pg_backend_pid();
 pg_backend_pid
----------------
             57
(1 row)
</code></pre>
<h2>実験1:同じ行をUPDATEすると片方が待つ</h2>
<h3>この実験で確認すること</h3>
<p>この実験では、同じ行を2つのトランザクションから<code>UPDATE</code>すると、後から実行した <code>UPDATE</code>が待たされることを確認します。ポイントは、先に<code>UPDATE</code>したセッションが<code>COMMIT</code>するまで、後続の <code>UPDATE</code>が進めないことです。</p>
<h3>手順1:セッションAでトランザクションを開始する</h3>
<p>まず、セッションAでトランザクションを開始します。</p>
<pre><code>-- Session A
BEGIN;
</code></pre>
<p>この時点で、セッションAはトランザクションの中に入ります。まだデータは更新していません。</p>
<h3>手順2:セッションAでid = 1の行を更新する</h3>
<p>続けて、セッションAで<code>id = 1</code>の行を更新します。</p>
<pre><code>-- Session A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
</code></pre>
<p>この<code>UPDATE</code>自体はすぐに終わります。ただし、まだ<code>COMMIT</code>していません。そのため、セッションA のトランザクションは開いたままです。</p>
<p>ここが重要です。<code>UPDATE</code>文の実行が終わっていても、トランザクションが終わっていなければ、PostgreSQL はその更新に関係するロックを保持し続けます。</p>
<h3>手順3:セッションBで同じ行を更新する</h3>
<p>次に、セッションBでもトランザクションを開始します。</p>
<pre><code>-- Session B
BEGIN;
</code></pre>
<p>そのあと、セッションBで同じ<code>id = 1</code>の行を更新します。</p>
<pre><code>-- Session B
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
</code></pre>
<p>この<code>UPDATE</code>は応答が返ってきません。アプリケーションから見ると、「UPDATEが終わらない」状態に見えます。</p>
<h3>なぜ待つのか</h3>
<p>セッションAは、<code>id = 1</code>の行を先に更新しました。しかし、まだ<code>COMMIT</code>も <code>ROLLBACK</code>もしていません。この状態で、セッションBが同じ行を更新しようとすると、PostgreSQLはセッションB の更新をすぐには進めません。セッションAの更新が確定するのか、取り消されるのかがまだ分からないからです。そのため、セッションBはセッションA のトランザクションが終わるのを待ちます。この待ちが、ロック待ちです。</p>
<h3>この実験で分かったこと</h3>
<p>同じ行を同時に更新しようとすると、後から実行した<code>UPDATE</code>が待つことがあります。ただし、画面上で <code>UPDATE</code>が返ってこないだけでは、SQLの処理が重いのか、ロックを待っているのかは分かりません。</p>
<p>次は、PostgreSQLの中でその待ち状態を確認します。</p>
<h2>実験2: pg_stat_activityで何を待っているかを確認する</h2>
<h3>この実験で確認すること</h3>
<p>この実験では、待っているセッションが本当にロック待ちなのかを確認します。使うのは、<code>pg_stat_activity</code>です。<code>pg_stat_activity</code> は、PostgreSQLに接続している各セッションの状態を確認できるビューです。</p>
<h3>確認するSQL</h3>
<p>セッションCから、次のSQLを実行します。</p>
<pre><code>-- Session C
SELECT
  pid
  , state
  , wait_event_type
  , wait_event
  , xact_start
  , query_start
  , query
FROM
  pg_stat_activity
WHERE
  datname = current_database()
  AND pid &lt;&gt; pg_backend_pid()
  AND query LIKE '%UPDATE accounts%'
ORDER BY
  query_start;
 pid |        state        | wait_event_type |  wait_event   |          xact_start           |          query_start          |                           query
-----+---------------------+-----------------+---------------+-------------------------------+-------------------------------+-----------------------------------------------------------
  52 | idle in transaction | Client          | ClientRead    | 2026-06-15 05:57:26.539576+00 | 2026-06-15 05:57:26.541089+00 | UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  57 | active              | Lock            | transactionid | 2026-06-15 05:57:31.885276+00 | 2026-06-15 05:57:31.886839+00 | UPDATE accounts SET balance = balance - 50 WHERE id = 1;
(2 rows)
</code></pre>
<h3>まず確認する列</h3>
<p>ここで最初に確認する列は、次の3つです。</p>
<ul>
<li><code>state</code></li>
<li><code>wait_event_type</code></li>
<li><code>wait_event</code></li>
</ul>
<p><code>state</code>は、そのセッションの大まかな状態を示します。ここでは、<code>pid = 57</code>のセッションBは <code>state = 'active'</code>になっています。これは、PostgreSQLがそのSQLを処理中として扱っているという意味です。ただし、<code>active</code><br />
だからといって、CPUを使って計算し続けているとは限りません。何かを待っていても、SQLの実行中であれば<br />
<code>active</code>と表示されることがあります。</p>
<p>そこで、<code>wait_event_type</code>と<code>wait_event</code>を合わせて確認します。</p>
<h3>結果の読み方</h3>
<p>セッションBの行を確認すると、次のようになっています。</p>
<pre><code>wait_event_type = Lock
wait_event      = transactionid
</code></pre>
<p><code>wait_event_type = Lock</code>は、ロックを待っていることを示します。<code>wait_event = transactionid</code> は、別のトランザクションが終わるのを待っていることを示します。この例では、セッションBがセッションAのトランザクション終了を待っています。</p>
<p>つまり、セッションBの<code>UPDATE</code>は、検索や計算に時間がかかっているのではありません。ロック待ちで止まっています。</p>
<h3>この実験で分かったこと</h3>
<p><code>UPDATE</code>が返ってこないときは、まず<code>pg_stat_activity</code>を確認します。特に、<code>state</code><br />
だけで判断しないことが重要です。<code>wait_event_type</code>と<br />
<code>wait_event</code>を合わせて確認すると、そのセッションが何を待っているのかを確認できます。</p>
<p>次は、どのセッションが待たせているのかを確認します。</p>
<h2>実験3: pg_blocking_pids()で待たせている相手を探す</h2>
<h3>この実験で確認すること</h3>
<p>この実験では、待っているセッションを止めている相手を特定します。待っている側を、ここではウェイターと呼びます。待たせている側を、ここではブロッカーと呼びます。</p>
<p>PostgreSQLでは、<code>pg_blocking_pids()</code>を使うと、当該PIDのブロッカーPIDを確認できます。</p>
<h3>確認するSQL</h3>
<p>セッションCで次のSQLを実行します。<code>pg_blocking_pids()</code>は引数に指定したPIDのブロッカーPIDの配列を返します。</p>
<pre><code>-- Session C
WITH activity AS (
  SELECT
    pid
    , state
    , wait_event_type
    , wait_event
    , pg_blocking_pids(pid) AS blocking_pids
    , query
  FROM
    pg_stat_activity
  WHERE
    datname = current_database()
    AND pid &lt;&gt; pg_backend_pid()
)
SELECT
  *
FROM
  activity
WHERE
  query LIKE '%UPDATE accounts%'
ORDER BY
  pid;
 pid |        state        | wait_event_type |  wait_event   | blocking_pids |                           query
-----+---------------------+-----------------+---------------+---------------+-----------------------------------------------------------
  57 | active              | Lock            | transactionid | {52}          | UPDATE accounts SET balance = balance - 50 WHERE id = 1;
  52 | idle in transaction | Client          | ClientRead    | {}            | UPDATE accounts SET balance = balance - 100 WHERE id = 1;
</code></pre>
<h3>結果の読み方</h3>
<p><code>pid = 57</code>の行を確認すると、<code>blocking_pids</code>が<code>{52}</code>になっています。これは、<code>pid = 57</code> のセッションが、<code>pid = 52</code>のセッションで待たされているという意味です。</p>
<p>この例では、次の関係になります。</p>
<pre><code>ウェイター（待っている側）:     pid = 57
ブロッカー（待たせている側）:   pid = 52
</code></pre>
<p>つまり、<code>pid = 57</code>のSQLだけを確認しても原因は分かりません。次に確認すべきなのは、ブロッカーの<code>pid = 52</code>の状態です。</p>
<h2>実験4: idle in transactionはロックを保持し得る状態</h2>
<h3>この実験で確認すること</h3>
<p>この実験では、<code>idle in transaction</code>の意味を確認します。<code>idle in transaction</code>は、トランザクションの中にいるが、現在はSQL を実行していない状態です。何もしていないように見えても、トランザクションが終わっていなければ、ロックを保持していることがあります。</p>
<h3>状況の確認</h3>
<p>ここまでの状態では、セッションAが<code>id = 1</code>の行を更新し、まだ<code>COMMIT</code>していません。セッションB は、同じ行を更新しようとして待っています。</p>
<p>セッションCから次のSQLを実行します。実験3のSQLに表示列(<code>xact_start</code>, <code>state_change</code>)を追加しています。</p>
<pre><code>-- Session C
WITH activity AS (
  SELECT
    pid
    , state
    , wait_event_type
    , wait_event
    , pg_blocking_pids(pid) AS blocking_pids
    , xact_start
    , state_change
    , query
  FROM
    pg_stat_activity
  WHERE
    datname = current_database()
    AND pid &lt;&gt; pg_backend_pid()
)
SELECT
  *
FROM
  activity
WHERE
  query LIKE '%UPDATE accounts%'
ORDER BY
  xact_start NULLS LAST;
 pid |        state        | wait_event_type |  wait_event   | blocking_pids |          xact_start           |         state_change          |                           query
-----+---------------------+-----------------+---------------+---------------+-------------------------------+-------------------------------+-----------------------------------------------------------
  52 | idle in transaction | Client          | ClientRead    | {}            | 2026-06-15 05:57:26.539576+00 | 2026-06-15 05:57:26.542127+00 | UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  57 | active              | Lock            | transactionid | {52}          | 2026-06-15 05:57:31.885276+00 | 2026-06-15 05:57:31.886842+00 | UPDATE accounts SET balance = balance - 50 WHERE id = 1;
(2 rows)
</code></pre>
<h3>結果の読み方</h3>
<p><code>pid = 52</code>のセッションは、<code>state = 'idle in transaction'</code>になっています。</p>
<p>これは、次の状態を意味します。</p>
<pre><code>トランザクションはまだ開いている
ただし、現在はSQLを実行していない
</code></pre>
<p>また、この行では<code>wait_event_type = Client</code>、<code>wait_event = ClientRead</code>になっています。これは、PostgreSQL がクライアントから次の命令を待っていることを示します。つまり、セッションAはデータベース内部でロックを待っているのではなく、クライアント側から次の SQL、<code>COMMIT</code>、または<code>ROLLBACK</code>が送られるのを待っている状態です。</p>
<p>この状態でも、セッションAはロックを解放していません。ロックは、トランザクションが<code>COMMIT</code>または <code>ROLLBACK</code>で終わるまで保持されます。そのため、セッションBは待ち続けます。</p>
<h3>なぜ問題になりやすいのか</h3>
<p><code>idle in transaction</code>は、アプリケーションの作りによって長引くことがあります。</p>
<p>たとえば、次のような処理です。</p>
<ul>
<li>トランザクションを開始したあとに、外部APIの応答を待つ</li>
<li>トランザクションを開始したあとに、ユーザー入力を待つ</li>
<li>例外が発生したのに、<code>ROLLBACK</code>しないまま接続を残す</li>
</ul>
<p>これらは、ロックを保持したまま待ち時間を伸ばす原因になります。</p>
<h3>実務で確認する列</h3>
<p>実務で<code>idle in transaction</code>を見つけたときは、すぐにセッションを終了する前に、まず次の列を確認します。</p>
<ul>
<li><code>xact_start</code></li>
<li><code>state_change</code></li>
<li><code>query</code></li>
<li><code>application_name</code></li>
<li><code>client_addr</code></li>
</ul>
<p>特に<br />
<code>xact_start</code><br />
が古い場合は、長時間トランザクションが開いたままになっている可能性があります。ただし、セッションを強制終了すると、アプリケーション側ではエラーになります。注文更新や残高更新の途中であれば、業務処理にも影響します。そのため、まずはどのアプリケーション、どの接続元、どの処理がトランザクションを開いたままにしたのかを特定することが重要です。</p>
<h3>timeout設定について</h3>
<p><code>idle in transaction</code>を長時間残したくない場合は、<code>idle_in_transaction_session_timeout</code> を使う方法もあります。この設定は、トランザクション内で何もしていないセッションを、指定時間後に切断します。ただし、切断された側のアプリケーションにはエラーとして見えます。本番環境で使う場合は、事前に影響を確認する必要があります。</p>
<h3>この実験で分かったこと</h3>
<p><code>idle in transaction</code><br />
は、何もしていない安全な状態とは限りません。トランザクションが終わっていなければ、ロックを保持し続けることがあります。ロック待ちを調べるときは、待っている側だけでなく、待たせている側の<br />
<code>state</code>と<code>xact_start</code>を確認します。</p>
<p>次は、通常のロック待ちとデッドロックの違いを確認します。</p>
<h2>実験5:通常のロック待ちとデッドロックは違う</h2>
<h3>この実験で確認すること</h3>
<p>ここまでの実験では、セッションBがセッションAを一方的に待っていました。この場合、セッションAが<code>COMMIT</code>または <code>ROLLBACK</code>すれば、セッションBは進めます。これは通常のロック待ちです。</p>
<p>一方で、デッドロックは違います。デッドロックは、複数のトランザクションが互いに相手を待ってしまい、どちらも先へ進めなくなる状態です。</p>
<h3>実験前の準備</h3>
<p>セッションAとセッションBの両方で<code>ROLLBACK</code>を実行し、前の実験の状態を終わらせます。</p>
<pre><code>-- Session A / Session B
ROLLBACK;
</code></pre>
<h3>手順1:セッションAがid = 1を更新する</h3>
<pre><code>-- Session A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
</code></pre>
<p>セッションAは、<code>id = 1</code>の行を更新します。まだ<code>COMMIT</code>はしません。</p>
<h3>手順2:セッションBがid = 2を更新する</h3>
<pre><code>-- Session B
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
</code></pre>
<p>セッションBは、<code>id = 2</code>の行を更新します。こちらも、まだ<code>COMMIT</code>はしません。</p>
<p>この時点では、セッションAとBは別々の行を更新しているため、互いに待っていません。</p>
<h3>手順3:セッションAがid = 2を更新しようとする</h3>
<p>次に、セッションAが<code>id = 2</code>の行を更新しようとします。</p>
<pre><code>-- Session A
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
</code></pre>
<p><code>id = 2</code>は、すでにセッションBが更新しています。そのため、セッションAはセッションBのトランザクション終了を待ちます。</p>
<p>この時点では、まだデッドロックではありません。セッションAが、セッションBを一方的に待っているだけです。</p>
<h3>手順4:セッションBがid = 1を更新しようとする</h3>
<p>最後に、セッションBが<code>id = 1</code>の行を更新しようとします。</p>
<pre><code>-- Session B
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
</code></pre>
<p><code>id = 1</code>は、すでにセッションAが更新しています。そのため、セッションBはセッションAのトランザクション終了を待ちます。</p>
<p>これで、次の状態になります。</p>
<pre><code>セッションAは、セッションBを待っている
セッションBは、セッションAを待っている
</code></pre>
<p>これがデッドロックです。</p>
<h3>デッドロック時のエラー</h3>
<p>PostgreSQLはデッドロックを検出すると、どちらか一方のトランザクションを中止します。</p>
<p>エラーの例です。</p>
<pre><code>ERROR:  deadlock detected
DETAIL:  Process 57 waits for ShareLock on transaction 2317; blocked by process 52.
Process 52 waits for ShareLock on transaction 2318; blocked by process 57.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,1) in relation "accounts"
</code></pre>
<p>このエラーが出たあとは、エラーになったトランザクションはそのまま続けられません。次の実験へ進む前に、セッションAとBの両方で <code>ROLLBACK</code>を実行します。</p>
<pre><code>-- Session A / Session B
ROLLBACK;
</code></pre>
<h3>通常のロック待ちとの違い</h3>
<p>通常のロック待ちは、ブロッカー側が終われば解消します。一方、デッドロックでは、互いに相手を待っています。そのままではどちらも進めません。</p>
<p>そのため、PostgreSQL はどちらか一方を中止して、待ち続ける状態を解消します。どちらのトランザクションが中止されるかをアプリケーション側で決め打ちしてはいけません。 デッドロックで失敗した場合は、失敗したSQLだけを再実行するのではなく、トランザクション全体を最初からやり直す必要があります。</p>
<h3>デッドロックを減らす考え方</h3>
<p>複数行を更新する処理では、更新する順序をそろえることが基本です。たとえば、ある処理は<code>id = 1</code>のあとに <code>id = 2</code>を更新し、別の処理は<code>id = 2</code>のあとに<code>id = 1</code>を更新すると、デッドロックが起きやすくなります。</p>
<p>更新順序を決められるなら、常に同じ順序で更新します。たとえば、常に<code>id</code>の小さい順に更新するなどが考えられます。</p>
<h3>lock_timeoutについて</h3>
<p>通常のロック待ちが長すぎる場合の安全装置として、<code>lock_timeout</code>があります。</p>
<pre><code>SET lock_timeout = '3s';
</code></pre>
<p><code>lock_timeout</code>は、ロックを取得できない状態が指定時間を超えたときに、そのSQL をエラーにします。これは、デッドロックを検出する設定ではありません。ロック待ちが長くなりすぎたSQLを失敗させるための設定です。また、<code>statement_timeout</code> との関係にも注意が必要です。<code>statement_timeout</code>は、SQL全体の実行時間に対する制限です。<code>lock_timeout</code><br />
は、ロック取得待ちに対する制限です。<code>statement_timeout</code>が<br />
<code>lock_timeout</code>以下に設定されていると、ロック待ち専用のエラーより先に <code>statement_timeout</code>が発生します。ロック待ちを切り分けたい場合は、両者の値を分けて設定します。</p>
<h3>この実験で分かったこと</h3>
<p>通常のロック待ちは、ブロッカー側が終われば解消します。一方、デッドロックは、互いに相手を待つため、そのままでは解消しません。PostgreSQL はどちらか一方のトランザクションを中止します。アプリケーションでは、デッドロックやロックタイムアウトで失敗した場合に、トランザクション全体をやり直せるようにしておく必要があります。</p>
<p>次は、書き込みではなく、読み取りがどう見えるのかを確認します。</p>
<h2>実験6:通常のSELECTは止まらない</h2>
<h3>この実験で確認すること</h3>
<p>ここまでの実験では、<code>UPDATE</code>同士の待ちを見てきました。では、あるセッションが行を更新してまだ <code>COMMIT</code>していないとき、別のセッションがその行を<code>SELECT</code>するとどうなるのでしょうか。</p>
<p><code>FOR UPDATE</code>などを付けない通常の<code>SELECT</code>は、多くの場合、更新中の行を待たずに結果を返します。これは、PostgreSQL が、読み取り時点で見えてよい行の版を選んで読みます。この仕組みをMVCC (Multi-Version Concurrency Control) と呼びます。日本語では、多版型同時実行制御と呼ばれます。</p>
<p>以降では、「見えてよい行の版を読む仕組み」を簡単に説明します。</p>
<h3>実験前の準備</h3>
<p>セッションAとセッションBの両方で<code>ROLLBACK</code>を実行します。</p>
<pre><code>-- Session A / Session B
ROLLBACK;
</code></pre>
<h3>手順1:セッションAでid = 1を更新する</h3>
<p>まず、更新前の値を確認します。</p>
<pre><code>-- Session A
SELECT id, owner_name, balance FROM accounts WHERE id = 1;
 id | owner_name | balance
----+------------+---------
  1 | alice      |    1000
(1 row)
</code></pre>
<p>セッションAでトランザクションを開始します。</p>
<pre><code>-- Session A
BEGIN;
</code></pre>
<p>次に、<code>id = 1</code>の残高を<code>999</code>に更新します。</p>
<pre><code>-- Session A
UPDATE accounts SET balance = 999 WHERE id = 1;
</code></pre>
<p>ここでも、まだ<code>COMMIT</code>はしません。つまり、セッションAの変更はまだ確定していません。</p>
<h3>手順2:セッションBで通常のSELECTを実行する</h3>
<p>セッションBから、<code>BEGIN</code>でトランザクションを開始して、同じ行を読み取ります。</p>
<pre><code>-- Session B
BEGIN;
SELECT id, owner_name, balance FROM accounts WHERE id = 1;
 id | owner_name | balance
----+------------+---------
  1 | alice      |    1000
(1 row)
</code></pre>
<p>セッションBの<code>SELECT</code>はロック待ちになりません。また、セッションAが更新した<code>balance = 999</code>も見えません。セッションB からは、最後に<code>COMMIT</code>済みの値である<code>1000</code>が見えます。</p>
<h3>なぜ止まらないのか</h3>
<p>セッションAの<code>UPDATE</code>は、まだ<code>COMMIT</code>されていません。もし、セッションBから未コミットの <code>999</code>が見えてしまうと、あとからセッションAが <code>ROLLBACK</code>したときに困ります。存在しなかった変更を読んだことになるからです。そのため、PostgreSQLは通常の <code>SELECT</code>に対して、未コミットの変更を見せません。代わりに、読み取り時点で見えてよい、古い行の版を返します。</p>
<p>PostgreSQLのデフォルト設定では、各SQLを実行するたびに、その時点で <code>COMMIT</code>済みのデータを読みます。この設定を、トランザクション分離レベルの<br />
<code>Read Committed</code><br />
と呼びます。トランザクション分離レベルとは、あるトランザクションから、他のトランザクションの変更をどこまで見せるかを決める設定です。</p>
<h3>SELECT FOR UPDATEは待つ</h3>
<p>ただし、すべての<code>SELECT</code>が待たないわけではありません。<code>SELECT FOR UPDATE</code> は、読み取った行をあとで更新する前提でロックしようとします。</p>
<pre><code>-- Session B
SELECT id, owner_name, balance FROM accounts WHERE id = 1 FOR UPDATE;
</code></pre>
<p>この場合は、セッションAのトランザクション終了を待ちます。通常の <code>SELECT</code>は、見えてよい行の版を読めるため、止まらないことが多いです。一方、<code>SELECT FOR UPDATE</code> は行をロックしようとするため、更新中の行と競合します。</p>
<h3>この実験で分かったこと</h3>
<p>PostgreSQLでは、未コミットの変更は通常の<code>SELECT</code>から見えません。この実験では、通常の <code>SELECT</code>は、更新中の行を待たずに、見えてよい行の版を読みます。一方、<code>SELECT FOR UPDATE</code> は行をロックしようとするため、対象の行が他のトランザクションで更新中の場合は、待つことがあります。</p>
<p>次は、ロック待ちしていた<code>UPDATE</code>が再開したときに、どの条件で更新されるのかを確認します。</p>
<h2>実験7: UPDATEは待ったあとにWHEREを再確認する</h2>
<h3>この実験で確認すること</h3>
<p>この実験では、ロック待ちしていた<code>UPDATE</code>が再開したときの動きを確認します。重要なのは、ロック待ちしていた <code>UPDATE</code>が、ロック待ちの前に見えていた古い行をそのまま更新するわけではない、という点です。</p>
<p>PostgreSQLのデフォルト設定では、ロック待ちが終わったあと、更新後の行に対して<code>WHERE</code>条件をもう一度確認します。</p>
<h3>実験前の準備</h3>
<p>セッションAとセッションBの両方で<code>ROLLBACK</code>を実行します。</p>
<pre><code>-- Session A / Session B
ROLLBACK;
</code></pre>
<h3>手順1:セッションAがid = 1を更新する</h3>
<p>まず、更新前の値を確認します。</p>
<pre><code>-- Session A
SELECT id, owner_name, balance FROM accounts WHERE id = 1;
 id | owner_name | balance
----+------------+---------
  1 | alice      |    1000
(1 row)
</code></pre>
<p>セッションAでトランザクションを開始します。</p>
<pre><code>-- Session A
BEGIN;
</code></pre>
<p>次に、<code>id = 1</code>の残高を<code>1000</code>にします。ここでは、同じ行を更新中の状態にするため、<code>id = 1</code>の残高を <code>1000</code>に更新します。値は変わりませんが、<code>UPDATE</code>は実行されるため、この行はセッションA のトランザクションが終わるまで、他の更新と競合する状態になります。</p>
<pre><code>-- Session A
UPDATE accounts SET balance = 1000 WHERE id = 1;
</code></pre>
<p>まだ<code>COMMIT</code>はしません。</p>
<h3>手順2:セッションBが条件付きUPDATEを実行する</h3>
<p>セッションBでトランザクションを開始します。</p>
<pre><code>-- Session B
BEGIN;
</code></pre>
<p>次に、残高が<code>500</code>以上の場合だけ、<code>50</code>減らす<code>UPDATE</code>を実行します。</p>
<pre><code>-- Session B
UPDATE accounts
SET balance = balance - 50
WHERE id = 1
  AND balance &gt;= 500
RETURNING *;
</code></pre>
<p>この<code>UPDATE</code>は待ちます。 セッションAが同じ<code>id = 1</code>の行を更新して、まだ<code>COMMIT</code>していないからです。</p>
<h3>手順3:セッションAが残高を400にしてCOMMITする</h3>
<p>セッションBが待っている間に、セッションAで同じ行の残高を<code>400</code>にします。</p>
<pre><code>-- Session A
UPDATE accounts SET balance = 400 WHERE id = 1;
</code></pre>
<p>そのあと、セッションAのトランザクションを確定します。</p>
<pre><code>-- Session A
COMMIT;
</code></pre>
<p>これで、セッションBの待ちが解除されます。</p>
<h3>セッションBの結果</h3>
<p>セッションBの<code>UPDATE</code>は再開します。ただし、セッションB は、ロック待ちの前に見えていた行をそのまま更新するわけではありません。セッションAが<code>COMMIT</code>した後の行に対して、<code>WHERE</code> 条件をもう一度確認します。</p>
<p>この時点で、<code>id = 1</code>の<code>balance</code>は<code>400</code>です。セッションBの条件は次のとおりでした。</p>
<pre><code>WHERE id = 1
  AND balance &gt;= 500
</code></pre>
<p><code>balance = 400</code>なので、<code>balance &gt;= 500</code>は成り立ちません。そのため、セッションBの<code>UPDATE</code>は0 行更新になります。</p>
<pre><code> id | owner_name | balance
----+------------+---------
(0 rows)
</code></pre>
<h3>この動きが重要な理由</h3>
<p>アプリケーションで避けたいのは、先に<code>SELECT</code>で値を確認し、その結果だけを前提にして、あとから無条件に <code>UPDATE</code>する書き方です。</p>
<pre><code>BEGIN;
SELECT balance FROM accounts WHERE id = 1;

UPDATE accounts
SET balance = balance - 500
WHERE id = 1;
COMMIT;
</code></pre>
<p>この書き方では、<code>SELECT</code>してから <code>UPDATE</code>するまでの間に、別のトランザクションが同じ行を更新する可能性があります。つまり、アプリケーションが事前に見た残高と、実際に <code>UPDATE</code>する時点の残高が同じとは限りません。</p>
<p>残高不足を防ぎたいなら、更新してよい条件を<code>UPDATE</code>の<code>WHERE</code>に入れます。</p>
<pre><code>BEGIN;
UPDATE accounts
SET balance = balance - 500
WHERE id = 1
  AND balance &gt;= 500;
COMMIT;
</code></pre>
<p>このSQLは、<code>UPDATE</code>する時点の<code>balance</code>が<code>500</code>以上の場合だけ、残高を減らします。1行更新されれば成功です。0 行なら、残高不足として扱えます。</p>
<h3>分離レベルを上げる場合</h3>
<p>トランザクション分離レベルを<code>Repeatable Read</code>や<br />
<code>Serializable</code><br />
に上げる方法もあります。ただし、その場合は競合する更新を検出して、トランザクションがエラーになることがあります。分離レベルを上げる場合は、失敗した SQLだけでなく、トランザクション全体を最初から再試行できる設計が必要です。</p>
<h3>この実験で分かったこと</h3>
<p>ロック待ちしていた<code>UPDATE</code>は、ロック待ちが終わったあとに <code>WHERE</code>条件を再確認します。そのため、並行更新に強い処理にしたい場合は、事前の<code>SELECT</code>結果だけに頼らず、<code>UPDATE</code> の<code>WHERE</code>に更新条件を入れることが重要です。</p>
<h2>実務での切り分け手順</h2>
<p>ここまでの実験で、ロック待ちの見方を段階的に確認しました。実務でも、まず見るところは同じです。</p>
<p><code>UPDATE</code>が返ってこないときは、最初に<code>pg_stat_activity</code>を確認します。ここで見るのは、待っているセッションの <code>state</code>、<code>wait_event_type</code>、<code>wait_event</code>です。<code>wait_event_type</code>が <code>Lock</code>であれば、そのセッションはロック待ちしている可能性が高いです。</p>
<p>次に、<code>pg_blocking_pids()</code>を使って、そのセッションを待たせているPIDを確認します。待っているSQL だけを見ても、原因は分かりません。重要なのは、待っている（ウェイター）側と、待たせている（ブロッカー）側を分けて見ることです。</p>
<p>待たせている（ブロッカー）側のPIDが分かったら、もう一度<br />
<code>pg_stat_activity</code><br />
を確認します。特に、<code>state</code>、<code>xact_start</code>、<code>state_change</code>、<code>application_name</code>、<code>client_addr</code>、<code>query</code> を見ます。<code>state = 'idle in transaction'</code>で、<code>xact_start</code> が古い場合は、トランザクションを開いたまま放置している可能性があります。ただし、すぐにセッションを終了すればよいとは限りません。まず、どのアプリケーション、どの接続元、どの処理がトランザクションを開いたままにしているのかを確認します。</p>
<h2>再発を減らす設計</h2>
<p>ロック待ちそのものは、PostgreSQLの異常ではありません。同じ行を同時に更新しようとすれば、待つことがあります。</p>
<p>問題になるのは、待たせているトランザクションが長く残り、他の処理を止め続けることです。</p>
<p>ここでは、再発を減らすための基本的な考え方を整理します。</p>
<h3>トランザクションを短くする</h3>
<p>更新や明示的なロックを取得したあとは、できるだけ早く<code>COMMIT</code>または<code>ROLLBACK</code>します。トランザクション中に外部API 呼び出し、ユーザー入力待ち、重いファイル処理などを入れると、ロックを持ったまま待ち時間が伸びます。また、例外が発生した場合は、必ず <code>ROLLBACK</code>します。</p>
<h3>更新順序をそろえる</h3>
<p>複数行を更新する処理では、更新順序をそろえます。ある処理は<code>id = 1</code>のあとに<code>id = 2</code>を更新し、別の処理は <code>id = 2</code>のあとに<code>id = 1</code>を更新すると、デッドロックが起きやすくなります。順序を決められるなら、常に同じ順序で更新します。</p>
<h3>lock_timeoutを検討する</h3>
<p>長時間待たせたくない処理では、<code>lock_timeout</code>を検討します。</p>
<pre><code>SET lock_timeout = '3s';
</code></pre>
<p>これは、ロックを取得できない状態が指定時間を超えたら、そのSQL をエラーにする設定です。ただし、エラーになった場合にアプリケーション側でどう扱うかを決めておく必要があります。</p>
<h3>log_lock_waitsを確認する</h3>
<p>本番環境では、<code>log_lock_waits</code>も確認対象になります。<code>log_lock_waits</code>を有効にすると、<code>deadlock_timeout</code> を超えたロック待ちをログに出せます。常に<code>pg_stat_activity</code>を手動で見続けるのではなく、ログから、どのSQL がいつ、どの程度ロック待ちしたのかを後から確認できます。</p>
<h3>アプリケーションで再試行を考える</h3>
<p>デッドロックやロックタイムアウトで失敗した場合、失敗したSQL だけを途中から再実行するのは危険です。基本は、トランザクション全体を最初からやり直します。ただし、無制限に再試行し続けるのではなく、再試行回数には上限を設けます。</p>
<h3>UPDATEのWHEREに条件を入れる</h3>
<p>並行更新で条件が変わる処理では、事前の<code>SELECT</code>結果だけに頼らないようにします。たとえば、残高が足りる場合だけ引き落とすなら、条件を <code>UPDATE</code>の<code>WHERE</code>に入れます。</p>
<pre><code>BEGIN;
UPDATE accounts
SET balance = balance - 500
WHERE id = 1
  AND balance &gt;= 500;
COMMIT;
</code></pre>
<p>以上、<code>UPDATE</code>が返ってこない状況を、同じ行への更新、ロック待ち、待たせているトランザクション、通常の <code>SELECT</code>の見え方に分けて確認しました。</p>
<p>ロック待ちそのものは、PostgreSQL の異常ではありません。同じデータを同時に矛盾なく更新するために必要な動きです。問題になるのは、ロックを持ったトランザクションが長く残り、他の処理を止め続けることです。</p>
<p><code>UPDATE</code>が返ってこないときは、待っているSQLだけを見るのではなく、PostgreSQLの中で何を待っているのかを確認します。そのうえで、<code>pg_blocking_pids()</code> を使い、待たせている相手を探します。</p>
<p>待っている側と、待たせている側を分けて見る。この見方を持っておくと、ロック待ちを原因の一つとして落ち着いて切り分けられます。</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>
		<item>
		<title>pgvectorの紹介</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/pgvector-intro/</link>
		
		<dc:creator><![CDATA[正野 裕大 (Yuta Masano)]]></dc:creator>
		<pubDate>Wed, 10 Jan 2024 00:42:45 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[pgvector]]></category>
		<category><![CDATA[サードパーティーソフトウェア]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=14204</guid>

					<description><![CDATA[pgvector(pgvector/pgvector: Open-source vector similarity search for Postgres)はベクトルデータの類似性検索機能を提供するPostgreSQLの ...]]></description>
										<content:encoded><![CDATA[<p>pgvector(<a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener">pgvector/pgvector: Open-source vector similarity search for Postgres</a>)はベクトルデータの類似性検索機能を提供するPostgreSQLの拡張機能です。データベース内にベクトルデータを格納し、特定のベクトルと最も近い要素を検索できます。近似値の算出はユークリッド距離、内積、コサイン距離による距離計算が提供されており、大規模なデータセットでの検索を高速化するためのインデックス機能も提供されています。レコメンデーションシステムや画像や文章の類似コンテンツ検索などのアプリケーション開発で有用になります。</p>
<p><span id="more-14204"></span></p>
<p><object data="https://gh-card.dev/repos/pgvector/pgvector.svg?link_target=_blank" type="image/svg+xml" width="300" height="150"></object></p>
<h2>pgvectorのインストールとセットアップ</h2>
<p>PostgreSQLにpgvectorをインストールします。OSはRocky Linux release 9.6、PostgreSQLは公式リポジトリからインストールしたバージョン16.1を使います。PostgreSQLを動作させるPATH等の環境変数は設定済みで、すでにPostgreSQLインスタンスは起動済みとします。</p>
<p>pgvectorはPostgreSQLの公式リポジトリ経由でPostgreSQL 16用のpgvector_16をパッケージからインストールします。</p>
<pre># dnf install pgvector_16</pre>
<p>続けて、testデータベースにpgvectorを登録します。</p>
<pre>$ psql -U postgres -d test
psql (16.1)
Type "help" for help.

test=# CREATE EXTENSION vector;
CREATE EXTENSION
test=# \dx
                             List of installed extensions
  Name   | Version |   Schema   |                     Description
---------+---------+------------+------------------------------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
 vector  | 0.5.1   | public     | vector data type and ivfflat and hnsw access methods</pre>
<h2>ベクトル列の作成とベクトルデータの挿入</h2>
<p>ベクトルデータを格納する列を作成します。pgvectorを登録すると、vector型が使えます。</p>
<pre>test=# CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));</pre>
<p>vector(3)は3次元のベクトル、つまり、ベクトル空間を形成する基底ベクトルの個数が3つであることを意味します。ここでは3次元に指定していますが、最大で16000次元まで指定できます。<br />
異なる次元のベクトルを同じ列に保存したい場合は（vector(3)ではなく）vectorを使用できます。ただしその場合、<a href="https://www.postgresql.org/docs/16/indexes-expressional.html" target="_blank" rel="noopener">式に対するインデックス</a>や<a href="https://www.postgresql.org/docs/16/indexes-partial.html" target="_blank" rel="noopener">部分インデックス</a>を使って同じ次元数の行にしかインデックスを作成できません。</p>
<p>また、vector型の各要素はPostgreSQLのreal型と同様の単精度浮動小数点数で格納されます。より高精度のベクトルデータ格納したい場合は、vector型ではなく、double precision[]やnumeric[]型が使えます。ただし、高精度の値を格納はできますが、pgvectorが提供する機能を使う場合はvector型に<a href="https://www.postgresql.org/docs/16/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS" target="_blank" rel="noopener">キャスト</a>する必要があり、格納したデータの検索時に精度値が喪失します。</p>
<pre>test=# CREATE TABLE items2 (id bigserial PRIMARY KEY, embedding double precision[]);</pre>
<p>データがvector型に変換可能であり、かつ、想定の次元数であることを保証するために<a href="https://www.postgresql.org/docs/16/ddl-constraints.html" target="_blank" rel="noopener">チェック制約</a>を追加してもよいでしょう。</p>
<pre>test=# ALTER TABLE items2 ADD CHECK (vector_dims(embedding::vector) = 3);</pre>
<p>次に、作成した列にベクトルデータを挿入します。vector型は以下のようにARRAYキーワードを使った配列や、[要素1,要素2,&#8230;]といった典型的な配列を表す文字列定数を解釈できます。<br />
なお、vector型ではなく、double precision[]やnumeric[]型を定義した場合は、ベクトルデータをPostgreSQLの配列リテラル定数で格納できます。</p>
<pre>test=# INSERT INTO items (embedding)
SELECT
  ARRAY[
  random() * 1000, -- 最初の次元のランダム値
  random() * 1000, -- 二番目の次元のランダム値
  random() * 1000  -- 三番目の次元のランダム値
]
FROM
  generate_series(1 , 10000);</pre>
<p>itemsテーブルに、乱数を使った3次元のベクトルデータを10000行挿入しました。</p>
<pre>test=# SELECT * FROM items LIMIT 5;
 id |            embedding
----+---------------------------------
  1 | [747.1946,30.812382,226.11743]
  2 | [389.10852,188.70128,151.08716]
  3 | [176.6698,954.8121,233.49654]
  4 | [295.3235,539.09283,721.9699]
  5 | [786.6494,895.81323,160.88304]
(5 rows)</pre>
<h2>最近傍検索の実行</h2>
<p>最近傍検索を実行して、特定のベクトルに最も近いデータのトップ3を検索します。</p>
<pre>test=# SELECT * FROM items ORDER BY embedding &lt;#&gt; '[10.0, 2, 0.3]' LIMIT 3;
  id  |            embedding
------+---------------------------------
 7945 | [998.16565,992.58215,926.16675]
 4633 | [996.8208,985.77405,989.8139]
 3345 | [997.30383,966.3805,949.19257]
(3 rows)</pre>
<p>&lt;#&gt;演算子はベクトル間の負の内積を表す演算子です。似ているベクトルデータほど小さい数値となります。これは&lt;#&gt;演算子が昇順のインデックススキャンのみに対応しているためです。昇順ソートで最近似のベクトルデータをインデックスを効かせて取り出せます。</p>
<h2>提供される演算子と関数</h2>
<p>pgvectorは2つのベクトルの乗算、減算、乗算、ユークリッド距離、内積、コサイン距離等の演算子が用意されています。</p>
<table>
<thead>
<tr>
<th style="text-align: center;">演算子</th>
<th style="text-align: center;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;">+</td>
<td style="text-align: center;">2つのベクトルの各要素同士を加算します</td>
</tr>
<tr>
<td style="text-align: center;">&#8211;</td>
<td style="text-align: center;">2つのベクトルの各要素同士を減算します</td>
</tr>
<tr>
<td style="text-align: center;">*</td>
<td style="text-align: center;">2つのベクトルの各要素同士を乗算します</td>
</tr>
<tr>
<td style="text-align: center;">&lt;-&gt;</td>
<td style="text-align: center;">2つのベクトルのユークリッド距離を計測します</td>
</tr>
<tr>
<td style="text-align: center;">&lt;#&gt;</td>
<td style="text-align: center;">2つのベクトルの内積に-1を乗算します</td>
</tr>
<tr>
<td style="text-align: center;">&lt;=&gt;</td>
<td style="text-align: center;">2つのベクトルのコサイン距離を計測します</td>
</tr>
</tbody>
</table>
<p>同様の機能を持った関数もあります。</p>
<table>
<thead>
<tr>
<th style="text-align: center;">関数</th>
<th style="text-align: center;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;">cosine_distance(vector, vector) → double precision</td>
<td style="text-align: center;">コサイン距離</td>
</tr>
<tr>
<td style="text-align: center;">inner_product(vector, vector) → double precision</td>
<td style="text-align: center;">内積</td>
</tr>
<tr>
<td style="text-align: center;">l2_distance(vector, vector) → double precision</td>
<td style="text-align: center;">ユークリッド距離</td>
</tr>
<tr>
<td style="text-align: center;">l1_distance(vector, vector) → double precision</td>
<td style="text-align: center;">マンハッタン距離</td>
</tr>
<tr>
<td style="text-align: center;">vector_dims(vector) → integer</td>
<td style="text-align: center;">次元数</td>
</tr>
<tr>
<td style="text-align: center;">vector_norm(vector) → double precision</td>
<td style="text-align: center;">ユークリッドノルム</td>
</tr>
</tbody>
</table>
<p>また、avg(vector) および sum(vector) でベクトルデータの平均と合計を算出する集約関数もあります。</p>
<h2>ベクトルデータのインデックス</h2>
<p>デフォルトのpgvectorは最近傍検索を行うので、リコール率（近傍アイテムを取りこぼさない確率）は100%ですが、線形探索のために速度が問題になる場合があります。<br />
そのため、pgvectorには近似最近傍検索を用いた、IVFFlatとHNSWの2種類のインデックスが追加できます。ただし、正確な最近傍検索でないので高速にデータを取得できますが、実行時間・メモリ・精度のトレードオフが発生します。</p>
<table>
<thead>
<tr>
<th style="text-align: center;">インデックス</th>
<th style="text-align: center;">説明</th>
<th style="text-align: center;">インデックス構築時間</th>
<th style="text-align: center;">メモリ使用量</th>
<th style="text-align: center;">クエリのパフォーマンス</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;">IVFFlat</td>
<td style="text-align: center;">ベクトルをリストに分割してクエリベクトルに最も近いリストのサブセットを検索する</td>
<td style="text-align: center;">短い</td>
<td style="text-align: center;">少ない</td>
<td style="text-align: center;">速度とリコールのトレードオフが大きい</td>
</tr>
<tr>
<td style="text-align: center;">HNSW</td>
<td style="text-align: center;">最下層に全てのベクトル点が含まれ上の層になるほどベクトル点が少なくなる層状のグラフ構造で上層から下層にアイテムを辿っていく</td>
<td style="text-align: center;">長い</td>
<td style="text-align: center;">多い</td>
<td style="text-align: center;">速度とリコールのトレードオフが少ない</td>
</tr>
</tbody>
</table>
<p>IVFFlatはHNSWよりもインデックス構築時間は短く、メモリ使用量は少ないですが、ベクトルデータがある程度テーブルに格納されていないとインデックスが構築できません。また、速度とリコールのトレードオフが大きく、速度を重視するとリコールが下がりやすくなり、データの取りこぼしが発生しやすくなります。<br />
一方、HNSWはIVFFlatよりもインデックス構築時間は長く、メモリ使用量は多くなりますが、テーブルにトレーニングデータを格納しておく必要はありません。また、速度とリコールのトレードオフが少なく、速度を重視してもリコール率の低下が見られず、両者のバランスが取れています。</p>
<p>また、2000次元以上のベクトルをインデックス化することはpgvector 0.5.1時点ではできません。ベクトルデータを次元削減して対応する必要があります。</p>
<p>ここではHNSWインデックスを構築します。</p>
<h3>HNSWインデックス</h3>
<p>インデックスの構築は<a href="https://www.postgresql.org/docs/16/sql-createindex.html" target="_blank" rel="noopener">CREATE INDEX文</a>で行います。<br />
利用できる演算子クラスは以下です。</p>
<p>演算子クラス説明vector_cosine_opsコサイン距離vector_ip_ops内積vector_l2_opsユークリッド距離</p>
<p>たとえば、以下でitemsテーブルのembedding列にコサイン距離に基づくHNSWインデックスを構築します。</p>
<pre>test=# CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);</pre>
<h3>HNSWインデックのスオプション</h3>
<p>CREATE INDEX文にWITH句を加えてHNSWインデクスの<a href="https://www.postgresql.org/docs/16/sql-createindex.html#SQL-CREATEINDEX-STORAGE-PARAMETERS" target="_blank" rel="noopener">インデックスパラメータ</a>を指定できます。</p>
<p>パラメータ説明m層ごとの最大接続数（デフォルトは16）ef_constructionグラフ構築用の動的候補リストのサイズ（デフォルトは64）</p>
<pre>test=# CREATE INDEX ON items USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);</pre>
<p>m、ef_constructionの値が高いほど、インデックスの構築時間、挿入速度を犠牲にしてリコールが向上します。また、ef_constructionはm*2未満だとエラーとなります。</p>
<h3>HNSWインデックのクエリオプション</h3>
<p><a href="https://www.postgresql.org/docs/16/sql-set.html" target="_blank" rel="noopener">SET文</a>で検索用の動的候補リストのサイズを指定します。デフォルトは40です。</p>
<pre>SET hnsw.ef_search = 100;</pre>
<p>高い値はリコールを向上させますが、速度を犠牲にします。</p>
<p>トランザクション内でSET LOCALを使用して、単一のクエリに設定します。</p>
<pre>test=# BEGIN;
test=# SET LOCAL hnsw.ef_search = 100;
test=# SELECT ...
test=# COMMIT;</pre>
<h3>HNSWインデックのス作成進行状況</h3>
<p>PostgreSQL 12以上であれば<a href="https://www.postgresql.org/docs/16/progress-reporting.html#CREATE-INDEX-PROGRESS-REPORTING" target="_blank" rel="noopener">インデックス作成の進行状況</a>を、たとえば以下のSQL文で、インデックスを作成中のバックエンド毎に確認できます。</p>
<pre>test=# SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;</pre>
<p>HNSWインデックスの場合、pg_stat_progress_create_indexのphase列はinitializing（初期化中）またはloading tuples（タプルの読み込み中）が表示されます。</p>
<h2>終わりに</h2>
<p>pgvectorは、PostgreSQLの拡張機能としてベクトルデータの効率的な管理と類似性検索を可能にします。この記事では、pgvectorのインストールから設定、ベクトルデータの操作方法、最近傍検索の実行、さらにはインデックスの構築と最適化について解説しました。<br />
pgvectorは近年目覚ましい技術発展を遂げている、大規模言語モデルによる文書生成にも利用可能です。大量のテキストデータに含まれるパターンや構造をベクトルデータとして抽出し、それを効率的に検索・分析することで文書の自動生成において重要な役割を果たします。<br />
データベース技術やAI分野に関心がある方は、ぜひpgvectorを使ってみてください</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>
		<item>
		<title>Barman（PostgreSQL PITR 補助ツール）</title>
		<link>https://www.sraoss.co.jp/tech-blog/pgsql/barman/</link>
		
		<dc:creator><![CDATA[正野 裕大 (Yuta Masano)]]></dc:creator>
		<pubDate>Thu, 10 Jan 2019 00:00:53 +0000</pubDate>
				<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Barman]]></category>
		<category><![CDATA[PITR]]></category>
		<category><![CDATA[サードパーティーソフトウェア]]></category>
		<guid isPermaLink="false">https://www.sraoss.co.jp/tech-blog/?p=857</guid>

					<description><![CDATA[Barman とは PostgreSQL のPITR（ポイントインタイムリカバリ）を簡単に管理してくれるツールです。]]></description>
										<content:encoded><![CDATA[<p>Barman とは PostgreSQL のPITR（ポイントインタイムリカバリ）を簡単に管理してくれるツールです。</p>
<p>PITRとはバックアップ／リストア方式の一つで、過去の指定した時点の状態に復元することが可能です。PITRは、データベースクラスタの物理コピー（ベースバックアップと呼びます）を取得し、以降のデータ変更が書き出される WALファイルを継続的に保存する（保存したものをWALアーカイブやアーカイブログと呼びます）ことで実現されます。現在稼働しているデータベースクラスタを障害やオペレーションミスから復旧したい場合には、保存していたベースバックアップにWALアーカイブを適用することで、ベースバックアップ取得時点からWALアーカイブが適用できる限りの任意の時点に復元することができます。</p>
<p><span id="more-857"></span></p>
<p>PITR は PostgreSQL のバックアップ方式としてよく用いられる手法ですが、DB 管理者が考慮すべきことや操作手順が増え、運用管理の手間が増えます。例えば、PITR の運用はベースバックアップの世代管理や、溜まり続ける古いアーカイブログファイルの削除は PostgreSQL の本体機能として備わっておらず、DB 管理者がそのような処理を行うスクリプトを用意しなければなりません。バックアップからのリカバリ作業も DB 管理者が手動で煩雑な手順を実施する必要があります。</p>
<p>Barman を利用すると、PITR に関する種々の操作を Barman のコマンド経由で簡単にできるようになります。また、Barman は複数のデータベースクラスタの PITR を一元管理できます。逆に言えば、PITR の世代管理やアーカイブログのパージ、リカバリ操作を自動で行えるようなスクリプトを既に自前で作成し運用している場合や、管理するデータベースクラスタが多くない場合は、Barman に利点を見出すことは少ないでしょう。</p>
<p>Barman 主な特徴は以下の通りです。</p>
<ul>
<li>PostgreSQL の PITR（ベースバックアップと WAL アーカイビング）を基にしている。</li>
<li>ベースバックアップは ssh + rsync や pg_basebackup が使える。</li>
<li>WAL アーカイビングは archive_command やストリーミングレプリケーションプロトコルが使える。</li>
<li>フルバックアップのインクリメンタルバックアップができる。</li>
<li>バックアップ・WAL の取得前後にフックスクリプトを呼び出せる。</li>
<li>ネットワーク制限・データ転送の圧縮・WAL の圧縮・並行ジョブの実施によってワークロードを調整できる。</li>
<li>複数のデータベースクラスタのバックアップを Barman でまとめて管理できる。</li>
<li>世代管理などのバックアップポリシーを規定できる。</li>
</ul>
<p>本文書では、Barman のインストールと基本的な使い方の解説をします。また、執筆時点で最新のBarmanバージョン 2.5 を基準に記載しています。</p>
<h2>Barman のシステム要求</h2>
<p>Barman 導入のための要件はドキュメントによると以下の通りです。</p>
<pre>対応 OS: Linux/Unix
使用言語: Python 2.6 or 2.7
Python modules:
argcomplete
argh &gt;= 0.21.2 &lt;= 0.26.2
argparse (Python 2.6 only)
psycopg2 &gt;= 2.4.2
python-dateutil &lt;&gt; 2.0
setuptools
対応 PostgreSQL: 8.3 以上
ただし、全ての機能を使いたい場合は 9.5.5 以上
オプション: rsync &gt;= 3.0.4 (optional for PostgreSQL &gt;= 9.2)
</pre>
<p>Barman は Windows には非対応ですが、Docker を通して Windows 上で動作させることは可能です。</p>
<h2>Barman のインストールとセットアップ</h2>
<p>この記事では 2 台の仮想マシン barman と pgsql01 を用い、barman サーバに Barman をインストールし、pgsql01 サーバには PostgreSQL 10 がセットアップ済みであるものとします。</p>
<p>それでは Barman を yumを使ってインストールいきます。</p>
<p>Barman の yumインストールは、EPEL リポジトリと PGDGリポジトリ（PostgreSQL の公式 yumリポジトリ）が登録されている必要があります。PGDGリポジトリはバックアップ対象の PostgreSQL と同一メジャーバージョンのリポジトリを選択するとよいです。異なるメジャーバージョンの PostgreSQL をバックアップ対象とする場合は、最も新しいメジャーバージョンの PGDG リポジトリを選択するとよいです。Barman は PGDGリポジトリから PostgreSQL の共有ライブラリをインストールしますが、共有ライブラリは後方互換性が保たれているからです。</p>
<pre>（PGDGリポジトリのインストール）
[root@barman ~]# yum --assumeyes install epel-release https://download.postgresql.org/pub/repos/yum/10/redhat/rhel-7-x86_64/pgdg-centos10-10-2.noarch.rpm
</pre>
<p>その後、Barman と PostgreSQL をインストールします。PostgreSQL（のクライアントアプリケーション）は Barman のバックアップ・リカバリ処理時に必要となります。</p>
<pre>（barmanパッケージのインストール）
[root@barman ~]# yum --assumeyes install barman postgresql10
</pre>
<p>インストールが完了すると、barman ユーザと barman グループが自動作成されます。barman ユーザのホームディレクトリは /var/lib/barman になります。しかし、barman ユーザを利用すると、バックアップファイルやリカバリ先の所有者権限が barman ユーザとなってしまいます。ここでは、PostgreSQL に合わせて postgresユーザを利用していきます。</p>
<pre>（barmanユーザが作られていることを確認する）
[root@barman ~]# id barman
uid=997(barman) gid=995(barman) groups=995(barman)
[root@barman ~]# grep barm /etc/passwd
barman:x:997:995:Backup and Recovery Manager for PostgreSQL:/var/lib/barman:/bin/bash
</pre>
<p>まずは Barman 自体と、管理対象のサーバ情報を設定します。設定ファイルは INI 形式です。Barman 自体のグローバル設定は /etc/barman.conf で行います。[barman] セクション以下に各パラメータを設定することになります。Barman 実行ユーザを postgres とし、Barman のホームディレクトリを postgres ユーザのホームディレクトリとする以外は、デフォルトのままとしておきます。</p>
<pre>（barmanのグローバル設定を変更する）
[root@barman ~]# vi /etc/barman.conf
barman_user = postgres
barman_home = /var/lib/pgsql
</pre>
<p>Barman の管理対象となる PostgreSQL サーバの情報は、/etc/barman.d 以下にサーバ単位でファイルを作成します。ファイル名は任意で構いませんが、PostgreSQL サーバ名と合わせておくと識別しやすいです。 ファイルの内容は、一意なセクションが一つのバックアップ設定となります。たとえば、以下では、pgsql01 サーバに対して、[ssh-pgsql01] セクションと [streaming-pgsqo01] セクションという 2 つのバックアップ設定を設定しています。Barman の公式ドキュメントではこのセクションにサーバ名を指定し、barman コマンドの &lt; server_name &gt; 引数で使うことが記載されていますが、とくにセクションとサーバ名を合わせる必要はありません。あくまで、一つのセクションが一つのバックアップ設定です。</p>
<pre>（barman個別設定を追加する）
[root@barman ~]# cat &lt;&lt; EOT &gt; /etc/barman.d/pgsql01.conf
&gt; [ssh-pgsql01]
&gt; ssh_command = ssh -q postgres@pgsql01
&gt; conninfo = host=pgsql01 user=postgres dbname=postgres
&gt; backup_method = rsync ; ベースバックアップは rsync で行なう
&gt; archiver = on ; archive_command による WAL アーカイビング
&gt;
&gt; [streaming-pgsql01]
&gt; conninfo = host=pgsql01 user=postgres dbname=postgres port=5433
&gt; backup_method = postgres ; ベースバックアップは pg_basebackup で行なう
&gt; streaming_archiver = on ; ストリーミングレプリケーションプロトコルによる WAL アーカイビング
&gt; slot_name = barman ; レプリケーションスロットを使う（streaming_archiver = on の場合に推奨）
&gt; EOT
</pre>
<p>その他のパラメータについては man 5 barman コマンドで調べることができます。</p>
<p>/etc/barman.d 以下にテンプレートファイルが用意されているので、それを流用してもよいです。なお、barman 実行ユーザが $HOME/.barman.conf を所有していると、そちらが優先されます。</p>
<h3>パスワード無しの SSH 接続設定</h3>
<p>ベースバックアップの取得に SSH と rsync を用いる場合は Barman 実行ユーザと PostgreSQL 実行ユーザが両方向にパスワード無しで SSH 接続できるようにしておく必要があります。以下のように SSH 鍵の作成と、公開鍵の配布を行います。事前に（セキュリティ上の制約があるならば、一時的に） barman 実行ユーザと PostgreSQL 実行ユーザにパスワードを設定し、パスワード認証で SSH 接続ができるようにしておく必要があります。</p>
<pre>（パスワード入力無しでssh接続できるようにする）
[postgres@barman ~]$ ssh-keygen -t rsa
[postgres@barman ~]$ ssh-copy-id postgres@pgsql01
/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/var/lib/pgsql/.ssh/id_rsa.pub"
/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
Warning: Permanently added 'pgsql01,192.168.1.12' (ECDSA) to the list of known hosts.
postgres@pgsql01's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'postgres@pgsql01'"
and check to make sure that only the key(s) you wanted were added.
</pre>
<pre>[postgres@pgsql01 ~]$ ssh-keygen -t rsa
[postgres@pgsql01 ~]$ ssh-copy-id postgres@barman
/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/var/lib/pgsql/.ssh/id_rsa.pub"
/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
Warning: Permanently added 'barman,192.168.1.11' (ECDSA) to the list of known hosts.
postgres@barman's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'postgres@barman'"
and check to make sure that only the key(s) you wanted were added.
</pre>
<p>ここまでで barman のインストールと設定ができました。</p>
<h2>archive_command による WALアーカイブと SSH + rsync によるベースバックアップ</h2>
<p>それでは /etc/barman.d/pgsql01.conf の [ssh-pgsql01] セクションで定義したバックアップを試していきましょう。</p>
<p>barman の check サブコマンドで指定のバックアップ設定が正しいかどうか確認します。また、これによってバックアップに必要なディレクトリが自動作成されます。この操作は新しいバックアップ設定を追加する都度、必須です。WAL archive と PostgreSQL 以外は全て OK となることを確認してください。なお、出力結果に「ssh output clean: FAILED (the configured ssh_command must not add anything to the remote command output)」た含まれる場合は、当該のバックアップセクションの ssh_command パラメータに設定した ssh コマンドに -q オプションを加えると、出力を抑制できます。</p>
<pre>（barmanバックアップ設定が正しいか確認する）
[postgres@barman ~]$ barman check ssh-pgsql01
Server ssh-pgsql01:
WAL archive: FAILED (please make sure WAL shipping is setup)
PostgreSQL: FAILED
directories: OK
retention policy settings: OK
backup maximum age: OK (no last_backup_maximum_age provided)
compression settings: OK
failed backups: OK (there are 0 failed backups)
minimum redundancy requirements: OK (have 0 backups, expected at least 0)
ssh: OK (PostgreSQL server)
not in recovery: OK
archiver errors: OK
</pre>
<p>続けて PostgreSQLサーバで WALアーカイビングが行われるように postgresql.conf を設定します。</p>
<pre>（postgresql.confファイル）
#wal_level = replica
archive_mode = on
archive_command = 'rsync -a %p postgres@barman:/var/lib/pgsql/ssh-pgsql01/incoming/%f'
</pre>
<p>なお、archive_command に指定するアーカイブWAL の出力先は barmanコマンドで確認できます。サブコマンド show-server の引数に確認したいバックアップ設定セクション名を指定すると、設定地を確認できます。</p>
<pre>（ssh-pgsql01 の incoming_wals_directory設定を確認する）
[postgres@barman ~]$ barman show-server ssh-pgsql01 | grep incoming_wals_directory
incoming_wals_directory: /var/lib/pgsql/ssh-pgsql01/incoming
</pre>
<p>また、Barman と PostgreSQL 間のクライアント認証を trust接続にしておきます。これは本記事の試験用の設定であってパスワード認証にすることもできます。</p>
<pre>host all all samenet trust
host replication all samenet trust
</pre>
<p>設定が完了したら PostgreSQL を起動します。</p>
<pre>[postgres@pgsql01 ~]$ pg_ctl -w start
</pre>
<p>pgbenchコマンドでデータを生成して barman サーバに WALアーカイビングが行われることを確認します。合わせて、Barman がテーブルスペースもバックアップできることを確認するため、テーブルスペースも作成しておきます。</p>
<pre>（テーブルスペース作成し、pgbenchでデータ生成する）
[postgres@pgsql01 ~]$ mkdir /tmp/ssh_tbls
[postgres@pgsql01 ~]$ psql &lt;&lt; EOQ
&gt; CREATE TABLESPACE ssh_tbls LOCATION '/tmp/ssh_tbls';
&gt; CREATE DATABASE bench TABLESPACE ssh_tbls;
&gt; EOQ
[postgres@pgsql01 ~]$ pgbench -i -s 10 bench

#### #### #### ####

[postgres@barman ~]$ find ssh-pgsql01 -name ???????????????????????? -ls
101300493 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:06 ssh-pgsql01/incoming/000000010000000000000001
101300494 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000002
101300495 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000003
101300496 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000004
101300497 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000005
101300498 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000006
101300499 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000007
101300500 16384 -rw------- 1 postgres postgres 16777216 Oct 25 11:08 ssh-pgsql01/incoming/000000010000000000000008
</pre>
<p>barmanサーバに WAL がアーカイブされています。PostgreSQL 側で設定したアーカイブ先は /var/lib/pgsql/ssh-pgsql01/incoming ですが、自動で /var/lib/pgsql/ssh-pgsql01/wals 以下にタイムライン ID + xlogID というディレクトリ毎に分けて格納されるようです。</p>
<p>barman からオンラインでベースバックアップを取得します。動作検証のため、バックアップを取るときに PostgreSQLサーバでは pgbenchコマンドを適当に 60 秒ほど実行させてトランザクションが発生している状態にしておきます。</p>
<pre>[postgres@pgsql01 ~]$ pgbench -T 60 bench

#### #### #### ####

[postgres@barman ~]$ barman backup --immediate-checkpoint ssh-pgsql01
Starting backup using rsync-exclusive method for server ssh-pgsql01 in /var/lib/pgsql/ssh-pgsql01/base/20181025T111311
Backup start at LSN: 0/A000070 (00000001000000000000000A, 00000070)
This is the first backup for server ssh-pgsql01
WAL segments preceding the current backup have been found:
000000010000000000000001 from server ssh-pgsql01 has been removed
000000010000000000000002 from server ssh-pgsql01 has been removed
000000010000000000000003 from server ssh-pgsql01 has been removed
000000010000000000000004 from server ssh-pgsql01 has been removed
000000010000000000000005 from server ssh-pgsql01 has been removed
000000010000000000000006 from server ssh-pgsql01 has been removed
000000010000000000000007 from server ssh-pgsql01 has been removed
000000010000000000000008 from server ssh-pgsql01 has been removed
000000010000000000000009 from server ssh-pgsql01 has been removed
Starting backup copy via rsync/SSH for 20181025T111311
Copy done (time: 5 seconds)
This is the first backup for server ssh-pgsql01
Asking PostgreSQL server to finalize the backup.
Backup size: 193.0 MiB
Backup end at LSN: 0/1527D560 (000000010000000000000015, 0027D560)
Backup completed (start time: 2018-10-25 11:14:59.514997, elapsed time: 7 seconds)
Processing xlog segments from file archival for ssh-pgsql01
00000001000000000000000A.00000070.backup
000000010000000000000015
</pre>
<p>出力結果から、rsync と SSH によってベースバックアップが /var/lib/pgsql/ssh-pgsql01/base/20181025T111311 に取得され、不要となったアーカイブログが自動で削除されていることが読み取れます。また、テーブルスペースも格納されています。</p>
<pre>（テーブルスペースの確認）
[postgres@pgsql01 ~]$ oid2name -s
All tablespaces:
Oid Tablespace Name
------------------------
1663 pg_default
1664 pg_global
16384 ssh_tbls

#### #### #### ####

[postgres@barman ~]$ ls ssh-pgsql01/base/20181025T111311/
16384 backup.info data
</pre>
<p>barman コマンドからも list-backup サブコマンドにバックアップ設定を渡すと指定の設定で取得したバックアップ情報を確認することができます。バックアップ情報のうち、「20181025T111311」といったタイムスタンプは barman の各種コマンドで指定する &lt; backup_id &gt; で使われます。</p>
<pre>（list-backup サブコマンドによる確認）
[postgres@barman ~]$ barman list-backup ssh-pgsql01
ssh-pgsql01 20181025T111311 - Thu Oct 25 11:14:00 2018 - Size: 385.0 MiB - WAL Size: 0 B (tablespaces: ssh_tbls:/tmp/ssh_tbls)
[postgres@barman ~]$ barman list-backup --minimal ssh-pgsql01
20181025T111311
</pre>
<h2>パラレルジョブによるリカバリ</h2>
<p>それでは取得したバックアップをリカバリしてみます。ここでは並列処理でのリカバリを試すことにします。リカバリ先は pgsql01 サーバですが、既存のデータベースクラスタはそのままにしておき、異なるデータベースクラスタのパスとテーブルスペースのパスを使うようにリカバリします。</p>
<p><code>--remote-ssh-command</code> オプションを指定しないと barmanコマンドを実行したサーバでリカバリがされます。<code>--jobs</code> オプションでリカバリ時の並列度を指定することができます。並列度は Barman の設定ファイルでも指定可能です。なお、並列処理が有効となるのはバックアップ方法（設定ファイルの backup_method）に rsync を指定した場合のみです。</p>
<pre>[postgres@barman ~]$ barman recover --jobs 4 --remote-ssh-command 'ssh postgres@pgsql01' --tablespace ssh_tbls:/tmp/ssh_tbls_recover ssh-pgsql01 20181025T111311 /var/lib/pgsql/10/data_recover
{{ 前略 }}

IMPORTANT
These settings have been modified to prevent data losses

postgresql.conf line 218: archive_command = false

Recovery completed (start time: 2018-10-25 14:11:10.084191, elapsed time: 17 seconds)

Your PostgreSQL server has been successfully prepared for recovery!
</pre>
<p>pgsql01 サーバにデータベースクラスタが配置されており、テーブルスペースのリンボリックリンクも正しく修正されていることが確認できます。</p>
<pre>[postgres@pgsql01 ~]$ ls -l /var/lib/pgsql/10/data_recover/base/
total 36
drwx------. 2 postgres postgres 8192 Oct 25 11:06 1
drwx------. 2 postgres postgres 8192 Oct 25 11:06 12952
drwx------. 2 postgres postgres 8192 Oct 25 11:07 12953
[postgres@pgsql01 ~]$ ls -l /var/lib/pgsql/10/data_recover/pg_tblspc/
total 0
lrwxrwxrwx. 1 postgres postgres 21 Oct 25 14:10 16384 -&gt; /tmp/ssh_tbls_recover
</pre>
<h2>ストリーミングレプリケーションプロトコルによる WALアーカイブとベースバックアップ</h2>
<p>今度はストリーミングレプリケーションプロトコルを使った WALアーカイブとベースバックアップを試します。<br />
/etc/barman.d/pgsql01.conf の [streaming-pgsql01] セクションで定義したバックアップを使用します。<br />
なお、この方法は、barman サーバ側に PostgreSQL コマンドの pg_receivewal と pg_basebackup が必要です。<br />
事前に Barman 実行ユーザが両コマンドを使えるようにしておく必要があります。</p>
<p>新しいサーバ設定を実行する場合は、check サブコマンドを実行しておきます。</p>
<pre>[postgres@barman ~]$ barman check streaming-pgsql01
</pre>
<p>先程リカバリしたデータベースクラスタ (/var/lib/pgsql/10/data_recover) のポート番号を変更し、アーカイブロギングをオフにして起動します。</p>
<pre>（postgresql.conf設定）
port = 5433
#wal_level = replica
#archive_mode = on
#max_wal_senders = 10
#max_replication_slots = 10
</pre>
<p>PostgreSQL を起動します。</p>
<pre>[postgres@pgsql01 ~]$ pg_ctl -w start -D 10/data_recover
</pre>
<p>ストリーミングレプリケーションプロトコルを使って WAL をアーカイビングを開始できるようにします。まず WAL の送信元に barman 向けのレプリケーションスロットを作成し、WAL アーカイブにそれを用いるようにオプション指定します。これはオンライン WAL がアーカイブされる前にそれが再利用または削除されるのを防ぐためです。</p>
<pre>[postgres@barman ~]$ barman receive-wal --create-slot streaming-pgsql01
Creating physical replication slot 'barman' on server 'streaming-pgsql01'
Replication slot 'barman' created
</pre>
<p>レプリケーションスロットが作成されたら WAL の受信を開始します。このコマンドはフォアグランドプロセスなので、バックグラウンドに回しておきます。</p>
<pre>（barmanによるWAL受信）
[postgres@barman ~]$ barman receive-wal streaming-pgsql01 &amp;&gt;/dev/null &amp;
[1] 26697
</pre>
<p>PostgreSQL サーバで pgbench コマンドを適当に 30 秒ほど実行させてトランザクション進めます。</p>
<pre>[postgres@pgsql01 ~]$ pgbench -T 30 -p 5433 bench
</pre>
<p>WAL がアーカイビングされています。</p>
<pre>[postgres@barman ~]$ find streaming-pgsql01 -name ???????????????????????? -ls
802583 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/000000010000000000000016
433861 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/000000010000000000000017
433874 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/000000010000000000000018
433875 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/000000010000000000000019
433876 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/00000001000000000000001A
433877 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/00000001000000000000001B
433878 16384 -rw------- 1 postgres postgres 16777216 Oct 25 15:54 streaming-pgsql01/streaming/00000001000000000000001C
</pre>
<p>barman からベースバックアップを取得します。ストリーミングレプリケーションプロトコルを用いた WAL アーカイビングの場合、予め Barman のメンテナンスコマンドを実行していなければいけません。</p>
<pre>[postgres@barman ~]$ barman archive-wal streaming-pgsql01
Processing xlog segments from streaming for streaming-pgsql01
00000001000000000000002B
00000001000000000000002C
00000001000000000000002D
00000001000000000000002E
00000001000000000000002F
000000010000000000000030
000000010000000000000031
</pre>
<p>これによって Barman サーバで受信した WAL アーカイブが、今度は Barman の wal ディレクトリにアーカイブされます。なお、この操作は barman cron サブコマンドでも可能です。cron サブコマンドの場合は Barman が管理している全てのバックアップを対象に WAL アーカイブのアーカイブ処理が行われます。加えて、追加の設定をしている場合は、WAL アーカイブの圧縮・ベースバックアップの世代管理・事前事後のフックスクリプト実行が行われます。/etc/crond.d/barman ファイルが Barman インストール時に用意されるので、定期実行したい場合はそのファイルを編集するとよいでしょう。</p>
<p>Barman のメンテナンスコマンドを実行したら、barman backup サブコマンドでベースバックアップの取得が行えます。</p>
<pre>[postgres@barman ~]$ barman backup streaming-pgsql01
Starting backup using postgres method for server streaming-pgsql01 in /var/lib/pgsql/streaming-pgsql01/base/20181026T090225
Backup start at LSN: 0/4B5B0898 (00000001000000000000004B, 005B0898)
Starting backup copy via pg_basebackup for 20181026T090225
Copy done (time: 1 second)
Finalising the backup.
Backup size: 191.7 MiB
Backup end at LSN: 0/4D000000 (00000001000000000000004C, 00000000)
Backup completed (start time: 2018-10-26 09:02:25.064266, elapsed time: 1 second)
Processing xlog segments from streaming for streaming-pgsql01
        00000001000000000000004B
</pre>
<p>barman コマンドの list-backup サブコマンドでバックアップが取得できていることが確認できます。</p>
<pre>（list-backup サブコマンドによる確認）
[postgres@barman ~]$ barman list-backup streaming-pgsql01
streaming-pgsql01 20181026T090225 - Fri Oct 26 09:02:27 2018 - Size: 207.7 MiB - WAL Size: 96.0 MiB (tablespaces: ssh_tbls:/tmp/ssh_tbls_recover)
</pre>
<h2>任意の時点にリカバリ</h2>
<p>それでは取得したバックアップをリカバリしてみます。PITR によって任意の時間にデータベースをリカバリしてみます。リカバリ先は pgsql01 サーバですが、既存のデータベースクラスタはそのままにしておき、異なるデータベースクラスタのパスを使うようにリカバリします。<br />
先程ベースバックアップを取得したので、PostgreSQL サーバで pgbench を実行してトランザクションを進めます。</p>
<pre>[postgres@pgsql01 ~]$ pgbench -T 30 -p 5433 bench
</pre>
<p>ベンチマークテーブルの pgbench_history テーブルに格納されているタイムスタンプを確認して、リカバリする時刻を決定します。</p>
<pre>（ベンチマークの開始と終了時刻を確認）
[postgres@pgsql01 ~]$ psql -p 5433 bench
bench=# select min(mtime), max(mtime) from pgbench_history;
            min             |            max
----------------------------+----------------------------
 2018-10-26 09:02:57.827964 | 2018-10-26 09:03:27.823867
(1 row)
</pre>
<p>ここでは、リカバリ時刻を 2018-10-26 09:03:15 として、barman コマンドの recover サブコマンドでリカバリします。</p>
<pre>[postgres@barman ~]$ barman recover --remote-ssh-command 'ssh postgres@pgsql01' --tablespace ssh_tbls:/tmp/ssh_tbls_recover02 --target-ti
me '2018-10-26 09:03:15' streaming-pgsql01 20181026T090225 /var/lib/pgsql/10/data_recover02
Starting remote restore for server streaming-pgsql01 using backup 20181026T090225
Destination directory: /var/lib/pgsql/10/data_recover02
Doing PITR. Recovery target time: '2018-10-26 09:03:15+00:00'
Using safe horizon time for smart rsync copy: 2018-10-26 08:11:09+00:00
        16384, ssh_tbls, /tmp/ssh_tbls_recover02
Copying the base backup.
Copying required WAL segments.
Generating recovery.conf
Identify dangerous settings in destination directory.

IMPORTANT
These settings have been modified to prevent data losses

postgresql.conf line 219: archive_command = false

Recovery completed (start time: 2018-10-26 09:07:15.312212, elapsed time: 21 seconds)

Your PostgreSQL server has been successfully prepared for recovery!
</pre>
<p>リカバリしたデータベースクラスタ (/var/lib/pgsql/10/data_recover02) のポート番号を変更して起動し、pgbench_history テーブルに格納されているタイムスタンプの最大値を確認します。</p>
<pre>（postgresql.conf設定）
port = 5434

#### #### #### ####

（PostgreSQL の起動）
[postgres@pgsql01 ~]$ pg_ctl -w start -D 10/data_recover02

#### #### #### ####

（ベンチマークテーブルのタイムスタンプを確認）
[postgres@pgsql01 ~]$ psql -p 5434 bench
psql (10.5)
Type "help" for help.

bench=# select min(mtime), max(mtime) from pgbench_history;
            min             |            max
----------------------------+---------------------------
 2018-10-26 09:02:57.827964 | 2018-10-26 09:03:14.99833
(1 row)
</pre>
<p>問題なく、指定の時刻までリカバリができています。</p>
<h2>まとめ</h2>
<p>以上、PostgreSQL の PITR 補助ツールの Barman を使って基本的なバックアップとリカバリの動作を確認しました。<br />
複数のデータベースインスタンスの PITR を一元管理できる、煩雑な PITR 操作を簡単にできるといった点に魅力を感じるようでしたら、一度試してみてはいかがでしょうか。</p>
<p>Barman は以下の公式サイトから入手可能です。</p>
<p>[Barman]<br />
<a href="https://www.pgbarman.org/" target="_blank" rel="noopener">https://www.pgbarman.org/</a></p>
<p>同サイトにマニュアルページもあります。</p>
<p>[Barman Manual]<br />
<a href="http://docs.pgbarman.org/release/2.5/" target="_blank" rel="noopener">http://docs.pgbarman.org/release/2.5/</a></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>
