全クエリがパスしたのにDatabricksダッシュボードは壊れていた話
はじめに
EXPLAINでヘルスチェックを実装した。117クエリすべてがパスした。だがダッシュボードは壊れていた。
この記事では、dbt更新後にDatabricks Lakeviewダッシュボードの破損を自動検知してSlack通知する仕組みを構築した経験を共有します。
- dbt更新後のダッシュボード破損を自動検知し、Slackに通知する仕組みを作った
- EXPLAINだけでは検知できない問題領域があり、query.historyとのハイブリッド方式に辿り着いた
- Unity Catalog標準機能のみ、追加インフラ不要で実現
背景:なぜ必要だったか
弊社のとある事業のデータ基盤では、dbtで100以上のモデルを管理し、Databricks Lakeviewで30以上のダッシュボードを運用しています。事業の成長に伴い開発スピードが上がる中、以下のような問題が繰り返し発生していました。
- dbtモデルでカラム名を
is_optional→is_optional_planにリネームしたら、ダッシュボードのフィルタが壊れた - 中間モデル
rpt_monthly_metrics_v2を削除したら、参照していたダッシュボードが全滅 - 問題に気づくのは常にユーザーがダッシュボードを開いたタイミング
「dbt更新後、壊れてるか分かる仕組みがほしい」。これが出発点です。
dbt exposuresでダッシュボードとモデルの依存関係を定義・管理する方法もありますが、exposuresは「依存関係のドキュメント化」であり、ダッシュボードが実際に壊れているかの実行時検証はスコープ外です。今回は実行時の破損検知が目的なので、別のアプローチが必要でした。
最初のアプローチ:EXPLAINベース
Lakeview APIの GET /api/2.0/lakeview/dashboards/{id} でダッシュボード定義を取得し、serialized_dashboard 内の datasets[].queryLines からSQLを抽出。各SQLに EXPLAIN を実行して構造的な正しさを検証するアプローチです。
EXPLAINの利点はデータスキャンが発生しないこと。実行計画の生成のみでテーブル消失・カラム不足・型不整合を検知できます。ウェアハウスの実行コスト(DBU)は発生しますが、データを読まない分、通常のクエリと比べて非常に軽量です。
import json
from dataclasses import dataclass
@dataclass
class DashboardQuery:
dashboard_id: str
dashboard_name: str
dataset_name: str
query: str
catalog: str
schema: str
def _is_safe_for_explain(query: str) -> bool:
"""SELECT/WITHで始まるクエリのみEXPLAIN対象にする安全チェック"""
for line in query.strip().splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("--"):
return stripped.upper().startswith(("SELECT", "WITH"))
return False
def extract_queries(dashboard: dict) -> list[DashboardQuery]:
dashboard_id = dashboard.get("dashboard_id", "")
dashboard_name = dashboard.get("display_name", "")
serialized = dashboard.get("serialized_dashboard")
if not serialized:
return []
if isinstance(serialized, str):
try:
serialized = json.loads(serialized)
except json.JSONDecodeError:
return []
queries = []
for dataset in serialized.get("datasets", []):
query_text = "\n".join(dataset.get("queryLines", []))
if not query_text.strip() or not _is_safe_for_explain(query_text):
continue
queries.append(DashboardQuery(
dashboard_id=dashboard_id,
dashboard_name=dashboard_name,
dataset_name=dataset.get("displayName", ""),
query=query_text,
catalog=dataset.get("catalog", ""),
schema=dataset.get("schema", ""),
))
return queries
_is_safe_for_explain でSQLコメント行をスキップしつつ、SELECT/WITH以外のクエリ(万が一DDLが含まれていた場合)をEXPLAIN対象から除外します。
壁にぶつかる:全クエリがパスしたのに壊れていた
本番で実行した結果、117クエリすべてパス。しかしダッシュボードは壊れていました。
原因はLakeviewダッシュボードのJSON構造にありました。
Lakeview内部JSONの構造例
{
"datasets": [
{
"queryLines": ["SELECT * FROM rpt_monthly_summary"],
"catalog": "my_catalog_stg",
"schema": "dbt"
}
],
"pages": [
{
"layout": [{
"widget": {
"spec": {
"encodings": {
"fields": [{"fieldName": "is_optional", "type": "filter"}]
}
}
}
}]
}
]
}
SQLクエリは SELECT * FROM rpt_monthly_summary。is_optional はSQL文中に一切登場せず、ウィジェットの encodings.fields に格納されています。EXPLAINはSQL文の実行計画しか検証しないため、ウィジェット側のカラム参照は検証範囲外です。
| 検知対象 | EXPLAIN | 備考 |
|---|---|---|
| テーブル消失 | 可 | DROP された場合 |
| SQL内のカラム不足 | 可 |
SELECT col_a のような明示参照 |
| フィルタ/ビジュアルのカラム参照 | 不可 | ウィジェットJSON内に格納 |
| ランタイムエラー | 不可 | データ依存のゼロ除算など |
解決策:query.historyとのハイブリッド方式
Unity Catalogの system.query.history には query_source.dashboard_id フィールドがあり、ダッシュボード起因の失敗クエリだけを抽出できます。
WITH failures AS (
SELECT
query_source.dashboard_id AS dashboard_id,
COUNT(*) AS fail_count,
ANY_VALUE(error_message) AS sample_error,
MAX(start_time) AS last_failed_at
FROM system.query.history
WHERE query_source.dashboard_id IS NOT NULL
AND execution_status = 'FAILED'
AND start_time >= current_timestamp() - INTERVAL 24 HOURS
GROUP BY query_source.dashboard_id
),
successes AS (
SELECT
query_source.dashboard_id AS dashboard_id,
MAX(start_time) AS last_success_at
FROM system.query.history
WHERE query_source.dashboard_id IS NOT NULL
AND execution_status = 'FINISHED'
AND start_time >= current_timestamp() - INTERVAL 24 HOURS
GROUP BY query_source.dashboard_id
)
SELECT f.*, s.last_success_at
FROM failures f
LEFT JOIN successes s ON f.dashboard_id = s.dashboard_id
last_success_at > last_failed_at なら修正済み(RESOLVED)としてスキップ。たとえばdbtビルド中にダッシュボード側の修正が入り、ヘルスチェック実行時点では既に直っているケースでも、誤って通知されることを防ぎます。
2層の役割分担
| 項目 | EXPLAIN(事前) | query.history(事後) |
|---|---|---|
| テーブル消失 | 可 | 可 |
| SQLカラム不足 | 可 | 可 |
| フィルタ/ウィジェット参照 | 不可 | 可 |
| 権限エラー | 不可 | 可 |
| 未実行ダッシュボード | 可 | 不可 |
| コスト | 軽微 | 軽微 |
互いの弱点を補完する設計です。EXPLAINは「まだ誰も開いていないダッシュボード」を事前検証。query.historyはウィジェット層の問題を事後的に拾います。
ただし、query.historyに失敗が記録されるには、ダッシュボードのクエリが実際に実行されている必要があります。まだ誰も開いておらず、スケジュール実行もされていないダッシュボードのエラーは検知できません。そこをEXPLAINが補完します。
lineageでスコープを絞る
system.access.table_lineage で、dbt管理テーブルに依存するダッシュボードだけに対象を絞ります。
SELECT
entity_id AS dashboard_id,
COLLECT_SET(source_table_full_name) AS source_tables
FROM system.access.table_lineage
WHERE entity_type = 'DASHBOARD_V3'
AND (source_table_full_name LIKE 'my_catalog.%'
OR source_table_full_name LIKE 'my_catalog_stg.%')
GROUP BY entity_id
entity_type = 'DASHBOARD_V3' でLakeviewダッシュボードに絞り込めます。この結果は、スコープ絞り込みとSlack通知での調査導線の両方に活用します。上流テーブル一覧があれば「どのdbtモデルの変更が原因か」を即座に特定できます。
アーキテクチャ全体像
run_if: ALL_SUCCESS がポイントです。dbtが成功してテーブルが更新されたときだけヘルスチェックを実行します。dbtが失敗した場合はテーブルが更新されておらず、ダッシュボードの失敗はノイズになるためスキップします。
Slack通知
Dashboard Health Check Results After dbt Run
EXPLAIN tested: 117 queries -> 0 broken
query.history: 2 dashboards with failures (last 24h)
---
[prod] Monthly Management Dashboard [query.history]
Issues:
3 distinct error(s) (6x): TABLE_OR_VIEW_NOT_FOUND: rpt_monthly_metrics_v2
Upstream tables:
- my_catalog_stg.dbt.rpt_monthly_metrics_v2
- my_catalog_stg.dbt.rpt_monthly_summary
- ...and 58 more
上流テーブルを含めることで、通知→dbtモデル特定→git log→原因PR特定の調査フローが通知1つで完結します。
コスト
| コンポーネント | コスト | 理由 |
|---|---|---|
| EXPLAIN | 軽微 | データスキャンなし。ウェアハウスDBUのみ |
| system.query.history / table_lineage | 軽微 | システムテーブルへの軽量クエリ |
| Lakeview API | $0 | REST APIコール |
| Slack Webhook | $0 | 標準機能 |
追加インフラや外部SaaSは不要。既存のDatabricksウェアハウスとUnity Catalogの標準機能だけで完結します。
まとめ
| 項目 | 内容 |
|---|---|
| 検知方式 | EXPLAIN(事前)+ query.history(事後)のハイブリッド |
| スコープ | dbt管理テーブルに依存するダッシュボードのみ(lineageで特定) |
| 通知 | Slack、上流テーブル情報付きで調査導線を確保 |
| コスト | 追加インフラ不要。既存ウェアハウスの軽微なDBUのみ |
今回の仕組みはDatabricksの標準機能(Lakeview API、Unity Catalogシステムテーブル)だけで構成されており、追加のSaaSやインフラは不要です。dbtとLakeviewダッシュボードを運用しているチームであれば、同じアプローチをすぐに導入できます。
ダッシュボード監視で別のアプローチを取っている方がいれば、ぜひコメントで教えてください。
Discussion