【ローカルで動作確認】Functions Framework を用いた Cloud Functions 開発

この記事は個人ブログと同じ内容です

www.ritolab.com


Functions Framework を用いた Cloud Functions 開発

Google Cloud Functions は、サーバーレスコンピューティングの強力なソリューションです。Functions Framework を使用することで、開発者はローカル環境で Cloud Functions を効率的に開発、テスト、デバッグすることができます。本記事では、Functions Framework を用いた Cloud Functions 開発の基本的な流れと、実際の実装方法について解説します。

Functions Framework の主な利点は以下の通りです:

  1. ローカル環境での迅速な開発とテスト
  2. 本番環境との一貫性の確保
  3. 複数の言語とランタイムのサポート
  4. オープンソースで柔軟なカスタマイズが可能

これから、Functions Framework のセットアップから、HTTP トリガーとイベントトリガーの関数の実装、そしてローカルでの動作確認までの手順を詳しく見ていきます。この記事を通じて、Cloud Functions の開発プロセスをスムーズに進められるようになることを目指します。

開発の流れ

Functions Framework を用いた CloudFunctions 開発の流れは以下になります。

  1. 関数の実装
  2. ローカルサーバー起動(TypeScript で実装する場合は起動前にビルド)
  3. 関数の実行(動作確認)

Functions Framework の導入

Functions Framework をインストールします。

pnpm add @google-cloud/functions-framework

TypeScript で実装する前提ですが、package.json は以下の状態になっています。

package.json

{
  "dependencies": {
    "@google-cloud/functions-framework": "^3.4.1"
  },
  "devDependencies": {
    "@types/node": "^20.14.10",
    "typescript": "^5.5.3"
  }
}

参考: Functions Framework を使用して関数を実行する - Google Cloud

ビルドとローカルサーバ起動の設定

CloudFunctions 関数をローカルで実行できるようにするために、ビルドとローカルサーバ起動の設定を行っておきます。

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2022",
    "strict": true,
    "outDir": "./dist"
  },
  "include": [
    "src"
  ]
}

package.json の script フィールドに以下を記述します。

package.json

{
  "scripts": {
    "build": "tsc",
    "start": "functions-framework --target=helloWorld --source=dist",
    "start:dev": "npm run build && npm run start"
  },
  "dependencies": {
    "@google-cloud/functions-framework": "^3.4.1"
  },
  "devDependencies": {
    "@types/node": "^20.14.10",
    "typescript": "^5.5.3"
  }
}

pnpm run start で、functions-framework コマンドを用いてローカルサーバを起動しています。

TypeScript で実装したソースコードを js ファイルにビルドし、それを実行する流れになるため、pnpm run start:dev でビルドとローカルサーバの起動を一度に行っています。

CloudFunctions 関数の実装と動作確認

では CloudFunctions 関数を実装し、ローカルで動作確認を行ってみましょう。

HTTP トリガー

まずは HTTP トリガーで実装してみます。

src/index.ts

import * as functions from '@google-cloud/functions-framework'

functions.http('helloWorld', (req: functions.Request, res: functions.Response) => {
    res.send('Hello, World');
})

実装したら動作確認です。以下コマンドでローカルサーバを起動します。

pnpm run start:dev

# % pnpm run start:dev
# 
# Serving function...
# Function: helloWorld
# Signature type: http
# URL: http://localhost:8080/

HTTP トリガーでローカルサーバが起動しました。続いて、別のターミナルからエンドポイントへリクエストを送信してみます。

% curl localhost:8080
Hello, World

CloudFunctions 関数をローカルの開発環境で実行できました。

参考: HTTP 関数を作成する - Google Cloud

イベントトリガー

次に、イベントトリガーでも実装してみます。

src/index.ts

import * as functions from '@google-cloud/functions-framework'

functions.cloudEvent('helloWorld', cloudEvent => {
    console.log('Hello, World')
})

再度、ローカルサーバを起動します。

pnpm run start:dev

# % pnpm run start:dev
# 
# Serving function...
# Function: helloWorld
# Signature type: cloudevent
# URL: http://localhost:8080/

イベントトリガーでローカルサーバが起動したことがわかります。続いて、別のターミナルからエンドポイントへイベントリクエストを送信してみます。

curl localhost:8080 \
  -X POST \
  -H "Content-Type: application/json" \
  -H "ce-id: 123451234512345" \
  -H "ce-specversion: 1.0" \
  -H "ce-time: 2024-07-15T10:11:10.789Z" \
  -H "ce-type: google.cloud.storage.object.v1.finalized" \
  -H "ce-source: //storage.googleapis.com/projects/_/buckets/my-bucket" \
  -H "ce-subject: objects/my-file.txt" \
  -d '{
    "bucket": "my-bucket",
    "contentType": "text/plain",
    "kind": "storage#object",
    "md5Hash": "...",
    "metageneration": "1",
    "name": "my-file.txt",
    "size": "352",
    "storageClass": "MULTI_REGIONAL",
    "timeCreated": "2024-07-15T10:11:10.789Z",
    "timeStorageClassUpdated": "2024-07-15T10:11:10.789Z",
    "updated": "2024-07-15T10:11:10.789Z"
  }'

ローカルサーバーを起動したターミナル側に Hello, World が出力され、関数が実行されたことがわかります。

参考: イベント ドリブン関数を作成する - Google Cloud

まとめ

本記事では、Functions Framework を用いた Cloud Functions 開発の基本的な流れと実装方法について解説しました。主なポイントは以下の通りです:

  1. Functions Framework の導入とセットアップ
  2. TypeScript を使用した開発環境の構築
  3. HTTP トリガーとイベントトリガーの関数実装
  4. ローカル環境での動作確認方法

Functions Framework を活用することで、開発者は以下のメリットを享受できます:

  • ローカル環境での迅速な開発サイクルの実現
  • 本番環境との一貫性を保ちながらのテストと検証
  • デプロイ前の問題の早期発見と修正

Functions Framework は、効率的で信頼性の高い Cloud Functions 開発をサポートする強力なツールです。ローカルでデバッグできれば開発スピードも格段に上がります。ぜひ試してみてください。

Functions Framework - Google Cloud

Looker Studio で Cloud Billing データ(料金・コスト)を可視化する

この記事は個人ブログと同じ内容です

www.ritolab.com


GCP コスト管理の重要性と可視化の必要性

クラウドコンピューティングの普及に伴い、多くの企業が Google Cloud Platform (GCP) を利用しています。しかし、クラウドサービスの利用が拡大するにつれて、コスト管理の重要性も増しています。効果的なコスト管理は、ビジネスの収益性と持続可能性に直接影響を与える重要な要素です。

GCP の課金システムは柔軟で詳細ですが、その複雑さゆえに、実際の使用状況と費用を把握することが困難な場合があります。特に、複数のプロジェクトや多様なサービスを利用している場合、全体像を把握することは容易ではありません。

そこで重要になるのが、コストデータの可視化です。

データを視覚的に表現することで、以下のような利点が得られます:

  1. 費用の傾向を素早く把握できる
  2. 異常な支出を迅速に発見できる
  3. リソースの最適化の機会を特定しやすくなる
  4. 部門間や経営陣とのコミュニケーションが円滑になる

本記事では、GCPの課金データを Looker Studio を使って可視化する方法を詳しく解説します。この方法を実践することで、データドリブンなコスト管理が可能となり、クラウド利用の最適化につながります。

