every Tech Blog

株式会社エブリーのTech Blogです。

Databricks Unity Catalog への移行と MCP 活用

はじめに

こんにちは。開発本部 開発1部 デリッシュリサーチチームの江﨑です。

本記事では、これまでHive Metastore上のDeltaテーブルで管理していたデリッシュリサーチ用データ(約40テーブル)をUnity Catalogへ移行したプロジェクトの全体像を、インフラ整備からAthena連携・Databricks Managed MCP活用まで紹介します。


背景:なぜ Unity Catalog に移行したか

移行前、デリッシュリサーチではDatabricksのHive Metastore上で約40個のDeltaテーブルを運用していました。その中で、運用上の課題が積み重なってきていました。

課題 1:テーブルスキーマが「コードを読まないと分からない」

「このテーブルに user_id カラムってあったっけ?」という確認をするたびに、Notebookを開いて display(spark.table("schema.table")) を実行するか、ETLコードを読み返す必要がありました。テーブルが増えるほど、この手間もかさんでいきます。

課題 2:データリネージを Mermaid で管理していた

テーブル間の依存関係(「このテーブルはどのテーブルから作られているか」)をMermaidのコードで手作業管理していましたが、40テーブルを超えると複雑すぎてメンテナンスが限界になり放置されていました。テーブルを追加するたびにMermaidを手で更新する運用は、明らかにスケールしません。

これらの課題を解消するためにUnity Catalogへの移行を決めました。


Unity Catalog とは

Unity CatalogはDatabricksの統合データガバナンスソリューションです。Hive Metastoreとの最大の違いは、三層の名前空間(Catalog > Schema > Table)を持つ点です。

Catalog(例: marketing_research)
  └── Schema(例: search)
        └── Table(例: search_count)

Hive Metastoreでは schema.table の二層構造でしたが、Unity Catalogでは catalog.schema.table の三層になります。この変更によって、チームやプロジェクトを跨いだデータの整理がしやすくなります。

主な機能は以下の3点です:

  1. データカタログ:テーブルのスキーマ・カラムの説明文をUI上で管理・参照できる
  2. データリネージ:データの流れ(どのテーブルがどのテーブルを参照しているか)を自動追跡・可視化
  3. アクセス制御:行・列レベルの細粒度なセキュリティ設定

Unity Catalogのオブジェクト階層。今回の移行ではCatalog > Schema > Tableの三層構造を利用。(出典: Databricks公式ドキュメント

Unity CatalogのUI。テーブルを選択するとカラム名・データ型・コメントを一覧で確認できる。


マネージドテーブル vs 外部テーブル

Unity Catalogへの移行を検討するとき、決めなければならないのがテーブルタイプです。

観点 マネージドテーブル 外部テーブル
データ保管場所 Unity Catalogが管理するパス 任意のストレージパス(S3など)
S3パスの形式 自動生成されたID形式になる 任意のS3パスを指定できる
テーブル削除時 データも削除される データは残る
Databricksの推奨 ほとんどのユースケース 既存ストレージとの互換性が必要な場合

Databricksはマネージドテーブルを推奨していますが、本プロジェクトでは外部テーブルを選択しました。

その理由は、データ参照構成にあります。デリッシュリサーチではダッシュボードを Amazon Quick Suite(以下Quick Suite) → Athena → Glue Crawler → S3 という構成で構築しています。Glue Crawlerはクロール先のS3パスを読み取ってテーブル名を付与します。

ここで問題になるのが、マネージドテーブルのS3パス形式です。マネージドテーブルに移行すると、S3パスは s3://unity-catalog-metastore/__unitystorage/... のような __unitystorage 配下のシステム管理ディレクトリ(ランダム生成IDを含むパス)に配置されます。Glue CrawlerはS3プレフィックス/フォルダ名ベースでテーブル名を付けるため、Athena側のテーブル名が人間可読でない名前になり、現実的な運用が難しくなります。

外部テーブルであればS3パスを s3://<バケット名>/table_name のようにテーブル名ベースで保持できるため、Athena上のテーブル名が人間にとって分かりやすい名前のままになり、移行コストを抑えつつ今後の管理も楽になります。


移行手順の全体像

移行は以下の5ステップで実施しました。

  1. Step 1:インフラ整備(IAM・Catalogの作成)
  2. Step 2:Unity Catalogテーブルの作成
  3. Step 3:既存ETLコードの変更(テーブル参照パスの更新)
  4. Step 4:AthenaからUnity Catalogのデータを参照できるようにする
  5. Step 5:Quick Suiteのデータセット移行

Step 1:インフラ整備

Unity Catalogを有効化するにあたって、AWS・Databricks側でいくつかの設定が必要でした。

IAMロールの設定

DatabricksがS3バケットにアクセスするためのIAMロールと、それをDatabricks側に登録するストレージクレデンシャルを新規作成しました。インフラ変更はTerraformで管理しています。概念的には以下のような構成です。

resource "aws_iam_role" "databricks_unity_catalog" {
  name = "<ロール名>"
  assume_role_policy = jsonencode({
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "arn:aws:iam::<Databricks の AWS アカウント ID>:role/unity-catalog-prod-role" }
      Action    = "sts:AssumeRole"
      Condition = { StringEquals = { "sts:ExternalId" = <databricks_sts_external_id> } }
    }]
  })
}

Catalogの作成

Unity Catalogを利用するには、Databricks上にCatalogを作成する必要があります。Catalog作成時のmanaged storage location指定は任意ですが、今回は運用上の理由から指定のS3バケットをmanaged storage locationとして指定しました。

参照:Unity Catalog の Catalog を作成する(Databricks 公式ドキュメント)


Step 2:Unity Catalog テーブルの作成

インフラが整ったら、既存のHive MetastoreテーブルのデータをUnity Catalogの外部テーブルとして再作成します。旧パスから schema/table というパス構成に移行するため、CTAS(CREATE TABLE AS SELECT)を使いました。

移行スクリプトの流れ

  1. 旧パスのDeltaテーブルを SELECT * で読み取る
  2. schema/table 形式のパスにDelta形式で書き込みながらUnity Catalogの外部テーブルを作成
