3行まとめ
- データに対する期待値を定義するData Reliability Levelという活動を始めています
- Data Reliability Levelを設定するには「XXXの場合にはAAAの項目は入力が必須」といった条件分岐や項目数も多く、レビューが大変という課題がありました
- 最近のJSON Schemaは記述力が高いため、上述のような制約も記述することができました
- 3行まとめ
- 前提: データの出口に対する期待値を明示的に設定したい
- 課題感: 各Data Reliability Levelに対する設定が適切に設定されているかを確認するレビューが大変
- 解決方法: 各Data Reliability Levelに対する必須入力項目をJSON Schemaで機械的にvalidateする
- 見所
- まとめ
前提: データの出口に対する期待値を明示的に設定したい
データ基盤の開発者とデータの活用者の間には期待値のギャップが埋まれがちです。こうしたギャップをなくすため、データに対する期待値をData Reliability Level*1として定義する活動を開始しています。これはデータの出口側に対する一種のData Contractと捉えることもできると思います。
Data Reliability Levelは大まかに3つの段階(Trusted
/ Business Insight
/ Adhoc
)を設定しており、Levelが高いほど期待値や水準が高いデータになります。
こうした水準が高いデータを作っていくためには、いくつかの観点でメタデータを適切に設定する必要があります。
課題感: 各Data Reliability Levelに対する設定が適切に設定されているかを確認するレビューが大変
Data Reliability Levelは適切に設定できれば強力な武器になり得る一方、これを運用していくのは少し大変です。具体的には、以下の観点で難しさがあります。
- 設定項目が多い
- 運用が可能と思われる程度には項目を絞ってはいるものの、それでも片手では収まらない設定項目数があります
- 項目の漏れがないかをレビュアーが目視で確認するのは大変ですし、普通に漏れが起き得るでしょう
- Level毎に設定が必須の項目が異なる
- 基本的には高水準のものほど設定必須の項目が増えますが、そうないものもあります
- 例えば「adhocなテーブルはずっと運用はしたくない。
deprecation_date
の項目の入力を必須としたい」といった具合です - こうしたLevel毎の必須入力項目が異なることはレビューをさらに難しくしてしまいます
解決方法: 各Data Reliability Levelに対する必須入力項目をJSON Schemaで機械的にvalidateする
人間には難しいことは機械にさせましょう。適当なスクリプトをでっち上げてもよいですが、今回は汎用的なツールとして使えるJSON SchemaでData Reliability Levelに対する設定をvalidateさせることにします。dbtのメタデータの記述先はjsonではなくyamlファイルになりますが、check-jsonschemaなどを使えばyamlファイルであってもJSON Schemaを元にvalidateさせることができます。
% check-jsonschema --schemafile my_schema.json --default-filetype yaml models/my_mart/my_model.yml
dbtのymlファイルに対するJSON Schemaをゼロから記述するのは骨が折れますが、幸いなことにdbtが公式でJSON Schemaを出してくれているので、ありがたく使わせてもらいましょう。全体は2000行以上あり割とゴツいですが、models
に関するところなど、必要な箇所のみをコピペすると大分コンパクトにできます。
細かい箇所はあとで説明するとして、コードの全体感は以下に置いておきます。
Data Reliability Levelに対するJSON Schema(クリックで開きます)
{ "title": "Data Reliability Level(DRL)用のJSON Schema", "type": "object", "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "version": { "type": "number", "const": 2 }, "models": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "columns": { "type": "array", "items": { "$ref": "#/$defs/column_properties" } }, "config": { "$ref": "#/$defs/model_configs" }, "constraints": { "$ref": "#/$defs/constraints" }, "data_tests": { "type": "array", "items": { "$ref": "#/$defs/data_tests" } }, "deprecation_date": { "type": "string" }, "docs": { "$ref": "#/$defs/docs_config" }, "meta": { "$ref": "#/$defs/meta" }, ..., }, "additionalProperties": false, "required": [ "name", "description", "meta" ], "allOf": [ { "if": { "$ref": "#/$defs/data_reliability_level_trusted_data" }, "then": { "properties": { "config": { "type": "object", "properties": { "contract": { "type": "object", "properties": { "enforced": { "type": "boolean", "const": true } }, "required": [ "enforced" ] } }, "required": [ "contract" ] }, "columns": { "items": { "required": [ "description" ], "allOf": [ { "$ref": "#/$defs/column_must_have_not_null" } ] } } }, "required": [ "config" ] } }, { "if": { "$ref": "#/$defs/data_reliability_level_adhoc" }, "then": { "required": [ "deprecation_date" ] } } ] } } }, "additionalProperties": false, "$defs": { "meta": { "type": "object", "properties": { "data_reliability_level": { "enum": [ "trusted_data", "business_insight", "adhoc" ] }, "manual_data_usage": { "type": "boolean" }, "direct_source_usage": { "type": "boolean" }, "business_owners": { "$ref": "#/$defs/string_or_array_of_strings" } }, "required": [ "data_reliability_level", "manual_data_usage", "direct_source_usage", "slo" ], "anyOf": [ { "if": { "$ref": "#/$defs/data_reliability_level_trusted_data" }, "then": { "required": [ "business_owners" ] } }, { "if": { "$ref": "#/$defs/data_reliability_level_business_insight" }, "then": { "required": [ "business_owners" ] } }, { "if": { "$ref": "#/$defs/data_reliability_level_adhoc" }, "then": { "required": [] } } ] }, "data_reliability_level_trusted_data": { "type": "object", "properties": { "meta": { "type": "object", "properties": { "data_reliability_level": { "const": "trusted_data" } } } } }, "data_reliability_level_business_insight": { "type": "object", "properties": { "meta": { "type": "object", "properties": { "data_reliability_level": { "const": "business_insight" } } } } }, "data_reliability_level_adhoc": { "type": "object", "properties": { "meta": { "type": "object", "properties": { "data_reliability_level": { "const": "adhoc" } } } } }, "column_properties": { ..., }, "column_must_have_not_null": { "description": "data_reliability_levelがtrusted_dataの場合、各カラムにdata_testsまたはconstraintsのいずれかにnot_nullが含まれている必要があります", "anyOf": [ { "required": ["constraints"], "properties": { "constraints": { "type": "array", "minItems": 1, "contains": { "type": "object", "properties": { "type": { "const": "not_null" } }, "required": ["type"] } } } }, { "required": ["data_tests"], "properties": { "data_tests": { "type": "array", "minItems": 1, "contains": { "anyOf": [ { "required": ["not_null"], "type": "object", "properties": { "type": "object", "not_null": { "type": "object", "properties": { "required": ["where"], "type": "object", "properties": { "where": { "type": "string" } } } }, "required": ["config"] } } ] } } } } ] }, "constraints": { ..., }, "data_tests": { ..., } } }
見所
私はJSON Schemaの初歩しか知らなかったので、今回の要件をJSON Schemaで記述できるか不安だったのですが、以下の記事により「最近のJSON Schemaは記述力が十分にあるんだな」と分かったので、見所をメモしておきます。
条件分岐を記述する
前述したように、Data Reliability LevelはLevelによって必須の入力項目が異なります。そのため、条件分岐を記述できることが必須条件となりますが、JSON Schemaでは条件を以下のように記述できます。
以下の例では「adhocだったら、deprecation_date
の項目が必須」という条件分岐を含む必須項目の出し分けを表わしています。
{ ..., "properties": { ..., "models": { "type": "array", "items": { ..., "allOf": [ { "if": { "$ref": "#/$defs/data_reliability_level_trusted_data" }, "then": { ..., } }, { "if": { "$ref": "#/$defs/data_reliability_level_adhoc" }, "then": { "required": [ "deprecation_date" ] } } ] } } } }
$ref
と$defs
で繰り返しの記述を避ける
Data Reliability LevelのJSON Schemaを書く際に条件分岐が至るところに登場しますが、「adhocだったら」の条件を毎回書くのはダルいです。JSON Schemaでは$defs
でサブスキーマを定義し、$ref
でそれを参照する、ということができます。
例えば以下のような具合です。
meta
の配下にdata_reliability_level
があり、設定値としてはtrusted_data
/business_insight
/adhoc
の3種類のみを許可する、と定義するmeta
の配下のdata_reliability_level
がtrusted_data
になっている条件をdata_reliability_level_trusted_data
と定義する
{ ..., "$defs": { "meta": { "type": "object", "properties": { "data_reliability_level": { "enum": [ "trusted_data", "business_insight", "adhoc" ] }, }, }, "data_reliability_level_trusted_data": { "type": "object", "properties": { "meta": { "type": "object", "properties": { "data_reliability_level": { "const": "trusted_data" } } } } }, ..., "data_reliability_level_adhoc": { "type": "object", "properties": { "meta": { "type": "object", "properties": { "data_reliability_level": { "const": "adhoc" } } } } }, } }
これらを定義することで、前述した$ref
を使う形で条件などを簡単に参照できます。
"$ref": "#/$defs/data_reliability_level_adhoc"
anyOfでより柔軟な制約を設定する
「このカラムにはNULLが入らない」という制約を記述する際にdbtのconstraints
は便利です(詳しくはこちらを参照してください)。DWHにBigQueryを使っている場合、テーブルのModeにRequired
を設定することができ、「物理的な制約としてそもそもNULLが入らない」ということが表現できます。これはテストにより後付けで「NULLが含まれていないようです」と分かるよりも強い制約になります。
そのため、特に高いData Reliability Levelが要求されるカラムについては、基本的に以下のようなnot_null
のconstraints
の制約を必須としたいです。
version: 2 models: - name: my_user_model config: contract: enforced: true columns: - name: user_id constraints: - type: not_null
しかし、条件によっては「この場合だけはどうしてもNULLが入ることが自然」という場合もありえます。その場合は妥協してconstraints
ではなくdata_tests
を記述することを必須としますが、この場合はどういう場合にNULLが入るかの条件を記述することを必須としたいです(後からNULLの条件を調査するコストが大きくなるため)。つまり、constraints
が書けない場合であっても、以下のようなyamlの記述を必須としたいです。
version: 2 models: - name: my_user_model columns: - name: complex_column data_tests: - not_null: config: where: "..." # NULLになる条件を記述する # 以下のようなwhere指定なしの場合はvalidateで弾きたい! # - not_null
こういった複雑な制約もJSON Schemaでは記述することができます。anyOf
を使うことで、複数の条件のどれかを満たせばよい、という制約を記述できます。それなりに混み入った条件ではありますが、きちんと表現できていて最近のJSON Schemaは表現力が高いんだな、ということが分かりました。
{ ..., "$defs": { ..., "column_must_have_not_null": { "description": "data_reliability_levelがtrusted_dataの場合、各カラムにdata_testsまたはconstraintsのいずれかにnot_nullが含まれている必要があります", "anyOf": [ { "required": ["constraints"], "properties": { "constraints": { "type": "array", "minItems": 1, "contains": { "type": "object", "properties": { "type": { "const": "not_null" } }, "required": ["type"] } } } }, { "required": ["data_tests"], "properties": { "data_tests": { "type": "array", "minItems": 1, "contains": { "anyOf": [ { "required": ["not_null"], "type": "object", "properties": { "type": "object", "not_null": { "type": "object", "properties": { "required": ["where"], "type": "object", "properties": { "where": { "type": "string" } } } }, "required": ["config"] } } ] } } } } ] }, ..., } }
まとめ
Data Reliability Levelというデータに対する期待値を設定する活動において、入力が必須となるメタデータが出てきました。必須となる条件も混み入ったものがあり、レビューアーの負荷になり得るものでしたが、JSON Schemaをうまく使うことで必須項目が入力されているかをうまくvalidateできるようになりました。
JSON Schemaはもっと簡単なものしか定義できないと思っていましたが、ちゃんと勉強しないといけないなと反省しました...!
*1:一般的な用語ではなく社内で定義した用語になります