それでは、具体的な手順を見ていきましょう。

BigQuery にデータセットを作成

まずは、Cloud Billing から BigQuery にデータをエクスポートするためのデータセットを作成します。

作成するデータセットは全部で 3 つです。データセット名は任意の名前で問題ありません。

データセット 用途
billing_standard 標準使用料金データ用
billing_detail 詳細使用料金データ用
billing_dashboard_views ビューテーブル用

それぞれのデータセットの中には、以下の形式を持ったパーティション分割テーブルが作成されます。

  • billing_standard
    • gcp_billing_export_v1_<BILLING_ACCOUNT_ID>
  • billing_detail
    • gcp_billing_export_resource_v1_<BILLING_ACCOUNT_ID>

請求アカウント別にテーブルが出来上がるようですね。

BigQuery の Cloud Billing データテーブルについて - Google Cloud

課金データをエクスポートする

GCP コンソール画面の「課金データをエクスポート」から、「標準の使用料金」「詳細な使用料金」それぞれに、作成したデータセットを指定します。

この設定を行うと、翌日ないし翌々日から Billing データが BigQuery へエクスポートされてくるようになります。

今回の例でいうと、データセット billing_standard と billing_detail にテーブルが作成されデータ入ります。

BigQuery への Cloud Billing データのエクスポートを設定する - Google Cloud

ちなみに、ここでエクスポートを有効にした日以降に発生した Google Cloud の使用量と費用のデータが BigQuery へ反映されます。過去のものは入りません。

Looker Studio: 課金利用状況と費用に関する分析情報のダッシュボード

Looker Studio で Google Cloud の費用情報を可視化していくにあたり、GCP の方で既にダッシュボードが公開されているので、こちらを使用していきます。

課金利用状況と費用に関する分析情報のダッシュボード - Google Cloud

GCP Professional Services

この、公開されているダッシュボードを、エクスポートした私たちのコスト情報に置き換えて新たに自分のダッシュボードを作成していきます。それには以下のツールを使用します。

GoogleCloudPlatform/professional-services - GitHub

このリポジトリは、Google Cloud のプロフェッショナルサービスチームが開発したツール、スクリプト、およびベストプラクティスのコレクションです。このリポジトリは、ユーザーが Google Cloud Platform (GCP) 環境を効果的に実装、管理、最適化するのを支援することを目的としています。

ここで提供しているツールの 1 つに、今回のダッシュボードを自分のデータにアタッチして新たなダッシュボードを作成できるスクリプトがあります。

ダッシュボード作成

では Looker Studio にダッシュボードを作成していきます。

Professional Services のスクリプトを使用するために、Cloud shell を使います。

Cloud Shell を利用して、Professional Services の GitHub リポジトリを clone します。

Cloud Shell で GitHub リポジトリを clone する(実際にあなたの GCP 環境へ遷移し Cloud Shell を開きます)

上記 URL へアクセスすると、以下の確認モーダルが表示されるので、「確認」を押下します。

環境構築が開始されるのでしばし待ちます。

Cloud Shell の環境構築が完了すると、Professional Services リポジトリをクローンした状態で Cloud Shell Editor が起動します。

この画面の下半分にある cloud shell コンソールに、以下のコマンドを順番に入力し、実行していきます。

# 1. 移動
cd examples/billboard

# 2. bill-env が既に存在する場合は削除
rm -rf bill-env

# 3. virtualenv をインストール
pip3 install virtualenv

# 4. 仮想環境を作成
virtualenv bill-env

# 5. 仮想環境をアクティベート
source bill-env/bin/activate

# 6. 必要なパッケージをインストール
pip3 install -r requirements.txt

# 7. ダッシュボード作成スクリプトを実行
python3 billboard.py \
  -pr '<<YOUR_PROJECT_ID>>' \
  -se 'billing_standard' \
  -de 'billing_detail' \
  -bb 'billing_dashboard_views'

7 つ目のコマンドを実行すると Looker Studio にダッシュボードが作成され、レポートの URL が Cloud Shell に出力されるので、これをクリックします。

これで、請求情報のダッシュボードを作成することができました。最後に、このダッシュボードを保存することで、継続的に請求情報を Looker Studio から確認できるようになります。

数日経過後

まとめ:Looker Studioで効果的なGCPコスト管理を実現

本記事では、Google Cloud Platform (GCP) の課金データを Looker Studio で可視化する方法について解説しました。主なポイントは以下の通りです:

  1. BigQuery にデータセットを作成し、Cloud Billing からデータをエクスポート
  2. Google 提供の「課金利用状況と費用に関する分析情報のダッシュボード」テンプレートを活用
  3. GCP Professional Services のツールを使用して、自身のデータに適用したダッシュボードを作成

この方法を実践することで、以下のメリットが得られます:

  • GCP の利用コストを視覚的に把握できる
  • 課金データは定期的に自動更新され、費用の推移を確認できる
  • プロジェクトやサービスごとの詳細な費用分析が可能

コスト管理は、クラウドサービスを効率的に利用する上で非常に重要です。Looker Studioを活用することで、データドリブンな意思決定が可能となり、最適なリソース配分やコスト削減の機会を見出すことができます。

とはいえ、GCP が提供するサービスは多岐にわたるため、1 から自分で請求情報のダッシュボードを作成するのは本当に手間がかかります。今回は Professional Services が提供しているスクリプトを利用し、公開されているレポートレイアウトを使用しましたが、こういったところも予め用意されているのは非常にありがたいです。

定期的にダッシュボードを確認し、必要に応じて最適化を行うことで、GCP の利用をより効果的かつ経済的なものにすることができると思います。是非試してみてください。

Looker Studio を使用して費用を可視化する - Google Cloud

IntelliJ + DevContainer + Zenn CLI でブログ執筆環境を構築する

この記事は個人ブログと同じ内容です


はじめに

この記事では、IntelliJ IDEA と DevContainer, Zenn CLI を利用してブログ執筆環境を構築する方法をご紹介します。

DevContainer とは

以下が参考になりました。

Devcontainer とは、 Dockerコンテナを開発環境とし作成することで、開発環境に必要なライブラリやランタイムのバージョン、Extension をコンテナ内に閉じ込めその中で完結させることができます。

  • Dockerコンテナ内に開発で必要なものをすべて閉じ込める ⇒ ローカル環境を汚染しない
  • コマンドで構築終わり ⇒ 開発環境の構築が楽になる
  • 構築時のヒューマンエラー発生率低減 ⇒ システムのコード化

Zenn CLI とは

ローカルの好きなエディターで Zenn の投稿コンテンツを管理・プレビューするためのツールです。

前提条件

コンテナを作成する

リポジトリをクローンする

作成済みの GitHub リポジトリをクローンします。

Dev Container テンプレートを選択する

  1. IntelliJ のエディターでクローンしたリポジトリを開きます。
  2. プロジェクトのトップディレクトリで、⌘ + N を押下して新規ファイル作成ダイアログを開きます。
  3. .devcontainer を選択します。
  4. 「Dev Container テンプレート」フィールドを選択して、「node」と入力し、検索結果から「Basic Node.js」を選択します。
  5. 今回タグは 20-bookworm-slim を選択します。
    • 20 = Node.js のバージョン。Zen CLI は Node.js 14 以上が必要です。
    • bookworm = Debian のコードネーム。
    • slim = 使用頻度の低いパッケージが除外された軽量なイメージ。

  6. 「OK」ボタンを押下すると、.devcontainer ディレクトリが作成され、devcontainer.jsonDockerfile が作成されます。

