🥁

Dataform のアサーションを理解する

2025/01/16に公開

はじめに

こんにちは、クラウドエース株式会社 第一開発部所属の工藤です。
本記事では、Dataform のアサーションについてまとめました。
この内容を詳しく書いている記事はあまりない印象なので、これから Dataform を使ってみようと考えている方はぜひご覧ください。

Dataform とは

Dataform は、BigQuery Studio に含まれるプロダクトであり、BigQuery に格納しているデータの加工処理やアサーション等のワークフローを作成できます。また、ワークフローの作成だけでなく、アサーションによるテストやソースコードのバージョン管理などもできます。

Dataform でワークフローを作成するときは SQLX(SQL の拡張機能) か JavaScript を使用します。この記事では SQLX をメインに使うため JavaScript を使ったワークフローの作成方法は、以下のドキュメントをご覧ください。

https://cloud.google.com/dataform/docs/javascript-in-dataform?hl=ja

ここで SQLX を使い作成するファイル(以下、SQLX ファイル)について、簡単に説明します。
例えば、test というカラムを持ち、「SELECT 1 AS test」という処理を内包するビューを作成する場合、以下のような SQLX ファイルを作成します。

-- config ブロック
config {
  type: "view",
  columns: {
    test: "A description for the test column",
  }
}

-- 処理内容
SELECT 1 as test

SQLX ファイルは、そのファイル内の処理で作成するテーブルの列名などのメタデータやアサーションと呼ばれるデータ品質テストの定義などを設定する config ブロックと、SQL で処理内容を記述する部分の 2 段構成になります(config ブロックは省略可能です)。

Dataform を初めて使う時は、以下のドキュメントを確認しながら SQLX ファイルを作成するとよさそうです。

https://cloud.google.com/dataform/docs/dataform-core?hl=ja#sqlx_file_body

なお、Dataform は無料で利用することができますが、BigQuery 等の関連するプロダクトの利用には料金が発生します。そのため、利用する時は、料金に関するドキュメントを確認することをおすすめします。

Dataform のアサーション

Dataform アサーションは、実装したワークフローをテストできるものになります。Dataform のアサーションには以下の 2 種類あり、SQLX ファイルでの定義方法がそれぞれ異なります。

  • 組み込みアサーション
  • 手動アサーション

以降、それぞれのアサーションについて説明します。

組み込みアサーション

組み込みアサーションは、テスト内容が Dataform 側で事前に準備されているものであり、NULL 値の有無や値の重複などを検査できます。組み込みアサーションで検査できる内容は以下の通りです。

  • 対象のカラムに NULL 値が含まれていないか(nonNull アサーション)
  • 対象のカラムに重複している値が含まれていないか(uniqueKey アサーション、uniqueKeys アサーション)
  • 特定の条件に合致しているデータがないか(rowConditions アサーション)

組み込みアサーションは、SQLX ファイルの config ブロック内の assertions パラメータを使って定義します。以下は、user_id カラムと customer_id カラムに NULL 値が含まれていないか検査する時の config ブロックの入力例になります。

config {
  type: "table",
  assertions: {
    nonNull: ["user_id", "customer_id"]
  }
}
SELECT ...
FROM ...

uniqueKey アサーション等の例はドキュメントに記載されていますので、気になる方はご覧ください。

組み込みアサーションの検査タイミングは、SQLX ファイル内に記述された SQL が実行された後になります。そのため、アサーションが失敗した場合にファイル内の SQL を実行したくないときは、後述の手動アサーションを使用する必要があります。
また、検査できる対象は SQLX ファイルに記述された SQL の実行結果に対してのみになります。そのため、SQL の入力等で使う BigQuery テーブルやビューなどに対してテストを行いたいときも手動アサーションを利用する必要があります。

手動アサーション

手動アサーションは、テスト内容をユーザーが独自に定義できるものであり、組み込みアサーションで検査できない内容も対応できます。手動アサーションでは、テスト用の SQL を含むアサーション専用の SQLX ファイルを用意します。このときの SQL の実行結果が 0 件であればアサーションは成功、1 件以上あれば失敗とみなされます。

例えば、sometable.sqlx で定義されたオブジェクトに格納されたデータと、その処理で期待する結果を格納した verification_table テーブルのデータに差異がないかを検査する場合、使用する SQLX ファイルは以下のようになります。

config {
    type: "assertion"
}

SELECT
  *
FROM
  ${ref("sometable")}
