Identity columns

# はじめに
にゃーん
この記事は、[PostgreSQL 10全部ぬこ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/pg10-nuko-only) の19日目のエントリです。
なんというか、だいぶネタ切れ感が出てきましたね。つらい。ネタ自体は勿論多いんだけど、検証したり説明するのが面倒なものも多いのよなあ。
今日も小ネタですいませんです。

# 今回のお題
PostgreSQLのCREATE TABLE文は指定可能なオプションが非常に多くて、自分も未だに全部覚えていないのだが、今回のお題となったIdentity columnというのは、そのCREATE TABLE文の変更に関わるものである。

# ID列制約

PostgreSQL 9.6までの*constraint*の指定には、NOT NULL, NULL, CHECK, DEFAULT, UNIQUE, PRIMARY KEY, REFERENCESの種類があったが、PostgreSQL 10では、これに加えてGENERATED

```postgres
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ] 
```
という指定が追加された。
たとえば、以下のようにCREATE TABLE文を実行してみる。

```postgres-console
ident=# CREATE TABLE parent (id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, data text);
CREATE TABLE
```
作成したparentテーブルを見ると以下のようになっている。

```postgres-console
ident=# \d parent
                           Table "public.parent"
 Column |  Type   | Collation | Nullable |             Default              
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity
 data   | text    |           |          | 
Indexes:
    "parent_pkey" PRIMARY KEY, btree (id)
```
id列が、Default列にデフォルト値に自動生成された値が格納されるという設定になっている。

このparentテーブルにデータを格納してみる。

```postgres-console
ident=# INSERT INTO parent (data) VALUES ('aaa'),('bbb');
INSERT 0 2
ident=# TABLE parent;
 id | data 
----+------
  1 | aaa
  2 | bbb
(2 rows)
```
id列に自動生成された値が格納されているのがわかる。

じゃあ、id列に明示的に値を入れてみると・・・

```postgres-console
ident=# INSERT INTO parent (id, data) VALUES (3,'ccc');
INSERT 0 1
ident=# INSERT INTO parent (data) VALUES ('ddd');
ERROR:  duplicate key value violates unique constraint "parent_pkey"
DETAIL:  Key (id)=(3) already exists.
```
入るには入るのだが、後続のINSERTでidが被る場合には、フツーにunique constraintに引っかかる可能性があるので、明示的にid列に値を入れるのは、やっぱり避けたほうが良さげ。

## serial型と何が違うの？
実際、Serial型の挙動似ているんだけど、どうやらこの指定方式はSQL標準に準拠しているっぽい。

> These are similar to SERIAL columns, but are SQL standard compliant.

# CREATE TABLEのlike_optionへの変更
なんかリリースノートにきちんと書かれていないような気がするのだが、identity columnに関して、CREATE TABLE文にもう一つ変更が入っているっぽい。

```postgres
CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [
  { column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ]
    | table_constraint
    | LIKE source_table [ like_option ... ] }
    [, ... ]
] )
```

この中に```like_option```というオプションを指定する句があるが、PostgreSQL 10からは、このlike_optionにが```IDENTITY```キーワードが指定できるようになった。

* PostgreSQL 9.6まで

```postgres
and like_option is:

{ INCLUDING | EXCLUDING } { DEFAULTS | CONSTRAINTS | INDEXES | STORAGE | COMMENTS | ALL }
```


* PostgreSQL 10から

```postgres
and like_option is:

{ INCLUDING | EXCLUDING } { DEFAULTS | CONSTRAINTS | IDENTITY | INDEXES | STORAGE | COMMENTS | ALL }
```

では、この```IDENTITY```という指定は何か。

# IDENTITYの効果
## like_option(おさらい)
と、その前にlike_optionについて念のために説明。ていうか自分もあんまり使わないオプションなので復習も兼ねて書いてみる。

like_optionというのは、非常にざっくりいうと、*source_table*にあるテーブルの全ての列名、そのデータ型、制約を新しいテーブルにコピーする、というもの。よく似た継承(INHERITS)機能と異なるのは、これで作成したテーブルは、元になったテーブルの変更の影響を受けない、ということ。

like_optionをつけたCREATE TABLEの例を以下に示す。

まず、コピー元となるテーブルを作成する。

```postgres-console
ident=# CREATE TABLE parent (id int primary key, data text);
CREATE TABLE
```
次にparentテーブルを*source_table*に指定したchildというテーブルを作成する。

```postgres-console
ident=# CREATE TABLE child (LIKE parent, child_data text);
CREATE TABLE
```
この状態で、両方のテーブル定義を確認してみる。

