🐘

[PostgreSQL] あんたは一体、何 length だい?

2024/04/29に公開

データベースにおける「文字数」の認識を調べてみました。
最初は仕事でもよく使っている PostgreSQL で検証します。

なおエンコーディング・照合は ja_JP.UTF-8 とします。

対象者

  • データベースの設計をしている
  • 文字コードの仕組みが気になる
  • 絵文字おもしれー

みたいな方には興味がある内容かなと思います。

やってみた

Docker で postgres:16-alpine コンテナを起動し、接続クライアントには DBeaver を用いて実施しました。

確認した環境

-- SELECT VERSION()
PostgreSQL 16.2 on x86_64-pc-linux-musl, compiled by gcc (Alpine 13.2.1_git20231014) 13.2.1 20231014, 64-bit

JIS 水準シリーズ

まずは日本語ではおなじみのJIS第1水準などなど、日本語関連を調べてみる。

SELECT
    -- ASCII
    length('A') AS ascii,
    -- ひらがな
    length('あ') AS hiragana,
    -- 半角カナ
    length('ア') AS hankaku_kana,
    -- 漢字
    length('来') AS kanji,
    -- JIS第2水準
    length('來') AS jis2,
    -- JIS第3水準
    length('俠') AS jis3,
    -- JIS第4水準
    length('丂') AS jis4

結果は全部 1 文字として認識した。

ascii hiragana hankaku_kana kanji jis2 jis3 jis4
1 1 1 1 1 1 1

※あとの画面ショットは省きます。

正規化形式シリーズ

ここが本命です。Windows/Linux ではおなじみの NFC と、UTF-8-MAC と言われたりする NFD での違いを見ます。ぱっと見で違いが分かりにくいのでバイト列ろ 16 進数でコメントに併記します。

SELECT
    -- ASCII - 41
    length('A') AS ascii_a,
    -- 片仮名
    length('ト') AS kana_to,
    -- NFC / 正規化形式C / Windows や Linux
    length('ド') AS nfc_do,
    -- NFD / 正規化形式D / macOS UTF-8-MAC
    length('ド') AS nfd_do,
    -- NFC オングストローム
    length('Å') AS nfc_a,
    -- NFD オングストローム
    length('Å') AS nfd_a

結果は、、、NFD だと 2 文字としてカウントしていますね。

ascii_a kana_to nfc_do nfd_do nfc_a nfd_a
1 1 1 2 1 2
余談:オングストローム (長さの単位)

日本語な UTF-8 バイト列だとよく E3 などから始まる 3 バイトがメインになってくるので、違う例として極小単位の オングストローム (wikipedia) に登場してもらいました。

1 オングストローム = 0.1 ナノメートル = 100 ピコメートル。見えねぇ。

🐘 絵文字シリーズ

しっかり巷では市民権を得た絵文字について。
多用するとおじさん構文などと言われ兼ねないですが、、実験です。多用します。

SELECT
    -- 絵文字(単文字)
    length('🐘') AS emoji_elephant,
    -- 絵文字(色変更) - 金髪
    length('👱🏽') AS emoji_blond_haired,
    -- 絵文字(組合せ)
    length('👩‍👨‍👧‍👦') AS emoji_zero_width_joiner

結果は、7 文字と認識するケースも。

emoji_elephant emoji_blond_haired emoji_zero_width_joiner
1 2 7

まとめ

改めて結果を表にまとめます。

文字 NOTE length() Unicode UTF-8 バイト列
A ASCII 1 U+0041 41
平仮名 1 U+3042 E3 81 82
片仮名(NFC) 1 U+30C8 E3 83 88
片仮名(NFC) 1 U+30C9 E3 83 89
ド 片仮名(NFD) 2 U+30C8 U+3099 E3 83 88 E3 82 99
Å 単位(NFC) 1 U+00C5 C3 85
単位(NFD) 2 U+0041 U+030A 41 CC 8A
🐘 絵文字(単文字) 1 U+1F418 F0 9F 90 98
👱🏽 絵文字(色変更) 2 U+1F46A U+1F3FD F0 9F 91 B1 F0 9F 8F BD
👩‍👨‍👧‍👦 絵文字(組合せ) 7 U+1F469 U+200D U+1F468 U+200D U+1F467 U+200D U+1F466 F0 9F 91 A9 E2 80 8D F0 9F 91 A8 E2 80 8D F0 9F 91 A7 E2 80 8D F0 9F 91 A6

Word Joiner

組み合わせで作られた絵文字に登場する E2 80 8D は単語結合子 (word joiner) と呼ばれる制御文字だそう。Unicode コードポイントでは U+200D となる。

https://ja.wikipedia.org/wiki/単語結合子

絵文字の仕様

「UTS #51 Unicode Emoji」として詳細にレポートがある。
https://www.unicode.org/reports/tr51/

NFD

Normalization Form Canonical Decomposition (NFD) では「ド」が「ト」+結合用の「゛」という風にコードポイントが分かれているのが分かる。

結合用については compart.com というサイトの表現分かりやすかった。

https://www.compart.com/en/unicode/U+3099

PostgreSQL は Unicode コードポイント数

まとめの表からも分かる通りですが、公式マニュアルで「文字数」と説明しているのは「Unicode で表したコードポイントの個数」ということになる。「見かけ上の文字数」ではない点に注意したい。

日本語翻訳版:

何に影響するか?

非ASCIIを許容する文字処理・マルチバイト処理において、とくに文字制限の要件があるとハマりやすいポイントになりそう。

  • length() で文字数判定して分岐したり弾いたりする
  • right()left() substring() で文字列をカットする
    • 文字が分断されて文字化け、表示時に違う文字とくっついて認識される
  • 100 文字まで許容だから varchar(100) としてカラム定義した
    • データ挿入・更新時に 100 文字に満たない状態で長さエラーで失敗する
    • 例: ERROR: value too long for type character varying

おわりに

ひとくちに「文字数」と表現しても、それを処理系(今回は PostgreSQL)がどう認識しているかをじっくり調べてみて色々と面白い発見になりました。特に Unicode の制御系が学びになった。これはこれで1つの記事になりそう。

MySQL や SQL Server では違う結果になるんでしょうか、折を見て調べたいですね。

それではまた!

コラボスタイル Developers

Discussion