SQL Server：文字コードへの理解を深める

SQL Serverにおける文字コードについていろいろと調べたことをまとめます。

#SQL Serverの文字コードについて
SQL Serverでは、nchar/nvarcharについてはUTF-16が固定で採用され、char/varcharについては、そのカラムに適用される照合順序に応じた文字コードが採用されます。
したがって、nchar/nvarcharについては照合順序の指定によって変化するのは文字列比較時の挙動だけとなります。

#実験

サンプルテーブルを作成し、データをINSERTして中身をSELECTしてみます。
c1は「japanese_ci_as」を照合順序として指定しており、対応する文字コードは「Shift-JIS」です。
c2は「latin1_general_ci_as」を照合順序として指定しており、対応する文字コードは「Latin-1」です。

```sql
create table sample_table
(
c1 varchar(10) collate japanese_ci_as
,c2 varchar(10) collate latin1_general_ci_as
)

insert into sample_table (c1, c2) values ('A', 'A')
insert into sample_table (c1, c2) values ('Ａ', 'Ａ')
insert into sample_table (c1, c2) values ('あ', 'あ')

select * from sample_table
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/689bf22b-603e-154d-fca8-b22375daec7f.png)
SELECT結果です。全角の「Ａ」については、半角に変換されてINSERTされる挙動のようです。
また、Latin-1のカラムに対して「あ」をINSERTした場合、SELECT結果は「?」となりました。

ページの中身を確認します。
まずは「sys.fn_PhysLocCracker」で該当カラムが格納されているページ番号を取得します。

```sql
select * from sample_table
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/29b0f71f-c6db-b959-f2e2-930c665d48b4.png)

```sql
DBCC TRACEON(3604)
DBCC PAGE (N'TEST', 1, 520, 3) 
DBCC TRACEOFF(3604)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/953083ec-4638-bb9b-7225-49fa40bbf0e3.png)
1レコード目の結果です。col1/col2の半角「A」が格納されており、その値はアスキーコードの0x41であることが分かります。
このように、ページの中身をみることで、文字がどういった数値として格納されているかを確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/80accb55-8490-bcfb-d1e7-32cbb592846e.png)
2レコード目の結果です。col1は全角の「Ａ」で、col2はSQL Serverの挙動で半角の「A」に変換されて入っていることが、実際の値としても確認できました。「0x8260」は、Shift-Jisの全角の「Ａ」に該当していると予想されます。確認すると、実際にそのようになっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/8d94e5ee-6315-cd1b-9a61-43e4353276b1.png)
参照元：https://uic.jp/charset/show/cp932/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/1f4376da-6bfd-49f3-6436-4df7d2325069.png)
3レコード目の結果です。0x82a0は「あ」に対応しており、0x3fはアスキーコードの「?」に対応しています。
SELECT時に「?」に変換されているわけではなく、本当に「?」という文字でDBに格納されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/aea3a914-a9e9-ad27-eef1-51b5e13340f8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/4875869b-71e7-8fc3-d6b7-99ce2cdca53e.png)
参照元：https://uic.jp/charset/show/cp932/


該当のコードページでは表現できない文字の場合、格納時にアスキーコードにおける「?」に変換される仕様になっているようです。

次に、「aあa」という、コードページに対応している文字と、未対応の文字をMIXしてINSERTしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/5d65ee37-179c-fcc8-3b2b-6352e2a4a23d.png)

「offset 0xd」というのは、col1が先頭から何バイト先にあるかを表しています。（0xdだと13バイト）
「Length 4」がそこから何バイトまでが該当データなのかを表しています。
つまり、↓のように、先頭から13バイト進んだ後の4バイトがカラムの値を実際に格納しているところである、と解釈できます。(1byte=16進数表記で2ケタ)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/079a168c-a7c6-adf9-f55c-7793ede7f86a.png)
格納されているデータは「0x6182a061」となります。
col2も同じ考え方で「0x613f61」となります。文字コードに対応している文字「a」は0x61でそのままINSERTされ、未対応文字「あ」については「?」である0x3fに変換されてINSERTされていることが分かります。

つまり、SQL Serverは文字コードで対応している文字はそのままINSERT、未対応文字は一律「?」でINSERTするということがわかりました。（全部を「?」ではなく、未対応だけを「?」に変換している）

# 各照合順序の文字コードを取得するクエリ
以下のクエリで、照合順序に対応する文字コードを特定できます。

```sql
select COLLATIONPROPERTY(name, 'CODEPAGE') as code_page, * from sys.fn_helpcollations()
where name like 'japanese_ci_as%' or name like 'latin1_general_ci_as%'
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/85e69c09-322d-1642-b4ef-b5f86718e3ec.png)
例えば「Japanese_CI_AS」はコードページ932(Shift-JIS)であると分かります。

