❄️

SnowflakeのData Quality Monitoring機能についての考察

に公開
2

データ品質のモニタリングにお悩みはありませんか?

Snowflakeは他のサードパーティのソリューションをプラグインしやすいエコシステムが充実している点がいいところですが、最近はSnowflake自体が簡単なインターフェースを用意してSnowflakeだけでデータ基盤運用に必要な機能が提供されていく進化をしているように感じます。

その姿はクラウドのサービスを組み合わせてシステムを構築していく考え方に近く、まさにData Cloud (最近はAI Data Cloudですね) な訳ですが、今回は比較的地味だけど便利なData Quality Monitoringに関して、こう使ってみたらいいのではないか?という考察をしてみようと思います。
https://docs.snowflake.com/en/user-guide/data-quality-intro

ややData Quality Monitoringに関しては捉え所が難しいところも多く、ただ機能を眺めるだけだと実践するイメージを持つのが難しいのかなと思います。そこで、この記事ではより具体的なユースケースに落とし込んで、その課題にフィットするのかどうか考えていく形で進めようと思います。

データ品質の監視という一般的な課題という事もあり、これが唯一の正解というわけではないのですが、Snowflakeが用意してくれている機能ならではの良さもあったりするので、この記事を通して面白い発見があったりすれ幸いです。
ぜひ、自分たちが携わっているプロジェクトだとどうだろうかという視点も踏まえつつ読んでみて下さい!

DMFに関しての基礎知識

詳細はこちらの検証記事などの方が詳しくまとまっているのでこちらをご覧ください。
https://zenn.dev/sts_snowflake/articles/dbff2d34195f88

今後の考察を解像度高く読むために主要な仕様に関してまとめていきます。

  • ALTER TABLEなどのDDLでスケジュールと取得するメトリクス (DMF) に関して設定する
  • Snowflakeが事前に定義したシステムDMFとユーザーが定義するカスタムDMFがある
  • メトリクスはSnowflake管理のイベントテーブルに格納され、テーブル関数やビューを介してアクセス可能
  • 取得したメトリクスを利用してアラートなどを設定することで通知の仕組みを構築できる
  • サーバーレスコンピューティングによる課金が発生する

DMFを利用する価値があるケースについて

DMFの一番の特徴は「データの状態を自動的に一貫して測定して記録できる」です。

とはいえ、これだけだとイメージが難しいと思うので、もう少し具体的なユースケースに落としてみます。

例えば、ストリーミング的な形でデータパイプラインが組まれているようなケースを想定します。そのデータを利用して重要な施策が動いているとします。
もし、障害が起きてそのパイプラインが停止してしまうとかなりの機会損失を起こしてしまいます。

もちろんパイプライン自体にもエラーハンドリングなどの仕組みを入れていますが、パイプラインの基盤自体が落ちてしまうとエラ〜通知がうまく飛ばないケースがあり、その対策が求められているとします。

そのようなケースに対しての対策としてfreshnessやnull_countなどのメトリクスを取得してモニタリングすることでデータの遅延などの品質問題を早期に検知して対応ができるようになります。

dbtのdata testなどとの棲み分けについて

(ここからが本編です)

モダンなデータ基盤では、dbtなどを利用してELTのTransformの機能を実現するのが当たり前の世の中になってきました。その場合データ品質に関してはdbtのdata testに任せるのが一般的で実用的なプラクティスでしょう。

そのようなケースにおいてどのようにこのDMFが活用できるのか考察してみます。

dbt自体が動かない時もある

dbt data testだけに頼った場合に起こる最もクリティカルな問題としてdbt自体が実行できなくなる or 完了しないケースに対してのモニタリングです。

通常dbt data testに頼ってデータ品質を担保しているようなケースにおいてはdbt buildやdbt testなどを利用してデータ品質の問題に対処します。