devcontainer.json を編集する

自動で生成された devcontainer.json は以下のようになっています。

{
   "name": "Basic Node.js",
   "build": { "dockerfile": "Dockerfile" },
   "remoteUser": "node",
   "customizations" : {
      "jetbrains" : {
         "backend" : "WebStorm"
      }
   }

}

以下のように編集します。

{
  "name": "Zenn Workspace", // 一覧に表示される DevContainer 名
  "build": {
      "dockerfile": "Dockerfile"
  },
  "remoteUser": "zennwriter",
  "runArgs": ["--name", "zenn_workspace"], // 任意の Dcoker コンテナ名を指定
  "customizations" : {
    "jetbrains" : {
      "backend" : "PhpStorm" // 利用したい IntelliJ のエディター
    }
  },
   "mounts": [
      "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/zennwriter/.ssh,type=bind,consistency=cached,readonly" // devcontainer 内で GitHub に push するため SSH 設定をマウント
   ]
}

Dockerfile を編集する

自動で生成された Dockerfile は以下のようになっています。

FROM library/node:20-bookworm-slim

ARG DEBIAN_FRONTEND=noninteractive
RUN apt update \
    && apt install -y --no-install-recommends sudo \
    && apt autoremove -y \
    && rm -rf /var/lib/apt/lists/* \
    && echo "node ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/node \
    && chmod 0440 /etc/sudoers.d/node

そのまま起動すると以下のエラーが発生しました。

Cannot find required dependencies in dev container: ps

どうやら必要なパッケージの一つである ps がインストールされていないため、エラーが発生しているようです。 procps パッケージをインストールすることで解決しました。 ユーザー作成や言語設定なども追加して、編集後の Dockerfile は以下のようになりました。

FROM node:20-bookworm-slim

ARG USERNAME=zennwriter
ARG USER_UID=1001
ARG USER_GID=$USER_UID

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ Asia/Tokyo

RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt update \
    && apt install -y --no-install-recommends sudo procps locales git vim ca-certificates \
    && apt autoremove -y \
    && sed -i -e 's/# \(ja_JP.UTF-8\)/\1/' /etc/locale.gen \
    && locale-gen \
    && ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \
    && rm -rf /var/lib/apt/lists/* \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

USER $USERNAME

コンテナを起動する

  1. devcontainer.json を開いている状態で、左側の青くて四角いアイコンを押下します。
  2. ダイアログが表示されるので、「Create Dev Container and Mount Sources」を選択します。
    • 「Create Dev Container and Clone Sources」の方が処理速度が早いため公式はこちらを推奨しているようですが、今回は個人ブログ用のため他の人と環境を共有する機会がないこと、devcontainer に変更があった場合 GitHub を介さずにすぐ反映できるようにするため、Mount Sources を選択しています。

  3. 「Environment prepared」と表示されることを確認します。
  4. デフォルトでは EAP (Early Access Program = 開発中のベータ版) のエディターが選択されているので、好みで変更します。
  5. 「Continue」ボタンを押下して、コンテナを起動します。
  6. 新しくエディターが開いたら起動完了です。

Zenn CLI をインストールする

新しく開いたエディターでターミナルを開き Node.js がインストールされていることを確認します。

$ node -v
# v20.14.0

公式のインストール方法に従って Zenn CLI をインストールします。

$ npm init --yes # プロジェクトをデフォルト設定で初期化
$ npm install zenn-cli # Zenn CLI を導入
$ npx zenn init # Zenn CLI を初期化
$ npx zenn preview # プレビューを起動

ターミナルのポート番号を押下し、Open in Browser を選択した後に、ブラウザでプレビューが開いたら Zenn CLI のインストールは成功です。

Zenn と GitHub リポジトリを連携する

GitHubリポジトリと連携することで、ローカルで記事を書いてそのまま投稿することができます。 導入方法は公式の手順をご覧ください。

おわりに

Zenn の執筆環境を構築する方法をご紹介しました。

普段 PhpStorm を利用しているため、慣れた設定やショートカットキーで環境構築したくて今回書いたのですが、VS Code の記事が多く IntelliJ の情報は少なくて苦戦しました。また IntelliJ の DevContainer は現在 Beta 版のため、ある程度の規模の開発プロジェクトで DevContainer を利用する場合は、VS Code と仲良くなった方がいいなと感じました。

とはいえ、ブログの環境構築程度でしたら IntelliJ でも十分だと思います。 DevContainer を使うことでローカル環境を汚さずに環境を構築できるので、ぜひ自分の好みに合わせてカスタマイズしてみてください。

男子新体操とはエンジニアリングである

この記事は個人ブログと同じ内容です


はじめに

私はエンジニアであり、男子新体操というスポーツをやっており、プライベートの時間に練習し、大会に出場してたりします。 私は個人競技をやっており、飽き性なため、4種目の演技を毎年作り変えています。 ※一般的には1つの演技を作ったら2~3年、長い人は7年ほど同じ演技をします

エンジニアを仕事としてやっていくうちに、男子新体操での演技作りと似ている部分が多いと、かなり前から感じていたので、面白いと思ってこの記事を書いてみました。

男子新体操とは

団体

youtu.be

個人

youtu.be

ダンスとアクロバットを混ぜたようなスポーツで、団体は6人で行い、個人は1人で4種目(スティック、リング、ロープ、クラブ)を行うスポーツです。 女子の新体操とは全く異なり、女子は華麗なスポーツであるのに対して、男子新体操はダイナミックで迫力があり、カッコいいスポーツです。 日本発祥で日本のみで、行われているスポーツです。

男子新体操のルール

  • 20点満点の減点方式
  • クロバット3回や投げの回数など、最低限のルールはあるがその他は自由
  • トリッキングやバレエ、ダンスなどのほか競技の技を入れても良い

男子新体操だけではなく、クリエイティビティを必要とするスポーツ全般に言えることですが、 100点の演技は存在しないので、曲を変えたり動きを変えたりして、無限に遊ぶことができるスポーツです。

答えのない不確実なものを試行錯誤し、形にしていくという点では、スポーツもビジネスも考え方は同じだと思いました。

演技(プロダクト)作りのやり方

私は以下の流れで演技作りをやっています。

市場調査 → 全体案を考える → 小さく作っていく → 全体で合わせる → 改修

私の場合は、新しいものを作るのではなく、上手い人のを盗んで、それを組み合わせて演技を作っていきます。 新しいものではなく、既存のものを組み合わせたり、プラスしていって作る感じです。 これは新しいビジネスアイディアを考える時と全く同じです。

市場調査

男子新体操は毎年レベルアップしているので、昔の演技をやっても点数が出るわけではなく、その年によって求められるものが変わってきます。 なので、はじめに前年の人の演技を見たり、審判からの聞き込み調査をして、どんな傾向があるかを調査します。

全体案考える

市場調査をもとに、ざっくり演技全体を作っていきます。 曲をベースに演技を作るので、曲を聴き、どこにどの動きをハメたいか、全体の動きの流れなどをざっくり決めます。

小さく作っていく

全体案をもとに、演技を4分割して、それぞれのパートを作っていきます。 それらを組み合わせて、全体感を見て違ったら、減点項目になりそうなものや違和感のある部分を直します。 ある程度の演技ができたら一旦完成です。

ここで意識しているのは、 最初から完璧なものを作らないし、求めない ということです。 自分が良くても、他の人から見ると違和感があるなど、演技には不確実要素が多いので、 私の場合、「音に合う、ある程度の流れが良い」という 最低限の要件を満たしたMVP(Minimum Viable Product)を作っていきます。

最低限の演技ができたら、 人のアドバイスや、もっと良くなりそうなポイントを練習で探し、それを元に演技を無限に改善していきます。

エラーハンドリングを考える

投げを落とした時などのミスをした後に、演技に影響が出ないように、そのパターンも想定して演技を作っています。 事前に復旧しやすい可用性の高い演技設計を意識しています。

最後に

この記事で語ったことは、あくまで私の場合なので、他の人の作り方は知りませんし、違っている可能性があります。 全日本大会で上位のチームたちは、私と違い、常に新しいものを開発し、取り入れてます。 この記事を通して少しでも男子新体操というスポーツを理解してもらえたら嬉しいです。


現在 back check 開発チームでは一緒に働く仲間を募集中です。 https://herp.careers/v1/scouter/j25fiAusU1v1herp.careers

TROCCO と BigQuery で始める ETL 入門

この記事は個人ブログと同じ内容です

www.ritolab.com


データ分析基盤を構築する上で欠かせないのが ETL ツールです。外部データソースの取り込みを一元管理し、分析基盤のデータを最新に保ちます。

今回は、TROCCO と BigQuery を使って分析基盤を構築し、データ処理の流れを体験してみましょう。

対象

本記事は、TROCCO を使ってみたい。分析基盤を構築してみたい人が対象です。

TROCCO と GCP を利用します。個人利用のアカウントを使い、ハンズオン形式で進めていけます。

基本的には無料の範囲で実施しますが、扱うデータによって GCP 側で課金が発生する場合があります。無料範囲で実施したい場合は、本記事で提供するデータを使用してください。

(本記事で作成、有効化していく各リソースは、ハンズオン終了後、不要になったら自身の環境から削除、ないし無効化してください。)

TROCCO

TROCCO は、外部データソースの取り込みからデータマートの生成までをカバーする ETL ツールです。

TROCCO®は、ETL/データ転送・データマート生成・ジョブ管理・データガバナンスなどのデータエンジニアリング領域をカバーした、分析基盤構築・運用の支援SaaSです。
あらゆるデータの連携・整備・運用を自動化し、スピーディーにデータ活用環境を整備。インサイトを得やすい状況に導きます。
https://trocco.io/lp

DB やログなどのデータを分析基盤に取り込んだり、分析基盤のデータからデータマートを作成したりを一括管理できます。さらにこれらをワークフローとして定義することで、分析基盤上のデータを最新に保つなどの仕組みも備えています。

TROCCO フリープランアカウント作成

TROCCO では、無料で使えるフリープランがあります。

https://trocco.io/lp/plan.html

フリープランを使用するには、あらかじめ申し込みが必要です。アカウント発行には 1 営業日程度かかるので、前もって申し込んでおきます。

アカウントが作成されたら、ログイン画面からログインします。

https://trocco.io/users/sign_in

TROCCO と BigQuery で構築する分析基盤

今回は、BigQuery を分析基盤として、TROCCO を ETL ツールとして使い、Google スプレッドシートのデータを取り込み、そこからさらにデータマートを作成するまでを行ってみましょう。

まずは、GCP と TROCCO で事前準備の設定を行っていきます。その後で、外部データの取り込みやデータマート作成を行います。

GCP 設定

まずは GCP の設定を行います。GCP でプロジェクトが作成されている前提で進めますのでまだ作成していない場合はプロジェクトを作成してください。

https://cloud.google.com/resource-manager/docs/creating-managing-projects?hl=ja

TROCCO 接続用のサービスアカウント作成

TROCCO から BigQuery に接続できるように、サービスアカウントを作成します。

「IAMと管理」より、「サービスアカウント」を押下し、サービスアカウントを作成します。

サービスアカウント画面にアクセスしたら、「サービスアカウントを作成」を押下します。

「サービス アカウントの詳細」では、サービスアカウントの名前を入力します。入力したら、「作成して続行」ボタンを押下します。

次に、このサービスアカウントに権限を付与していきます。「BigQuery ジョブユーザー」と「BigQuery データオーナー」を付与します。

完了ボタンを押下し、サービスアカウントの作成は完了です。

なお、サービスアカウントへのロール付与について、業務で使用する場合は管理者に相談のもと、適切な権限を付与するようにしてください。
https://cloud.google.com/bigquery/docs/access-control?hl=ja

サービスアカウントが作成されました。続いて、キーを発行します。

「鍵を追加」より、「新しい鍵を作成」を押下します。

キーのタイプは「JSON」を選択し、作成ボタンを押下します。

これでキーが生成されました。生成されキーは JSON ファイルで自動的にダウンロードされます。JSON ファイルを開いてみると以下が収録されています。キーは後工程で使用します。

{
  "type": "xxx",
  "project_id": "xxx",
  "private_key_id": "xxx",
  "private_key": "xxx",
  "client_email": "xxx",
  "client_id": "xxx",
  "auth_uri": "xxx",
  "token_uri": "xxx",
  "auth_provider_x509_cert_url": "xxx",
  "client_x509_cert_url": "xxx",
  "universe_domain": "xxx"
}

Google Sheets API の有効化

今回は、外部データソースとして Google Sheets を使います。TROCCO はスプレッドシートへのアクセスに Sheets API と Drive API を用いますので、これを有効化しておきます。

GCP のコンソール画面から、「APIとサービス」にアクセスし、検索窓に「Google Sheets API」と入力すると出てくる「Google Sheets API」を押下します。

Google Sheets API を有効にするボタン押下します。

これで Google Sheets API の有効化は完了です。

Google Drive API の有効化

同じく、Drive API も有効化します。検索窓に「Google Drive API」と入力すると出てくる「Google Drive API」を押下します。

「有効にする」ボタンを押下します。

TROCCO 設定

続いて、TROCCO 側の設定を行っていきます。

接続情報の登録

TROCCO がスプレッドシートに接続するための接続情報と、BigQuery に接続するための接続情報 2 つを登録します。

左メニュー「接続一覧」を押下し接続情報一覧へアクセスしたら、画面右上にある「新規作成」ボタンを押下します。

Google Spreadsheets 接続情報

「ファイル・ストレージサービス」タブを押下すると、Google Spreadsheets が表示されますので、これを押下します。

「サービスアカウントで作成」ボタンを押下します。

接続情報の作成画面で、必要な情報を入力します。認証方式は JSON を選択し、JSON Key には、GCP で TROCCO 接続用のサービスアカウントを作成した際にダウンロードした JSON ファイルの中身を全てコピーし、ここにペーストします。

保存ボタンを押下し、接続情報を登録します。

BigQuery 接続情報

Google Spreadsheets 接続情報の登録と同じ要領で、BigQuery への接続情報も登録します。

DWH を押下すると、BigQuery が表示されますので、これを押下します。

こちらも先ほど同じく、認証方式は JSON を選択し、JSON Key には、GCP で TROCCO 接続用のサービスアカウントを作成した際にダウンロードした JSON ファイルの中身を全てコピーし、ここにペーストします。

入力できたら、「接続を確認」ボタンを押下してみましょう。接続結果に正しく接続が行えたか表示されます。

接続確認ができたら、保存ボタンを押下し、接続情報を登録します。

以上で接続情報の登録は完了です。

これで一通りの事前準備が完了しました。

サンプルデータ

今回は、外部データソースとして Google Spreadsheets を用いますが、そこに収録するデータとして、以下を利用します。

(これらのデータに関してや、BigQuery に関しては BigQuery やさしいはじめの一歩〜実際に触って理解するデータ操作ワークショップ〜 を参照してください)

自分のアカウント上にスプレッドシートを作成し、上記のデータをコピーしてください。そして、作成したスプレッドシートをサービスアカウントに共有します。

共有先ですが、ダウンロードしたサービスアカウントの JSON ファイルに client_email の記載がありますので、これを指定します。権限は閲覧者で問題ありません。

データ転送設定

それでは実際に分析基盤にデータの取り込みを行ってみましょう。TROCCO の「データ転送」画面から「新規転送設定作成」ボタンを押下します。

転送元は Google Spreadsheets, 転送先は BigQuery を選択して「この内容で作成」ボタンを押下します。

遷移後の転送設定の新規作成画面では、以下を入力していきます。

  1. 概要設定
    • 名前: 転送設定に任意の名前をつけます。ここでは「sample_ice_cream__ice_cream_sales」としています。
  2. 転送元 Google Spreadsheetsの設定
    • Google Spreadsheets接続情報: 作成した接続を指定します。
    • シートのURL: 自分のアカウント上に作成したサンプルデータのスプレッドシート URL を入力します。
    • シート名: 取り込みたいシート名を入力します。
    • カラム設定: 「カラム情報を抽出」ボタンを押下します。スプレッドシートに接続できれば、自動でカラム情報を取得してくれます。

  1. 転送先 Google BigQueryの設定
    • Google BigQuery接続情報: 作成した接続を指定します。
    • データセット: sample_ice_cream と入力します。
    • テーブル: ice_cream_sales と入力します。
    • データセットのロケーション: お住まいの地域に近いロケーションを指定します。東京が最寄りなら asia-northeast-1(東京)を指定します。
    • データセットの自動生成オプション: 「dataset を自動で生成する」を選択します。
    • 転送モード: 全件洗い替え(REPLACE)を選択します。
    • 接続確認: 「接続を確認」ボタンを押下して、接続が正常に行えるか確認しましょう。エラーが出なければ正常に接続できています。

全て入力できたら、画面最下部にある「次の STEP へ」 ボタンを押下します。

次の「データプレビュー・詳細設定」では、実際に BigQuery に転送されるデータのプレビューやカラム定義、フィルターや文字列変換、マスキング設定など細かい設定が行えます。

今回は特に変換処理は設定しませんので、プレビューが正常に表示されたら、「確認画面へ」ボタンを押下し先に進みます。

遷移先の「内容の確認」画面は、これまで設定した情報の確認です。問題なければ「保存して適用」ボタンを押下すると、データ転送設定が登録されます。

データ転送実行

では、登録した転送設定を使用して、スプレッドシートのデータを BigQuery に転送してみましょう。作成したデータ転送画面の右上にある「実行」ボタンを押下します。

「新規転送ジョブの実行」画面に遷移するので、そのまま「実行」ボタンを押下すれば、データ転送が開始されます。

ステータスが SUCCESS になれば、転送は成功です。

実際に BigQuery を確認してみましょう。データセット「sample_ice_cream」に、テーブル「ice_cream_sales」が作成されていることが確認できます。

おつかれさまでした。これで、外部データソースからのデータの取り込みができました。

次に進む前に

同じ要領で、「ポイントメンバー会員」データについても、データ転送設定を作成し、データ取り込みまで行ってください。この後のデータマート作成で、これら 2 つのテーブルを使用します。

  • 転送設定名: sample_ice_cream__members
  • データセット: sample_ice_cream
  • テーブル: members

データマート作成

前章で、外部のデータソースを分析基盤に取り込むことを行っていきました。

ここでは、分析基盤に既にあるテーブルから、さらに新しいテーブルを作成するための設定を行います。

現在、sample_ice_cream データセットには 2 つのテーブル「ice_cream_sales」と「sample_ice_cream」があります。この 2 つのテーブル使用して、「各商品ごとの男女購入比率」を算出した新しいテーブルを作成します。

テーブルのレコードとしては、以下になる想定です。

WITH t1 AS (
  SELECT 
    item_name,
    gender
  FROM `sample_ice_cream.ice_cream_sales` sales
  INNER JOIN `sample_ice_cream.members` members ON sales.member_id=members.id
  WHERE gender in ('', '')
)
, t2 AS (
  SELECT 
    item_name,
    SUM(CASE WHEN gender='' THEN 1 ELSE 0 END) as male,
    SUM(CASE WHEN gender='' THEN 1 ELSE 0 END) as female 
  FROM t1
  GROUP BY item_name
)
SELECT
  item_name,
  male + female as total_sales,
  ROUND(male / (male + female), 2) as male_ratio,
  ROUND(female / (male + female), 2) as female_ratio,
FROM t2
;

データマート定義

それでは TROCCO で設定を行っていきます。

「データマート定義」より、「新規データマート定義作成」ボタンを押下します。

データマート定義の新規作成画面に遷移するので、BigQuery を押下します。

データマート定義の新規作成フォームが表示されたら、以下を入力していきます。

  1. 概要設定
    • データマート定義名: データマート定義に任意の名前をつけます。ここでは「sample_ice_cream__item_gender_sales_ratio」としています。
  2. 基本設定
    • Google BigQuery接続情報: 作成した接続を指定します。
  3. クエリ設定
    • クエリ実行モード:「データ転送モード」を選択します。
    • クエリ: 前項のクエリをペーストします。
      • フォームの右上に「プレビュー実行」ボタンがあるので押下します。クエリが正常に実行できれば、プレビュー結果が表示されます。
    • 出力先データセット: sample_ice_cream と入力します。
    • 出力先テーブル: item_gender_sales_ratio と入力します。
    • 書き込みモード: 「全件洗い替え」を選択します。

入力が完了したら、確認画面へ進み、適用ボタンを押下します。これでデータマート定義が作成できました。

データマート作成実行

では、登録したデータマート定義を使用して、BigQuery にテーブルを作成してみましょう。右上にある「シンク」ボタンを押下します。

「新規シンクジョブの実行」画面に遷移するので、そのまま「シンクジョブを実行」ボタンを押下すれば、データマート作成が開始されます。

ステータスが SUCCESS になれば、作成は成功です。

BigQuery を確認してみましょう。データセット「sample_ice_cream」に、新たなテーブル「item_gender_sales_ratio」が作成されていることが確認できます。

おつかれさまでした。これで、データマートの作成が完了しました。

ワークフロー

ここまで、「外部データソースの取り込み」そして「データマートの作成」を行ってきました。

TROCCO のような ETL ツールの役割は、これらの一元管理はもちろんのこと、定期的にデータの最新化を行うことも重要な役割の一つです。

つまり、外部のデータソースから定期的に新しいデータを取得し、それを使ってデータマートのデータも最新化していきます。

これらを行うために、ワークフローを定義して、一連のデータの処理の流れを自動化します。

具体的には、TROCCO でこれまで作成してきた以下の 3 つのデータ処理を一連のフローとして定義し、定期的にこれらを自動で実行できるようにします。

  1. スプレッドシートを読み込み BigQuery に ice_cream_sales テーブルを作成する
  2. スプレッドシートを読み込み BigQuery に members テーブルを作成する
  3. BigQuery の ice_cream_sales テーブル、members テーブルから、item_gender_sales_ratio テーブルを作成する

ワークフロー定義

ワークフロー定義 より、画面右上にある「新規ワークフロー作成」ボタンを押下します。

ワークフロー定義の新規作成画面に遷移したら、以下を入力していきます。

  • 概要設定
    • ワークフロー名: ワークフローに任意の名前をつけます。ここでは「ice_cream_sales_workflow」としています。
  • ジョブ実行設定
    • タスク同時実行上限数: 2
    • タイムアウト設定: 有効, 3 分
    • リトライ回数: 0
    • ジョブの重複実行: 許可する

入力したら「保存」ボタンを押下します。

フロー編集

フロー編集画面に遷移したら、「TROCCO転送ジョブ」を押下します。

モーダルが表示されるので、「sample_ice_creamice_cream_sales」「sample_ice_creammembers」両方にチェックを入れ、「追加」ボタンを押下します。

「sample_ice_creamice_cream_sales」と「sample_ice_creammembers」を、それぞれ「START」から矢印をドラッグして並列につなぎます。それぞれのオブジェクトに点(ポイント)があるので、そこからドラッグすると以下のように START と転送設定を矢印で繋げられます。

次に、「TROCCOデータマートシンク」を押下します。

モーダルが表示されるので、「sample_ice_cream__item_gender_sales_ratio」にチェックを入れ、「追加」ボタンを押下します。

新しく追加された「sample_ice_creamitem_gender_sales_ratio」のオブジェクトを、それぞれ「sample_ice_creamice_cream_sales」「sample_ice_cream__members」から矢印でつなぎます。

こうすることで、データ転送の 2 つが完了したら、データマート作成を行う。という依存関係も定義できます。元のデータが両方最新化されていなければ、データマートを作成してもデータが不十分ですから、こうして依存関係を定義することで、データの不整合を防ぐことができます。

ここまで設定できたら、画面右上の「保存」ボタンを押下してワークフロー定義を保存します。

ワークフロー実行

ワークフローを実行してみましょう。画面右上の「実行」ボタンからワークフローを実行できます。

ステータスが SUCCESS になれば、ワークフローの実行は成功です。

ワークフローの実行によって、外部データソースからのデータ取り込み、そして既存テーブルを利用した新たなデータマートの作成までが一気通貫で行えるようになりました。

ワークフローのスケジュール設定

ワークフローを定期的に実行することで、外部データソースのデータが更新されても定期的に取り込めるようになり、分析基盤を最新の状態に保つことができます。

ワークフローのスケジュール設定は、概要設定から行えますので、興味があれば設定してみてください。

まとめ

本記事では、TROCCO と BigQuery を使って、外部データソースからのデータ取り込みから、データマートの作成、そしてワークフローによる一連の処理の自動化までを体験してきました。

ETL ツールを使うことで、データエンジニアリングの作業を効率化し、データ分析基盤を強化することができます。TROCCO は、直感的な操作性とワークフロー機能により、データの取り込みからデータマート作成までを一元管理できる便利なツールです。

今回は、サンプルデータを使った簡単な例でしたが、実際のビジネスにおいては、様々なデータソースから大量のデータを取り込み、複雑な変換処理を行うことが求められます。そのような場合でも、TROCCO を活用することで、データエンジニアリングのタスクを効率的に進められるでしょう。

データ活用が進むにつれ、データエンジニアリングの重要性はますます高まっています。TROCCO のような ETL ツールを上手に活用し、データ分析基盤を強化していくことが、ビジネスの意思決定の質を高め、競争力を向上させる鍵となるでしょう。

ぜひ、TROCCO を使ってみて、データエンジニアリングの世界に触れてみてください。データの力を活用し、ビジネスの可能性を広げていきましょう。

SQL クエリを効率よく書こう: CTE, Window 関数編

この記事は個人ブログと同じ内容です

www.ritolab.com


データベースを操作する SQL

SQL(Structured Query Language)は、関係データベース管理システムRDBMS)でデータを管理・操作するための標準化された言語です。データベースに格納された情報を効果的に取得、挿入、更新、削除するために使用されます。

SQLは、テーブルと呼ばれるデータの形式で情報を格納し、クエリを使用してデータベースとの対話を行います。初心者にとっても覚えやすく、構文も直感的であるため、多くのデータベース管理システムで広く利用されています。

SQL の歴史は古く、1970 年代初頭に IBM 社において、Edgar F. Codd(エドガー・F・コッド)博士によって革新的な関係モデルが提唱されたことから始まりました。

現代においては、MySQL, PostgreSQL, BigQuery などではこの SQL をベースとしたクエリを組み立て、発行(実行)していくものになっており、データベース製品が異なっても、ベースの文法は同じものです。

クエリを効率良く書こう

データを取得するクエリを組み立てる際のクエリの書き方は必ず 1 つというわけではありません。文法など、クエリとして成立していれば異なった書き方でも同じ結果を取得することができます。

例えば、本の売上を収録しているテーブルがあったとします。

> SELECT * FROM book_salses GROUP BY sale_date;

  |---------|-----------|
  | book_id | sale_date |
  |---------|-----------|
  | 1       | 2024-01-01|
  | 1       | 2024-01-01|
  | 1       | 2024-01-01|
  | 1       | 2024-01-01|
  .
  .
  .
  | 1       | 2024-01-05|
  |---------|-----------|

このテーブルを操作して、本の日次販売数と、累計販売数を出力します。

このとき、以下 2 つの SQL クエリは同じ結果を出力します。

-- クエリ 1
SELECT
    daily_salses.sale_date,
    daily_salses.book_id,
    daily_salses.sales_count,
    SUM(t1.sales_count) as cumulative_sales_volume
FROM (
     SELECT
         sale_date,
         book_id,
         COUNT(book_id) as sales_count,
     FROM book_sales
     GROUP BY sale_date, book_id
 ) as daily_salses
INNER JOIN (
    SELECT
        sale_date,
        book_id,
        COUNT(book_id) as sales_count,
    FROM book_sales
    GROUP BY sale_date, book_id
) t1 ON daily_salses.sale_date >= t1.sale_date
GROUP BY daily_salses.sale_date, daily_salses.book_id, daily_salses.sales_count
;

-- クエリ 2
SELECT
    sale_date,
    book_id,
    COUNT(book_id) as sales_count,
    SUM(COUNT(book_id)) OVER (PARTITION BY book_id ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_sales_volume
FROM book_sales
GROUP BY sale_date, book_id
;

出力される結果は 2 つのクエリとも同じ以下が出力されます。

sale_date book_id sales_count cumulative_sales_volume
2024-01-01 1 12 12
2024-01-02 1 12 24
2024-01-03 1 12 36
2024-01-04 1 12 48
2024-01-05 1 12 60

2 つのクエリを見比べてみてどう感じるでしょう。少なくとも、ぱっと見て記述量が明らかに違うことはわかると思います。記述量が多いということは、それだけクエリを組み立てるのにも時間がかかりそうですね。

「メラ!」で唱えられる魔法と、「汝の身は我が下に、我が命運は汝の剣に ブツブツブツブツ......」みたいな詠唱して唱える魔法では魔法を発動するまでにかかる時間が違うのと似たようなものです。(仕事は早く済ませたい)

このように、どんなクエリだとしても欲しい結果が得られば何でも良いのかというと必ずしもそうとは言えません。クエリによってパフォーマンスに違いがあったり、可読性にも違いがあります。

今回は、最近では当たり前になりつつある記法を使って、クエリを効率よく組み立てていく方法を見ていきましょう。

CTE

CTE(Common Table Expressions)は、SQLで一時的な結果セットを定義するための構文です。主に複雑なクエリをより読みやすく、管理しやすくするために使用されます。CTEを使用すると、サブクエリやビューと同様に、クエリ内で一時的なテーブルを作成し、それを後続の処理で参照することができます。特に再帰的なクエリや複数の階層的なクエリを処理する際に、CTEは非常に有用です。

最初に見た、1 つめのクエリを見てみましょう。

-- クエリ 1
SELECT
    daily_salses.sale_date,
    daily_salses.book_id,
    daily_salses.sales_count,
    SUM(t1.sales_count) as cumulative_sales_volume
FROM (
     SELECT
         sale_date,
         book_id,
         COUNT(book_id) as sales_count,
     FROM book_sales
     GROUP BY sale_date, book_id
 ) as daily_salses
INNER JOIN (
    SELECT
        sale_date,
        book_id,
        COUNT(book_id) as sales_count,
    FROM book_sales
    GROUP BY sale_date, book_id
) t1 ON daily_salses.sale_date >= t1.sale_date
GROUP BY daily_salses.sale_date, daily_salses.book_id, daily_salses.sales_count
;

このクエリでは、全く同じクエリを記述している箇所があります。FROM 句の中と INNER JOIN 句の中で行っている SELECT 文です。

こういった同じクエリを記述している箇所を、CTE を使えば共通化することができます。CTE は WITH 句を使って定義します。

WITH <cte_name> AS (<query>)

WITH daily_salses AS (
    SELECT
        sale_date,
        book_id,
        COUNT(book_id) as sales_count,
    FROM book_sales
    GROUP BY sale_date, book_id
)
SELECT
    daily_salses.sale_date,
    daily_salses.book_id,
    daily_salses.sales_count,
    SUM(t1.sales_count) as cumulative_sales_volume
FROM daily_salses
INNER JOIN daily_salses t1 ON daily_salses.sale_date >= t1.sale_date
GROUP BY daily_salses.sale_date, daily_salses.book_id, daily_salses.sales_count
;

通化したいクエリを CTE として定義し、実際の SELECT 文で利用することによって、クエリ全体をすっきりさせることができます。

CTE はサブクエリに似ていますが、再利用可能で名前を付けられるので、長いクエリをモジュール化して見通しを良くできるというメリットがあります。特に複数の派生テーブルを作成する必要がある場合に、CTE は非常に便利です。

Window 関数

Window 関数は、SQLでデータセット内の特定の行に対して計算を行うための機能です。通常の集約関数(例:SUM、AVG、COUNT)とは異なり、Window 関数は各行に対して個別に計算を実行し、その結果を結果セットの各行に返します。

前項で CTE に触れたので、それを適用させた先ほどのクエリを見てみましょう。

WITH daily_salses AS (
    SELECT
        sale_date,
        book_id,
        COUNT(book_id) as sales_count,
    FROM book_sales
    GROUP BY sale_date, book_id
)
SELECT
    daily_salses.sale_date,
    daily_salses.book_id,
    daily_salses.sales_count,
    SUM(t1.sales_count) as cumulative_sales_volume
FROM daily_salses
INNER JOIN daily_salses t1 ON daily_salses.sale_date >= t1.sale_date
GROUP BY daily_salses.sale_date, daily_salses.book_id, daily_salses.sales_count
;

累計を出すために同じ daily_salses を join させ、再度 GROUP BY を実行し累計を SUM() 関数で集計しています。少し冗長な気もしますね。でも累計を出したいのだから仕方ない。

ところが、Window 関数を使えば、上記のような記述をしなくても累計を算出できます。以下は、Window 関数として SUM() 関数を用いた方法です。

WITH daily_salses AS (
    SELECT
        sale_date,
        book_id,
        COUNT(book_id) as sales_count,
    FROM book_sales
    GROUP BY sale_date, book_id
)
SELECT
    sale_date,
    book_id,
    sales_count,
    SUM(sales_count) OVER (PARTITION BY book_id ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_sales_volume
FROM daily_salses
;

SELECT 句の最後で Window 関数として SUM() 関数を使用しています。再度の JOIN も GROUP BY も不要になりました。

これらはもちろん、同じ結果を出力します。

sale_date book_id sales_count cumulative_sales_volume
2024-01-01 1 12 12
2024-01-02 1 12 24
2024-01-03 1 12 36
2024-01-04 1 12 48
2024-01-05 1 12 60

Window 関数の基本書式

Window 関数の基本的な構文は以下のようになります。

<ウィンドウ関数>([引数]) OVER (
  [PARTITION BY1, 列2, ...]
  [ORDER BY1 [ASC|DESC], 列2 [ASC|DESC], ...]
  [ROWS | RANGE 制約]  
)
  • <ウィンドウ関数> - 使用する Window 関数 (SUM、AVG、COUNT、RANK、DENSE_RANK、ROW_NUMBER など)
  • [引数] - Window 関数に渡す引数 (カラム名など)
  • PARTITION BY - データをグループ化する列を指定 (オプション)
  • ORDER BY - Window 内の並び順を指定 (一部の関数で必須)
  • ROWS | RANGE - Window のレンジを行ベースまたは値ベースで指定 (オプション)

Window 関数は OVER 句の中で設定されます。PARTITION BY で指定した列でデータを分割し、ORDER BY で指定した順序でウィンドウが作成されます。さらに ROWS または RANGE を使ってウィンドウのレンジを細かく制御できます。

今回の例に置き換えると、以下のようなことになります。

SUM(sales_count) OVER (PARTITION BY book_id ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_sales_volume

「book_id ごとに計算します。それぞれは sale_date 昇順で並べます。計算するウィンドウフレームの開始位置は最初の行から、現在の行までの全ての行です。」

SUM(sales_count) -- 加法集計します
    OVER (
        -- book_id ごとに集計します
        PARTITION BY book_id
        -- sale_date を昇順に並び替えた上で集計します
        ORDER BY sale_date
        /*
         * Window の範囲指定は、
         * ROWS BETWEEN
         *      UNBOUNDED PRECEDING(最初の行)
         * AND(から)
         *      CURRENT ROW(現在行)
         * までです
         */
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) as cumulative_sales_volume

