学べること

  • SQLでは不可能/困難なアルゴリズムの実装
  • 5つの高度なPython UDF(編集距離、Luhn、HMAC、UUID v5、Base64)
  • 実際のローカルテスト結果
  • 「SQLでもできること」と「UDFでないとできないこと」の明確な区別

はじめに

「BigQuery Python UDFは便利だが、SQLだけでもできるのでは?」という疑問に答えます。

本記事では、Python UDFでないと実現困難/不可能な事例のみを厳選し、実装・検証します。


重要な前提:SQLでもできることは多い

多くの一般的な処理は、SQLだけでも実現可能です:

処理SQLPython UDF推奨
メール検証REGEXP_CONTAINSSQL(シンプル)
電話番号クレンジングREGEXP_REPLACESQL(高速)
年齢計算DATE_DIFFSQL(標準関数で十分)
JSON解析JSON_EXTRACTSQL(組み込み関数)
Base64エンコードTO_BASE64()SQL(組み込み関数)

結論: 上記のような処理にPython UDFは不要です。SQLの方がシンプルで高速です。


Python UDFが本当に必要なケース

以下の5つは、SQLでは不可能または極めて困難です:

  1. レーベンシュタイン距離(編集距離)- 動的計画法
  2. Luhnアルゴリズム(クレジットカード検証)- 複雑な桁操作
  3. HMAC-SHA256署名 - 暗号学的ハッシュ
  4. UUID v5生成 - 決定的UUID(名前空間ベース)
  5. Base64デコード - BYTES→STRING変換(SQLは面倒)

1. レーベンシュタイン距離(編集距離)

SQLでは不可能な理由

  • 動的計画法による2次元配列の計算が必要
  • 再帰的なロジックをSQLで表現するのは極めて困難
  • BigQueryにはLEVENSHTEIN()関数が存在しない

Python実装

"""
レーベンシュタイン距離(編集距離)計算UDF
 
用途:
- あいまい検索(Fuzzy Search)
- スペルチェック
- 重複データ検出(似た名前の統合)
- データクレンジング(表記ゆれの統一)
"""
 
 
def main(str1: str, str2: str) -> int:
    """
    2つの文字列間のレーベンシュタイン距離を計算
 
    Args:
        str1: 比較元の文字列
        str2: 比較先の文字列
 
    Returns:
        int: 編集距離(小さいほど類似)
 
    Examples:
        >>> main("kitten", "sitting")
        3  # k→s, e→i, 末尾にg挿入
 
        >>> main("Saturday", "Sunday")
        3
 
        >>> main("山田太郎", "山田花子")
        2  # 日本語対応
    """
    if str1 is None:
        str1 = ""
    if str2 is None:
        str2 = ""
 
    m, n = len(str1), len(str2)
 
    # DP配列の初期化
    dp = [[0] * (n + 1) for _ in range(m + 1)]
 
    # ベースケース
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
 
    # DP計算
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if str1[i - 1] == str2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j],      # 削除
                    dp[i][j - 1],      # 挿入
                    dp[i - 1][j - 1],  # 置換
                )
 
    return dp[m][n]

ローカルテスト結果

$ python3 functions/levenshtein_distance.py
 
Testing Levenshtein Distance UDF...
 main("kitten", "sitting") = 3 (expected 3)
 main("Saturday", "Sunday") = 3 (expected 3)
 main("", "abc") = 3 (expected 3)
 main("abc", "") = 3 (expected 3)
 main("", "") = 0 (expected 0)
 main("test", "test") = 0 (expected 0)
 main("apple", "aple") = 1 (expected 1)
 main("John Smith", "Jon Smith") = 1 (expected 1)
 main("山田太郎", "山田花子") = 2 (expected 2)
 
 All tests passed!

BigQueryでの使用例

-- 顧客名の重複検出(あいまい検索)
SELECT
  c1.customer_id as id1,
  c2.customer_id as id2,
  c1.name as name1,
  c2.name as name2,
  {{ function('levenshtein_distance') }}(c1.name, c2.name) as distance,
  CASE
    WHEN {{ function('levenshtein_distance') }}(c1.name, c2.name) <= 2 THEN '類似'
    WHEN {{ function('levenshtein_distance') }}(c1.name, c2.name) <= 5 THEN 'やや類似'
    ELSE '非類似'
  END as similarity