このdbt実行する基盤が障害の影響を受けたり、ジョブの設定ミスなどが発生したりしてうまく動作しない場合にはエラーを検知するプロセス自体が走らなくなり、通知が飛びません。時々稀にクエリのプロファイルが最適な処理順序を通ってくれなくて処理が何倍もの時間掛かる(下手したら終わらない)ような事象も経験した人がいるかもしれません。

そういった通常は起こり得ないがたまに発生するレアケースに対しての検知の方法はなくはありません。その一つとして正常実行が完了した場合でも通知を行い「ヨシ!」を確認する方法です。

ただ、現実的には正常実行時の通知をいちいち確認するほどの余裕はなく設定される事は少ないと思います。結果として異常時の通知が届かないことによりパイプラインが動いていないことに気づくことができずに、データの更新止まっていることを利用者が「はて?」と実際にデータ見ている中で検知したりすることが発生します。

つまり、正常じゃないけど、エラー通知が飛ばないというケースに対してモニタリングを行いたい時が発生するのです。

そんな時に役に立つのがdbtのプロセスとは別で動いてくれるDMFとアラートの仕組みです。

freshnessやrow_countなどを利用してデータの更新が停止していることを検知するように仕込めばdbtに依存しないモニタリングの冗長化が実現できます。

特殊なデータモニタリングの要件にも対応しやすい

他にもご利益が少しあるのでポエム的にまとめます。

個人的にはソースとして利用されているデータの更新が止まっていないかというのがクリティカルなので最も優先的にテストを設定する対象だと思っています。

一方で、世の中には土日祝日などのタイミングではほぼデータの更新が発生しないデータというのは存在します。そういった特殊なモデルの鮮度チェックは土日祝日では動いてほしくありません。
じゃあ、dbtをそもそも実行しなければいいのではないか?という話もあるかもしれませんが、ほぼ更新が発生しないだけで発生していたらデータとしては日次のバッチ処理の対象としたいというケースがあるかもしれません。つまりdbtは止められないということです。
(特にBtoB系は土日祝日にガッツリデータの更新が止まりがちだったりします。必ずしも毎回更新がないわけではないパターンもあったりするようなケースです。)

そういったケースでは、dbtでdata testをかなり作り込まないと偽陽性のエラー検知が発生してしまいオオカミ少年のようなモニタリングになってしまいがちです。

そんな場合でも柔軟に設定できるのがDMFなのです。

DMFはその仕組み上、モニタリング時の閾値の設計などはSQLベースでかなり柔軟に設定可能なので、土日祝日カレンダーなどを組み合わせて営業日かどうかの判定を組み込みやすいです。
(alertはtrue or falseで発火の条件を設定できますが、dbtのtestはエラー対象のレコードを返すように設計しないといけないので、ちょっと表現が複雑になってしまうケースがあります。ちょっとだけなので気にならない方は気にならないと思うレベルですが…)

鮮度以外でもメトリクスという形式で溜め込んでいるメリットはとても多くあります。

また、その日の断面だけでなくて、数日連続してある閾値を下回っていると通知をしたいなどの場合にはそもそもテーブルの設定を大きく変えないとモニタリングがうまくできないようなケースも出てきたりする場合もあります。DMFの場合はメトリクスとして過去の連続したデータを組み合わせてアラートのロジックを組めるのでとても柔軟性の高いモニタリングを後付けできます。

あと、dbtをbuildで動かしている場合はdata testが敏感すぎると後続のモデルのbuildがスキップされちゃうので偽陽性の出るテストは組みにくくて、結局じゃあテストに入れなくていいかみたいな妥協をすることもあるかもしれないです。更新されていないのが異常じゃないテーブルの扱いは永遠の課題です。

DMFじゃないとできないことは何一つないけど…

これにつきますが、DMFなんて無くても誰も困らない、そんな世の中なんだと思います。いい時代です。

