PostgreSQL Advent Calendar 9日目用のエントリです。
PostgreSQL 10のICUコレーション(照合順序)サポートの概要と基本的な使い方は以下のエントリに記載しています。ICUコレーションの使い方は以下を参照してください。
今回は日本語ソート順のJIS規格である JIS X 4061-1996にどの程度対応しているのか確かめてみます。
TL;DR; 仕様書にソート順を書く場合、JIS規格のソート順ではなく、Unicode技術標準(特定ICU バージョン)のソート順序であることを明記しておくとよい。
テストにはICU 60と一緒にビルドしたPostgreSQL gitのREL_10_STABLEブランチを使用しました。
JIS X 4061とテストデータ
まず規格を確認する必要があります。JIS検索のページ
http://www.jisc.go.jp/app/jis/general/GnrJISSearch.html
からJIS規格は参照できます。JIS X 4061を読むと、かなり複雑な比較ルールであることが解ります。意図通りにソートされているか確認する為のサンプルが欲しいです。
JIS X 4061にはテスト用のデータが記載されています。
漢字の照合順序も定義していますが、テストデータにはありません。
いくつかバグと思われる順序もあります。「チータ g」は”た行”としては「ちょこ」の前にくるはずですが、「テェタ G」の前になっています。「びゆーあー」< 「ビューアー」となっています。これは平仮名、片仮名、濁音、捨て仮名、長音、長音の特殊ルールが関連したソート結果になります。「びゆーあー」の位置が規格書の仕様に対して正しいのか、判断が難しいです。
PostgreSQLのマニュアルにはJIS X 4061用に必要な照合順序設定が記載されていません。そこでICUを使っているDB2のマニュアルを見てみます。ks-level4(ksは比較の強さ)とひらがな属性が有効な場合にJIS X 4061のソート順序になると記載されています。
Unicode技術標準書を見ると現在ではひらがな属性は廃止されています。ICU 60で利用しようとするとエラーになります。代わりにlevel4の比較の強さを使うようにと記載されています。
実際にテスト用データをソートしてみる
テスト用データは以下のSQLで準備しました。
INSERT INTO jis2 (t) VALUES ('∞ r ∞'), ('∞ R #'), ('∞ t ∞'), ('# r ∞'), ('# R #'), ('# t %'), ('# T %'), ('8 t ∞'), ('8 T ∞'), ('8 t #'), ('8 T #'), ('8 t %'), ('8 T %'), ('8 t 8'), ('8 T 8'), ('ω r ∞'), ('Ω R %'), ('r r ∞'), ('r R ∞'), ('R r ∞'), ('R R ∞'), ('R T %'), ('r t 8'), ('t r ∞'), ('t r 8'), ('T R 8'), ('t t 8'), ('シャーレ'), ('シャイ'), ('シヤィ'), ('シャレ'), ('ちょこ'), ('ちよこ'), ('チョコレート'), ('てーた'), ('テータ'), ('テェタ'), ('てえた'), ('でーた'), ('データ'), ('デェタ'), ('でえた'), ('チータ g'), ('テェタ G'), ('てぇた g'), ('てぇた G'), ('てーたー'), ('テータァ'), ('てーたあ'), ('テェター'), ('てぇたぁ'), ('てえたー'), ('でーたー'), ('データァ'), ('でェたァ'), ('デぇタぁ'), ('デエタア'), ('ひゆ'), ('びゅあ'), ('ぴゅあ'), ('びゅあー'), ('ビュアー'), ('ぴゅあー'), ('ピュアー'), ('ヒュウ'), ('ヒユウ'), ('ビュウア'), ('びゆーあー'), ('ビューアー'), ('ビュウアー'), ('ひゅん'), ('ぴゅん'), ('ふーり'), ('フーリ'), ('ふぅリ'), ('ふゥリ'), ('ふゥり'), ('フウリ'), ('ブーリ'), ('ぶぅり'), ('ブゥり'), ('ぷうり'), ('プウリ'), ('ふーりー'), ('フゥリー'), ('ふゥりィ'), ('フぅりぃ'), ('フウリー'), ('ふうりぃ'), ('ブウリイ'), ('ぷーりー'), ('ぷゥりイ'), ('ぷうりー'), ('プウリイ'), ('フヽ'), ('ふゞ'), ('ぶゝ'), ('ぶふ'), ('ぶフ'), ('ブふ'), ('ブフ'), ('ぶゞ'), ('ぶぷ'), ('ブぷ'), ('ぶーり'), ('ぷゝ'), ('プヽ'), ('ぷふ');
%は全角にも見えますが、他の記号や英数字は半角なので半角を利用しています。
CREATE COLLATIONで以下のコレーションを作成します。
CREATE COLLATION lev1 (provider = icu, locale = 'ja-u-ks-level1'); CREATE COLLATION lev2 (provider = icu, locale = 'ja-u-ks-level2'); CREATE COLLATION lev3 (provider = icu, locale = 'ja-u-ks-level3'); CREATE COLLATION lev4 (provider = icu, locale = 'ja-u-ks-level4'); CREATE COLLATION ulev4 (provider = icu, locale = 'und-u-ks-level4');
一つ一つの実行結果を目視で比較するのは手間なので、psqlの-cオプションを使い、以下のような要領で
/usr/local/pgsql-10/bin/psql -p 5410 -h 127.0.0.1 test7 -c ‘select * from jis2 order by t collate “ulev4”;’ > ulev4.txt
ソート結果をテキストファイルに保存します。
lev1.txt、lev2.txt、lev3.txt、lev4.txt、ulev4.txt 、C.txt(バイナリ比較)、ja_JP.txt(libcの照合順序)、def.txt(JISの試験データの順序)
ja-u-ks-level4(lev4.txt)の場合、JIS X 4061相当のソート順序になるハズです。
lev4.txtの中身
ks-level4でソートした結果は以下の通りです。
Timing is on. Null display is "¤". You are connected to database "test7" as user "yohgaki" on host "127.0.0.1" at port "5410". t -------------- # R # # r ∞ # t % # T % ∞ R # ∞ r ∞ ∞ t ∞ 8 t # 8 T # 8 t % 8 T % 8 t ∞ 8 T ∞ 8 t 8 8 T 8 r r ∞ r R ∞ R r ∞ R R ∞ R T % r t 8 t r ∞ t r 8 T R 8 t t 8 シャーレ シャイ シヤィ シャレ チータ g ちょこ ちよこ チョコレート てーた テータ テェタ てえた でーた データ デェタ でえた てぇた g てぇた G テェタ G てーたー テータァ てーたあ テェター てぇたぁ てえたー でーたー データァ でェたァ デぇタぁ デエタア ひゆ びゅあ ぴゅあ びゅあー ビュアー ぴゅあー ピュアー ヒュウ ヒユウ ビュウア ビューアー ビュウアー びゆーあー ひゅん ぴゅん ふーり フーリ ふぅリ ふゥり ふゥリ フウリ ぶーり ブーリ ぶぅり ブゥり ぷうり プウリ ふーりー フゥリー ふゥりィ フぅりぃ フウリー ふうりぃ ブウリイ ぷーりー ぷゥりイ ぷうりー プウリイ フヽ ふゞ ぶゝ ぶふ ぶフ ブふ ブフ ぶゞ ぶぷ ブぷ ぷゝ プヽ ぷふ Ω R % ω r ∞ (108 rows) Time: 11.513 ms
JIS X 4061のソート結果サンプルとは異る結果になっています。記号のソート順序が異ります。また以下のωなどは最後の方にソートされており明らかに異なります。
Ω R % ω r ∞
試験データ通りの場合との差分は以下の通りです。
[yohgaki@dev ~]$ diff -u def.txt lev4.txt --- def.txt 2017-12-07 13:15:54.519062304 +0900 +++ lev4.txt 2017-12-07 12:44:34.656825846 +0900 @@ -3,23 +3,21 @@ You are connected to database "test7" as user "yohgaki" on host "127.0.0.1" at port "5410". t -------------- - ∞ r ∞ - ∞ R # - ∞ t ∞ - # r ∞ # R # + # r ∞ # t % # T % - 8 t ∞ - 8 T ∞ + ∞ R # + ∞ r ∞ + ∞ t ∞ 8 t # 8 T # 8 t % 8 T % + 8 t ∞ + 8 T ∞ 8 t 8 8 T 8 - ω r ∞ - Ω R % r r ∞ r R ∞ R r ∞ @@ -34,6 +32,7 @@ シャイ シヤィ シャレ + チータ g ちょこ ちよこ チョコレート @@ -45,10 +44,9 @@ データ デェタ でえた - チータ g - テェタ G てぇた g てぇた G + テェタ G てーたー テータァ てーたあ @@ -70,9 +68,9 @@ ヒュウ ヒユウ ビュウア - びゆーあー ビューアー ビュウアー + びゆーあー ひゅん ぴゅん ふーり @@ -111,6 +109,8 @@ ぷゝ プヽ ぷふ + Ω R % + ω r ∞ (108 rows) -Time: 0.739 ms +Time: 11.513 ms
「チータ g」以外に「びゆーあー」「テェタ G」の順序も異なっています。(JIS規格の照合ルールを精査していないので「びゆーあー」「テェタ G」もサンプルの誤り?DB2でも試してみたいことろですが省略)
とにかく、ざっと比較の強度が変わるとどうなるか見てみましょう。
def.txtとlev1.txtの差分
[yohgaki@dev ~]$ diff -u def.txt lev1.txt --- def.txt 2017-12-07 12:49:15.169710398 +0900 +++ lev1.txt 2017-12-07 12:44:48.424771098 +0900 @@ -3,114 +3,114 @@ You are connected to database "test7" as user "yohgaki" on host "127.0.0.1" at port "5410". t -------------- - ∞ r ∞ - ∞ R # - ∞ t ∞ - # r ∞ # R # - # t % + # r ∞ # T % - 8 t ∞ - 8 T ∞ - 8 t # + # t % + ∞ R # + ∞ r ∞ + ∞ t ∞ 8 T # - 8 t % + 8 t # 8 T % - 8 t 8 + 8 t % + 8 T ∞ + 8 t ∞ 8 T 8 - ω r ∞ - Ω R % - r r ∞ - r R ∞ - R r ∞ + 8 t 8 R R ∞ + R r ∞ + r R ∞ + r r ∞ R T % r t 8 t r ∞ - t r 8 T R 8 + t r 8 t t 8 シャーレ シャイ シヤィ シャレ + チータ g ちょこ ちよこ チョコレート - てーた - テータ - テェタ てえた + てーた + でえた でーた - データ + テェタ + テータ デェタ - でえた - チータ g - テェタ G - てぇた g + データ てぇた G - てーたー - テータァ - てーたあ - テェター + てぇた g + テェタ G てぇたぁ てえたー - でーたー - データァ + てーたあ + てーたー でェたァ + でーたー + テェター + テータァ デぇタぁ デエタア + データァ ひゆ びゅあ ぴゅあ びゅあー - ビュアー ぴゅあー + ビュアー ピュアー ヒュウ ヒユウ ビュウア びゆーあー - ビューアー ビュウアー + ビューアー ひゅん ぴゅん - ふーり - フーリ ふぅリ - ふゥリ ふゥり - フウリ - ブーリ + ふゥリ + ふーり ぶぅり - ブゥり + ぶーり ぷうり + フウリ + フーリ + ブゥり + ブーリ プウリ - ふーりー - フゥリー + ふうりぃ ふゥりィ + ふーりー + ぷうりー + ぷゥりイ + ぷーりー フぅりぃ + フゥリー フウリー - ふうりぃ ブウリイ - ぷーりー - ぷゥりイ - ぷうりー プウリイ - フヽ ふゞ - ぶゝ ぶふ + ぶぷ + ぶゝ + ぶゞ ぶフ + ぷふ + ぷゝ + フヽ ブふ - ブフ - ぶゞ - ぶぷ ブぷ - ぶーり - ぷゝ + ブフ プヽ - ぷふ + Ω R % + ω r ∞ (108 rows) -Time: 0.505 ms +Time: 11.195 ms
lev1.txtとlev2.txtの差分
[yohgaki@dev ~]$ diff -u lev1.txt lev2.txt --- lev1.txt 2017-12-07 12:44:48.424771098 +0900 +++ lev2.txt 2017-12-07 12:44:44.093788320 +0900 @@ -38,10 +38,10 @@ チョコレート てえた てーた - でえた - でーた テェタ テータ + でえた + でーた デェタ データ てぇた G @@ -51,10 +51,10 @@ てえたー てーたあ てーたー - でェたァ - でーたー テェター テータァ + でェたァ + でーたー デぇタぁ デエタア データァ @@ -62,8 +62,8 @@ びゅあ ぴゅあ びゅあー - ぴゅあー ビュアー + ぴゅあー ピュアー ヒュウ ヒユウ @@ -77,40 +77,40 @@ ふゥり ふゥリ ふーり - ぶぅり - ぶーり - ぷうり フウリ フーリ + ぶぅり + ぶーり ブゥり ブーリ + ぷうり プウリ ふうりぃ ふゥりィ ふーりー - ぷうりー - ぷゥりイ - ぷーりー フぅりぃ フゥリー フウリー ブウリイ + ぷうりー + ぷゥりイ + ぷーりー プウリイ + フヽ ふゞ ぶふ - ぶぷ ぶゝ - ぶゞ ぶフ - ぷふ - ぷゝ - フヽ ブふ - ブぷ ブフ + ぶゞ + ぶぷ + ブぷ + ぷふ + ぷゝ プヽ Ω R % ω r ∞ (108 rows) -Time: 11.195 ms +Time: 10.512 ms
lev2.txtとlev3.txtの差分
[yohgaki@dev ~]$ diff -u lev2.txt lev3.txt --- lev2.txt 2017-12-07 12:44:44.093788320 +0900 +++ lev3.txt 2017-12-07 12:44:38.851809165 +0900 @@ -5,28 +5,28 @@ -------------- # R # # r ∞ - # T % # t % + # T % ∞ R # ∞ r ∞ ∞ t ∞ - 8 T # 8 t # - 8 T % + 8 T # 8 t % - 8 T ∞ + 8 T % 8 t ∞ - 8 T 8 + 8 T ∞ 8 t 8 - R R ∞ - R r ∞ - r R ∞ + 8 T 8 r r ∞ + r R ∞ + R r ∞ + R R ∞ R T % r t 8 t r ∞ - T R 8 t r 8 + T R 8 t t 8 シャーレ シャイ @@ -36,28 +36,28 @@ ちょこ ちよこ チョコレート - てえた てーた - テェタ テータ - でえた + テェタ + てえた でーた - デェタ データ - てぇた G + デェタ + でえた てぇた g + てぇた G テェタ G - てぇたぁ - てえたー - てーたあ てーたー - テェター テータァ - でェたァ + てーたあ + テェター + てぇたぁ + てえたー でーたー + データァ + でェたァ デぇタぁ デエタア - データァ ひゆ びゅあ ぴゅあ @@ -68,49 +68,49 @@ ヒュウ ヒユウ ビュウア - びゆーあー - ビュウアー ビューアー + ビュウアー + びゆーあー ひゅん ぴゅん + ふーり + フーリ ふぅリ ふゥり ふゥリ - ふーり フウリ - フーリ - ぶぅり ぶーり - ブゥり ブーリ + ぶぅり + ブゥり ぷうり プウリ - ふうりぃ - ふゥりィ ふーりー - フぅりぃ フゥリー + ふゥりィ + フぅりぃ フウリー + ふうりぃ ブウリイ - ぷうりー - ぷゥりイ ぷーりー + ぷゥりイ + ぷうりー プウリイ フヽ ふゞ - ぶふ ぶゝ + ぶふ ぶフ ブふ ブフ ぶゞ ぶぷ ブぷ - ぷふ ぷゝ プヽ + ぷふ Ω R % ω r ∞ (108 rows) -Time: 10.512 ms +Time: 14.765 ms
lev3.txtとlev4.txtの差分
level3とlevel4では少なくとも記号や英数字、仮名での照合順序の違いはないようです。
[yohgaki@dev ~]$ diff -u lev3.txt lev4.txt --- lev3.txt 2017-12-07 12:44:38.851809165 +0900 +++ lev4.txt 2017-12-07 12:44:34.656825846 +0900 @@ -113,4 +113,4 @@ ω r ∞ (108 rows) -Time: 14.765 ms +Time: 11.513 ms
lev4.txtのulev4.txtの差分
lev4はja-u-ks-level4、ulev4はund-u-ks-level4ロケールの照合順序の結果です。(jaは日本語、undは言語未定義のロケール)全角を含む英数字ソートの場合、jaとundロケールとの違いはあまりありません。今回のテストデータではかなり違うことが判ります。
[yohgaki@dev ~]$ diff -u lev4.txt ulev4.txt --- lev4.txt 2017-12-07 12:44:34.656825846 +0900 +++ ulev4.txt 2017-12-07 13:27:39.314642846 +0900 @@ -28,6 +28,8 @@ t r 8 T R 8 t t 8 + Ω R % + ω r ∞ シャーレ シャイ シヤィ @@ -38,27 +40,29 @@ チョコレート てーた テータ - テェタ - てえた でーた データ - デェタ + てーたー + でーたー + てーたあ + テータァ + データァ + てえた + テェタ でえた + デェタ てぇた g てぇた G テェタ G - てーたー - テータァ - てーたあ + てえたー テェター てぇたぁ - てえたー - でーたー - データァ でェたァ デぇタぁ デエタア ひゆ + びゆーあー + ビューアー びゅあ ぴゅあ びゅあー @@ -68,49 +72,45 @@ ヒュウ ヒユウ ビュウア - ビューアー ビュウアー - びゆーあー ひゅん ぴゅん + ふゞ + ぶゝ + ぶゞ + ぷゝ ふーり フーリ + ぶーり + ブーリ + ふーりー + ぷーりー + フヽ + プヽ ふぅリ ふゥり ふゥリ フウリ - ぶーり - ブーリ ぶぅり ブゥり ぷうり プウリ - ふーりー フゥリー - ふゥりィ - フぅりぃ フウリー + ぷうりー ふうりぃ + ふゥりィ + フぅりぃ ブウリイ - ぷーりー ぷゥりイ - ぷうりー プウリイ - フヽ - ふゞ - ぶゝ ぶふ ぶフ ブふ ブフ - ぶゞ ぶぷ ブぷ - ぷゝ - プヽ ぷふ - Ω R % - ω r ∞ (108 rows) -Time: 11.513 ms +Time: 14.419 ms
他のサンプルソート例との比較
JIS X 4061にはサンプルのソート例が幾つか載っています。
これらのデータを入れてソートしてみます。
yohgaki@127 test7=# select * from jis order by t collate "lev4"; t ---------- さと さど さとう さどう さとうや サトー さとおや しょう しよう じょう じよう しょうし しょうじ しようじ じょうし じょうじ ショー ショオ ジョー じょおう ジョージ ファール ぶあい ファウル ファウル ファン ふあん フアン ぶあん 沢田 沢島 沢嶋 澤田 澤島 澤嶋 (35 rows) Time: 0.686 ms
PostgreSQLは仮名に関してはサンプルのソート順と同じ結果を出力します。
漢字(名字)もJIS規格に記載されていたサンプルデータですが、こちらの順序は規格書に記載された順序にはなりませんでした。
念の為、日本語のソート順でややこしい”ー”をチェックしてみます。
yohgaki@127 test7=# select 'アー' collate "lev4" < 'アイ' collate "lev4"; ?column? ---------- t (1 row) Time: 0.254 ms yohgaki@127 test7=# select 'アー' collate "C" < 'アイ' collate "C"; ?column? ---------- f (1 row)
正しく比較されています。
まとめ
日本語(ja)でレベル4の比較の強度(ks-level4)
Quaternary Level: When punctuation is ignored (see Ignoring Punctuations (§)) at level 1-3, an additional level can be used to distinguish words with and without punctuation (for example, “ab” < “a-b” < “aB”). This difference is ignored when there is a primary, secondary or tertiary difference. This is also known as the level-4 strength. The quaternary level should only be used if ignoring punctuation is required or when processing Japanese text (see Hiragana processing (§)).
http://userguide.icu-project.org/collation/concepts
を使えばカスタマイズ無しでJIS X 4061定義のソート順序になるかも、と期待していましたが、平仮名/片仮名の順序、記号の順序などを変えないテスト用データ通りにはなりません。
平仮名/片仮名は概ねJISの試験用データに近いソート順序になりましたが、試験データの「チータ g」の位置は明らかにおかしいので、規格書に書いてあるとはいえ明らかな誤りがある試験データが確実に正しい、とするには疑問符がつきます。「適合性試験用データ」には「規定の一部ではない」との但し書きもあります。
JIS規格末尾のテスト用データのソート結果を見ると順序を調整しないとダメなように見えますが、JIS規格本文(12p)の「ファン ふあん フアン」の平仮名/片仮名の順序はks-level3/4で正しくソートされています。長音の「ー」の処理も正しいようです。Unicode Technical Standard #10の 1.3 Contextual Sensitivityに記載されている「カー < カア, but キー > キア」も正しく処理できています。
しかし、JIS規格末尾のテスト用データのソート結果からはPostgreSQL 10 + ICU 60 のja-u-ks-level4はJIS規格の平仮名/片仮名ソートに問題があるように見えます。それでも実は規格書の仕様通り(?)なのかも知れません。記号と漢字のソート順は明らかにJIS規格とは異なります。
たかがソート、されどソート、思った様にソートするのはなかなか難しいです。混乱の元なのでJIS規格は間違っていると思われる箇所を修正すると共に、より詳細かつ完全なテストデータを規格と一緒に配布してもらえると助かります。いっそのこと、Unicodeの策定に関わりUnicode規格をそのままJIS規格にした方が話が早いですね。
PostgreSQL 10がICU照合順序をサポートしているとはいっても、JIS X 4061でソートできます!と安請け合いはしない方が良さそうです。そもそもJIS X 4061は20年も前の規格でエラータさえ修正されていません。最新のUnicode技術標準に準拠した方が良いでしょう。
結論: 仕様書にソート順を書く場合、JIS規格のソート順ではなく、Unicode技術標準(特定ICU バージョン)のソート順序であることを明記しておくとよい。
参考リンク
- http://www.unicode.org/repos/cldr/trunk/common/bcp47/collation.xml
- https://tools.ietf.org/html/bcp47
- http://unicode.org/reports/tr35/tr35-collation.html
- http://www.unicode.org/reports/tr10/
- http://unicode.org/reports/tr35/tr35-collation.html
- https://ssl.icu-project.org/icu-bin/locexp?_=ja_JP&SHOWScripts=1#Scripts
- http://kikakurui.com/x4/X4061-1996-01.html
- http://www.jisc.go.jp/app/jis/general/GnrJISSearch.html
- https://www.postgresql.org/docs/10/static/collation.html