FROM customers c1
CROSS JOIN customers c2
WHERE c1.customer_id < c2.customer_id
  AND {{ function('levenshtein_distance') }}(c1.name, c2.name) <= 3
ORDER BY distance

実用例:

  • 「山田太郎」と「山田花子」の編集距離は2 → 同一人物の可能性
  • 「John Smith」と「Jon Smith」の編集距離は1 → スペルミスの可能性

2. Luhnアルゴリズム(クレジットカード検証)

SQLでは不可能な理由

  • 奇数/偶数位置で処理が異なる(複雑な条件分岐)
  • 各桁を右から左へ反転して処理する必要がある
  • 2倍にした値が9を超える場合に-9または桁を合計
  • BigQueryにはLUHN()関数が存在しない

Python実装

"""
Luhnアルゴリズム(クレジットカード番号検証)UDF
 
用途:
- クレジットカード番号の形式検証(決済前)
- データ入力エラーの検出
- テストデータの妥当性チェック
"""
 
 
def main(card_number: str) -> bool:
    """
    Luhnアルゴリズムでクレジットカード番号を検証
 
    Args:
        card_number: クレジットカード番号(ハイフン・スペース可)
 
    Returns:
        bool: Luhnチェックに合格した場合 True
 
    Examples:
        >>> main("4532015112830366")  # Visa
        True
 
        >>> main("5425-2334-3010-9903")  # Mastercard(ハイフン付き)
        True
 
        >>> main("1234567890123456")  # 不正な番号
        False
    """
    if card_number is None or card_number == "":
        return False
 
    # ハイフン、スペースを除去
    digits_only = "".join(c for c in card_number if c.isdigit())
 
    # 長さチェック(一般的に13-19桁)
    if len(digits_only) < 13 or len(digits_only) > 19:
        return False
 
    # Luhnアルゴリズム
    total = 0
    reverse_digits = digits_only[::-1]  # 右から処理するため反転
 
    for i, digit in enumerate(reverse_digits):
        n = int(digit)
 
        if i % 2 == 1:  # 奇数位置(右から2番目、4番目...)
            n *= 2
            if n > 9:
                n = n - 9  # または (n // 10) + (n % 10)
 
        total += n
 
    return total % 10 == 0

ローカルテスト結果

$ python3 functions/luhn_check.py
 
Testing Luhn Check UDF...
 main("4532015112830366") = True (expected True)  # Visa
 main("5425233430109903") = True (expected True)  # Mastercard
 main("374245455400126") = True (expected True)   # Amex
 main("6011000991300009") = True (expected True)  # Discover
 main("3566002020360505") = True (expected True)  # JCB
 main("4532-0151-1283-0366") = True (expected True)  # ハイフン付き
 main("5425 2334 3010 9903") = True (expected True)  # スペース付き
 main("1234567890123456") = False (expected False)  # 不正
 main("4532015112830367") = False (expected False)  # 最後の桁が違う
 
 All tests passed!

BigQueryでの使用例

-- 決済前のカード番号検証
SELECT
  payment_id,
  card_number,
  {{ function('luhn_check') }}(card_number) as is_valid_card,
  CASE
    WHEN {{ function('luhn_check') }}(card_number) THEN '有効'
    ELSE '無効(決済拒否)'
  END as validation_status
FROM payment_methods
WHERE card_number IS NOT NULL

重要: これは形式検証のみ。実際の決済可能性は確認できません。


3. HMAC-SHA256署名

SQLでは不可能な理由

  • BigQueryにはHMAC()関数が存在しない
  • 暗号学的ハッシュ関数の実装が必要
  • hmacモジュールはPython標準ライブラリでのみ利用可能

Python実装

"""
HMAC-SHA256署名生成UDF
 
用途:
- APIリクエストの署名(Webhook検証)
- データの改ざん検出
- セキュアなトークン生成
- 外部サービスとの連携(署名検証)
"""
import hmac
import hashlib
 
 
def main(message: str, auth_key: str) -> str:
    """
    HMAC-SHA256署名を生成
 
    Args:
        message: 署名対象のメッセージ
        auth_key: 認証キー
 
    Returns:
        str: HMAC-SHA256署名(16進数文字列)
 
    Examples:
        >>> main("test message", "demo_key")
        '3bcebf43c85d20bba6e3b6ba278af1d2ba3ab0d57de271b0ad30b833e851c5a6'
 
    セキュリティノート:
        - auth_keyは環境変数から取得すべき
        - クエリに直接埋め込まない
    """
    if message is None or auth_key is None:
        return None
 
    signature = hmac.new(
        key=auth_key.encode("utf-8"),
        msg=message.encode("utf-8"),
        digestmod=hashlib.sha256,
    ).hexdigest()
 
    return signature