※コードページと文字コードは同一のものを指しており、各コードページ値に対応する文字コードがあるイメージです。コードページに対応する文字コードは例えば[こちら](https://www.ipentec.com/document/windows-codepage-list)を参照

[こちら](https://www.ipentec.com/document/windows-codepage-list)を参照すると、コードページ1252はasciiではないことが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/ef91d36c-2027-2ebc-1703-0d0b1d0f4858.png)
Latin-1の照合順序はwindowsで使われているコードページ1252となっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/edc771da-7020-50ab-2792-c1fe3540f2e5.png)
[wikipedia](https://en.wikipedia.org/wiki/Windows-1252)をみるとコードページ1252の文字コードは分類が「extended ASCII」と書いてあり、asciiを拡張した文字コードであることが分かります。したがって0x7fまではASCIIと共通となっているはずです。

#各カラムのコードページ確認方法

各カラムのコードページは以下のクエリで確認することができます。
ただし、nvarchar/ncharには対応しておりません。

```sql
SELECT object_name(a.object_id) AS table_name
	,a.name AS column_name
	,a.collation_name
	,b.*
FROM (
	SELECT *
	FROM sys.all_columns
	) AS a
LEFT JOIN (
	SELECT COLLATIONPROPERTY(name, 'CODEPAGE') AS code_page
		,*
	FROM sys.fn_helpcollations()
	) AS b ON a.collation_name = b.name
WHERE code_page IS NOT NULL
ORDER BY object_name(a.object_id)
	,a.name
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/6b413ac4-0991-ac6a-e956-545918f93c7e.png)

#utf-8とutf-16で同じ文字でも格納されるサイズが違うことの確認
###utf-16

```sql
create table sample_table_n
(
c1 nvarchar(10) collate japanese_ci_as
,c2 nvarchar(10) collate latin1_general_ci_as
)

insert into sample_table_n (c1, c2) values ('A', 'A')
insert into sample_table_n (c1, c2) values ('あ', 'あ')

select * from sample_table_n

select * from sample_table_n
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%)

DBCC TRACEON(3604)
DBCC PAGE (N'TEST', 1, 528, 3)
DBCC TRACEOFF(3604)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/98926560-92c4-8dc8-e6ec-9b1010f2ff57.png)

nvarchar/ncharの場合はutf-16固定となるため、半角英数字でも2バイト消費していることが分かります。(varcharの場合は半角「A」は1バイト）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/3e7f7525-065a-152c-6bf4-8b264fcb472c.png)
「あ」はutf-16でいうと0x3042となります。DBCC PAGEの結果はバイトが逆になって表示されるため、4230が「あ」に該当していると分かります。

ちなみに、任意の文字のutf-16の文字コードは以下のクエリで確認できます。

```sql
SELECT CONVERT(binary(3),UNICODE(N'あ'),1)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/2dc275f4-6cd2-e3a1-7474-6c77738203e1.png)

###utf-8
SQL Server2019からは、vchar/charに対してutf-8を指定することも可能になりました。

```sql
select COLLATIONPROPERTY(name, 'CODEPAGE') as code_page, * from sys.fn_helpcollations()
where name like 'japanese_%utf8%' or name like 'latin1_%utf8%'
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/b7432bf6-df36-44c0-4d7e-cbf4a40ccf77.png)

```sql
create table sample_table_utf8
(
c1 varchar(10) collate Japanese_XJIS_140_CI_AS_UTF8
,c2 varchar(10) collate Latin1_General_100_CI_AS_SC_UTF8
)

insert into sample_table_utf8 (c1, c2) values ('A', 'A')
insert into sample_table_utf8 (c1, c2) values ('あ', 'あ')

select * from sample_table_utf8

select * from sample_table_utf8
CROSS APPLY sys.fn_PhysLocCracker(%%physloc%%)

DBCC TRACEON(3604)
DBCC PAGE (N'TEST', 1, 536, 3)
DBCC TRACEOFF(3604)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/673d9ff1-f9ce-7eed-bde7-a1ab37c9c01e.png)

utf-16の時と違って、半角文字は1バイトで格納されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/67f88a3b-029c-8eb7-3bdf-a3bf04bc0194.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148310/60aacad2-8096-d0b4-dfe8-49683c38f122.png)

utf-8の場合は全角で3バイトつかっていることが分かります。

したがって、全角文字と半角文字が混在して格納されるカラムの場合は、全角文字が多ければUTF-16を、半角文字が多ければUTF-8を採用したほうが格納サイズを小さく保つことができるといえます。

#まとめ
いろいろと検証して、文字コードへの理解が深まりました。