こんな感じで、Window 関数は、加法集計以外にも、データの順序付け、ランキングなどのさまざまな用途に使用できます。例えば、各顧客の売上を日付ごとにランク付けする、直近の日付ごとに合計売上を計算する、あるいは移動平均などの時系列解析を行う際に、Window関数が役立ちます。

どんな関数があるのかについては、以下を参照してください。

WINDOW 関数を一通り試してみよう

パフォーマンスアップにも寄与

Window 関数を使えば無駄な JOIN や GROUP BY を行わなくて済む例を見ましたが、これによって、内部的な処理量も減り、パフォーマンスにも寄与します。

例えば以下は、BigQuery で 2 つのクエリを実行した際のパフォーマンスです。

クエリ 1

クエリ 2

Window 関数を用いたクエリ 2 の方が、JOIN が無くなった分処理量も減っており、パフォーマンスが良くなっています。

BigQuery に限らず、読み込むデータの量や結合数がもっと大きな分析を行う際は、こうしたところで SQL クエリのパフォーマンスが大きく変わってきますので、地味に大切なポイントです。

まとめ

SQL クエリを効率よく書くために、CTE と Window 関数を見ていきました。

CTE で共通化すれば、再利用できて便利ですし、それによって冗長な記述を排除すれば、可読性も上がります。自分ひとりでクエリを組み立て使用している分には良いですが、チームでクエリを管理したり、レビューしたりする時には、クエリの可読性はアプリケーションのプログラミングと同じくらい重要です。