ローカルテスト結果

$ python3 functions/generate_hmac.py
 
Testing HMAC-SHA256 UDF...
 main("test message", "demo_key")
   = 3bcebf43c85d20bba6e3b6ba278af1d2ba3ab0d57de271b0ad30b833e851c5a6
 
 main("hello", "world")
   = 3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7567c272e007b
 
 All tests passed!

BigQueryでの使用例

-- Webhook署名検証
SELECT
  webhook_id,
  payload,
  received_signature,
  {{ function('generate_hmac') }}(payload, 'your-api-key') as computed_signature,
  received_signature = {{ function('generate_hmac') }}(payload, 'your-api-key') as is_valid_signature
FROM webhook_logs
WHERE payload IS NOT NULL

実用例: Stripe, GitHub, Slackなどのwebhook検証に使用可能。


4. UUID v5生成(決定的UUID)

SQLでは不可能な理由

  • BigQueryのGENERATE_UUID()v4(ランダム)のみ
  • v5はSHA-1ハッシュ + 名前空間による決定的UUID
  • uuidモジュールはPython標準ライブラリでのみ利用可能

Python実装

"""
UUID v5(名前空間付きUUID)生成UDF
 
UUID v4との違い:
- v4: ランダム生成(同じ入力でも毎回異なる)
- v5: 決定的生成(同じ入力なら常に同じ)
 
用途:
- 決定的な一意IDの生成
- 異なるシステム間での同じデータの識別
- 冪等性が必要なデータ統合
"""
import uuid
 
 
def main(namespace: str, name: str) -> str:
    """
    UUID v5(名前空間ベース)を生成
 
    Args:
        namespace: 名前空間("dns", "url", "oid", "x500")
        name: 一意にしたい文字列
 
    Returns:
        str: UUID v5文字列
 
    Examples:
        >>> main("dns", "example.com")
        'cfbff0d1-9375-5685-968c-48ce8b15ae17'
 
        >>> main("url", "https://example.com")
        '4fd35a71-71ef-5a55-a9d9-aa75c889a6d0'
 
        # 決定的: 同じ入力なら常に同じUUID
        >>> main("dns", "test.com") == main("dns", "test.com")
        True
    """
    if namespace is None or name is None:
        return None
 
    namespace_map = {
        "dns": uuid.NAMESPACE_DNS,
        "url": uuid.NAMESPACE_URL,
        "oid": uuid.NAMESPACE_OID,
        "x500": uuid.NAMESPACE_X500,
    }
 
    try:
        if namespace.lower() in namespace_map:
            ns_uuid = namespace_map[namespace.lower()]
        else:
            ns_uuid = uuid.UUID(namespace)
    except (ValueError, AttributeError):
        return None
 
    result_uuid = uuid.uuid5(ns_uuid, name)
    return str(result_uuid)

ローカルテスト結果

$ python3 functions/generate_uuid5.py
 
Testing UUID v5 UDF...
 Deterministic: main("dns", "deterministic.com") = 89784d37-5af0-5f46-b4a3-0e43aba50b77
   Second call returns same: True  # 決定的であることを確認
 
 main("dns", "example.com") = cfbff0d1-9375-5685-968c-48ce8b15ae17
 main("url", "https://example.com") = 4fd35a71-71ef-5a55-a9d9-aa75c889a6d0
 
 All tests completed!

BigQueryでの使用例

-- 異なるシステム間で同じデータを識別
SELECT
  customer_email,
  {{ function('generate_uuid5') }}('dns', customer_email) as customer_uuid
FROM customers
-- 同じメールアドレスは常に同じUUIDになる(決定的)
 
UNION ALL
 
SELECT
  customer_email,
  {{ function('generate_uuid5') }}('dns', customer_email) as customer_uuid
FROM external_system_customers
-- 外部システムでも同じUUIDが生成される

v4(ランダム)との使い分け:

UUID v4UUID v5
完全にランダム決定的(同じ入力→同じUUID)
衝突の心配なし名前空間管理が必要
トランザクションID外部システム連携
ユーザーセッションIDデータ統合キー