# 旧 Delta からデータをコピーしながら UC 外部テーブルを作成
spark.sql(f"""
  CREATE TABLE IF NOT EXISTS {catalog}.{schema}.{table_name}
  USING DELTA
  LOCATION '{new_s3_path}'
  AS SELECT * FROM delta.`{old_s3_path}`
""")

Step 3:既存 ETL コードの変更

主な変更:テーブル参照パスの書き換え

Hive MetastoreではS3パスを直接指定してデータを読み書きしていましたが、Unity Catalogでは catalog.schema.table の三層構造で参照するよう書き換えました。

# Before(Hive Metastore):S3 パスを直接指定
df = spark.read.format("delta").load("s3://path/to/delta")
df.write.format("delta").save("s3://path/to/delta")

# After(Unity Catalog):カタログ名で参照
df = spark.table("catalog.schema.table")
df.write.saveAsTable("catalog.schema.table")

Step 4:Athena から Unity Catalog のデータを参照する

前述の通り、Quick Suiteのデータソースは Athena → Glue Crawler → S3 という構成です。Unity Catalogに移行しても、この構成を維持する必要があります。

実際に行った変更

Glue Crawlerのクロール先を、Unity Catalog外部テーブルのS3パスに変更しました。

Before:
旧 S3 バケット(Hive Metastore 用)→ Glue Crawler → Athena

After:
Unity Catalog 外部テーブルの S3 バケット → Glue Crawler → Athena

具体的な変更内容:

  • Glue Crawlerのクロール先S3パスをUnity Catalog外部テーブルのパスに変更
  • GlueのIAMポリシーにS3バケットへのアクセス権限を追加

Terraformで管理しているIAMポリシーとGlueリソースを更新し、terraform apply 後にGlueコンソールからクローラーを手動実行してテーブルが正しく作成されることを確認しました。


Step 5:Quick Suite のデータセット移行

Step 4でGlue → Athena側の変更が完了したら、Quick Suiteのデータセット参照先を新しいAthenaテーブルに切り替えます。

変更自体はQuick SuiteのUIから接続設定を変更するだけで完結しますが、切り替え前にテーブルスキーマの互換性確認が重要です。カラム名・データ型が一致していないと、ダッシュボードの集計が壊れます。


移行結果 Before / After

移行の成果をBefore / Afterでまとめます。

Before(Hive Metastore 時代)

項目 状態
テーブルスキーマ確認 Notebookを実行するかコードを読む必要がある
データリネージ Mermaidで手作業管理

After(Unity Catalog 移行後)

項目 状態
テーブルスキーマ確認 Unity CatalogのUIでカラム一覧・データ型・説明文を即時参照
データリネージ UI上で自動生成・可視化(Mermaidでの手作業管理が不要に)

特にデータリネージの自動可視化は、Mermaidの維持コストをゼロにしてくれる大きな恩恵でした。Unity CatalogにETLジョブが書き込むと、どのテーブルがどのテーブルから作られているかが自動でグラフとして記録されていきます。

Unity Catalogのデータリネージ画面。どのテーブルがどのテーブルから作られているかが自動でグラフ表示される。


Databricks Managed MCP

Unity Catalogへの移行をきっかけに、Databricks Managed MCPも使えるようになったので紹介します。

Databricks Managed MCP とは

Databricks Managed MCPとは、DatabricksがホストするMCP(Model Context Protocol)サーバーです。Claude CodeなどのAIエージェントからDatabricksのリソースをツールとして呼び出せるようにする仕組みです。

Databricks Managed MCPはUnity Catalogとの統合を前提に設計されています。今回の移行後、実際に使えるようになりました。

DBSQL MCP:ETL 開発が変わる

Databricks Managed MCPの中でも、ETL開発が中心のデリッシュリサーチにとって特に相性が良さそうだと感じたのが DBSQL MCP です。これはClaude CodeやCursorのMCPとして設定することで、ETL開発中にSQLをその場で実行・確認できるようになるツールです。

提供されるツール:

  • execute_sql_read_only:テーブルの内容・カラム定義・データ分布をその場で確認
  • execute_sql:SQLの実行
  • poll_sql_result:長時間クエリの結果をポーリング

※ ツール名や提供機能はアップデートで変更される可能性があります。上記は執筆時点(2026年3月6日)で使用できるツールです。

具体的な開発体験

ETLコードを書きながら、Claude Codeに対して次のような依頼ができるようになります:

  • catalog.schema.table のカラム構成を確認して」
  • product_id カラムのユニーク数を調べて」
  • 「このテーブルとJOINするテーブルのスキーマを見せて」

DBSQL MCPを使うことでCursorやVS CodeなどのエディタからDatabricksのテーブルの中身・スキーマをリアルタイムで確認しながらコーディングができます。

なお、DBSQL MCP以外にもUnity Catalog functionsやGenie spaceなども使えるようになっています。

参照:Databricks Managed MCP 公式ドキュメント

MCPの詳細な設定方法や活用例については、こちらの記事も参照してください。


まとめ

約40テーブルのHive Metastore → Unity Catalog移行を通じて得た主な成果を3点でまとめます。

  1. データカタログとデータリネージの整備:Unity CatalogのUIでスキーマ情報を参照でき、データリネージも自動管理されるため、「コードを読まないと分からない」問題が解消されました
  2. 外部テーブルで構成互換性を維持:Athena連携(Glue Crawler)がある場合は外部テーブルを選ぶことで、既存のBI構成に影響を与えずに移行できました
  3. Databricks Managed MCPが使えるようになった:ETL開発中にエディタから出ずにテーブル情報を確認できるようになり、開発体験が向上しました

今後は、Unity Catalogへの移行によって使えるようになった機能の検証・活用をしていきたいと考えています。

Unity Catalogへの移行を検討している方の参考になれば幸いです。

Go Wasm の js.Value.Call はなぜ遅い? wasm_exec.js の内部実装から理解する

こんにちは @kyo です!

2026年2月21日に開催された Go Conference mini in Sendai 2026 にて、「GoとWasmでつくる軽量ブラウザUI」というタイトルで登壇させていただきました。この記事では、発表中にいただいたフィードバックについて深掘りをして得られた知見をご共有できたらと思います。

