シーケンスの代わりにuuidをIDとして使う

Heroku PostgreSQL 担当の人が id には uuid を使えと発表していたのでメモ。(時間がない人は、↓の動画の15分くらいからを見ましょう)

stop using numbers as IDs.
just use UUIDs. seriously
— Postgres: The Bits You Haven’t Found by pvh

UUID の違い

v1
Generate a UUID from a host ID, sequence number, and the current time.

v3
Generate a UUID from the MD5 hash of a namespace UUID and a name.

v4
Generate a random UUID

v5
Generate a UUID from the SHA-1 hash of a namespace UUID and a name.

この内、ID として利用できるのは v1 と v4 の2つ。v1 は最後 48 ビットがハード固有のノードを表し、uuid から生成時刻も復元できる。
各バージョンの違いは、この辺りの uuid ライブラリ実装者からもコメントも参考になる。

pros/cons

スライドには詳細が書かれていないので hacker news の投稿から掘り起こす。

uuid の一番のメリットは、universally unique な ID であること。(UUID そのままですが)

The biggest reason is that your IDs become universally unique – across shards, across database recoveries, rollbacks and session problems, you name it. These are all the kinds of things that can happen over the lifespan of a dataset.
https://news.ycombinator.com/item?id=5310637

これ以外にも

  • 外と通信せずに重複しない ID を採番できる
  • ID を推測できない。ID を外に公開するようなシステムの場合、特に重要。
  • うっかり別の ID を渡したり、別の ID で処理する確率がシーケンスの場合に比べて低い

などがある。
https://news.ycombinator.com/item?id=5311271

instagram ID との比較

Instagram が "timestamp + shard id + sequence" で ID を生成するようにした時、uuid も検討している。

Instagram にとっては ID が時系列順にソートされる必要があり、そのために "timestamp + uuid" で ID 生成することを検討していたが、データサイズが増えることもあり、見送っている。
また、Instagram 式では ID からどのシャードにあるのか簡単に特定できるので、運用面では助かっているそうだ。

ここからは、PostgreSQL で uuid を実際に使ってみる。

uuid モジュール(uuid-ossp)をインストール

スーパーユーザで実行する

# \dx
                 List of installed extensions
  Name   | Version |   Schema   |         Description
---------+---------+------------+------------------------------
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(1 row)

# CREATE EXTENSION "uuid-ossp";
CREATE EXTENSION
# \dx
                            List of installed extensions
   Name    | Version |   Schema   |                   Description
-----------+---------+------------+-------------------------------------------------
 plpgsql   | 1.0     | pg_catalog | PL/pgSQL procedural language
 uuid-ossp | 1.0     | public     | generate universally unique identifiers (UUIDs)
(2 rows)

PostgreSQL から uuid を生成

バージョン 1, 3, 4, 5 に対応している。

# select uuid_generate_v1();
           uuid_generate_v1
--------------------------------------
 e4ca39c4-8d9f-11e2-9f70-000c294a4696
(1 row)

# SELECT uuid_generate_v3(uuid_ns_url(), 'http://www.postgresql.org');
           uuid_generate_v3
--------------------------------------
 cf16fe52-3365-3a1f-8572-288d8d2aaa46
(1 row)

# SELECT uuid_generate_v4();
           uuid_generate_v4
--------------------------------------
 e630decd-ea28-4906-98ab-64e665183cc3
(1 row)

# SELECT uuid_generate_v5(uuid_ns_url(), 'http://www.postgresql.org');
           uuid_generate_v5
--------------------------------------
 e1ee1ad4-cd4e-5889-962a-4f605a68d94e
(1 row)

テーブルにデータを挿入

ID カラムのデフォルトを uuid v4 に設定し、データを挿入する。

CREATE TABLE t (
  uuid uuid PRIMARY KEY
            DEFAULT uuid_generate_v4(),
  name text);

# insert into t(name) values('a');
INSERT 0 1
# insert into t(name) values('b');
INSERT 0 1
# insert into t(name) values('c');
INSERT 0 1
# select * from t;
                 uuid                 | name
--------------------------------------+------
 ca130384-646d-459f-88e7-6ab9aa363070 | a
 1b03ade0-54f3-4875-bf4d-a4135bf66cbc | b
 ca6b4e72-e915-4359-8acc-3a0ea4348652 | c
(3 rows)

インデックスサイズを比較

100万データを serial と uuid それぞれで生成し、インデックスのサイズを比較。

CREATE TABLE t_uuid (
  uuid uuid PRIMARY KEY
            DEFAULT uuid_generate_v4(),
  num integer);
CREATE TABLE t_serial (
  id serial primary key,
  num integer);

# insert into t_uuid(num) select n from generate_series(1, 1000000) as n;
INSERT 0 1000000

# insert into t_serial(num) select n from generate_series(1, 1000000) as n;
INSERT 0 1000000

-- http://wiki.postgresql.org/wiki/Index_Maintenance#Index_size.2Fusage_statistics
SELECT
    t.tablename,
    indexname,
    c.reltuples AS num_rows,
    pg_size_pretty(pg_relation_size(quote_ident(t.tablename)::text)) AS table_size,
    pg_size_pretty(pg_relation_size(quote_ident(indexrelname)::text)) AS index_size,
    CASE WHEN x.is_unique = 1  THEN 'Y'
       ELSE 'N'
    END AS UNIQUE,
    idx_scan AS number_of_scans,
    idx_tup_read AS tuples_read,
    idx_tup_fetch AS tuples_fetched
FROM pg_tables t
LEFT OUTER JOIN pg_class c ON t.tablename=c.relname
LEFT OUTER JOIN
       (SELECT indrelid,
           max(CAST(indisunique AS integer)) AS is_unique
       FROM pg_index
       GROUP BY indrelid) x
       ON c.oid = x.indrelid
LEFT OUTER JOIN
    ( SELECT c.relname AS ctablename, ipg.relname AS indexname, x.indnatts AS number_of_columns, idx_scan, idx_tup_read, idx_tup_fetch,indexrelname FROM pg_index x
           JOIN pg_class c ON c.oid = x.indrelid
           JOIN pg_class ipg ON ipg.oid = x.indexrelid
           JOIN pg_stat_all_indexes psai ON x.indexrelid = psai.indexrelid )
    AS foo
    ON t.tablename = foo.ctablename
WHERE t.schemaname='public'
ORDER BY 1,2;

 tablename |   indexname   | num_rows | table_size | index_size | unique | number_of_scans | tuples_read | tuples_fetched
-----------+---------------+----------+------------+------------+--------+-----------------+-------------+----------------
 t_serial  | t_serial_pkey |    1e+06 | 35 MB      | 21 MB      | Y      |               0 |           0 |              0
 t_uuid    | t_uuid_pkey   |    1e+06 | 50 MB      | 39 MB      | Y      |               0 |           0 |              0
(2 rows)

uuid のほうが容量を約2倍消費している。

References

One thought on “シーケンスの代わりにuuidをIDとして使う

Leave a comment