EXCEPT DISTINCT
SELECT
  *
FROM (
  SELECT
    1 AS a,
    2 AS b ) AS verification_table

手動アサーションを含む処理において、アサーションの検査タイミングは後続のアサーションの依存関係に指定する SQLX ファイル内に記述された SQL が実行される前になります。また、検査できる対象は、SQLX ファイルに記述された SQL の実行結果だけでなく、入力等に使う BigQuery テーブルやビューなども対象にできます。

以降では、上記のようなアサーションをワークフローに含める方法を説明しますが、アサーション専用のファイルのみを単体で実行することもできます。その際の手順は、以下のドキュメントに記載されていますので、気になる方はご覧ください。

https://cloud.google.com/dataform/docs/trigger-execution?hl=ja#manually_execute_selected_actions

アサーションをワークフローに含める方法

アサーションをワークフローに含めるには、SQLX ファイルの config ブロックにそのアサーションを依存関係として設定する必要があります。この設定により、アサーションが失敗した(テストで NG の結果が見つかった)時は、後続の処理を実行せずにワークフロー自体を終了させることができます。

例えば、アサーションである「assertion1」と「assertion2」が失敗したときに後続の処理を実行せずにワークフローを終了させたいときは、以下のような SQLX ファイルを作成します。

config {
    type: "table",
    dependencies: [ "assertion1", "assertion2" ]
}

SELECT
  *
FROM
  `project_id.dataset_id.table_id`

依存関係の設定には、dependencies パラメータを使用します。上記の例では、依存関係としてアサーションの「assertion1」と「assertion2」を設定しており、これらのアサーションが失敗したときにはワークフローが終了します。

アサーションを含むワークフローを作る際に利用できるものとして dependOnDependencyAssertions パラメータがあります。
dependOnDependencyAssertions パラメータは、処理 B が依存する処理 A の結果に対するアサーションを、処理 B の実行前に実施させることができます。
つまり、dependencies パラメータなしで処理 A に対するアサーションを、処理 B の依存関係に自動的に設定できます。

例えば、集計処理「aggregate」と依存関係にあるデータ抽出処理「extract」にいくつかアサーションがある場合、dependencies パラメータを使ってこれらのアサーションを集計処理の依存関係として設定する場合は、以下のようになります。
※ このとき、データ抽出処理のアサーションを集計処理の依存関係として設定していない場合、そのアサーションが失敗しても集計処理は実行されてしまいます。

aggregate.sqlx
config {
    type: "table",
    dependencies: [ "extract_assertion1", "extract_assertion2" ]
}

SELECT
  *
FROM
  ${ref("extract")}

上記の例では、対象のアサーションを個別に記述していますが、dependOnDependencyAssertions パラメータを使う場合は以下の内容だけで済みます。

aggregate.sqlx
config {
    type: "table",
    dependOnDependencyAssertions: true
}

SELECT
  *
FROM
  ${ref("extract")}

依存関係として設定したいアサーションが多数ある場合は、dependOnDependencyAssertions パラメータを使うことで設定の手間が省けてよさそうです。
ここでは、dependencies パラメータと dependOnDependencyAssertions パラメータを使用する例を紹介しましたが、以下のドキュメントには dependOnDependencyAssertions パラメータと includeDependentAssertions というパラメータを組み合わせる例もありますので、興味のある方はご覧ください。

https://cloud.google.com/dataform/docs/assertions?hl=ja#add_assertions_as_dependencies

アサーションを含むワークフローを作る

それではアサーションを含むワークフローを作ってみます。
今回作成するワークフローは以下の構成になります。

ワークフローの構成
処理構成

上記ワークフローの各要素は以下のような役割になります。

要素 説明
usa_1910_2013 テーブル 一般公開データセットの usa_names テーブルのデータを一部加工し、格納したテーブル
処理 1 usa_1910_2013 テーブルの state カラムの NULL 値を除外して extract テーブルを作成する処理。また、その処理結果を検証する組み込みアサーションを含む。
extract テーブル 処理 1 で加工されたデータを格納するテーブル
処理 2 extract テーブルの name カラムと gender カラム を集計軸に number カラムの値を集計して aggregate テーブルを作成する処理。また、その処理結果を検証する手動アサーションを含む。
aggregate テーブル 処理 2 で加工されたデータを格納するテーブル

上記の処理 1,2 で作成した SQLX ファイルは、それぞれ以下のとおりです。

