PostgreSQL で住所マスタを作る 〜郵便住所.jp の CSV を使って〜
こんにちは。株式会社 Interfamilia の Kuma です。
会員情報に住所を持たせたい、入力フォームで郵便番号から住所を自動補完したい、都道府県のプルダウンを作りたい……。
システムを開発していると、郵便番号や住所のマスタが必要になる場面はしょっちゅうあります。
当社では、そんなときに使えるサービスとして 郵便住所.jp
を公開しています。
日本郵便さんが公開している郵便番号データを整理して、欲しい項目だけを選んで CSV でダウンロードできるサービスです。
郵便住所.jp
郵便番号検索、住所検索、住所マスタ用 CSV ダウンロードサービス
今回の記事では、この 郵便住所.jp
からダウンロードした CSV を使って、PostgreSQL に住所マスタのデータベースを作るところまでを、ひととおりやってみたいと思います。docker compose up を実行したらデータベースもテーブルもデータも全部できあがっている、という状態をゴールにします。
※初学者向けの記事になっています。
目次
まずは、これから使うサービスについて簡単に紹介させてください。
郵便住所.jp
は、日本郵便さんが公開している 郵便番号データ
を整理して公開しているサイトです。
Web 上で郵便番号検索・住所検索ができるほか、データを RDB に展開しやすいかたちで CSV としてダウンロードできるようになっています。
特徴はこのあたりです。
- 欲しい項目だけを選べる
チェックボックスで項目を選ぶと、その並びどおりの CSV が生成されます。テーブルのカラム構成に合わせて CSV を作れるので、取り込みが非常に楽になります。 - 登録不要・無料
会員登録も API キーも不要です。フォームでポチポチ選んでボタンを押すだけです。 - 都道府県・市区町村・郵便住所の3つに分かれている
それぞれにコードが振られているので、そのままリレーションを張れます。 - 月に1回くらいのペースで更新
日本郵便さんの更新に追従しています。ローマ字だけは元データの更新が年1回程度なので、そのペースになります。
なお、ダウンロードした CSV の再配布は利用規約
で禁止しています。
この記事でも CSV そのものは配布せず、ご自身でダウンロードしていただく手順で進めます。最新のデータが手に入るので、そのほうが都合も良いはずです。
今回作るのは、こんな構成のデータベースです。
erDiagram
prefecture ||--o{ municipality : "prefecture_code"
municipality ||--o{ postal_address : "municipality_code"
- データベース
postaladdress(郵便住所)
- テーブル
prefecture(都道府県)municipality(市区町村) …prefectureへの FK を持つpostal_address(郵便住所) …municipalityへの FK を持つ
prefecture → municipality → postal_address と、上から下に親子関係になっています。
都道府県は 47 件、市区町村は 1,892 件、郵便住所は 122,653 件です(2026年8月31日版のデータの場合)。
prefecture (都道府県)
| カラム | 型 | 内容 | 例 |
|---|---|---|---|
prefecture_code | char(2) | 都道府県コード (PK) | 13 |
prefecture_name | text | 都道府県名 | 東京都 |
prefecture_name_kana | text | 都道府県名カナ | トウキョウト |
prefecture_name_rome | text | 都道府県名ローマ字 | TOKYO TO |
municipality (市区町村)
| カラム | 型 | 内容 | 例 |
|---|---|---|---|
municipality_code | char(5) | 市区町村コード (PK) | 13101 |
municipality_name | text | 市区町村名 | 千代田区 |
municipality_name_kana | text | 市区町村名カナ | チヨダク |
municipality_name_rome | text | 市区町村名ローマ字 | CHIYODA KU |
prefecture_code | char(2) | 都道府県コード (FK) | 13 |
postal_address (郵便住所)
| カラム | 型 | 内容 | 例 |
|---|---|---|---|
id | bigint | ID (PK) | 1 |
postal_code | char(7) | 郵便番号 | 1000001 |
municipality_code | char(5) | 市区町村コード (FK) | 13101 |
street_name | text | 町域名 | 千代田 |
street_name_kana | text | 町域名カナ | チヨダ |
street_name_rome | text | 町域名ローマ字 | CHIYODA |
postal_address にだけ id という項目があります。これは CSV には無い、こちらで採番する項目です。
理由は後ほど説明します。
事前に以下の準備をお願いします。
- Docker Desktop がインストールされており
docker composeコマンドが使える。
PostgreSQL のインストールは不要です。Docker のコンテナ上に作ります。
なお、今回作成する一式は当社の GitHub リポジトリでも公開しています。
手っ取り早く試したい方はこちらをご利用ください。
interfamilia/testing-postgresql-docker
PostgreSQL 検証用 Docker 環境
まず、作業用のディレクトリを以下の構成で作ります。
testing-postgresql-docker/
├── docker-compose.yml
├── csv/ ← ダウンロードした CSV を置く
└── initdb/ ← 起動時に実行される SQL を置く
C:\Users\Kuma> mkdir testing-postgresql-docker
C:\Users\Kuma> cd ./testing-postgresql-docker
C:\Users\Kuma\testing-postgresql-docker> mkdir csv
C:\Users\Kuma\testing-postgresql-docker> mkdir initdb
郵便住所.jp から、都道府県・市区町村・郵便住所の3つの CSV をダウンロードします。
ここが今回いちばん大事なところです。
CSV の項目の並び順は、画面に並んでいる順で固定されています。チェックした順ではありません。
なので、これから作るテーブルのカラムの並びと、チェックする項目の並びを合わせる必要があります。
また、いくつかの項目は最初からチェックが入っています。今回は使わない項目もあるので、チェックを外すのを忘れないでください。
都道府県の情報を CSV でダウンロードする を開いて、以下のようにチェックします。
| 項目 | チェック |
|---|---|
| 都道府県コード | ✅ |
| 都道府県名 | ✅ |
| 都道府県名カナ | ✅ |
| 都道府県名ローマ字 | ✅ |
4つとも全部チェックします。
[都道府県を CSV でダウンロードする]ボタンをクリックすると 都道府県.zip がダウンロードされます。
市区町村の情報を CSV でダウンロードする を開いて、以下のようにチェックします。
| 項目 | チェック | |
|---|---|---|
| 市区町村コード | ✅ | |
| 市区町村名 | ✅ | |
| 市区町村名カナ | ✅ | |
| 市区町村名ローマ字 | ✅ | |
| 都道府県コード | ✅ | |
| 都道府県名 | ❌ | prefecture テーブルにあるので不要 |
| 都道府県名カナ | ❌ | 同上 |
| 都道府県名ローマ字 | ❌ | 同上 |
| 廃止フラグ | ❌ |
都道府県名は prefecture テーブルに持っているので、municipality 側では持ちません。
同じ情報を2か所に持つと、片方だけ直して食い違う、ということが起きます。コードだけを持って、名前が必要なときは JOIN して取るのが基本の形です。
郵便番号や住所の情報を CSV でダウンロードする を開いて、以下のようにチェックします。
| 項目 | チェック | |
|---|---|---|
| 郵便番号 | ✅ | |
| 都道府県コード | ❌ | 最初から ON なので外す |
| 都道府県名 | ❌ | 最初から ON なので外す |
| 都道府県名カナ | ❌ | |
| 都道府県名ローマ字 | ❌ | |
| 市区町村コード | ✅ | |
| 市区町村名 | ❌ | 最初から ON なので外す |
| 市区町村名カナ | ❌ | |
| 市区町村名ローマ字 | ❌ | |
| 町域名 | ✅ | |
| 町域名カナ | ✅ | |
| 町域名ローマ字 | ✅ | |
| 廃止フラグ | ❌ |
下のほうにある「廃止データを含める」「以下に掲載がない場合を含める」「町域を集約する」の3つのオプションは、すべてチェックしません。
都道府県コードも、municipality を経由してたどれるので持ちません。
ダウンロードした3つの ZIP を解凍します。
中に入っている CSV は 2026-08-07.145642.0012399962363261963683.csv のような毎回変わるファイル名になっているので、以下の名前にリネームして csv ディレクトリに置いてください。
testing-postgresql-docker/
└── csv/
├── prefecture.csv
├── municipality.csv
└── postal_address.csv
中身はこんな感じの CSV になっています。
prefecture.csv
"01","北海道","ホッカイドウ","HOKKAIDO"
"02","青森県","アオモリケン","AOMORI KEN"
municipality.csv
"01101","札幌市中央区","サッポロシチュウオウク","SAPPORO SHI CHUO KU","01"
"01102","札幌市北区","サッポロシキタク","SAPPORO SHI KITA KU","01"
postal_address.csv
"0010010","01102","北十条西(1~4丁目)","キタ10ジョウニシ(1-4チョウメ)","KITA10-JONISHI(1-4-CHOME)"
"0010011","01102","北十一条西(1~4丁目)","キタ11ジョウニシ(1-4チョウメ)","KITA11-JONISHI(1-4-CHOME)"
郵便住所.jp の CSV の仕様は以下のとおりです。あとで取り込むときに効いてきます。
- 文字コード:UTF-8(BOM なし)
- 改行コード:LF
- ヘッダー行なし
- すべての項目がダブルクォートで囲まれている
- 値が無い項目は
""(空文字)
docker-compose.yml を作成します。
services:
postgres:
image: postgres:17
container_name: testing-postgresql
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postaladdress
TZ: Asia/Tokyo
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d
- ./csv:/csv
volumes:
postgres-data:
ポイントは3つです。
POSTGRES_DB: postaladdress
これが postaladdress データベースの作成にあたります。コンテナが最初に起動するときに、この名前のデータベースを作ってくれます。
Docker を使わず、すでにある PostgreSQL に作る場合は、以下の SQL が同じことをします。
CREATE DATABASE postaladdress ENCODING 'UTF8';
./initdb:/docker-entrypoint-initdb.d
PostgreSQL の公式イメージは、初回起動時に /docker-entrypoint-initdb.d の中の .sql ファイルを名前順に実行してくれます。
ここにテーブル作成とデータ取り込みの SQL を置いておけば、docker compose up するだけで全部できあがる、というわけです。
./csv:/csv
CSV をコンテナの中から見えるようにしています。コンテナの中では /csv/prefecture.csv というパスになります。
initdb/01_create_table.sql を作成します。
ファイル名の先頭に 01_ と付けているのは、02_ より先に実行させたいからです。テーブルが無いとデータは入れられません。
CREATE TABLE prefecture (
prefecture_code char(2) NOT NULL,
prefecture_name text NOT NULL,
prefecture_name_kana text NOT NULL,
prefecture_name_rome text NOT NULL,
CONSTRAINT pk_prefecture PRIMARY KEY (prefecture_code)
);
COMMENT ON TABLE prefecture IS '都道府県';
COMMENT ON COLUMN prefecture.prefecture_code IS '都道府県コード';
COMMENT ON COLUMN prefecture.prefecture_name IS '都道府県名';
COMMENT ON COLUMN prefecture.prefecture_name_kana IS '都道府県名カナ';
COMMENT ON COLUMN prefecture.prefecture_name_rome IS '都道府県名ローマ字';
COMMENT ON は無くても動きますが、書いておくと psql や GUI ツールで日本語の説明が見えるようになるので、個人的には気に入っています。
【コラム】都道府県コードを
integerにしてはいけない都道府県コードは
01〜47です。数字に見えるのでintegerにしたくなりますが、絶対にやめましょう。integerに入れると01が1になってしまい、先頭のゼロが消えます。01でなければならない場面(他システムとの連携など)で必ず困ります。
市区町村コード01101や郵便番号0010010も同じです。数字に見えてもコードは文字列、と覚えておくと安全です。
【コラム】
charとvarcharとtextPostgreSQL の文字列型は3つありますが、性能上の差はほとんどありません。使い分けの考え方はこうです。
char(n)… 必ず n 文字になるもの。足りないぶんは空白で埋められますvarchar(n)… n 文字までのものtext… 上限を決めないもの今回、コード類は郵便住所.jp が「2文字(固定)」「5文字(固定)」「7文字(固定)」と明記しているので
charにしています。
一方、名前のほうはtextにしました。郵便住所.jp のサイトにも最大文字数の一覧がありますが、「ある時点での情報となっており、名称などの最大文字数は増減します」と注意書きがあります。町域名は最大 252 文字とかなり長いものもあるので、無理に上限を決めずにtextにしておくのが安全です。
CREATE TABLE municipality (
municipality_code char(5) NOT NULL,
municipality_name text NOT NULL,
municipality_name_kana text NOT NULL,
municipality_name_rome text,
prefecture_code char(2) NOT NULL,
CONSTRAINT pk_municipality PRIMARY KEY (municipality_code),
CONSTRAINT fk_municipality_prefecture FOREIGN KEY (prefecture_code)
REFERENCES prefecture (prefecture_code)
);
COMMENT ON TABLE municipality IS '市区町村';
COMMENT ON COLUMN municipality.municipality_code IS '市区町村コード';
COMMENT ON COLUMN municipality.municipality_name IS '市区町村名';
COMMENT ON COLUMN municipality.municipality_name_kana IS '市区町村名カナ';
COMMENT ON COLUMN municipality.municipality_name_rome IS '市区町村名ローマ字';
COMMENT ON COLUMN municipality.prefecture_code IS '都道府県コード';
FOREIGN KEY (prefecture_code) REFERENCES prefecture (prefecture_code) の部分が外部キー制約 (FK) です。
「municipality.prefecture_code には、prefecture テーブルに存在する都道府県コードしか入れられない」という制約になります。
これを付けておくと、
prefectureに無いコードを間違って入れようとするとエラーで止まってくれるprefectureの行を消そうとしても、それを参照している市区町村があれば消せない
というふうに、データベースが不整合を防いでくれます。
なお municipality_name_rome にだけ NOT NULL を付けていません。ローマ字は元データの更新が年1回のため、未設定のものがあり得るからです。
CREATE TABLE postal_address (
id bigint GENERATED ALWAYS AS IDENTITY,
postal_code char(7) NOT NULL,
municipality_code char(5) NOT NULL,
street_name text NOT NULL,
street_name_kana text NOT NULL,
street_name_rome text,
CONSTRAINT pk_postal_address PRIMARY KEY (id),
CONSTRAINT fk_postal_address_municipality FOREIGN KEY (municipality_code)
REFERENCES municipality (municipality_code)
);
COMMENT ON TABLE postal_address IS '郵便住所';
COMMENT ON COLUMN postal_address.id IS 'ID';
COMMENT ON COLUMN postal_address.postal_code IS '郵便番号';
COMMENT ON COLUMN postal_address.municipality_code IS '市区町村コード';
COMMENT ON COLUMN postal_address.street_name IS '町域名';
COMMENT ON COLUMN postal_address.street_name_kana IS '町域名カナ';
COMMENT ON COLUMN postal_address.street_name_rome IS '町域名ローマ字';
ここで id bigint GENERATED ALWAYS AS IDENTITY という項目を足しています。
これは自動採番される連番で、1, 2, 3, ... が勝手に入ります。
なぜこれが必要なのでしょうか。
【コラム】郵便番号は主キーにできない
「郵便番号がユニークなんだから、
postal_codeを主キーにすればいいのでは?」と思いますよね。できません。郵便番号は、1つの郵便番号に複数の町域が対応することがあります。たとえば愛知県清須市の
4520961は、春日一本松・春日一番割・春日三番割……と 66 件もあります。
実際、今回のデータでは 1,261 件の郵便番号が複数行になっています。では
(postal_code, municipality_code)の組み合わせならどうか、というとこれもダメです。上の例のとおり、同じ郵便番号・同じ市区町村で町域だけが違う行が並ぶからです。この組み合わせでも 1,205 件が重複します。このようにデータそのものの中に主キーにできる項目が無い場合、こちらで連番を振ってしまうのが定石です。こうした「意味を持たない、主キーにするためだけの項目」を**サロゲートキー(代理キー)**と呼びます。
ちなみに郵便住所.jp には「町域を集約する」というオプションがあって、これを ON にすると同じ郵便番号・市区町村の町域が
/区切りで1行にまとめられます。こちらを使えば(postal_code, municipality_code)を主キーにできます。用途に応じて選んでみてください。
最後にインデックスを追加します。同じ 01_create_table.sql の末尾に書きます。
CREATE INDEX idx_municipality_prefecture_code ON municipality (prefecture_code);
CREATE INDEX idx_postal_address_postal_code ON postal_address (postal_code);
インデックスは、本でいうところの索引です。無くても検索はできますが、テーブルを頭から全部見に行くことになるので遅くなります。
主キーには自動でインデックスが作られますが、それ以外の項目には作られません。
今回でいうと postal_address.postal_code は主キーではないので、「郵便番号で住所を検索する」といういちばんやりたいことが遅くなってしまいます。なので明示的に作っています。
municipality.prefecture_code も「東京都の市区町村一覧を出す」ときに効いてきます。
initdb/02_import_csv.sql を作成します。
\copy prefecture FROM '/csv/prefecture.csv' WITH (FORMAT csv, ENCODING 'UTF8')
\copy municipality (municipality_code, municipality_name, municipality_name_kana, municipality_name_rome, prefecture_code) FROM '/csv/municipality.csv' WITH (FORMAT csv, ENCODING 'UTF8', FORCE_NULL (municipality_name_rome))
\copy postal_address (postal_code, municipality_code, street_name, street_name_kana, street_name_rome) FROM '/csv/postal_address.csv' WITH (FORMAT csv, ENCODING 'UTF8', FORCE_NULL (street_name_rome))
3行だけですが、ハマりどころが詰まっているので順番に説明します。
\copy は SQL ではなく、psql のメタコマンドです(バックスラッシュで始まるコマンド)。そのため、普通の SQL とは書き方のルールが違い、1行で書く必要があります。途中で改行はできません。上の SQL が横に長いのはこのためです。
似たものに COPY という SQL 文もありますが、こちらはサーバ側のファイルを読みます。\copy は psql を実行している側のファイルを読みます。スーパーユーザー権限が要らないので、こちらのほうが使える場面が多いです。
CSV にヘッダー行があるときは HEADER を付けますが、郵便住所.jp の CSV にはヘッダー行がありません。なので付けません。
うっかり付けると、1行目のデータが見出しとして読み飛ばされて、1件足りないという地味に気づきにくい事故になります。
ダブルクォート囲みについては、FORMAT csv を指定していれば PostgreSQL が自動的に外してくれるので、特に指定は不要です。
prefecture は CSV の項目とテーブルのカラムがぴったり一致しているので、テーブル名だけで済みます。
一方 postal_address には、CSV に存在しない id があります。
このまま何も指定しないと「CSV の1列目を id に入れる」と解釈されて失敗するので、どのカラムに入れるかを明示します。
\copy postal_address (postal_code, municipality_code, street_name, street_name_kana, street_name_rome) FROM ...
こう書くと、指定しなかった id には GENERATED ALWAYS AS IDENTITY による連番が自動で入ります。
municipality は一致していますが、わかりやすさのために同じく明示しています。
郵便住所.jp の CSV では、値が無い項目は ""(空文字)になっています。
たとえば町域名ローマ字が未設定の行はこうなっています。
"0130310","05203","大雄佐加里西","タイユウサガリニシ",""
このまま取り込むと street_name_rome には空文字が入ります。NULL にはなりません。
「値が無い」ことを NULL で表したい場合は FORCE_NULL を使います。
WITH (FORMAT csv, ENCODING 'UTF8', FORCE_NULL (street_name_rome))
こう書くと、"" が NULL として取り込まれます。
空文字と NULL が混ざっていると WHERE street_name_rome IS NULL では引っかからない、といった面倒が起きるので、どちらかに寄せておくのがおすすめです。
FK があるので、親から先に入れる必要があります。
prefecture → municipality → postal_address の順です。
逆の順番で入れようとすると、参照先がまだ存在しないので FK 違反でエラーになります。01_ 02_ と同じく、SQL の中の並び順がそのまま実行順になるので、この順で書いておけば大丈夫です。
準備ができました。ファイルはこうなっているはずです。
testing-postgresql-docker/
├── docker-compose.yml
├── csv/
│ ├── prefecture.csv
│ ├── municipality.csv
│ └── postal_address.csv
└── initdb/
├── 01_create_table.sql
└── 02_import_csv.sql
Docker Desktop を起動してから、docker compose up -d を実行します。
C:\Users\Kuma\testing-postgresql-docker> docker compose up -d
12万件の取り込みがあるので、少し時間がかかります。
ログを見て、初期化が終わったか確認しましょう。
C:\Users\Kuma\testing-postgresql-docker> docker compose logs postgres
PostgreSQL init process complete; ready for start up. と出ていれば成功です。ERROR の文字が無いこともあわせて確認しておくと安心です。
では、psql でつないでみます。
C:\Users\Kuma\testing-postgresql-docker> docker compose exec postgres psql -U postgres -d postaladdress
\dt でテーブルの一覧が見られます。3つできていれば成功です。
postaladdress=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------------+-------+----------
public | municipality | table | postgres
public | postal_address | table | postgres
public | prefecture | table | postgres
(3 rows)
件数も確認してみましょう。
postaladdress=# SELECT count(*) FROM prefecture;
count
-------
47
(1 row)
postaladdress=# SELECT count(*) FROM municipality;
count
-------
1892
(1 row)
postaladdress=# SELECT count(*) FROM postal_address;
count
--------
122653
(1 row)
47 件・1,892 件・122,653 件。郵便住所.jp のサイトに表示されている件数と一致しました🎉
せっかくデータが入ったので、いろいろ検索してみましょう。
いちばん単純な SELECT です。プルダウンを作るときなどはこれで十分です。
SELECT prefecture_code, prefecture_name
FROM prefecture
ORDER BY prefecture_code;
prefecture_code | prefecture_name
-----------------+-----------------
01 | 北海道
02 | 青森県
03 | 岩手県
...
47 | 沖縄県
(47 rows)
ここで FK を張った prefecture_code が効いてきます。
都道府県名で絞り込みたいので、prefecture と JOIN します。
SELECT m.municipality_code, m.municipality_name
FROM municipality m
JOIN prefecture p ON p.prefecture_code = m.prefecture_code
WHERE p.prefecture_name = '東京都'
ORDER BY m.municipality_code;
municipality_code | municipality_name
-------------------+-------------------
13101 | 千代田区
13102 | 中央区
13103 | 港区
...
(62 rows)
municipality に都道府県名を持たせなかったのは、こうやって JOIN で取れるからです。
いちばんやりたいことです。3つのテーブルを JOIN します。
SELECT a.postal_code,
p.prefecture_name,
m.municipality_name,
a.street_name
FROM postal_address a
JOIN municipality m ON m.municipality_code = a.municipality_code
JOIN prefecture p ON p.prefecture_code = m.prefecture_code
WHERE a.postal_code = '1000001';
postal_code | prefecture_name | municipality_name | street_name
-------------+-----------------+-------------------+-------------
1000001 | 東京都 | 千代田区 | 千代田
(1 row)
郵便番号 1000001 から「東京都 千代田区 千代田」が取得できました。
入力フォームの住所自動補完は、まさにこの SELECT です。
なお、郵便番号にハイフンは入っていません。
画面から 100-0001 という形式で受け取る場合は、検索する前にハイフンを取り除いてください。
逆向きの検索です。町域名の部分一致で探します。
SELECT a.postal_code,
p.prefecture_name,
m.municipality_name,
a.street_name
FROM postal_address a
JOIN municipality m ON m.municipality_code = a.municipality_code
JOIN prefecture p ON p.prefecture_code = m.prefecture_code
WHERE m.municipality_name = '千代田区'
AND a.street_name LIKE '大手町%'
ORDER BY a.postal_code;
postal_code | prefecture_name | municipality_name | street_name
-------------+-----------------+-------------------+----------------------------------
1000004 | 東京都 | 千代田区 | 大手町(次のビルを除く)
1006801 | 東京都 | 千代田区 | 大手町JAビル(1階)
1006802 | 東京都 | 千代田区 | 大手町JAビル(2階)
...
1006890 | 東京都 | 千代田区 | 大手町JAビル(地階・階層不明)
(39 rows)
大きなビルは階ごとに郵便番号が振られているんですね。
LIKE '大手町%' は「大手町で始まる」という意味です。LIKE '%大手町%' のように前後に % を付けると「大手町を含む」になります。
集計してみます。
SELECT p.prefecture_name,
count(*) AS address_count
FROM postal_address a
JOIN municipality m ON m.municipality_code = a.municipality_code
JOIN prefecture p ON p.prefecture_code = m.prefecture_code
GROUP BY p.prefecture_code, p.prefecture_name
ORDER BY address_count DESC
LIMIT 5;
prefecture_name | address_count
-----------------+---------------
北海道 | 8002
愛知県 | 7666
京都府 | 6421
新潟県 | 5451
兵庫県 | 5179
(5 rows)
GROUP BY に prefecture_code も入れておくと、同じ名前の都道府県があった場合でも正しくまとまります(実際には無いのですが、癖にしておくと安全です)。
先ほどコラムで触れた、サロゲートキーが必要になる理由を実際に見てみましょう。
SELECT postal_code, count(*) AS street_count
FROM postal_address
GROUP BY postal_code
HAVING count(*) > 1
ORDER BY street_count DESC
LIMIT 5;
postal_code | street_count
-------------+--------------
4520961 | 66
4801103 | 65
4410302 | 46
7793405 | 44
0294205 | 42
(5 rows)
WHERE ではなく HAVING を使っているところがポイントです。WHERE は集計する前の行を絞り込むもので、HAVING は集計した後の結果を絞り込むものです。count(*) は集計した後にしか決まらないので、HAVING を使います。
いちばん多い 4520961 の中身も見てみます。
SELECT postal_code, street_name
FROM postal_address
WHERE postal_code = '4520961'
ORDER BY street_name
LIMIT 3;
postal_code | street_name
-------------+-------------
4520961 | 春日一本松
4520961 | 春日一番割
4520961 | 春日三番割
(3 rows)
同じ郵便番号に町域が 66 件ぶら下がっていることがわかります。
これが「郵便番号を主キーにできない」理由です。
FORCE_NULL が効いているか確認してみましょう。
SELECT count(*)
FROM postal_address
WHERE street_name_rome IS NULL;
count
-------
177
(1 row)
177 件が NULL になっています。FORCE_NULL を書かなかった場合、ここは 0 になります(空文字が入っているため)。
/docker-entrypoint-initdb.d の SQL は、データベースがまだ存在しない初回起動時にしか実行されません。
SQL を修正してコンテナを再起動しても、何も起こりません。ここは最初に必ずハマるところです。
やり直すときは、ボリュームごと削除してから起動し直します。
C:\Users\Kuma\testing-postgresql-docker> docker compose down -v
C:\Users\Kuma\testing-postgresql-docker> docker compose up -d
-v がボリュームを消すオプションです。これを付けないとデータが残ったままなので、忘れないようにしましょう。
検証用の環境なので、気軽に壊して作り直せます。
テーブルのカラム数に対して、CSV の項目が足りていません。
ダウンロードするときに、チェックを付け忘れた項目が無いかを確認してみてください。
\copy にカラム名のリストを書き忘れている可能性が高いです。
ERROR: value too long for type character(5)
CONTEXT: COPY postal_address, line 1, column municipality_code: "北十条西(1~4丁目)"
書き忘れると CSV の1列目が id に入り、以降が1つずつずれます。
その結果、3列目の町域名が char(5) の municipality_code に入ろうとして、このエラーになります。
FK 違反です。参照先に無いコードを入れようとしています。
- CSV をダウンロードするときに、片方だけ「廃止データを含める」にチェックを入れていないか
- 取り込む順番が
prefecture→municipality→postal_addressになっているか
このあたりを確認してみてください。
CSV の項目数とテーブルのカラム数が合っていません。
ダウンロードするときのチェックの付け方を、もう一度この記事の表と見比べてみてください。最初から ON になっている項目を外し忘れているケースが多いです。
CSV がコンテナから見えていません。csv ディレクトリの場所と、リネームしたファイル名を確認してみてください。
Windows のコマンドプロンプトで psql を使うと文字化けすることがあります。psql を実行する前に、文字コードを UTF-8 に切り替えてみてください。
C:\Users\Kuma> chcp 65001
郵便住所.jp の CSV を PostgreSQL に取り込んで、住所マスタとして使えるところまでを見てきました。
「欲しい項目だけ選べる」というのは地味な機能に見えるかもしれませんが、テーブルのカラム構成に合わせて CSV を作れるので、取り込みの前処理がほとんど不要になります。今回も \copy 3行で終わりました。
データは月に1回ほど更新されるので、テーブルを作り直して入れ直せば、いつでも最新の状態に保てます。
ぜひ 郵便住所.jp
を使ってみてください。
それではまた次回!
- 郵便住所.jp
- 郵便住所.jp 利用規約
- interfamilia/testing-postgresql-docker
- 郵便番号データダウンロード - 日本郵便
- PostgreSQL 文書 - COPY
- PostgreSQL 文書 - psql
- PostgreSQL 文書 - 文字型
- postgres - Docker Official Image
応募フォームには、外部サービスengageを利用しています。
ご応募に際して、何かわからないことなどございましたら こちら
からお気軽にお問い合わせください。
Interfamilia Engineer Blog