Window 関数もまた、記述量を減らせる上、パフォーマンスもアップさせることができます。分析時には多くのクエリを定義することも多いため、一つ一つのクエリにおいて最大のパフォーマンスで実行できるように定義していけると、最終的なパフォーマンスにも無駄なく実行できます。

まさにクエリのパフォーマンスは「塵も積もれば山となる」です。

CTE も Window 関数も、SQL が 1970 年に生まれたことを考えれば比較的新しい機能ですが、現代においてはそこそこお馴染みになっているものです。

代表的なデータベース製品である MySQL は 8.0 から、PostgreSQL は 8.4 から Window 関数が利用できます。

資料やサンプルも沢山あると思いますので、ぜひ活用してみてください。


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers

外部ライブラリのfinalクラスをモックする

この記事は個人ブログと同じ内容です


はじめに

外部のライブラリを使用して実装し、テストを書いていたらfinalクラスがモックできずに詰まったので、その解決方法を解説していきます。 また、解決にあたりサービスコンテナ周りで得た知見の共有をしていきます。

問題

以下のようにLaravelのAuth0 SDKを使用して実装し、テストを書いていたら、 Auth0 SDKで実装されているクラスがfinalなため、直でモック化することができませんでした...

https://github.com/auth0/laravel-auth0

