0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIと大規模データ処理について学ぼう【第8回】データの不変性とは

Posted at

今回はデータの不変性について学びます。

一言サマリ

データを変更しないことでメリットたくさん


以下からAIが文章を生成しています。

データの不変性の定義

データの不変性(Immutability)とは、一度作成されたデータを変更しないという原則です。データを更新・削除する代わりに、新しいバージョンとして追加していきます。

例えば、顧客の住所が変わったとき、既存レコードを更新するのではなく、新しい住所レコードを追加します。これにより、「いつ、どのように変更されたか」という履歴が完全に保存されます。

なぜ不変性が重要なのか

再現性の確保
過去のある時点の状態を正確に再現できます。「先月末時点での顧客リスト」を、いつでも正確に復元できます。

デバッグの容易さ
問題が発生したとき、どのデータがどう処理されたかを追跡できます。データが上書きされていないため、原因の特定が容易です。

並行処理の安全性
複数のプロセスが同時にデータを読んでも、競合が発生しません。データが変更されないため、ロック機構が不要になります。

Append-Onlyパターン

データを追加するだけで、更新・削除を行いません。

# 悪い例: データを更新(可変)
def update_customer_address(customer_id, new_address):
    query = f"""
    UPDATE customers
    SET address = '{new_address}',
        updated_at = CURRENT_TIMESTAMP
    WHERE customer_id = {customer_id}
    """
    execute_query(query)
    # 古い住所の情報が失われる

# 良い例: 新しいレコードを追加(不変)
def add_customer_address(customer_id, new_address):
    record = {
        'customer_id': customer_id,
        'address': new_address,
        'valid_from': datetime.now(),
        'valid_to': None,  # 現在有効
        'is_current': True
    }
    
    # 前のレコードを無効化
    query = f"""
    UPDATE customer_addresses
    SET valid_to = CURRENT_TIMESTAMP, is_current = False
    WHERE customer_id = {customer_id} AND is_current = True
    """
    execute_query(query)
    
    # 新しいレコードを追加
    df = pd.DataFrame([record])
    df.to_sql('customer_addresses', con=engine, if_exists='append', index=False)

イベントソーシング

すべての変更を「イベント」として記録します。

class CustomerEventStore:
    def record_event(self, event_type, customer_id, data):
        event = {
            'event_id': generate_uuid(),
            'event_type': event_type,
            'customer_id': customer_id,
            'data': data,
            'timestamp': datetime.now().isoformat()
        }
        
        self.events.append(event)
        self.save_event(event)
        return event
    
    def get_customer_state(self, customer_id, as_of=None):
        """イベントから現在の状態を再構築"""
        events = self.get_events(customer_id, until=as_of)
        state = {'customer_id': customer_id}
        
        for event in events:
            state = self.apply_event(state, event)
        
        return state

タイムトラベルクエリ

不変性により、過去の任意の時点のデータを参照できます。

def query_at_point_in_time(table, as_of_date):
    query = f"""
    SELECT *
    FROM {table}
    WHERE valid_from <= '{as_of_date}'
    AND (valid_to > '{as_of_date}' OR valid_to IS NULL)
    """
    return pd.read_sql(query, con=engine)

# 2024年12月31日時点の顧客リスト
customers_eoy_2024 = query_at_point_in_time('customer_addresses', '2024-12-31 23:59:59')

不変性のトレードオフ

ストレージコスト
履歴を全て保存するため、ストレージ容量が増加します。古いデータは圧縮・アーカイブして対応します。

クエリの複雑さ
「現在の状態」を取得するのに、複雑なクエリが必要になることがあります。マテリアライズドビューで最適化できます。

まとめ

データの不変性は、再現性、デバッグの容易さ、並行処理の安全性をもたらします。Append-Onlyパターンやイベントソーシングで実装でき、タイムトラベルクエリなどの強力な機能を実現できます。

感想

個人的にイベントソーシングとタイムトラベルクエリが直感的にわからなかったので調べました。

銀行口座を例にすると以下のようになるイメージです。

①従来の方法:
預金残高:100,000←これを直接書き換える

②append-only:
預金残高:100,000 2025/11/01
預金残高:150,000 2025/11/03←更新があったらレコードを足して新しいものを正とする

③イベントソーシング:
預金残高:+100,000 2025/11/01
預金残高: +50,000 2025/11/03←イベント(変化分)を記録する

③タイムトラベルクエリ:
預金残高:100,000 2025/11/01
預金残高:100,000 2025/11/02←設定した時間断面で記録する
預金残高:150,000 2025/11/03

普遍性のトレードオフにもある通り、メリットの代償としてストレージは膨大になりそうですね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?