そんな世の中だとしても私はDMFを推したい。なぜならサーバレスコンピューティングによる課金が平均的にならすとめちゃ安いからです。
(環境ごとに異なるので責任は取りませんが、小さめのテーブルでメタデータのクエリのみで完結するメトリクスの場合は平均でならすとXSの最小課金時間の60秒の10%以下くらい安くできる場合があります。元が大したことないのですが、チリつもです。)

ちなみに小話コーナー

DMFを色々検証している中で気になったことをつらつら書きます

freshnessにTIMESTAMP_NTZが入れられない件について

これはtimestampあるあるだと思うのですが、timestampのエイリアスがtimestamp_ntzだったりするので、何も考えずにぼーっとデータを取り込んでいるとtimestamp_ntzのまま取り込みがちですが、実際には結構多くのデータはtimestamp_ltzやtimestap_tzの方がよかったりします。(個人的には基本的にtimetamp_ltzで正しい時間になるように補正したりするのが直感的なのでわかりやすいと思っています。)

えっ!そもそも違いがわからないって!そんな人はドキュメントを読み込んでください!!!
https://docs.snowflake.com/ja/sql-reference/data-types-datetime
(読んでもわからなかったら、SnowSightを開いて適当にそれぞれのデータ型での値を用意して = で比較するselect文を投げて、true/falseが完全に当てられるようになって下さい)

timestamp_ntzはタイムゾーンの情報を持たないので実はAsia/Tokyoの時間と思っていたらUTCだったとか、その逆のパターンなどもあったりするかもしれません。つまり、timestamp_ntzはタイムゾーン問題を後回しにしている感があるデータ型だと思っています(個人の見解です)。
(ちゃんとタイムゾーンを把握した上であえてNTZを利用しているケースを否定するのはものではありません)

NTZを使うケースはそこそこあると思うのですが、ことfreshnessを利用する際にはタイムゾーンはクリティカルな影響を受けるのです。
なぜなら、freshnessはタイムスタンプの差分の秒数をメトリクスとして収集するのですが、その起点となるタイムスタンプはUTCではなくて実際のタイムゾーンになるからです。
もし取得対象のtimestampがAsia/Tokyoだったケースであればいいのですが、UTCだった場合にそのメトリクスは勝手に9時間ずれた状態になってしまいます。(と思うのですが、別にそこを考慮してアラームの閾値を調整すればいいだけなのでそこまでクリティカルかと言われるとそうでもないのです…)

つまり、DMFを利用してfreshnessを扱う時には基本的にローカルのタイムゾーンに合わせるべきという話です。(まぁ普通に揃えておくに越した事はないです。)

つまり、DMFを使いたいからきっちりとタイムゾーンを意識した設計を導入できるようになるわけです。TIMESTAMP_NTZでお茶を濁しているそこのあなた!これを機にちゃんとTIMESTAMPをタイムゾーンありの状態で扱えるようにしてみて下さい!

dbtでtableのmaterializationを利用して毎回create or replaceしているとせっかく設定したDMFも一緒に消える悲しみがある

dbtは便利ですが、materializedがtableとかviewだと毎回create or replaceが発生するという地味に痛い仕様があります。

replaceされるとせっかくDMF設定しても消えちゃうので、dbtのmacroを組んでyamlのconfigにmetaで設定を書いておいて、post_hookでそのマクロを呼び出して設定できるようにしておくといいと思います。

tableのIDが変わりますが、同じ名前なのでSQLでテーブル名で扱う分には問題なく扱えますのでモニタリングの面では問題ないです。

2025/11/02追記

GISHI2439さん、コメントありがとうございました。

Codexで作ってもらって一旦動作確認できたコードを貼っておきます。(適宜プロジェクトごとに固有のカスタマイズしてみてください。)
記事執筆時点から時間が経ち、Expectationが追加されているのでExpectation対応版を改めて一から作ってみました。