//Auth0 SDKの初期化
$auth0 = new \Auth0\SDK\Auth0([
    'domain' => $_ENV['AUTH0_DOMAIN'],
    'clientId' => $_ENV['AUTH0_CLIENT_ID'],
    'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'],
    'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET']
]);

$auth0->getIdToken()
//Auth0のテスト
app()->bind(Auth0::class, function () {
    $mockedAuth0 = Mockery::mock(Auth0::class);
    $mockedAuth0->shouldReceive('getIdToken')->once()
                ->andReturn('test');
    return $mockedAuth0;
});

エラー文

The class Auth0\SDK\Auth0 is marked final and its methods cannot be replaced. Classes marked final can be passed in to \Mockery::mock() as instantiated objects to create a partial mock, but only if the mock is not subject to type hinting checks.

解決方法

Auth0 SDKは外部のライブラリなので、finalを消すことができないです... ですが、以下のようにコードを見てみると Auth0クラスはAuth0Interfaceクラスをimplementsしているので、 Auth0Interfaceクラスを使用して実装してあげればできそう!

// SDKのコード
final class Auth0 implements Auth0Interface
{
        ・
        ・
}

サービスプロバイダーを使用する

サービスプロバイダーを作成し、Auth0Interface::classにAuth0::classを注入します。 そうすると、app()->make(Auth0Interface::class)で作成したインスタンスで、 Auth0Interface::classで実装しているAuth0::classのすべてのメソッドが使用できるようになります。