フィードバック: 「(*js.Value).Call は遅いので、bind したうえで Invoke するといいですよ」 from Hajime Hoshiさん、Go製ゲームエンジンEbitengineの作者

発表スライド speakerdeck.com


背景

Go の syscall/js パッケージでは、JS のメソッドを呼び出す方法が2つあります。

方法 Go コード 特徴
Call document.Call("getElementById", "myDiv") シンプルだが毎回オーバーヘッドあり
bind + Invoke getElementById.Invoke("myDiv") 初期化が必要だが高速

Call が遅い理由

前提知識: Go Wasm の仕組み

Go で書いた Wasm コードがブラウザの JS を呼び出すとき、直接呼べるわけではありません。 間に Wasm メモリwasm_exec.js(Go 公式提供の橋渡しスクリプト)を挟んでやりとりします。

Wasm メモリ(Linear Memory)とは?

Wasm メモリは WebAssembly の仕様で定義された WebAssembly.Memory オブジェクトで、 実体は Go(Wasm)と JavaScript の 両方からアクセスできる巨大なバイト配列ArrayBuffer)です。 「リニアメモリ(Linear Memory)」とも呼ばれます。

developer.mozilla.org

wasmbyexample.dev

普通、Go と JS はお互いの変数を直接見ることができませんが、 この共有のバイト配列を「伝言板」のように使うことで、データをやりとりできます。