処理 1
この処理では、usa_1910_2013 テーブルのデータから state カラムが NULL であるデータを除外して extract テーブルを作成します。また、組み込みアサーションの nonNull アサーションを使い、extract テーブルの state カラムに NULL 値が含まれているか検査します。

definitions/extract.sqlx
config {
    type: "table",
    assertions: {
        nonNull: ["state"]
    }
}

SELECT
  *
FROM
  `project_id.dataset_id.usa_1910_2013`
WHERE
  state IS NOT NULL

処理 2
この処理では、処理 1 で作成された extract テーブルにおいて name,gender カラムを集計軸に number カラムの値を集計し、aggregate テーブルを作成します。また、手動アサーションを使い、処理後の結果が合っているかを検証しています。config ブロック内で処理 1 と手動アサーションを依存関係として設定しており、これらの処理およびアサーションが失敗した場合は、処理 2 の集計処理が実行されないようにしています。

以下の「definitions/aggregate.sqlx」ファイルは処理 2 の集計処理を記述しているもので、「definitions/assertion.sqlx」ファイルは処理 2 の実行結果が、期待する処理結果を格納している aggregate テーブルのデータと差異がないかを検査するアサーション専用のファイルです。

definitions/aggregate.sqlx
config {
    type: "table",
    dependOnDependencyAssertions: true,
}

SELECT
  name,
  gender,
  SUM(number) AS total
FROM
  ${ref("extract")}
GROUP BY
  name,
  gender
definitions/assertion.sqlx
config {
    type: "assertion"
}

SELECT
  name,
  gender,
  SUM(number) AS total
FROM
  ${ref("extract")}
GROUP BY
  name,
  gender
EXCEPT DISTINCT
SELECT
  *
FROM
  `project_id.test.aggregate`

このワークフローを「COMPILED GRAPH」タブから確認すると、ワークフロー内の各処理とアサーションの関係を確認できます(以下画像)。

ワークフロー

上記の画像から、処理 1(extract)のアサーション(article_extract_assertion...)は処理 1 の SQL が実行された後にアサーションが実行されそうなのに対し、処理 2(aggregate)のアサーション(assertion)は処理 2 の SQL が実行される前にアサーションが実行されそうです。

このワークフローを実行して、どんな結果になるか確認してみます。
「実行を開始」から「操作を実行」を押下すると「Execute」タブが開き、タブ内の「ALL ACTIONS」を選択した上で「実行を開始」を押下するとワークフローが実行されます。

画面操作
画面操作

ワークフローの実行結果は「EXECUTIONS」タブから確認することができ、今回の実行が正常終了したことがわかります。

実行結果

作成された extract テーブル
実行結果

作成された aggregate テーブル
実行結果

「EXECUTIONS」タブから確認できる実行結果は、該当の開始時刻を押下することでの詳細な内容を確認できます。

実行結果

この実行結果の詳細を確認すると、ワークフロー内でどの処理がどの順番で実行されたか確認することができます。

実行結果

上記の実行結果より、先ほど「COMPILED GRAPH」タブから確認した通り、処理 1(extract)のアサーション(article_extract_assertion...)は SQL が実行された後にアサーションが実行されているのに対し、処理 2(aggregate)のアサーション(assertion)は SQL が実行される前にアサーションが実行されたことがわかります。

次にアサーションが失敗したときにワークフローの実行結果がどうなるか確認してみます。
ここでは、処理 1(extract)のアサーション(article_extract_assertion...)が失敗するようにコードを修正した上でワークフローを実行してみます。

修正したコードはこちら
definitions/extract.sqlx
config {
    type: "table",
    assertions: {
        nonNull: ["state"]
    }
}

SELECT
  *
FROM
  `project_id.dataset_id.usa_1910_2013`
-- WHERE
--   state IS NOT NULL

実行した結果は以下画像のとおりであり、実行したワークフローは異常終了しています。
詳細な実行結果を見ると、アサーションが失敗したところで処理は終わっており、後続の処理である aggregate が実行されていないことがわかります。

実行結果
実行結果

おわりに

本記事では、Dataform のアサーションについてまとめました。
BigQuery にデータがある状態でワークフローを作成する場合は、自動テストの導入やコードのバージョン管理などの観点から、Dataform を利用するのがよいと思いました。
今回はアサーションについてまとめましたが、Dataform を利用する上でのベストプラクティスもあり、理解すべきことが他にもありそうなので、時間があればその辺りもまとめたいと思います。

最後までご覧いただきありがとうございました。

Discussion