class Auth0ServiceProvider extends ServiceProvider
{
    public array $bindings = [
        // ここでもいい
        Auth0Interface::class => Auth0::class,
    ];
    public function register()
    {
        // こっちでもいい
        app()->bind(Auth0Interface::class, Auth0::class);
    }
}

サービスプロバイダについてはこの記事がわかりやすかったです https://note.com/sasshiii/n/n554298c93077

サービスプロバイダー使用後の実装側のコード

app()->make()を使用し、Auth0Interfaceクラスを呼び出すこと、 サービスプロバイダーに登録した、Auth0クラスが注入されたAuth0Interfaceクラスが作成されます。 なので、Auth0Interfaceクラスのメソッドが呼び出されると、Auth0で実装されたメソッドが実行されるので、 Auth0クラスのメソッドを使用した時と変わらないです。

// サービスプロバイダに登録したので、Auth0Interfaceを呼ぶと、Auth0が作られる
$auth0 = app()->make(Auth0Interface::class,[
            'domain' => $_ENV['AUTH0_DOMAIN'],
            'clientId' => $_ENV['AUTH0_CLIENT_ID'],
            'clientSecret' => $_ENV['AUTH0_CLIENT_SECRET'],
            'cookieSecret' => $_ENV['AUTH0_COOKIE_SECRET']
]);
// Auth0::classのgetIdToken()が呼ばれる
$auth0->getIdToken()