例: document.Call("getElementById", "myDiv") の場合

  1. Go 側が "getElementById" という文字列をバイト列に変換して Wasm メモリに書き込む
  2. JS 側(wasm_exec.js)が Wasm メモリからそのバイト列を読み出す
  3. TextDecoder で JS の文字列に変換する(= loadString()
  4. その文字列を使って document["getElementById"] を探す(= Reflect.get()
  5. 見つけた関数を実行する

Invoke が速い理由は、このステップ 1〜4 を丸ごとスキップできるからです。 事前に関数への参照を取得しておけば、Wasm メモリを経由した文字列のやりとりが不要になります。

Call の処理の流れ

Go 側で document.Call("getElementById", "myDiv") を呼ぶと、 wasm_exec.js の以下のコードが実行されます:

// wasm_exec.js
"syscall/js.valueCall": (sp) => {
    sp >>>= 0;
    try {
        const v = loadValue(sp + 8);                    // ① オブジェクトを取得(例: document)
        const m = Reflect.get(v, loadString(sp + 16));  // ② ここが遅い(後述)
        const args = loadSliceOfValues(sp + 32);        // ③ 引数を取得(例: "myDiv")
        const result = Reflect.apply(m, v, args);       // ④ 関数を実行
        sp = this._inst.exports.getsp() >>> 0;
        storeValue(sp + 56, result);                    // ⑤ 結果をメモリに書き戻す
        this.mem.setUint8(sp + 64, 1);                  // ⑥ 成功フラグ
    } catch (err) {
        // エラー処理...
    }
},

② が遅い理由には二つの原因があります

const m = Reflect.get(v, loadString(sp + 16));
//                       ^^^^^^^^^^^^^^^^^^    ← (A) 文字列デコード
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^← (B) プロパティ検索

(A) loadString() — 文字列デコード

const loadString = (addr) => {
  const saddr = getInt64(addr + 0); // Wasmメモリ上の文字列の開始位置
  const len = getInt64(addr + 8); // 文字列の長さ(バイト数)
  return decoder.decode(
    // TextDecoder で バイト列 → JS文字列に変換
    new DataView(this._inst.exports.mem.buffer, saddr, len),
  );
};

Go が Wasm メモリに書き込んだバイト列を、TextDecoder を使って JavaScript の文字列 "getElementById" に変換しています。 この処理では毎回 new DataView の生成と decoder.decode() が走っています。

(B) Reflect.get() — プロパティ検索

補足: プロパティとプロパティ検索とは?
JavaScript のオブジェクトは、名前(キー)と値のペアの集まり です。 この「名前と値のペア」1つ1つを プロパティ と呼びます。
// document オブジェクトのイメージ(実際はもっと多い)
document = {
    "getElementById":    function(...) { ... },  // ← プロパティ
    "createElement":     function(...) { ... },  // ← プロパティ
    "querySelector":     function(...) { ... },  // ← プロパティ
    "title":             "My Page",              // ← プロパティ
    // ... 他にも数百のプロパティがある
};
プロパティ検索 とは、この中から名前を指定して値を探す処理です。 Go でいえば `map[string]any` から `map["getElementById"]` でキーを探すのに近いイメージです。
// プロパティ検索の例(どれも同じ意味)
document.getElementById; // ドット記法
document["getElementById"]; // ブラケット記法
Reflect.get(document, "getElementById"); // Reflect API(wasm_exec.js が使う方法)
Reflect.get(v, "getElementById");
// これは実質的に v["getElementById"] と同じ
// = document オブジェクトから "getElementById" という名前の関数を探す

JavaScript のオブジェクトからプロパティ名で関数を検索します。 ここの処理でも毎回この探索処理が走ります。

Invoke の処理の流れ

一方、getElementById.Invoke("myDiv") を呼ぶと

// wasm_exec.js
"syscall/js.valueInvoke": (sp) => {
    sp >>>= 0;
    try {
        const v = loadValue(sp + 8);                     // ① 関数そのものを取得(文字列ではない)
        const args = loadSliceOfValues(sp + 16);         // ② 引数を取得
        const result = Reflect.apply(v, undefined, args); // ③ 関数を直接実行
        sp = this._inst.exports.getsp() >>> 0;
        storeValue(sp + 40, result);                     // ④ 結果をメモリに書き戻す
        this.mem.setUint8(sp + 48, 1);                   // ⑤ 成功フラグ
    } catch (err) {
        // エラー処理...
    }
},

Call との違い

  • loadString() がない → 文字列デコードが不要
  • Reflect.get() がない → プロパティ検索が不要
  • v はすでに関数への参照なので、Reflect.apply() で直接呼ぶだけ

処理の違いまとめ

Call の処理:
  Go → [メソッド名をメモリに書く] → JS: loadString() → Reflect.get() → Reflect.apply()
       ~~~~~~~~~~~~~~~~~~~~~        ~~~~~~~~~~~~   ~~~~~~~~~~~~~
       毎回発生するオーバーヘッド       文字列デコード    プロパティ検索

Invoke の処理:
  Go → JS: Reflect.apply()

図解

1. Call パターン(毎回のオーバーヘッド)

2. bind + Invoke パターン(初回のみオーバーヘッド)

3. 処理ステップの比較

4. bind が必要な理由

JS ではメソッドをオブジェクトから切り離すと this コンテキストが失われます。 bindthis を固定しないと Invoke 時にエラーになります。


コード例

遅いパターン(毎回 Call

document := js.Global().Get("document")

for i := 0; i < 1000; i++ {
    // 毎回: 文字列書き込み → デコード → プロパティ検索 → 実行
    element := document.Call("getElementById", "myElement")
    element.Call("setAttribute", "data-index", i)
}

速いパターン(bind + Invoke

document := js.Global().Get("document")

// 初期化: bind で this を固定
getElementById := document.Get("getElementById").Call("bind", document)

for i := 0; i < 1000; i++ {
    // 毎回: 関数実行のみ(文字列処理・プロパティ検索なし)
    element := getElementById.Invoke("myElement")
    // ...
}

実用的なパターン: よく使うメソッドをまとめて事前バインド

var (
    document       = js.Global().Get("document")
    getElementById = document.Get("getElementById").Call("bind", document)
    createElement  = document.Get("createElement").Call("bind", document)
    querySelector  = document.Get("querySelector").Call("bind", document)
    consoleLog     = js.Global().Get("console").Get("log").Call("bind", js.Global().Get("console"))
)

func getElement(id string) js.Value {
    return getElementById.Invoke(id)
}

func newElement(tag string) js.Value {
    return createElement.Invoke(tag)
}

オーバーヘッド比較表

処理 Call bind + Invoke
文字列の Wasm メモリ書き込み 毎回 初回のみ
TextDecoder によるデコード 毎回 初回のみ
Reflect.get(プロパティ検索) 毎回 初回のみ
Reflect.apply(関数呼び出し) 毎回 毎回
makeArgSlices + storeArgs 毎回 毎回

ベンチマーク結果(10,000回呼び出し)

各メソッドを計測した実測結果

DOM操作(実際のJS API)

JS API自体の実行コストが含まれるため、相対的な差は小さい。

// Call パターン
document.Call("getElementById", "myElement")

// bind+Invoke パターン
getElementById := document.Get("getElementById").Call("bind", document)
getElementById.Invoke("myElement")
対象メソッド Call (ms) bind+Invoke (ms) 差分 速度比
document.getElementById 48.7 46.6 +2.1 ms 1.05倍
console.log 68.3 59.3 +9.0 ms 1.15倍
element.setAttribute 26.8 25.8 +1.0 ms 1.04倍

DOM操作自体のコストが大きいため、Call と bind+Invoke の差は 3〜15% 程度に留まる。

純粋なオーバーヘッド検証

JS側の処理コストを排除し、Call 固有のオーバーヘッドを可視化。

空の関数(何もしない関数)

JS側に何もしない関数を用意し、呼び出しオーバーヘッドだけを測定。

// Call パターン: 毎回「文字列デコード → プロパティ検索 → 関数実行」
noopObj.Call("noop")

// bind+Invoke パターン: 事前バインド済みなので「関数実行」のみ
noop := noopObj.Get("noop").Call("bind", noopObj)
noop.Invoke()
対象 Call (ms) bind+Invoke (ms) 差分 速度比
noop 1.90 0.40 +1.50 ms 4.76倍

JS側の処理コストがないため、Call 固有のオーバーヘッド(文字列デコード + プロパティ検索)が約4〜5倍の差としてはっきり現れる。

メソッド名の長さによる影響

Call は毎回メソッド名を文字列デコードするため、名前が長いほどコストが増えるか検証。

// 短いメソッド名(1文字)
obj.Call("a")

// 長いメソッド名(30文字)
obj.Call("abcdefghijklmnopqrstuvwxyz1234")

// bind+Invoke はどちらも同じ(事前バインド済み)
fn := obj.Get("a").Call("bind", obj)
fn.Invoke()
対象 Call (ms) bind+Invoke (ms) 差分 速度比
メソッド名 "a"(1文字) 1.80 0.50 +1.30 ms 3.61倍
メソッド名 "abcdefghij...1234"(30文字) 2.10 0.60 +1.50 ms 3.50倍

メソッド名の長さはほぼ影響しない。TextDecoder のコストは小さく、Go↔JS間の境界越え自体(valueCall のスタック操作 + Reflect.get)の方がはるかに大きい。(メソッドをたくさん増やしたらもっと差が出るかも)


いつ使い分けるか

シナリオ 推奨 理由
高頻度呼び出し(60fps 描画、大量 DOM 操作) bind + Invoke オーバーヘッド削減の効果が大きい
低頻度呼び出し(ボタンクリック等) Call でOK 可読性を優先、パフォーマンス差は体感できない
同じメソッドをループで繰り返し呼ぶ bind + Invoke 最もメリットが出るケース

まとめ

  • Call は毎回「文字列の Wasm メモリ書き込み → TextDecoder によるデコード → Reflect.get によるプロパティ検索」という3つのオーバーヘッドが発生する
  • bind + Invoke は事前に関数参照を取得・固定しておくことで、これらのオーバーヘッドをすべてスキップし、Reflect.apply で直接関数を実行できる
  • 純粋なオーバーヘッド比較では約4〜5倍の差があり、高頻度呼び出し(描画ループや大量DOM操作)では効果が大きい
  • 一方、DOM操作自体のコストが大きい場面では差は数%程度に留まるため、低頻度の呼び出しでは Call のシンプルさを優先して良さそう
  • よく使うメソッドを var でまとめて事前バインドしておくのが実用的なパターン

最後に

Go Conference mini in Sendai で Hajime Hoshi さんからいただいた「Call は遅いので bind + Invoke がいいですよ」というフィードバックは、最初は「そういうテクニックがあるんだな」程度の理解でした。しかし実際に wasm_exec.js のソースコードを読んでみると、Call が遅い理由は単なる「関数呼び出しの方法の違い」ではなく、Go と JavaScript という2つの異なるランタイムが Wasm メモリという共有バイト配列を介してやりとりする仕組みそのものに起因していることがわかりました。

普段 Go を書いているだけでは意識しない「文字列がバイト列として Wasm メモリに書き込まれ、JS 側で TextDecoder によってデコードされる」という一連の流れを知ったことで、Go Wasm が裏側でどれだけの処理をしているのかを実感できました。と同時に、wasm_exec.js がたった1つのファイルで Go と JS の橋渡しをすべて担っていることに、改めてすごさを感じました。

カンファレンスでのたった一言のフィードバックが、ここまで深い学びにつながるとは思っていませんでした。発表して、フィードバックをもらって、それを深掘りする——このサイクルの価値を改めて実感しています。


参考




    
    
    
  

エンジニアのマネージャーになって1年半。意思決定の失敗から学んだこと

はじめに

こんにちは。リテールハブ開発部小売アプリチームの池です。

エンジニアチームのマネージャーになってから、気づけば1年半が経ちました。

この1年半を振り返ると、悩みながら行動を続けてきた時間でした。マネージャーとしてどう行動すべきか日々悩みながら試行錯誤し、周りの支援を借りつつ、自分なりにこれだと思うことを試しては失敗を重ね、走り続けてきました。その中で意識していたのは、ただ失敗を繰り返すだけではなく、そこから得られる学びを積み重ねて次に活かすことです。

この記事では、マネージャーが日々何を考え、どんな判断をしているのかを共有したいと思います。また、失敗談が中心にはなりますが、同時にマネージャーという仕事は「人やチームと向き合う仕事」であり、多くの魅力とやりがいがあることも伝えられたらと思っています。マネージャーに興味があるエンジニアの方、同じ立場で悩んでいるマネージャーの方、あるいはマネージャーが何を考えているか知りたいチームメンバーの方にとって、少しでも参考になれば幸いです。

今回は、自身の振り返りも兼ねて、その中でも特に強く心に残っているマネージャーとしての意思決定に関する学びをピックアップして振り返ります。

背景

私がマネージャーになった当時のチーム状況を説明します。

1年半前の当時、私のチームでは以下の業務を並行して進めていました。

  • 自社でゼロから新規開発している小売アプリの開発
  • 事業譲渡で引き継いだ5つの小売アプリの運用保守

エンジニアは私を含めて3名、デザイナー1名、PdM1名の計5名を中心に、上長の支援を得ながらそれらの業務をこなす必要がありました。

引き継いだアプリそれぞれの全容は十分に把握できておらず、わからないことだらけです。コードから仕組みを読み解きながら運用保守、顧客からの問い合わせ・要望に対応する日々でした。チームも発足して間もなく体制が整っておらず、PdMも入れ替わりで着任したばかり。そこに新規開発も並行して進めていて、カオスな状態でした。

そんな中で、私は初めてマネージャーを担うことになりました。

失敗①:チームの方針を明確に示さなかった

一つ目の失敗は、マネージャーとして方針を明確に示すことの重要性と、それがチームに与える影響の大きさを理解していなかったことです。

その当時、少ない人数のチームで新規開発と既存運用が同時に走り、毎日やることが尽きません。私自身もプレイヤーとして動かないと回りませんでした。「まずは足元の開発を回すこと」が最優先で、開発や調整、障害対応と、自身も一人のプレイヤーとして目の前のタスクを一つずつ処理し、なんとか回すことに必死でした。

一方で、私はマネージャーになったばかりです。方針を示すことの大事さを理解していませんでした。どこを目指すのか、何を優先するのか。そういったことを言語化するという発想自体が薄かったのだと思います。

そして、方針を明確に示さずに全てをうまく回そうという意識のまま、大きな対策を打たずに走り続けていました。その結果、チームはどこに向かっていいかわからない状態になっていきました。

  • 重要でないことに時間を使ってしまう
  • 自律的な判断が難しくなる

方針がないと、迷いながら働くことになります。技術負債をどこまで許容するのか、属人化をどこまで受け入れるのか、ドメイン理解にどれだけ時間をかけるのか、作り込みすぎないラインはどこか、各々の判断のズレが積み重なりチームはさらに忙しくなっていきました。

結果的に「全部をそのままやる」ということが暗黙の方針となり、当然ながら、すべてが中途半端になり目標達成も遠のきます。メンバーの不満も溜まり、私自身も時間で解決しようと夜遅くまで働くことが増えました。疲弊するばかりで状況は良くなりません。

方針が全てを解決するわけではないですが、方針を示さなかったからこそ、余計な忙しさを生んでいたのだと思います。

学び

忙しくても、方針を示すことだけは省いてはいけません。

  • 何を最優先にするのか
  • 何を後回しにするのか
  • どこまでやれば十分か

このような方針があるだけで、チームは「何に時間を使うべきか」を考えられるようになります。

今は、「やること/やらないこと」を明確にすることを意識し、忙しい時ほど立ち止まるようにしています。

失敗②:チームを見ずに手法を当てはめた

2つ目の失敗は、解決策から入ってしまったことです。

マネージャーに役割が変わると求められるスキルは変わり、人やチームを動かすスキルが必要になります。しかし当時の私は、その変化を十分に受け止めきれていませんでした。マネージャーとしての理解も引き出しもなく、何をすればいいのかわからない。そんな状態です。

わからないなら学ぶしかないと思い、本や記事を読み、過去の自身の成功体験や他社の成功事例に答えを求め、「これが正解だろう」と思ったものをチームに適用しました。

その一つがスクラム開発の導入です。自分の過去の経験からスクラムをやることでチームが良い方向に進むと、どこかで信じていました。マネジメントに自信が持てない中で、実績のある手法を頼ろうとしていたのだと思います。

スクラム自体は良い手法ですが、そのときのチームのフェーズや状況には合っていませんでした。本来私がやるべきだったのはプロセス改善ではなく、チームの課題を見つけてどう解決するかを考えることです。

スクラムをうまく運用できなかったことにも問題はありますが、チームの課題を見ずに形式的に導入しても効果は限定的になります。その結果、重要ではない会議や作業、議論が増えていきました。

たまたまチームの問題とスクラムの手法がマッチしていた箇所では効果が出たものの、全体としては納得感も高まらず、次第に形骸化して空回りしていき、最終的にはスクラムをやめる判断をしました。

失敗の原因は手法そのものではなく、チームを見ていなかったことでした。

学び

まずやるべきことは、手法を探すことではなく、チームの状態を観察して明らかにすることでした。

  • 何が一番のボトルネックなのか
  • どこにエネルギーを割くべきなのか
  • メンバーは何に困っているのか

それらを言語化した上で解決策を考えるべきでした。

チームはそれぞれ、プロダクトのフェーズや事業状況も、メンバーの性格・スキルも異なります。当然、課題やボトルネックもチームごとに違います。同じ状況のチームは存在しません。だからこそ、マネジメントにどのチームにも当てはまる画一的な手法はありません。

特に、ベンチャー企業の新規事業で限られたリソースと期間で目標を達成する必要がある環境を踏まえると、何を優先し何を捨てるかの判断は大きく変わってきます。そのためにも、まずはチームを観察し、置かれた状況を把握した上で、行動を考える必要がありました。

一方で、考え過ぎて動けなくなるのもまた問題です。すべてを理解してから動くことはできません。実際には、軽く試し、軽く失敗し、そこから学ぶことも多くあります。行動してみて初めて見えてくる課題もあれば、後から納得感がついてくるケースもあります。

今は、観察しながら動き、チームの反応を見て調整することを意識しています。

失敗③:一度決めた方針を続け過ぎてしまった

3つ目の失敗は方針を見直さなかったことです。

失敗①②を経て、私はチームの状況を見て方針や行動の意思決定を意識するようになりました。

たとえば、初期フェーズで作るものがある程度決まっている状況では、ドメイン理解を一定に留めることや属人化を許容するという判断をしました。その時点では合理的な判断だったと思います。

しかし問題は、その方針を見直すべきタイミングで見直さなかったことでした。

プロダクトの状況は変化し、チームの構成も変わり、メンバーも成長しているのにもかかわらず、その判断だけが更新されないままになっていました。

その結果、以下のような影響が徐々に現れてきました。

  • 技術的な判断の拠り所が持てない場面が増えていく
  • 「自分たちが作っているものは本当に価値があるのか」という空気がチームに漂い始める
  • 属人化が固定して急な休みが取りづらくなる
  • アラート対応の担当者が偏る

変化の兆しには気づいていましたが、対策を打てていない自分もいました。方針を決めた後の運用ができていなかったのです。

学び

方針にもメンテナンスが必要です。定期的に見直さないと現実との乖離が大きくなっていきます。

  • チームが不健全な状態になっていないか
  • チームの熱量や納得感は下がっていないか

このような、出ていたはずのシグナルをしっかりと見逃さず、短いサイクルで方針が実状に合っているかを問い続ける必要があります。

方針は一度決めて終わりではなく、状況の変化に合わせて更新し続けるものだと学びました。

おわりに

本記事では、マネージャーとしての意思決定に関する3つの失敗を振り返ってみました。実際にはもっと多くの失敗をしています。

大事なことは、失敗しないことではないと思っています。

マネージャーの仕事に正解はないと、この1年半で実感しました。完璧な判断を下し続けることはできません。それでも、打席に立ち続けることはできます。迷いながらでも決める。うまくいかなければ振り返って次に活かす。その繰り返しで前に進めると思っています。

一方で、失敗ばかりを書いてきましたが、マネージャーの仕事にはそれ以上のやりがいがあると感じています。一人では到底成し遂げられないことをチームで実現できたときの達成感。エンジニアとは異なる視点やスキルが求められる中で、自分自身が成長していく実感。そして何より、メンバーの成長や変化に向き合いながら、チームが前に進んでいく過程を間近で見られることの面白さ。マネージャーの醍醐味だと思っています。

まだまだ未熟ですが、これからも打席に立ち続け、学び続けていければと思います。

Datadog Learning Centerをやってみた

はじめに

こんにちは。リテールハブ開発部の清水です。

先日SRE Kaigi 2026に参加してきまして、私の中でSRE熱がかなり高まっています。

私たちはDatadogをオブザーバビリティ基盤として使用しているのですが、私自身はDatadogをまともに触った経験がありませんでした。
Datadogの画面を開くと左のメニューだけでも大量の項目があって、何ができるのか把握すること自体に大きなハードルを感じていました。
そのような中で、Datadog Learning Centerというものを知りました。無料でハンズオン形式の学習ができるとのことだったので、実際にやってみることにしました。

Datadog Learning Centerとは

Datadog Learning Centerは、Datadogが公式に提供している無料のオンライン学習プラットフォームです。
ブラウザ上でハンズオン形式でDatadogの各機能を実際に操作して学ぶことができます。

コースはGetting Started、APM、Logs、Kubernetes、Securityなどのカテゴリに分類されていて、初心者向けの入門コースから特定のケースにフォーカスしたコースまで幅広く用意されています。

学習環境について

ラボという学習環境が提供されるので、ブラウザさえあれば学習できる形になっていました。

ラボの画面では左半分にターミナル、右半分に学習教材が表示されます。
学習用のDatadogアカウントが自動作成されて、IDとパスワードがターミナル上に表示される仕組みです。
これをDatadogのログイン画面に入力すれば、学習用のアカウントで実際のDatadog環境を自由に触ることができます。
自分の本番環境を壊す心配がないので、気軽に色々試せるのが良いところです。

なお、コンテンツはすべて英語です。英語が苦手な方にとってはややハードルが高いかもしれません。

最初に取り組んだコースについて

Datadog Learning Centerのコース一覧画面を開くと多数のコースがずらりと並んでおり、圧倒されます。

画面上部にLEARNING PATHSというリンクがあるので、ここを開くと目的別に整理された学習順序が紹介されています。

私はこの画面で紹介されているCore Skills Learning Pathから始めました。
以下の6コースで構成されており、Datadogの基本的な操作スキルを身につけるための入門パスです。

  • Datadog Quick Start
  • Tagging Best Practices
  • Getting Started with Metrics
  • Getting Started with Monitors
  • Introduction to Dashboards
  • Getting Started with Notebooks

所要時間

じっくり内容を確認しながら進めて、各コースで30分~1時間程度、合計4時間で終わりました。

学習した感想

Datadogに対する恐怖感が薄れた

一番大きな収穫は、Datadogでざっくりどんなことができるのかわかったことです。

学習前はDatadogの画面を開いても、左メニューに並ぶ項目が何を意味しているのかわからず、触ること自体に抵抗がありました。
学習後は、メトリクス、ダッシュボード、モニター、ログ、APM、トレース、Software Catalog、ノートブックといった用語がそれぞれ何を指しているのか、なんとなく掴めるようになりました。

正直なところ、すべてを完璧に理解できたわけではありません。ただ、「何がわからないかがわからない」状態から「各機能の役割はわかった上で、詳細はこれから深掘りしていけばいい」という状態になれたのは大きな進歩でした。

今回学習したCore Skills Learning Pathはよく使う部分をざっと紹介してもらうような内容となっており、他のコースで実践的な内容を学んでいけるのではないかと思います。

学習用Datadogアカウントの中身を見るだけで勉強になった

学習用Datadogアカウントには、架空のWebサービスを題材として本番さながらの設定やデータが最初から用意されています。
AIに「これはどんな機能ですか?」と会話しながら各画面を見ていくと各種機能について素早く理解が進むと感じました。

特にダッシュボードの作り方がすぐ真似できる要素があって参考になりました。

元々、私たちはテナントごとに別々のダッシュボードを用意していて、それぞれのダッシュボードの中でも各種リソース状況がフラットに並んでいるだけの状態でした。

学習用Datadogアカウントのダッシュボードから以下の内容を真似することにしました:

  • ダッシュボードを全テナント共通にして、テンプレート変数で切り替える
  • 画面上部にはリンク集とMonitor Summaryを設置する
  • 画面要素をGroupウィジェットでグループ分けして表示する

改善前と見比べると、情報の整理や視認性がだいぶ向上したのではないかと思います。同じようにダッシュボードの構成に悩んでいる方の参考になれば幸いです。

おわりに

Core Skills Learning Pathは、Datadogを初めて触る人にとって良いスタート地点でした。4時間程度の投資で、Datadogの主要機能の全体像と基本操作を一通り学ぶことができます。

Learning Centerにはこのパス以外にも、さまざまな機能に特化したコースが多数用意されています。今後も引き続き他のコースに取り組み、Datadogの活用の幅を広げていきたいと考えています。

Datadogを使っているけれどちゃんと学んだことがないという方は、ぜひCore Skills Learning Pathから始めてみてはいかがでしょうか。

個人情報を含むデータ移行で学んだことと反省点(事前準備・リハーサル・当日の心構え)

はじめに

こんにちは、リテールハブ開発部でバックエンドエンジニアをしているホシと申します。
現在、Laravel などを利用しながら小売アプリ開発に取り組んでいます。

先日、サービスのリリースに伴い、旧サービスの外部システムから当社のMySQL DBへユーザーデータ移行を行う機会がありました。 ただ今回、今まで行ったデータ移行と大きく違うのは、ユーザーの個人情報を含んだデータ移行でした。 データ移行自体はこれまでも経験していましたが、個人情報を含む移行は前提が異なり、多くの学びと反省点がありました。

そこで本記事では以下の点についてお話できればと思います。

  • 実データを自由に扱えない状況での事前準備の進め方
  • リハーサルの重要性と、実施できる場合の考え方
  • 今回採用した移行方式と「変換できたのに正しく入っていない」問題と確認点
  • 移行当日に意識しておくべき判断と心構え
  • 次回のデータ移行に向けた改善点

それぞれの中で経験したことや反省点を記載しながら、今後に向けた改善点などを記載しています。 また、個別の移行内容というよりは、データ移行を進める際の準備・検証・判断の考え方に焦点を当てています。


1. 実データを自由に扱えない状況での事前準備の進め方

個人情報を含むデータは「自由に触れない」

個人情報が含まれる場合の制約

  • 開発環境・検証環境に簡単に持ち込めない
  • ログ出力、スクリーンショット、共有方法にも制限がある
  • データ配置・保管場所にも慎重な判断が必要
  • 事前確認が十分にできない
  • 「本番で初めて見るデータ」が発生しがち

など通常とは異なる制約がある中で行う必要がありました。 この部分をもっと考慮した上で、事前準備が必要であったところは大きな反省点でした。

「できないから仕方ない」で済ませると危険

事前にできることはできる限りやり、仕様書・IF定義だけで満足しないことが大事です。
とはいえ対応できる範囲も限界はあるので以下の点を特に注意できればと思っています。

  • NULL / 空文字 / 不正値の扱いは各項目でどうなっているのかを明確にする。
  • 桁数・文字種・フォーマットのばらつきがあることを前提とした考慮をする。
  • 仕様上はOKでも、実データでは違う可能性を考える。
    (ここは正直難しいですが、データパターンを確認し、できる限り考慮したいです。)

あるあるの話ですが、通常は仕様に基づいて対応するのが当たり前ですが、 こういった大量データになるとおかしなデータはほぼ混ざっているのが逆に普通かと思っています。


2. リハーサルの重要性と、実施できる場合の考え方

今回は事前にリハーサルが実施できる状況でした。 しかし、以下の点でしか検証を行えていませんでした。

  • データ移行手順が問題ないこと(スムーズに移行作業が完了できる)
  • 個人情報データの取り扱いに問題がないこと
  • データ変換がエラーなく行われ、正常にDBへ投入できること(想定通りのフォーマットであること)
  • 実際に移行したデータを使用して簡単な動作確認を実施し問題ないこと

上記はどれもリハーサルで行う必要な項目で、どれも大事なのですが、 ただ、本番データを想定した観点としては以下の点でもっと時間をかけて行うべきでした。

  • DBに入った内容に想定していないデータが入っていないか
  • データパターンを各項目で洗い出し、システム上問題ない値であること
  • 想定していないパターンの場合、どう対処するかを事前に明確にすること

しかし、これも個人情報のデータとなると、あまり時間をかけてのリハーサルや ローカルに保存して後ほど詳細に確認もしにくいのもあり、簡単な確認で済ませてしまっていました。

元データを最大限に利用する

一番確実なのはやはり元データを使用できることです。 当日のデータ移行の検証という意味では、これを利用した検証が一番確実だと思います。

リハーサル当日だけで出来ない部分は、別日に詳細にできればと良いと思います。 しかし、今回のような個人情報データは自由に扱えないことを理由にうまく利用できていなかったのも反省点です。

いかにここでそのファイルを活用したダミーデータを作れるかも非常に有効だったのではと思っています。


3. 今回採用した移行方式と「変換できたのに正しく入っていない」問題と確認点

どうやって移行を行おうとしたか

今回は以下の方法で行いました。

移行元データ
↓
phpでデータを読み込み、MySQLのLoadData用TSVファイルにコンバートするコードを作成
↓
Load DataでTSVデータをDBへ投入
↓
データ移行完了

特別な処理は特になく、シンプルな変換処理でした。

「変換できたのに正しく入っていない」問題と確認点

コンバートまでは特に問題なく変換できて、件数・エラーチェックは行えていましたが、 以下の点が見落としポイントでした。

  • データ投入できていて、かつ全件「自動変換」など発生なく投入できているか?
  • Load時にWarningなどが発生していないか?(投入はできていても実は正常に入っていないケースがある)
  • カラム定義上は問題なく入っているが、値が想定していないものではないか?

Load Data はデータ投入時に非常に便利ですが、上記の点をしっかりと考慮した上で使用しないとデータ移行時の確認内容としては不十分になってしまいます。

特に日付はNULLの場合と「0000-00-00」で入る場合で全く挙動が異なります。 MySQLでは「0000-00-00」はNULLではなく「値」として扱われます。 そのためアプリケーション側では未設定ではなく異常値として存在してしまうことになり、 日付計算・バリデーション・ORM変換で後から問題を引き起こす可能性があるので特に注意が必要です。

今回の移行処理の責務を分離して考える

本来は以下のようにそれぞれのフェーズの責務を明確にして、対応をしていく必要がありました。

フェーズ 役割 保証すること 保証できないこと
コンバーター 生データをLoad Data用形式へ変換 TSV構造・文字コード・基本的な値変換 DBがどう解釈するか、業務的な正しさ
Load Data DBへ高速に一括投入 指定件数の取り込み、物理的な格納 型変換の影響、暗黙変換、Warningの発生
DB格納後の確認 実データの妥当性検証 業務的に正しい値であることの確認 この確認を行わないと移行成功とは言えない

特に今回は、Load Data の部分とDB格納後の確認が混在した確認になっていたと反省しています。

実際のデータパターンの検証例

パターンは膨大にあるようで、実はシンプルなものをいくつか用意しておくだけでも違うと思っています。

-- 不正日付確認
SELECT COUNT(*) 
FROM users
WHERE birthday = '0000-00-00';

-- NULL発生率確認
SELECT COUNT(*), SUM(email IS NULL)
FROM users;

-- 想定外形式確認(都道府県以外のパターン確認)
SELECT prefecture, COUNT(*)
FROM users
GROUP BY prefecture;
AIを活用したパターン洗い出し・事前テストの試み

今回、AIをうまく使いこなせていなかった点も反省点だったと思っています。 まさにAIをうまく活用できる点として、

  • 前述したデータパターンを様々な観点で出すことができる
  • 個人情報部分は適当なパターンデータに変換して検証することもできる

当時は本番データをそのまま使用できないことで、ちょっとしたサンプルデータ、パターンデータで行い、 十分な事前検証も不足していました。


4. 移行当日に意識しておくべき判断と心構え

今までいくつか事前準備と慎重な検証が必要と記載をしていますが、 当日は必ず想定外が起きると思って計画を立てた方が良いかと思っています。 特に膨大なデータや内容が多いほど顕著になるかと思います。

以下の点に注意して当日は望めるようにしておきたいです。

  • 1つ1つの作業完了条件を明確にしておくこと。
  • ロジックや仕様変更をその場でする修正は原則しないこと。
  • 移行を中止、延期する、または後日対応でOKかなどの判断が当日できる人がいること。
  • 移行がもし出来ない場合のリカバリ方法があること。

また、時間も想定しているよりも掛かってしまうことが多いと思います。 なるべく余裕をもった計画にしたいです。

今回は投入データの件数確認でさえ、すぐに終わるつもりが件数の正解を事前に用意していなかったため、 非常に大きく時間をかけてしまったのも今回の反省点でした。(完了条件が明確になっていない)


5. 次回のデータ移行に向けた改善点

今回の経験から以下の点を意識し改善できるようにしていきたいです。

  • 一連の手順、動作確認は事前リハーサルでできる限り行うこと。
  • 本番当日に向けた事前準備はしっかり時間を確保して対応する。(当日は必ず何か起きる前提)
  • 当日の作業の完了基準の明確化(データ件数、エラー有無、動作確認など)
  • Load Dataなどデータ投入後の部分が一番大事かつ明確にする部分であること。
  • 仕様通りに作成しても、不正データは必ずある前提で行い、それらも考慮すること。
  • 個人情報を扱うデータの場合は、同等のダミーデータを用意できると検証時に非常に有効。
  • 検証パターン、データ生成などはAIも活用し、検証精度やコスト減に役立てる。

6. まとめ

通常、データ移行は当日一度きりの作業で、 同じ内容で再度実施するケースは少なく、個別のノウハウは蓄積しにくいところもありますが、

共通の考え方、対応として、

  • 事前準備の重要性
  • 検証設計の明確化
  • 当日の判断基準
  • 何かあった時のリカバリ方法

これらを意識することで、当日の障害確率は下げられると思っています。 改めて今回を通じて、データ移行は技術作業というより、検証設計と判断設計の仕事だったと感じています。

正直、完璧に準備をして全てスムーズに完了させることはなかなか難しいところですが、 もし今後データ移行などの作業をする際に本記事が少しでも参考になれば幸いです。

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