```postgres-console
ident=# \d parent
               Table "public.parent"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 id     | integer |           | not null | 
 data   | text    |           |          | 
Indexes:
    "parent_pkey" PRIMARY KEY, btree (id)

ident=# \d child
                 Table "public.child"
   Column   |  Type   | Collation | Nullable | Default 
------------+---------+-----------+----------+---------
 id         | integer |           | not null | 
 data       | text    |           |          | 
 child_data | text    |           |          | 
```
コピー元のテーブル(parent)にあった、id, dataの列もchildテーブルにあることがわかる。

```postgres-console
ident=# \d parent
               Table "public.parent"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 id     | integer |           | not null | 
 data   | text    |           |          | 
Indexes:
    "parent_pkey" PRIMARY KEY, btree (id)

ident=# \d child
                 Table "public.child"
   Column   |  Type   | Collation | Nullable | Default 
------------+---------+-----------+----------+---------
 id         | integer |           | not null | 
 data       | text    |           |          | 
 child_data | text    |           |          | 
```
ただ、見てわかるように、childテーブルのid列には、もともとparentテーブルのid列に指定したあった、primary key指定はなく、またそれによって暗黙のうちに作成されるprimary keyインデックスは存在しない。
primary keyインデックスもコピーしたい場合には、like_optionとして、明示的に指定が必要になる。

```postgres-console
ident=# CREATE TABLE child (LIKE parent INCLUDING INDEXES, child_data text);
CREATE TABLE
ident=# \d child
                 Table "public.child"
   Column   |  Type   | Collation | Nullable | Default 
------------+---------+-----------+----------+---------
 id         | integer |           | not null | 
 data       | text    |           |          | 
 child_data | text    |           |          | 
Indexes:
    "child_pkey" PRIMARY KEY, btree (id)
```

## IDENTITY指定
前置きが長くなったが、PostgreSQL 10ではlike_optionとして、新たに```IDENTITY```という指定が可能になった。これは最初のほうに書いた、```GENERATED ... AS IDENTITY```に指定していた列のシーケンスもコピー先に反映させる、というものだ。

さっきのようにidentity columnをもったparentテーブルを作成して、それをソースとしたchildというテーブルを作成してみる。

まず、parentテーブルを作成。

```postgres-console
ident=# CREATE TABLE parent (id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, data text);
CREATE TABLE
ident=# \d parent
                           Table "public.parent"
 Column |  Type   | Collation | Nullable |             Default              
--------+---------+-----------+----------+----------------------------------
 id     | integer |           | not null | generated by default as identity
 data   | text    |           |          | 
Indexes:
    "parent_pkey" PRIMARY KEY, btree (id)
```
つぎに、「IDENTIY指定なし」のlike_optionでchildテーブルを作成する。

```postgres-console
ident=# CREATE TABLE child (LIKE parent , child_data text);
CREATE TABLE
ident=# \d child 
                 Table "public.child"
   Column   |  Type   | Collation | Nullable | Default 
------------+---------+-----------+----------+---------
 id         | integer |           | not null | 
 data       | text    |           |          | 
 child_data | text    |           |          | 
```
これだと、id列は単なるnot null制約のついたinteger型にしかすぎない。
で、childテーブルを一旦削除し、「IDENTIY指定あり」のlike_optionでchildテーブルを作成する。

```postgres-console
ident=# CREATE TABLE child (LIKE parent INCLUDING IDENTITY, child_data text);
CREATE TABLE
ident=# \d child
                              Table "public.child"
   Column   |  Type   | Collation | Nullable |             Default              
------------+---------+-----------+----------+----------------------------------
 id         | integer |           | not null | generated by default as identity
 data       | text    |           |          | 
 child_data | text    |           |          | 
```
今度はデフォルト時に自動生成されるという属性も引き継げた。
なお、Primary key指定による、暗黙のインデックス設定を引き継ぎたい場合には、INCLUDING INDEXESも併記しないといけないので注意。

```postgres-console
ident=# CREATE TABLE child (LIKE parent INCLUDING IDENTITY INCLUDING INDEXES, child_data text);
CREATE TABLE
ident=# \d child
                              Table "public.child"
   Column   |  Type   | Collation | Nullable |             Default              
------------+---------+-----------+----------+----------------------------------
 id         | integer |           | not null | generated by default as identity
 data       | text    |           |          | 
 child_data | text    |           |          | 
Indexes:
    "child_pkey" PRIMARY KEY, btree (id)
```

# おわりに

* PostgreSQLって独自のSQLコマンドや、独自の文法拡張もそれなりに多いんだけど、スタンスとしてはSQL標準にはなるべく従おうというスタンスなんだろうなあ。今回説明した、identtity columnsもその例なんだろう。
* like_option修正がリリースノートに明記されていないような気がする件は、docsのMLに投げておいたほうがいいんだろうかなあ。

# 参考：該当するリリースノート
本エントリに関連するPostgreSQL 10リリースノートの記載です。

E.2.3.5. Data Types

> * Add identity columns for assigning a numeric value to columns on insert (Peter Eisentraut)