BigQuery Advent Calendar 2024 5日目の記事です。
この記事では Data Contract CLI を使った BigQuery のデータ品質の検査方法を紹介します。
Data Contract CLI とは
公式ドキュメントに以下の記載のとおり、データコントラクトの lint, test, import, export ができるコマンドラインツールとPythonライブラリです。
The Data Contract CLI is a command line tool and Python library to lint, test, import and export data contracts.
端的に言えば次のことができます。
- yaml に基づいたデータのバリデーション
- yaml から BigQuery のスキーマなど他の形式へのインポート/エクスポート
ちなみに今回は BigQuery で扱いますがいろんな DB で使用することが可能です。
Data Contract とはなにか
Data Contract をよく知らない方のために簡単に説明します。
Data Contract は直訳するとデータ契約ですが、個人的にはデータ提供ルールみたいな感じだと理解しています。
言うなればデータの API のようなもので、データの定義を詳細にまとめたものになります。
Data Contract は最近日本のデータエンジニアリング界隈でもよく聞くようになりました。
背景として、ここ最近日本のデータエンジニアリング界隈ではデータ基盤チームの作るデータ基盤の整備が進み、問題の発生元が自分たちのデータ基盤からデータソースにシフトしてきているのではないかと思います。
例えばデータソースから予期せぬスキーマの変更が発生した場合、データ加工パイプラインが止まってしまう可能性があります。
しかもこれはデータ基盤側ではコントロールできないため、データソース側で提供ルールを設けてもらい、変更があるときは事前に通知していただくなどの対処が必要になります。
その提供ルールが Data Contract です。
詳細については書籍の Driving Data Quality with Data Contracts や O'Reilly Learning Platform のアーリーアクセスの Data Contract
をご参照ください。
先日も datatech-jp で Data Contract のイベント があり参加しましたが、登壇された方の発表もその後の交流もとても有意義な時間を過ごせました。
そちらの資料もよろしければご参照ください。
Data Contract CLI でできること
すでに先見の明高く Data Contract CLI をまとめている記事があるためそちらを紹介します。
この記事ではなるべく上記の記事に無いことを補足していこうと思います。
下準備
poetry で次のような環境を用意しました。
[tool.poetry]
name = "data-contract"
version = "0.1.0"
description = "Qiitaの記事の執筆"
authors = []
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
datacontract-cli = { extras = ["bigquery"], version = "^0.10.13" }
google-cloud-bigquery-storage = "^2.27.0"
soda-core-bigquery = "3.3.22"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
DDL から datacontract.yaml を生成できるため、次の SQL を使いました。
CREATE TABLE ogata-health.sleep.sleep_origin (
value STRING OPTIONS(description = "睡眠の種類。レム睡眠、コア睡眠など。"),
SOURCE STRING OPTIONS(description = "記録を取得したデバイス"),
startDate STRING OPTIONS(description = "睡眠の開始時刻"),
qty FLOAT64 OPTIONS(description = "睡眠時間"),
endDate STRING OPTIONS(description = "睡眠の終了時刻")
) OPTIONS(description = "睡眠データのオリジナルデータのテーブル");
上記の SQL に対して以下を実行。
datacontract import --format sql --source create_sleep_origin.sql > datacontract.yml
datacontract lint datacontract.yml
で lint の警告を確認しつつ yaml を修正して以下のような状態にしました。
dataContractSpecification: 1.1.0
id: my-data-contract-id
info:
title: My Data Contract
version: 0.0.1
description: "データコントラクトのサンプル"
servers:
production:
type: bigquery
project: project_name
dataset: sleep
models:
sleep_origin:
type: table
description: "睡眠データのオリジナルデータのテーブル"
fields:
value:
type: string
description: "睡眠の種類。レム睡眠、コア睡眠など。"
SOURCE:
type: string
description: "記録を取得したデバイス"
startDate:
type: string
description: "睡眠の開始時刻"
qty:
type: float
description: "睡眠時間"
endDate:
type: string
description: "睡眠の終了時刻"
test するには BigQuery にアクセスするためのクレデンシャルが必要です。
Data Contract のリポジトリに以下の記載があります。
We support authentication to BigQuery using Service Account Key. The used Service Account should include the roles:
BigQuery Job User
BigQuery Data Viewer
上記の権限を持ったサービスアカウントを作成してクレデンシャルキーを発行して DATACONTRACT_BIGQUERY_ACCOUNT_INFO_JSON_PATH
でクレデンシャルキーのパスを指定してください。
ここまでやったら test が実行できる準備完了。
datacontract test datacontract.yml
こんな感じで実行すると次のような結果になりました。
なにやら WARNING が出ているものの、とりあえずパスできたようです。
ちなみに BigQuery のジョブ履歴を見たら次のようなクエリが発行されていました。
SELECT column_name, data_type, is_nullable FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'sleep_origin';
今回の test は INFORMATION_SCHEMA から取ってくるだけで完結する内容だったのでこのようなクエリで取ってきているようですね。
datacontract.yaml に設定できる項目は多岐にわたるため、設定を増やすともっといろんなクエリを発行することになるのだと思います。
例えば not null 制約の項目を追記して test を実行します。
value:
type: string
description: "睡眠の種類。レム睡眠、コア睡眠など。"
required: true # NOT NULL 制約を contract に追加。
この状態でテストすると、BigQuery には次のクエリも発行されます。
SELECT
COUNT(CASE WHEN value IS NULL THEN 1 END)
FROM sleep_origin
このように required: true
で設定されているカラムに対して全ての値が NULL かどうかを判定するためのクエリを発行しています。
こうなると気になるのは BigQuery のスキャン料が嵩んでくるのではないかということなんですが、このあたりはまだ詳しく調査できてません。
test のたびにテーブルをフルスキャンすると運用上きついと思うんですが、また調べたら追記したいです。
基本的な内容は紹介したのでもう少し詳細を見ていきます。
test 機能
余裕があったら追記します。
いろんな観点でテスト可能です。
文字列の長さや最大値・最小値などはもちろん、SQL のクエリ結果がしきい値を超えているかなどの判定も可能です。
engine 指定で great-expectation などのテストも実施することができます。
かなり柔軟にテストできるため、求めているものはだいたい揃うのではないでしょうか。
export 機能
export は様々なフォーマットで export 可能です。
一覧はこんな感じです。
jsonschema
pydantic-model
sodacl
dbt
dbt-sources
db
t-staging-sql
odcs
odcs_v2
odcs_v3
rdf
avro
protobuf
great-expectations
terraform
avro-idl
sql
sql-query
html
go
bigquery
dbml
spark
sqlalchemy
data-caterer
dcs
試しに sql で export してみました。
datacontract export --format sql datacontract.yml > sample.sql
結果
-- Data Contract: my-data-contract-id
-- SQL Dialect: snowflake
CREATE TABLE sleep_origin (
value STRING not null,
SOURCE STRING,
startDate STRING,
qty FLOAT,
endDate STRING
);
description は書いてないです。
とはいえ素の DDL はあんまり生成して使うことはなさそうなので気にしなくてもいいかもしれません。
BigQuery だとこんな感じです。
datacontract export --format bigquery --server production datacontract.yml > bigquery_sample.json
{
"kind": "bigquery#table",
"tableReference": {
"datasetId": "sleep",
"projectId": "ogata-health",
"tableId": "sleep_origin"
},
"description": "\u7761\u7720\u30c7\u30fc\u30bf\u306e\u30aa\u30ea\u30b8\u30ca\u30eb\u30c7\u30fc\u30bf\u306e\u30c6\u30fc\u30d6\u30eb",
"schema": {
"fields": [
{
"name": "value",
"type": "STRING",
"mode": "REQUIRED",
"description": "\u7761\u7720\u306e\u7a2e\u985e\u3002\u30ec\u30e0\u7761\u7720\u3001\u30b3\u30a2\u7761\u7720\u306a\u3069\u3002",
"maxLength": null
},
{
"name": "SOURCE",
"type": "STRING",
"mode": "NULLABLE",
"description": "\u8a18\u9332\u3092\u53d6\u5f97\u3057\u305f\u30c7\u30d0\u30a4\u30b9",
"maxLength": null
},
{
"name": "startDate",
"type": "STRING",
"mode": "NULLABLE",
"description": "\u7761\u7720\u306e\u958b\u59cb\u6642\u523b",
"maxLength": null
},
{
"name": "qty",
"type": "FLOAT64",
"mode": "NULLABLE",
"description": "\u7761\u7720\u6642\u9593"
},
{
"name": "endDate",
"type": "STRING",
"mode": "NULLABLE",
"description": "\u7761\u7720\u306e\u7d42\u4e86\u6642\u523b",
"maxLength": null
}
]
}
}
description が文字化けしました。
dbt だとこんな感じ。
version: 2
models:
- name: sleep_origin
config:
meta:
data_contract: my-data-contract-id
materialized: table
contract:
enforced: true
description: 睡眠データのオリジナルデータのテーブル
columns:
- data_type: STRING
description: 睡眠の種類。レム睡眠、コア睡眠など。
constraints:
- type: not_null
name: value
- data_type: STRING
description: 記録を取得したデバイス
name: SOURCE
- data_type: STRING
description: 睡眠の開始時刻
name: startDate
- data_type: FLOAT
description: 睡眠時間
name: qty
- data_type: STRING
description: 睡眠の終了時刻
name: endDate
とくに注目している export を紹介します。
datacontract.yaml を HTML で export できる
これはかなりいい機能だと思っているのですが、 datacontract.yaml
が美しい HTML で出力できます。
次のようなコマンドで HTML を生成できます。
datacontract export --format html datacontract.yml > sample.html
Data Contract でデータ仕様を確認する際に yaml では読みにくいため、運用上忌避されてしまう懸念があります。
HTML で出力すれば yaml に慣れていない人でも(慣れている人でも)Contract の内容を確認しやすくなり、運用しやすくなります。
エラー集
エラーや思いも寄らない仕様に遭遇したので共有します。
Data Contract CLI は 2024/12/5 時点でバージョン 0.10.15 が最新です。
メジャーバージョンが 0 なだけあってけっこうバグがでたり納得のいかない仕様になってたりします。
とはいえ有望なツールであることは間違いないと思うので、今後に期待しています。
というか OSS なのでいっそ contributor になれという話かもしれません。
DDL に description が書いていてもその DDL から datacontract.yaml を import したときに description が反映されない。
サンプルとして健康データのテーブルの SQL を持ってたので使いました。
CREATE TABLE project_name.sleep.sleep_origin (
value STRING OPTIONS(description = "睡眠の種類。レム睡眠、コア睡眠など。"),
SOURCE STRING OPTIONS(description = "記録を取得したデバイス"),
startDate STRING OPTIONS(description = "睡眠の開始時刻"),
qty FLOAT64 OPTIONS(description = "睡眠時間"),
endDate STRING OPTIONS(description = "睡眠の終了時刻")
) OPTIONS(description = "睡眠データのオリジナルデータのテーブル");
datacontract import --format sql --source create_sleep_origin.sql > datacontract_sample.yml
で datacontract.yaml
を生成すると次の結果になりました。
dataContractSpecification: 1.1.0
id: my-data-contract-id
info:
title: My Data Contract
version: 0.0.1
models:
sleep_origin:
type: table
fields:
value:
type: string
required: true
SOURCE:
type: string
startDate:
type: string
required: true
qty:
type: float
required: true
endDate:
type: string
required: true
required はうまく追加されていますが description は追加されないようですね。悲しい気持ちになりました。
これから既存のテーブルを元に datacontract.yaml を作ろうとしている方には悲報だと思います。
手動で description を追記しました。
datacontract test datacontract.yaml
を実行した際に次のエラーが出る
datacontract test datacontract.yml
Testing datacontract.yml
ERROR:soda.scan:[02:42:07] Error occurred while executing scan.
ERROR:soda.scan: | Data source type "bigquery" not found. Did you spell bigquery correctly? Did you install module soda-core-bigquery?
datacontract-cli を追加した後に soda-core-bigquery
を poetry add soda-core-bigquery
で追加しようとしたらエラーが出ました。
datacontract-cli と soda-core-bigquery のバージョンを最新のものではなく適当に下げたらインストールできました。検証に使用したバージョンは次のとおりです。
[tool.poetry.dependencies]
python = "^3.12"
datacontract-cli = { extras = ["bigquery"], version = "^0.10.13" }
google-cloud-bigquery-storage = "^2.27.0"
soda-core-bigquery = "3.3.22"
datacontract-cli[all]
で install するとエラーが出て test コマンドが実行できない。
datacontract リポジトリの REAMDE に書いてあったので無邪気にやったら次のエラーがでて実行できませんでした。
ERROR:soda.scan:[03:39:15] Error occurred while executing scan.
ERROR:soda.scan: | numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject
WARNING:root:Engine soda-core has errors. See the logs for details.
素直に datacontract-cli[bigquery]
のみにしたら解消できました。
終わりに
バグがでたりよくわからない仕様になってたりしますが、データの品質確認を手軽に実施できる将来有望なツールです。
僕の関心は他のツールとの競合です。
すでにデータ基盤を効率よく運用するためにいろんなツールを入れている方も多いかと思いますが、その場合機能が重複してしまうケースが発生しそうです。
その場合に既存のツールと共存するのか、既存のツールを捨てて datacontract を導入するのかなど検討する必要があります。
そのあたりの判断基準はきっとこれからのデータエンジニアリング界隈で色んなお話が出てくるかと思うので、導入された方は何かしら記事にしたりイベント登壇していただけますととても助かります!
この記事で予期せぬデータのトラブルを防げる方が増えることを祈っています。