5. Base64デコード

SQLでも可能だが、UDFの方が優れている理由

  • BigQueryのFROM_BASE64()BYTES型を返す
  • BYTES → STRING変換が面倒(SAFE_CONVERT_BYTES_TO_STRING()必要)
  • エラーハンドリングが困難(不正なBase64でクエリ失敗)

Python実装

"""
Base64デコード専用UDF
 
SQLより優れている点:
- 自動的にSTRING型を返す(BYTES変換不要)
- エラーハンドリング内蔵(不正なBase64でもNoneを返す)
- マルチバイト文字(日本語)も正しく処理
"""
import base64
 
 
def main(encoded_text: str) -> str:
    """
    Base64エンコードされた文字列をデコード
 
    Args:
        encoded_text: Base64エンコードされた文字列
 
    Returns:
        str: デコードされた文字列、エラー時はNone
 
    Examples:
        >>> main("SGVsbG8sIFdvcmxkIQ==")
        'Hello, World!'
 
        >>> main("5pel5pys6Kqe")
        '日本語'
 
        >>> main("invalid!!!!")
        None  # エラー時はNone(クエリは継続)
    """
    if encoded_text is None or encoded_text == "":
        return None
 
    try:
        decoded_bytes = base64.b64decode(encoded_text)
        return decoded_bytes.decode("utf-8")
    except Exception:
        return None  # 不正なBase64でもクエリを止めない

SQLとの比較

-- ❌ SQLでやるとこうなる(面倒)
SELECT
  encoded_data,
  SAFE_CONVERT_BYTES_TO_STRING(FROM_BASE64(encoded_data)) as decoded
FROM logs
-- 問題: 不正なBase64があるとクエリ全体が失敗
 
-- ✅ Python UDFなら1行(エラーセーフ)
SELECT
  encoded_data,
  {{ function('base64_decode') }}(encoded_data) as decoded
FROM logs
-- 不正なBase64はNoneになるだけ(クエリは継続)

ローカルテスト結果

$ python3 functions/base64_decode.py
 
Testing Base64 Decode UDF...
 main("SGVsbG8sIFdvcmxkIQ==") = Hello, World!
 main("YWRtaW46cGFzc3dvcmQ=") = admin:demo_pass
 main("5pel5pys6Kqe") = 日本語
 main("invalid!!!!") = None  # エラーセーフ
 
 All tests passed!

まとめ: Python UDFが本当に必要なケース

✅ Python UDFが必須

ユースケース理由
レーベンシュタイン距離動的計画法(2次元配列)が必要
Luhnアルゴリズム複雑な桁操作(右から左、奇数/偶数位置)
HMAC署名暗号学的ハッシュ(BigQueryに関数なし)
UUID v5決定的UUID生成(v4はランダムのみ)
Base64デコード(エラーセーフ)SQLは面倒 + エラーハンドリング困難

❌ Python UDF不要(SQLで十分)

ユースケースSQL代替
メール検証REGEXP_CONTAINS
電話番号クレンジングREGEXP_REPLACE
年齢計算DATE_DIFF
JSON解析JSON_EXTRACT
Base64エンコードTO_BASE64()

実装ファイル構成

users/yada/jaffle_shop_bigquery/
├── functions/
│   ├── levenshtein_distance.py  ← レーベンシュタイン距離
│   ├── luhn_check.py            ← Luhnアルゴリズム
│   ├── generate_hmac.py         ← HMAC-SHA256署名
│   ├── generate_uuid5.py        ← UUID v5生成
│   ├── base64_decode.py         ← Base64デコード
│   └── schema.yml               ← 全UDF定義
└── models/
    └── tests/
        └── test_advanced_udfs.sql  ← BigQuery実行テスト

BigQueryでの実行確認

# 1. UDFをBigQueryにデプロイ
dbt build --select resource_type:function --profiles-dir . --target sandbox
 
# 2. テストモデルを実行
dbt run --select test_advanced_udfs --profiles-dir . --target sandbox
 
# 3. 結果確認
bq query --use_legacy_sql=false "SELECT * FROM \`your-project.dbt_sandbox.test_advanced_udfs\`"

執筆日: 2026-02-17 検証環境: dbt 1.11.5, dbt-bigquery 1.11.0, Python 3.11 実行確認: ✅ すべてのUDFをローカルテスト済み


本記事があなたのプロジェクトに役立つことを願っています! 🚀