Feedforce Developer Blog

フィードフォース開発者ブログ

Looker で Join 先の view の primary_key をいい感じにテストする方法をようやく見つけた

こんにちは、id:masutaka26 です。 先週はまるっと夏休みにしてました。今日からまた Looker と戯れる日々が始まります。

丸1年 Looker と戯れてきて最近ようやく、Join 先の view でも primary_key が壊れてないことを保証するテストの書き方が分かったので、今回紹介します。

Looker における primary_key の役割

Looker には Symmetric Aggregates という、合計を重複させない素晴らしい仕組みがあります。以前このブログでも紹介しました。

その Symmetric Aggregates では primary_key が重要な役割を果たします。適切に設定されていないと、以下のような問題が発生します。

  • primary_key が重複すると fanout エラーが発生することがある
  • primary_key が null だと Measure が 0 になることがある

このような問題は大概、ふわっと質問が来て発覚します。今のタスクを保留にして調査することは精神的になかなか辛いものがあり、それなりに時間も費やすことになるため、可能な限り事前に避けたいところです。

primary_key の実装例

私が所属する Feedmatic では、ウェブ広告や Google Analytics のデータを扱っています。正規化されたきれいなデータは少なく、Rails の id のようなユニークなカラムは存在しないことが多いです。

そのため、このようにいくつかの Dimension を組み合わせて primary_key を定義します。

dimension: id {
  primary_key: yes
  type: string
  sql: CONCAT(${dimension1}, ${dimension2}, IFNULL(${dimension3}, '')) ;;
  hidden: yes
}

DWH は BigQuery を使っています。

これで済めばよいのですが、上の例だとある日突然 dimension2 が null になり始めたり、全ての string 型の Dimension を使っても重複し始めることがあります。データの性格は理解していたつもりでしたが、実際どちらもありました。😭

LookML 開発におけるテスト

以上の課題を解決するために、test パラメータが使えます。

例えばこのような parent explore があったとします。Join がないのでシンプルです。

explore: parent {
  ...
}

view: parent {
  dimension: id {
    primary_key: yes
    ...
  }

  measure: count {
    type: count
  }
}

私はこのようなテストを書いて、全ての parent.id が null でないことと、重複しないことを保証させています。

test: parent_id_is_not_null {
  explore_source: parent {
    column: id {}
    sorts: [parent.id: asc]
    limit: 1
  }
  assert: id_is_not_null {
    expression: NOT is_null(${parent.id}) ;;
  }
}

test: parent_id_is_unique {
  explore_source: parent {
    column: id {}
    column: count {}
    sorts: [parent.count: desc]
    limit: 1
  }
  assert: id_is_unique {
    expression: ${parent.count} = 1 ;;
  }
}

👉 ソート時に null が先頭と末尾のどちらに来るかは、DWH の実装によります。

Join 先の view は primary_key をテスト出来ないことがある

さて、child view を Join する必要が出てきました。

explore: parent {
  join: child {
    type: left_outer
    relationship: one_to_many
    sql_on: ... ;;
  }
}

view: parent {
  dimension: id {
    primary_key: yes
    ...
  }

  measure: count {
    type: count
  }
}

view: child {
  dimension: id {
    primary_key: yes
    ...
  }

  measure: count {
    type: count
    hidden: yes
  }
}

同じように child.id のテストを書きましたが、うまくいきません。is_not_null はまだしも、is_unique がダメです。

# これは OK
test: parent_child_id_is_not_null {
  explore_source: parent {
    column: id { field: child.id }
    sorts: [child.id: asc]
    limit: 1
  }
  assert: child_id_is_not_null {
    expression: NOT is_null(${child.id}) ;;
  }
}

# parent の count になり、テストが通らない。
test: parent_child_id_is_unique {
  explore_source: parent {
    column: id { field: child.id }
    column: count { field: child.count }
    sorts: [parent.count: desc]
    limit: 1
  }
  assert: child_id_is_unique {
    expression: ${child.count} = 1 ;;
  }
}

よく考えれば当たり前の話で、Join した状態でテストを書いているからです。そもそも child view の primary_key のテストをしたいだけなのに、Join は邪魔です。

Join 先の view の primary_key をいい感じにテストする

child view と同じファイルに、こっそり child explore を定義します。hidden にして存在を消しています。さらに required_access_grants で、開発者以外の URL 直打ちによるアクセスも防いでいます。

view: child {
  dimension: id {
    primary_key: yes
    ...
  }

  measure: count {
    type: count
    hidden: yes
  }
}

# Define for test
explore: child {
  hidden: yes
  required_access_grants: [can_view_explores_for_tests]
}

access_grant である can_view_explores_for_tests はこのような定義です。

# For tests
access_grant: can_view_explores_for_tests {
  user_attribute: view_explores_for_tests
  allowed_values: ["yes"]
}

User attribute である view_explores_for_tests は、今回のような「Join 先の view をテストすること」全般に使います。User Access は None、Default Value も no です。開発者用の Group を作り、その Group value を yes にしました。

ここまでやらずとも全員アクセス不可でも良いのですが、テストが落ちた時に「クエリの探索」からの調査ができなくなるので、開発者にはアクセス権を与えるポリシーにしています。

あとは parent explore と同じようにテストを書くだけです。

test: child_id_is_not_null {
  explore_source: child {
    column: id {}
    sorts: [child.id: asc]
    limit: 1
  }
  assert: id_is_not_null {
    expression: NOT is_null(${child.id}) ;;
  }
}

test: child_id_is_unique {
  explore_source: child {
    column: id {}
    column: count {}
    sorts: [child.count: desc]
    limit: 1
  }
  assert: id_is_unique {
    expression: ${child.count} = 1 ;;
  }
}

テストは通っても、LookML validation error が発生するかもしれません。その時は fields パラメータを使って、露出する field を限定すると良いでしょう。

# Define for test
explore: child {
  hidden: yes
  required_access_grants: [can_view_explores_for_tests]
  fields: [child.id, child.count] # Avoid LookML validation error
}

このテクニックは Error: Unknown or Inaccessible Field – Looker Help Center でも紹介されています。

まとめと所感

LookML 開発者で、且つテストを書いていて、且つ Join 先の view の primary_key に課題を抱えている、大変ニッチな層向けに記事を書きました。どこかの誰かに参考になれば幸いです。

もっと良い方法や、今回のやり方はここがマズイとかあれば @masutaka にお知らせ頂けると大変うれしいです。🙏

Feedmatic では今回のような view は数十もあり、primary_key のテストはまだ書き始めたばかりです。

Looker ではテストは直列でしか実行されないようで、書けば書くほど全テストが遅くなるのはモヤモヤしています。さすがに要望しようと思ってますが。

それに関連して、最近ディレクトリやファイル構成を再検討しました。次回はその記事を書く予定です。

おまけ

今回の記事を書く過程で、中の人が書いたベストプラクティスを見つけました。

今回の記事ほど細かいテクニックは書かれていませんが、全体を網羅した良記事なので要チェックです。