サービスプロバイダー使用後のテストのコード

先ほどはAuth0クラスをモックしていましたが、実装側ではAuth0Interfaceクラスが使用されているので、 Auth0Interfaceクラスをモックします。

//Auth0のテスト
app()->bind(Auth0Interface::class, function () {
    // 実装したコードで呼ばれるのはAuth0Interfaceなので、そっちをモックする
    $mockedAuth0 = Mockery::mock(Auth0Interface::class);
    $mockedAuth0->shouldReceive('getIdToken')->once()
                ->andReturn('test');
    return $mockedAuth0;
});

番外編

app()->instance()を使用するケース

既存のインスタンスを注入したい場合に使用します。

例えば、以下のコードのように$hogeService->test()が外部APIを叩くメソッドで、APIを叩くたびに課金が発生するならば、本番では呼ばれていいが、テストでは呼ばれるべきではないと思います。 SampleServiceのhello()のテストを書くときに、$hogeService->test()が実際に呼ばれないようにする時に使用します。

実装側のコード

class SampleService
{
    public function __construct(
        private readonly HogeService $hogeService
    ) {}

    public function hello()
    {
        $hogeService->test();
                ・
                ・
    }
}

テストコード

テストで、app()->instance()を使用し、$hogeServiceをMockに置き換え、実際に呼ばれないようにします。 今回は例が単調すぎますが、実際にはLaravelのコントローラーのテストでよく使います。

public function testHello(): void
{
    // HogeServiceのモックを作成
    $mockedHogeService = Mockery::mock(HogeService::class);
    // HogeServiceのtest()が1回呼ばれ、helloを返すように設定
    $mockedHogeService->shouldReceive('test')->once()
                     ->andReturn('hello')
    // HogeServiceが呼ばれるときは、mockedHogeServiceが呼ばれる
    app()->instance(HogeService::class, $mockedHogeService);

    // こっちでもいい
    $sampleService = new SampleService(mockedHogeService);
}

bindとsingletonの違い

singletonは呼び出された時の値が変わらないです。 例: ランダムな値を返す関数を持つクラスを注入したら、 bindは呼び出されるごとに値が変わるが、singletonは変わらないです。

最後に

今までサービスコンテナについてはなんとなくしか理解していなかったのですが、この問題を通じてかなり理解を深めることができました。 サービスコンテナとかサービスプロバイダーは、あるクラスを別のクラスなどに置き換えて、クラスが登録されている箱の中から、呼び出すだけなのに、 インスタンスやらコンテナなど横文字を使用し、Dockerと似た表現をするから頭がゴチャゴチャしてわかりにくかったんだなと思いました。(かと言って他のいい表現は思いつかない、、、)

他にもっと簡単な方法などありましたら気軽にコメントいただけると幸いです。

参考

https://note.com/sasshiii/n/n554298c93077


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers