UUIDは一意な値だが、ソートできない問題がある。
その課題を解決したソート可能な一意な値を生成できるULIDは非常に便利!!
ULIDとは
ULIDは、ソート可能な一意の識別子。
辞書順にソート可能であり、UUIDと128bitの互換性がある。
ULIDの構造は、48bitがタイムスタンプとなっており、80bitがランダムな値。
インストール方法
python-ulid
をインストールするだけ。
pip install python-ulid
使ってみた
基本的な使い方は下記のページに載っている。
- 現在のタイムスタンプからULIDオブジェクトを生成
現在のタイムスタンプからULIDオブジェクトを生成する場合は、ULID
を実行する。
# 現在のタイムスタンプからULIDオブジェクトを生成 ulid = ULID() print("ULID = " + str(ulid))
- タイムスタンプ指定でULIDオブジェクトを生成
指定のタイムスタンプからULIDオブジェクトを生成する場合、ULIDクラスのfrom_datetime
クラスメソッドを実行する。
# 2023-03-02 18:00:00 からULIDオブジェクトを生成 specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid = ULID.from_datetime(specfic_datetime) print("ULID = " + str(ulid))
- ULIDに含まれる日時を確認
ULIDに含まれる日時を確認する場合、datetime
プロパティを確認する。
# ulidに含まれる日時を確認 print("Datetime = " + ulid.datetime.strftime('%Y-%m-%d %H:%M:%S'))
- ULIDからUUIDを生成
ULIDからUUIDを生成する場合、to_uuid
メソッドを実行する。
# UUIDに変換 uuid_from_ulid = ulid.to_uuid() print("UUID = " + str(uuid_from_ulid))
- UUIDからULIDを生成
UUIDからULIDオブジェクトを生成する場合、ULIDクラスのfrom_uuid
クラスメソッドを実行する。
# UUIDからulidを生成 ULID.from_uuid(uuid_from_ulid) print("ULID from uuid = " + str(ulid2))
ソートしてみる
日時指定でULIDオブジェクトを生成する。このときに1日単位でずらして生成。
順序をばらばらに挿入したリストを生成して、sorted
関数でソートした結果を確認する。
def sort_execute(): # 日時指定で1日ずらして、ULIDオブジェクトを生成 specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid1 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid1 = " + str(ulid1)) specfic_datetime = datetime.datetime.strptime("2023-03-03 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid2 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid2 = " + str(ulid2)) specfic_datetime = datetime.datetime.strptime("2023-03-04 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid3 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid3 = " + str(ulid3)) specfic_datetime = datetime.datetime.strptime("2023-03-05 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid4 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid4 = " + str(ulid4)) # 整列した順番のリストを作成 expected_list = list() expected_list.append(ulid1) expected_list.append(ulid2) expected_list.append(ulid3) expected_list.append(ulid4) # 順番をばらばらにしたリストを作成 list_ulid = list() list_ulid.append(ulid2) list_ulid.append(ulid4) list_ulid.append(ulid1) list_ulid.append(ulid3) # 整列した順番のリストとばらばらなリストを比較 print("Before = " + str(list_ulid)) print("Is Same = " + str(expected_list == list_ulid)) # ソート後にソートしたリストと整列した順番のリストを比較 sorted_list = sorted(list_ulid) print("After = " + str(sorted_list)) print("Is Same = " + str(expected_list == sorted_list))
実行結果を確認すると、ソートできていることがわかる。
ulid1 = 0186a37b-1d00-223a-1718-56350b798aec ulid2 = 0186a8a1-7900-b053-3768-799da20ec1a3 ulid3 = 0186adc7-d500-9872-0ecd-d7341e5b680a ulid4 = 0186b2ee-3100-7041-5e91-7f7813f8d84d Before = [UUID('0186a8a1-7900-b053-3768-799da20ec1a3'), UUID('0186b2ee-3100-7041-5e91-7f7813f8d84d'), UUID('0186a37b-1d00-223a-1718-56350b798aec'), UUID('0186adc7-d500-9872-0ecd-d7341e5b680a')] Is Same = False After = [UUID('0186a37b-1d00-223a-1718-56350b798aec'), UUID('0186a8a1-7900-b053-3768-799da20ec1a3'), UUID('0186adc7-d500-9872-0ecd-d7341e5b680a'), UUID('0186b2ee-3100-7041-5e91-7f7813f8d84d')] Is Same = True
uuidモジュールで生成したuuidをULIDオブジェクトに変換できる?
from_uuidクラスメソッドを使用すれば変換できる。 ただし、日時は異常な日時になる。
ulid2 = ULID.from_uuid(uuid.uuid4()) print("ULID from uuid = " + str(ulid2)) print(ulid2.datetime)
ULID from uuid = 1S57355C1B940S5RB1VZD2P2FW 3961-09-08 21:15:50.186996+00:00
PostgreSQLのorder by でソート
ULIDで生成したUUIDをPostgreSQLのテーブルに挿入し、OrderByでソートしても期待する順になるか確認してみた。 先ほどのリストのときと同じように、日時をずらしたULIDオブジェクトを生成、ULIDから生成したUUIDをばらばらに挿入。
from ulid import ULID import datetime import uuid from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy import select from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import uuid class Base(DeclarativeBase): pass class UUID_Sample(Base): __tablename__ = 'uuid_sample' id: Mapped[uuid.UUID] = mapped_column(primary_key=True) # DBエンジンを作成 url = "postgresql://postgres:example@localhost:5432/postgres" engine = create_engine(url, echo=False) # テーブルをDBに作成 Base.metadata.create_all(engine) SessionClass = sessionmaker(engine) session = SessionClass() def use_db(): # 日時指定で1日ずらして、ULIDオブジェクトを生成 specfic_datetime = datetime.datetime.strptime("2023-03-02 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid1 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid1 = " + str(ulid1)) ulid1_obj = UUID_Sample(id=ulid1) specfic_datetime = datetime.datetime.strptime("2023-03-03 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid2 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid2 = " + str(ulid2)) ulid2_obj = UUID_Sample(id=ulid2) specfic_datetime = datetime.datetime.strptime("2023-03-04 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid3 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid3 = " + str(ulid3)) ulid3_obj = UUID_Sample(id=ulid3) specfic_datetime = datetime.datetime.strptime("2023-03-05 18:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") ulid4 = ULID.from_datetime(specfic_datetime).to_uuid() print("ulid4 = " + str(ulid4)) ulid4_obj = UUID_Sample(id=ulid4) session.add_all([ulid2_obj, ulid4_obj, ulid1_obj, ulid3_obj]) session.commit() # 整列した順番のリストを作成 expected_list = list() expected_list.append(ulid1) expected_list.append(ulid2) expected_list.append(ulid3) expected_list.append(ulid4) stmt = select(UUID_Sample) results = session.scalars(stmt) print("Not use order by") i = 0 for result in results: print("id = " + str(result.id)) print("Is Sample = " + str(result.id == expected_list[i])) i += 1 print("Use order by") stmt = select(UUID_Sample).order_by(UUID_Sample.id) results = session.scalars(stmt) i = 0 for result in results: print("id = " + str(result.id)) print("Is Sample = " + str(result.id == expected_list[i])) i += 1
結果はOrder byでソートされていることを確認できた。
ulid1 = 0186a37b-1d00-20ba-f545-49810830ce9d ulid2 = 0186a8a1-7900-d435-b373-beeca474a2ed ulid3 = 0186adc7-d500-26fa-2d95-ee12269bf390 ulid4 = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d Not use order by id = 0186a8a1-7900-d435-b373-beeca474a2ed id = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d id = 0186a37b-1d00-20ba-f545-49810830ce9d id = 0186adc7-d500-26fa-2d95-ee12269bf390 Use order by id = 0186a37b-1d00-20ba-f545-49810830ce9d id = 0186a8a1-7900-d435-b373-beeca474a2ed id = 0186adc7-d500-26fa-2d95-ee12269bf390 id = 0186b2ee-3100-9cce-b5d3-4ab2c0ca769d
最後に
UUIDは一意な値であるが、ソートができない課題を持っている。
ULIDがその課題を解決してくれている。
ULIDで生成したUUIDであればソート可能な値となっているので、テーブルのINDEXはUUIDにだけ設定しておけば不要なINDEXが増えなくてすむのかなと考える。
非常に便利なので、積極的に使っていきたい。
参考
ULIDに関するライブラリが多いとのこと。 今回使用したpython-ulidはメンテされている。
同一ミリ秒内での生成でも、同じミリ秒内に280 を超えない限り、ソート順が保証されるみたいで、良さそうです。
ここ重要ですね。同一ミリ秒で生成されることはないだろうかと。