dbtマクロのサンプルコード
macros/util__set_dmf.sql
{%- macro util__set_dmf() -%}
  {%- if not execute -%}
    {{- return('') -}}
  {%- endif -%}

  {%- if model is not defined or model is none -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set meta = model.meta if model.meta is not none else {} -%}
  {%- set dmf_config = meta.get('dmf_config') -%}
  {%- if not dmf_config -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set resolved_relation = adapter.get_relation(
    database=this.database,
    schema=this.schema,
    identifier=this.identifier
  ) -%}

  {%- if resolved_relation is none -%}
    {%- do log('util__set_dmf: Could not fetch relation ' ~ this.render() ~ ', skipping', info=True) -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set object_type = resolved_relation.type.upper() -%}

  {%- if object_type not in ['TABLE', 'VIEW'] -%}
    {%- do log('util__set_dmf: Unsupported relation type ' ~ object_type, info=True) -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set relation_name = this.render() -%}
  {%- set statements = [] -%}

  {#-- Configure schedule --#}
  {%- set schedule_value = _util__dmf_schedule_value(dmf_config.get('schedule')) -%}
  {%- if schedule_value is not none -%}
    {%- set escaped_schedule = schedule_value.replace("'", "''") -%}
    {%- set schedule_statement -%}
ALTER {{ object_type }} {{ relation_name }}
  SET DATA_METRIC_SCHEDULE = '{{ escaped_schedule }}';
    {%- endset -%}
    {%- do statements.append(schedule_statement) -%}
  {%- endif -%}

  {#-- Configure ROW_COUNT DMF --#}
  {%- set has_row_count_cfg = (dmf_config is mapping) and ('row_count' in dmf_config) -%}
  {%- set row_count_cfg = dmf_config.get('row_count') if has_row_count_cfg else none -%}
  {%- if has_row_count_cfg -%}
    {%- set row_count_cfg = {} if row_count_cfg is none else row_count_cfg -%}
    {%- if row_count_cfg not in [True, False] and not (row_count_cfg is mapping) -%}
      {%- do exceptions.raise_compiler_error(
        "dmf_config.row_count must be true/false or a dictionary"
      ) -%}
    {%- endif -%}
    {%- set row_expectation_cfg = none -%}
    {%- if row_count_cfg is mapping -%}
      {%- if 'expectation' in row_count_cfg -%}
        {%- set row_expectation_cfg = row_count_cfg.get('expectation') -%}
      {%- elif 'expectations' in row_count_cfg -%}
        {%- set row_expectation_cfg = row_count_cfg.get('expectations') -%}
      {%- endif -%}
    {%- endif -%}
    {%- set expectation_clause = _util__dmf_expectation_clause(row_expectation_cfg, 'row_count') -%}
    {%- set metric_name = 'SNOWFLAKE.CORE.ROW_COUNT' -%}
    {%- set args_sql = '()' -%}
    {%- set existing_count = _util__dmf_existing_count(this, metric_name, object_type) | int -%}
    {%- if existing_count > 0 -%}
      {%- do log('util__set_dmf: dropping existing ROW_COUNT association (count=' ~ existing_count ~ ')') -%}
      {%- set drop_statement -%}
ALTER {{ object_type }} {{ relation_name }}
  DROP DATA METRIC FUNCTION {{ metric_name }}
    ON {{ args_sql }};
      {%- endset -%}
      {%- do statements.append(drop_statement) -%}
    {%- endif -%}
    {%- set add_statement -%}
ALTER {{ object_type }} {{ relation_name }}
  ADD DATA METRIC FUNCTION {{ metric_name }}
    ON {{ args_sql }}{{ expectation_clause }};
    {%- endset -%}
    {%- do statements.append(add_statement) -%}
  {%- endif -%}

  {#-- Configure FRESHNESS DMF --#}
  {%- set freshness_cfg = dmf_config.get('freshness') -%}
  {%- if freshness_cfg is not none and freshness_cfg != False -%}
    {%- if freshness_cfg not in [True, False] and not (freshness_cfg is mapping) -%}
      {%- do exceptions.raise_compiler_error(
        "dmf_config.freshness must be true/false or a dictionary"
      ) -%}
    {%- endif -%}
    {%- set freshness_dict = freshness_cfg if (freshness_cfg is mapping) else {} -%}
    {%- set column_name = freshness_dict.get('column') -%}
    {%- if 'expectation' in freshness_dict -%}
      {%- set expectation_cfg = freshness_dict.get('expectation') -%}
    {%- elif 'expectations' in freshness_dict -%}
      {%- set expectation_cfg = freshness_dict.get('expectations') -%}
    {%- else -%}
      {%- set expectation_cfg = none -%}
    {%- endif -%}
    {%- set expectation_clause = _util__dmf_expectation_clause(expectation_cfg, 'freshness') -%}
    {%- set quote_column = freshness_dict.get('quote_column', False) -%}

    {%- if column_name -%}
      {%- if column_name is string and column_name.startswith('"') and column_name.endswith('"') -%}
        {%- set column_identifier = column_name -%}
      {%- elif quote_column -%}
        {%- set column_identifier = adapter.quote(column_name) -%}
      {%- else -%}
        {%- set column_identifier = column_name -%}
      {%- endif -%}
      {%- set args_sql = '(' ~ column_identifier ~ ')' -%}
    {%- else -%}
      {%- set args_sql = '()' -%}
    {%- endif -%}

    {%- set metric_name = 'SNOWFLAKE.CORE.FRESHNESS' -%}
    {%- set existing_count = _util__dmf_existing_count(this, metric_name, object_type) | int -%}
    {%- if existing_count > 0 -%}
      {%- do log('util__set_dmf: dropping existing FRESHNESS association (count=' ~ existing_count ~ ')') -%}
      {%- set drop_statement -%}
ALTER {{ object_type }} {{ relation_name }}
  DROP DATA METRIC FUNCTION {{ metric_name }}
    ON {{ args_sql }};
      {%- endset -%}
      {%- do statements.append(drop_statement) -%}
    {%- endif -%}
    {%- set add_statement -%}
ALTER {{ object_type }} {{ relation_name }}
  ADD DATA METRIC FUNCTION {{ metric_name }}
    ON {{ args_sql }}{{ expectation_clause }};
    {%- endset -%}
    {%- do statements.append(add_statement) -%}
  {%- endif -%}

  {%- if statements | length == 0 -%}
    {{- return('') -}}
  {%- endif -%}

  {%- do log('[util__set_dmf] generated ' ~ (statements | length) ~ ' ALTER statement(s) to ' ~ this, info=True) -%}
  {{- return(statements | join('\n')) -}}
{%- endmacro -%}

macros/util_set_dmf/_util__dmf_existing_count.sql
{%- macro _util__dmf_existing_count(relation, metric_name, object_type) -%}
  {%- if not execute -%}
    {{- return(0) -}}
  {%- endif -%}

  {%- if relation is none -%}
    {{- return(0) -}}
  {%- endif -%}

  {%- if object_type is none -%}
    {{- return(0) -}}
  {%- endif -%}

  {%- set info_schema = relation.database | upper ~ '.INFORMATION_SCHEMA' -%}
  {%- set ref_entity = relation.render() -%}
  {%- set ref_literal = "'" ~ ref_entity.replace("'", "''") ~ "'" -%}
  {%- set domain = object_type -%}

  {%- set query -%}
    select count(*) as cnt
    from table({{ info_schema }}.data_metric_function_references(
      ref_entity_name => {{ ref_literal }},
      ref_entity_domain => '{{ domain }}'
    ))
    where metric_database_name || '.' || metric_schema_name || '.' || metric_name = '{{ metric_name }}'
  {%- endset -%}

  {%- set result = run_query(query) -%}
  {%- if result is not none and result.columns|length > 0 and result.columns[0].values()|length > 0 -%}
    {{- return(result.columns[0].values()[0] | int) -}}
  {%- endif -%}

  {{- return(0) -}}
{%- endmacro -%}
macros/util_set_dmf/_util__dmf_expectation_clause.sql
{%- macro _util__dmf_expectation_clause(expectation_config, metric_label) -%}
  {%- if expectation_config is none -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set expectation_entries = [] -%}
  {%- if expectation_config is mapping -%}
    {%- if 'expectations' in expectation_config -%}
      {%- set expectation_entries = expectation_config.get('expectations') -%}
      {%- if expectation_entries is none -%}
        {{- return('') -}}
      {%- endif -%}
    {%- else -%}
      {%- set expectation_entries = [expectation_config] -%}
    {%- endif -%}
  {%- elif expectation_config is string -%}
    {%- do exceptions.raise_compiler_error(
      "dmf_config." ~ metric_label ~ ".expectation must be a dictionary or list of dictionaries"
    ) -%}
  {%- else -%}
    {%- set expectation_entries = expectation_config -%}
  {%- endif -%}

  {%- if expectation_entries is none -%}
    {{- return('') -}}
  {%- endif -%}

  {%- if not (expectation_entries is sequence) -%}
    {%- do exceptions.raise_compiler_error(
      "dmf_config." ~ metric_label ~ ".expectation must be a dictionary or list of dictionaries"
    ) -%}
  {%- endif -%}

  {%- set clauses = [] -%}
  {%- for expectation in expectation_entries -%}
    {%- if not (expectation is mapping) -%}
      {%- do exceptions.raise_compiler_error(
        "dmf_config." ~ metric_label ~ ".expectation entries must be dictionaries with name and expression"
      ) -%}
    {%- endif -%}

    {%- set expectation_name = expectation.get('name') -%}
    {%- set expectation_expression = expectation.get('expression') -%}

    {%- if not expectation_name -%}
      {%- do exceptions.raise_compiler_error(
        "dmf_config." ~ metric_label ~ ".expectation.name must be provided for every expectation"
      ) -%}
    {%- endif -%}

    {%- if not expectation_expression -%}
      {%- do exceptions.raise_compiler_error(
        "dmf_config." ~ metric_label ~ ".expectation.expression must be provided for every expectation"
      ) -%}
    {%- endif -%}

    {%- do clauses.append(expectation_name ~ ' (' ~ expectation_expression ~ ')') -%}
  {%- endfor -%}

  {%- if clauses | length == 0 -%}
    {{- return('') -}}
  {%- endif -%}

  {%- set formatted_clauses = [] -%}
  {%- for clause in clauses -%}
    {%- if loop.first -%}
      {%- do formatted_clauses.append('EXPECTATION ' ~ clause) -%}
    {%- else -%}
      {%- do formatted_clauses.append(clause) -%}
    {%- endif -%}
  {%- endfor -%}

  {{- return('\n    ' ~ (formatted_clauses | join(',\n    '))) -}}
{%- endmacro -%}
macros/util_set_dmf/_util__dmf_schedule_value.sql
{%- macro _util__dmf_schedule_value(schedule_config) -%}
  {%- set default_schedule = "USING CRON 0 9 * * * Asia/Tokyo" -%}
  {%- if schedule_config is none -%}
    {{- return(default_schedule) -}}
  {%- endif -%}

  {%- if schedule_config in [False, 'skip', 'SKIP'] -%}
    {{- return(none) -}}
  {%- endif -%}

  {%- if schedule_config is mapping -%}
    {%- if schedule_config.get('expression') -%}
      {{- return(schedule_config.get('expression')) -}}
    {%- elif schedule_config.get('cron') -%}
      {%- set tz = schedule_config.get('timezone', 'Asia/Tokyo') -%}
      {{- return("USING CRON " ~ schedule_config.get('cron') ~ " " ~ tz) -}}
    {%- elif schedule_config.get('minutes') -%}
      {{- return(schedule_config.get('minutes') ~ " MINUTE") -}}
    {%- endif -%}
    {{- return(default_schedule) -}}
  {%- elif schedule_config is string -%}
    {{- return(schedule_config) -}}
  {%- endif -%}

  {{- return(default_schedule) -}}
{%- endmacro -%}

動作確認用のモデル

models/test_dmf.sql
{{
    config(
        materialized='incremental',
    )
}}

select
    current_date() as current_date,
    current_timestamp() as updated_at
models/test_dmf.yml
version: 2

models:
  - name: test_dmf
    description: "A simple model that selects the current timestamp"
    config:
      post_hook:
        - "{{ util__set_dmf() }}"
    meta:
      dmf_config:
        # schedule:
          # cron: "0 8 * * *"
          # timezone: UTC
        # schedyle:
          # expression: "TRIGGER_ON_CHANGES"
        # schedyle:
          # minutes: 60  # every hours
        row_count:
          expectations:
            - name: row_count_positive
              expression: VALUE > 0
            - name: row_count_under_10
              expression: VALUE < 10
        freshness:
          column: updated_at
          expectations:
            - name: freshness_lt_3600
              expression: VALUE < 3600
    columns:
      - name: current_date
        description: "The current date when the query is run"
      - name: updated_at
        description: "The current timestamp when the query is run"

メタデータのみで完結するメトリクスを基本的には取得するべき

地味な話ですが、そこそこ複雑なjoinなどのロジックを含むviewだと単にrow_countを取ろうとしても毎回クエリを実行する必要があり、コストがすごくかかります。そしてそのコストはサーバレスの課金体系なので、ちょっとお高いです。

テーブルもしくは簡単な加工しかしていないでメタデータから情報を取得できるviewだけに適用するがDMFをコスパよく使うコツです。メタデータだけで済む場合は特にサーバーレスの課金体系のいいところだけ享受できて、XSの最小課金単位である1分よりもクレジットの消費が抑えられたりしました。
(もちろん、XSで1分丸々処理を詰め込めばwarehouseのクレジットの消費の方が低く抑えられたりする可能性もあります。ただ、そもそもDMFはサーバレスしか選択肢がないのであんまり深く考えても意味はないのですが)

あと異論は認めるのですが、個人的にはカスタムDMFは本当に必要に駆られた場合以外は作らない方がいいと思います。なぜならそのメトリクスの意味をちゃんと共通認識でいつまでも誰でもわかりやすく管理できますか?という問いに答えれれる人は少ないと思うからです。(私には無理だと思いました)

特に日次のレコードの増減などはwindow関数などを利用してメトリクスのテーブルから生成できたりするので無理にカスタムメトリクスを作らなくてもいいのです。メタデータのみで完結することが多いシステムDMFを組み合わせて利用するのがおすすめです。

最後に

個人的にはDMFって結構埋もれがちないい機能なのかもしれないと思っています。

ただ、なくてはならない存在でもないとも思います。

そんな君(DMF)にはこの言葉を捧げたいと思います。

土の中の水道管 高いビルの下の下水 大事なものは表に出ない。
by 相田みつを

昨今の煌びやかな"AI" Data Cloudじゃない方のSnowflakeの一面をフィーチャーしてみました。
実務は地味なところも多くありますが、そんなところにも寄り添ってくれるSnowflakeが好きです!

Discussion

GISHI2439GISHI2439

とても素晴らしい記事でした。非常に勉強になりました。よろしければマクロのサンプルコードを貼っていただけないでしょうか。恐れ入りますがよろしくお願いします。

jimatomojimatomo

参考になったようでしたら幸いです!お時間いただいちゃいましたが、コード貼り付けておきました。