データ分析の最近の記事

  1. はじめに
  2. 環境構築手順
  3. Store Manager Agentで実現できること
  4. まとめ


1.はじめに

店舗運営において、こんなお悩みはありませんか。

  • 売上データは見ているが、次に何をすべきか判断に迷う
  • 売場づくりや品揃えが、どうしてもベテラン頼みになってしまう
  • 在庫・売上・時間帯など、考えるべき要素が多すぎる
  • 数字の振り返りはしているものの、改善アクションに落とし込めない


こうした課題は、特定の業種だけのものではありません。
例えば、

  • スーパーマーケットでは、時間帯等を踏まえた発注や値引き判断
  • コンビニでは、限られた売場での品揃えや補充タイミングの最適化
  • アパレル店舗では、売場づくりやセールタイミングの判断
  • ドラッグストアでは、多数の商品カテゴリを踏まえた陳列や販促設計
  • 家電量販店では、在庫・販売戦略と接客のバランスをみた在庫管理


といったように、店舗運営では日々さまざまな意思決定が求められています。

一つひとつの判断はシンプルに見えても、実際には売上・在庫・時間帯・顧客動向など、多数の要素が複雑に絡み合っています。 その結果、多くの現場では経験豊富な店長やベテランスタッフの判断に依存しているケースも少なくありません。 また、多くの企業ではPOSデータや売上データは蓄積されているものの、「次に何をすべきか」という具体的な改善行動まで落とし込めていないケースも多くあります。

こうした背景の中で注目されているのが、AIエージェントが店舗運営の意思決定を支援する、 店舗運営支援エージェント によるアプローチです。
Store Manager Agent for Retail Tech(SMART) は、売上データや現場の声を店舗運営支援エージェントが分析し、「今どのような改善行動を取るべきか」を支援するサンプルアプリケーションです。なお、このアプリケーションはAWSよりOSSとしてGitHub上で公開されているため、誰でもソースコードを確認したり、自身の環境で試したりすることができます。

本記事では、実際にStore Manager Agent for Retail Techを構築・検証してみた結果をご紹介します。


2.環境構築手順

Store Manager Agent for Retail Techの構成は下記の通りです。

architecture.webp

主要コンポーネント

アプリケーション構成

  • S3: フロントエンドアセットの配信
  • CloudFront: グローバルCDN配信
  • WAF: アクセス制御
  • API Gateway: REST APIエンドポイント
  • Lambda: API処理とビジネスロジック実行
  • AgentCore Runtime: Strands Agentsの実行環境
  • AgentCore Memory: エージェントの会話履歴管理


データベース構成

  • S3 Tables: Iceberg形式のテーブルストレージ
  • Amazon Athena: SQLベースのデータクエリエンジン


構築手順の概要

AWS公式の DEPLOYMENT.md に従ってデプロイを進めていきます。本ブログでは今回実行した手順と、注意点を記載します。

1.環境のセットアップ

認証情報の設定を行います。当社はAWS IAM Identity Centerからのログインを行っているため、今回はaws configure ssoにて認証情報を設定しました。
デプロイ先の AWS アカウントとリージョンを決定します。今回は環境変数でリージョン指定を行いました。
ローカル環境において、Lambdaで作成したAPIの修正や追加などの開発を行う場合はDockerをセットアップしましょう。今回はDockerを使用しませんでしたが、アプリケーションは問題なく動作してくれました。

2.AWS 環境の準備

自分が環境を構築するリージョンで、 Amazon Bedrockの利用ができるかを確認しましょう。
AWSマネジメントコンソールを開き、リージョンを設定してAmazon Bedrockを開きます。左メニューのテストからPlaygroundを選択して、カテゴリはAnthropic、モデルはClaude Haiku4.5、推論は推論プロファイルのJP Anthropic Claude Haiku4.5を選択して適用します。
呼び出しが成功すれば、対象リージョンでClaude Haiku4.5が利用できることがわかります。
次に、Amazon API Gateway のタイムアウトに関するクォータ(Quota)緩和申請を行います。 設定はAWSマネジメントコンソールのTimeout Quota (Maximum integration timeout in milliseconds) から行うことができます。デプロイ対象のリージョンであることを確認し、申請しましょう。
次に、Amazon S3 Tables と分析サービスの統合設定を行います。S3 tables コンソール画面から、簡単に設定することができました。

3.プロジェクトの取得

今回はgit cloneを行いました。

4.AWS CDK 環境の準備

npm モジュールのインストールを行います。 初回デプロイ時のみ、AWS CDK の Bootstrap 実行を行います。 次に、 AWS Lake Formation で2つのプリンシパルを追加します。CLIでもコンソールでも設定できますが、今回はCLIで実行しました。

5.CDK 設定のカスタマイズ

アプリケーション画面でのユーザー作成可否、サインアップ可能なメールアドレスのドメイン制限、IP 制限などのパラメータを調整することができます。
今回はデフォルトのままで設定しました。

6.CDK デプロイ

cdk deploy を実行します。私の場合は10分程度で終了しました。

7.初期データのセットアップ

Amazon S3 Tables のテーブルを作成し、テストデータを投入します。

↓実際にS3に投入されるテーブル一覧。例えば下から2つ目の「t_sales」には、売上情報が登録されています。

スクリーンショット 2026-05-21 13.37.01.webp


↓「t_sales」のデータ

スクリーンショット 2026-05-21 14.24.35.webp

8.デプロイ結果の確認

AWS CloudFormation のスタックの出力から、アプリケーションの URL を取得しましょう。

9.ユーザー作成

AgentとAdminでそれぞれ初回ログイン用のユーザーを作成しましょう。Adminユーザーは、Agentユーザーに提供されるサービスに加えて、アンケート質問の編集、メッセージ管理等の機能を使用することができます。

10.Amazon Bedrock モデル呼び出し権限の追加

2025年10月に実施された「モデルアクセスの自動有効化」に関する仕様変更に伴い、「Daily Insights」機能で追加の権限が必要となる場合があります。
その場合、sample-store-manager-agent-for-retail-tech/cdk/lib/constructs/agentcore-resources.ts に以下の AWS Marketplace 関連の権限を付与することで解消する可能性があります。

  • aws-marketplace:ViewSubscriptions
  • aws-marketplace:Subscribe


3. Store Manager Agentで実現できること

早速Agentユーザーでログインしてみます。ログインすると、以下のような画面になりました。


スクリーンショット 2026-05-20 12.52.03.webp

機能は大きく分けて

1. Daily Survey(日次アンケート、AIチャット)
2. Daily Insights(インサイト生成)
3. History(Daily SurveyとDaily Insightsの履歴)

の3つがあり、それぞれの機能はDashboardまたは左メニューから開くことができます。

(1)Daily Survey(日次アンケート、AIチャット)

Daily Surveyは、日次アンケートとAIチャットが連動した機能です。
スタッフがその日の固定アンケートに回答すると、AIエージェントがその回答内容をトリガーにして、自然な対話形式でさらに深いヒアリングを開始します。
最初に表示されるアンケートにすべて回答すると、AIチャットに移動することができます。


スクリーンショット 2026-05-21 12.02.54.webp


AIチャット画面では、AIエージェントがアンケートに関連した追加質問を行います。
その質問に答えると、AIエージェントがさらに深堀り質問をしてくれるので、それに答えていく形式で会話を進めます。
例えば今回は、「パンツがよく売れたが、それはなぜか?」という観点で質問をされました。その後、「新商品のデニムのデザインが人気だったから」と回答すると、「どんなお客様に人気だったか?」というように深堀り質問がありました。


AIチャット例1.webp


AIチャット例2.webp


このように、AIエージェントの質問に答えるだけで一日の営業の振り返りを行うことができ、手書きやExcelでの日報作成にかかっていた時間を店舗運営や接客に充てることができます。
また、現場のスタッフから日々の「生の声」を収集・蓄積することで、今後の仕入れや販売戦略の立案、商品の強みの再発見につなげることができます。

(2)Daily Insights(インサイト生成)

Daily Insightsでは、Daily SurveyによるアンケートやAIチャットから得られた定性データと、売上や来客数などの定量データが掛け合わせて分析され、AIエージェントが改善提案付きレポートを生成します。
例えば今回のフィードバックでは、まず昨日の売上や来客数、購入件数、購入率を振り返ったあとに分析に入ります。


Insights 売上達成分析.webp


1. 売上達成/阻害要因の分析

  • 定量データより、来客数の増加に対して購入件数は変化しておらず、来客一人あたりの購入率は低下
  • 定性データより、サービス満足度は高く、スタッフの営業評価も高い

よって、サービス品質は良好だが、商品ラインナップや価格設定、販売促進の工夫が不足していた可能性が考えられる。


Insights AIの気づき.webp


2. AIの気づき

  • 商品の品揃え、価格、販売提案に課題がある可能性がある
  • サービス満足度は高いが購買行動に結びついていない

Insights アクション提案.webp


3. アクション提案

  • 購入率を上げるため、スタッフへの販売スキル研修を実施する
  • 好調なパンツカテゴリーの在庫を確保しつつ、トップスや小物の売上向上策を検討する

などのフィードバックが得られました。


Daily Insights機能では、単なる日報の記録やレポート出力ではなく、売上データに加え、

  • どの商品を強化すべきか
  • どの売場改善を優先すべきか
  • どの時間帯の施策を見直すべきか

といった、次のアクションにつながる示唆を得られる点が特徴です。
なお、売上や来客数などの定量データは別途S3に登録する必要があります。

<サンプルデータの利用に際して注意点>
初期デプロイ時に投入されるテストデータは「前日」および「前々日」のデータです。 システムは直近のデータを参照する仕様のため、デプロイから1日以上経過するとデータが過去のものとなり、Insights 機能が動作しなくなります。 その場合は、再度テストデータを投入してください。

(3)History(Daily SurveyとDaily Insightsの履歴)

Daily SurveyとDaily Insightsの履歴を一覧で確認できる画面です。
他店舗間でもインサイトを共有することができます。

スクリーンショット 2026-05-21 16.12.04.webp


売上データに加え、

  • アンケート回答
  • AIエージェントとの対話履歴
  • 日々の気づき

などの定性データも蓄積されます。
これにより、売上などの定量データだけでは見えなかった「現場の背景」まで含めて分析できるようになります。

(4)Admin

Adminユーザーでログインすると以下の画面になり、左メニューにAdminの画面が増えていることがわかります。
Adminユーザーは管理者専用の機能を使うことができます。


スクリーンショット 2026-05-21 16.09.43.webp

管理者(Admin)専用の機能は以下の2つです。

1. Announcementsの編集(店舗へのお知らせの編集)
2. Survey Questionsの編集(店舗への固定された質問の編集)

ここで編集したお知らせと質問が、Daily Surveyのアンケート画面に反映されます。


4. まとめ

店舗運営では、これまで「経験」や「勘」に依存する意思決定が数多く存在していました。
しかし今後は、AIエージェントによって現場データと日々の気づきを蓄積・分析し、「誰でも改善判断ができる状態」を作れる時代になりつつあります。
特にポイントは以下の4点です。

4-1.店舗スタッフの業務負担軽減と本来業務への集中

手書きやExcelでの日報作成から解放されます。
AIエージェントがスタッフの回答をもとに自然な対話で深掘り質問をしてくれるため、現場の状況を簡単にデータ化できます。
これによって店舗スタッフの事務作業の負担を減らし、スタッフは本来の店舗運営や接客により集中できるようになります。

4-2.データに基づいた客観的フィードバックの早期獲得

AIエージェントとの対話やアンケートから得た「定性データ」と、売上などの「定量データ」を掛け合わせて統合分析された、改善インサイトを取得できます。
また、客観的な分析による「改善提案付きレポート」が自動生成され、店舗側でフィードバックとして受け取ることができます。

4-3.リアルな現場の声の蓄積と、ナレッジの組織的な横展開

これまでの定型フォーマットでは伝わらなかった現場の細かいニュアンスやリアルな声が、構造化されたデータとして蓄積され、本部に届くようになります。
個人の頭の中に留まりがちだった成功事例や工夫がデータとして可視化されるため、店舗間でのナレッジ共有が進み、組織全体の学習と横展開がしやすくなります。

4-4.導入のしやすさと柔軟なカスタマイズ性

GitHubから20分かからずにデプロイが完了し、すぐに利用を開始して効果検証(PoC)に進むことができます。
本部側の管理画面から、アンケートの質問項目(選択式、テキスト、評価など)をノーコードで簡単に追加・編集できます。
プロンプト(テキストファイル)を編集するだけでコードを変更せずにAIエージェントの振る舞いを調整できるほか、SQLを記述してAIエージェントが参照するデータソース(商品情報や在庫など)を拡張することも可能です。


「店舗運営にAIエージェントをどう組み込めるのか」を検討している方は、ぜひ一度試してみてはいかがでしょうか。デモ視聴や、導入時の課題整理・改善提案なども可能ですので、お気軽にお問い合わせください。

AUCへのお問い合わせ

アマゾンジャパン品川オフィス


  1. はじめに
  2. AI BPRとは
  3. ワークショップの内容
  4. 参加者の声
  5. 組織への展開と本格導入


1.はじめに

売上や現場の数字を見ながら、次々と判断を下す毎日。「これAIでやってくれないかなぁ」と感じたことはありませんか。

生成 AI のニュースは毎日のように流れてきますが、自社の業務で「使える」という実感を持てている方は、まだ少ないのではないでしょうか。業務の中で日々判断を重ねている方ほど、目の前の業務を AI が肩代わりしてくれる姿は具体的にイメージしにくいものです。

こうした課題に向き合う手法として、アマゾン ウェブ サービス ジャパン合同会社(以下、AWS Japan)が今年3月に提唱した手法が AI BPR(AI を前提とした業務プロセスの再設計)です。AWS Japan がすでに複数の大手企業で実施した AI BPR では、参加者の81%が「AI エージェントによってビジネスモデルが変わると確信した」と回答するなど、確かな成果が出ています。 ※ AI BPRの紹介はこちら:Amazon Web Services ブログ/AI 駆動の業務変革手法 :「課題は何ですか?」と聞くのをやめた日

アクセルユニバース(以下、AUC)は、AWS Japan と共催で AI BPR ワークショップを継続的に開催しています。今回は2回目の開催(前回開催報告はこちら)で、Data Analytics Agent をテーマに 7 社 30 名のお客様にご参加いただきました。終了後アンケートでは、参加者の 85% が「AI エージェント前提への業務変換は実現可能」、90% 以上が「ワークショップ全体に満足」、60%以上 が「経営層に報告したい」、つまり全社を巻き込んで実施する価値があると回答しています。

本記事では、当日の内容と参加者の声、そしてワークショップを起点に何を変えていけるのかをご紹介します。


2.AI BPRとは

AI BPR とは、ビジネスモデルや業務プロセスに対し、AIエージェントが業務の一部を担うことを前提に組み替える4〜5 時間のプログラムです。

従来型の BPRは、「何が問題か」から出発するアプローチであり3〜4 割しか成功してこなかったと言われています。 理由は次の3点に集約されます。

  • 問題から始めると、人は防衛的になる
  • ヒアリングと資料化の往復で、変革の主体が現場から奪われる
  • AI 導入は本来「適応課題」なのに「技術課題」として扱ってしまう


また、「とりあえず AI を導入してから業務を考える」というデジタルファースト型のアプローチでも、現場で定着せずに止まってしまうケースが多くあります。AI BPR は、こうした失敗パターンを回避するために設計されています。

これまでの「業務改善」や「DX」と AI BPR が決定的に違うところは、既存業務を効率化することにとどまらず、「AI エージェントが業務の一部を担う」ことを前提に、仕事の組み立てそのものを設計し直すという点です。

特徴は大きく 4 つあります。

  1. 強み起点:問題点を洗い出すのではなく、自社が何で価値を出せているか、強みは何かから出発する
  2. 対話的アウトプット:AI と対話しながら、その場で成果物を作成する(持ち帰って資料化する時間を必要としない)
  3. 実用性の実証:実データを使って、明日から本当に使えるかをその場で検証する
  4. 再現可能性:進め方がパッケージ化されており、特定の人の職人芸に頼らず一定の成果が出せる


たとえば「強み起点」は、「クレーム対応が遅い」という問題から出発するのではなく、「当社の強みは常連客のリピート率の高さ。この強みをさらに伸ばすために、AI に何を任せるか?」と問い直す。問題ではなく、もっと伸ばせる余地に目を向けるからこそ、参加者の議論がポジティブに前進します。

そして進め方は、Observe(業務フロー可視化)・Shift(AI 委譲判断)・Simulate(プロトタイプ検証)・Forecast(展開計画策定)の 4 ステップ。「見て、決めて、試して、計画する」というシンプルな流れに沿って、半日で業務変革の第一周目を回します。

これだー.webp


3.ワークショップの内容

今回のワークショップは、参加企業ごとに事前に決定した自社業務を題材に、AI BPR を体験していただくことを目的に、座学とハンズオンの2部構成で実施しました。

成果物として、参加企業様には次の2点を持ち帰っていただきました。

  • 実際に動くデータ分析エージェント環境
  • 「明日からどの業務をどの AI エージェントに任せ、そのことで自分たちのどんな強み・価値が強化されるのか」というストーリーがまとまった資料


当日の流れは以下の通りです。各ステップの間で参加者が迷わず進められるよう、環境のデプロイは AI エディタ Kiro との対話で完結する設計とし、事前にデータ取り込みの動作確認を済ませた状態でワークショップを開始しました。

タイムスケジュール表.webp

3-1.第1部:座学パート

ハンズオンに入る前に、AI BPR の考え方と、当日構築する Data Analytics Agent についての座学セッションを実施しました。

3-1-1.AI BPR vs 従来型BPR とその理由

AI BPR セッションの様子
座学セッションはオンラインセミナー形式で行いました。

ぼかし済み.webp

アクセルユニバース株式会社

竹中 涼香


AUC は、AWS Japan が提唱する AI BPR について公式のトレーニングを受け、AI BPR ワークショップを提供しています。

セッションでは、従来型 BPR の3つの失敗理由(問題起点/ヒアリング往復/技術課題扱い)と、それに対して AI BPR がどう答えているかを対比しながらご説明しました。

ここで大切にしているのは、AI BPR を「今日限りのワークショップの方法論」として伝えるのではなく、参加者ご自身が社内で AI 活用を進める際の思考の型として持ち帰っていただくことです。そのため、対比はあえてシンプルな表現に絞り込み、社内で説明する場面を想像していただきながら進めました。

従来型とAIBPR.webp

AWS Japan 様が実施した AI BPR では、すでに複数の大手企業で確かな成果が出ており、顧客満足度は 5 点満点で 4.86、参加者の 81% が「AI エージェントによってビジネスモデルが変わると確信した」と回答しています。今回のワークショップでも、こうした手法を単に説明するのではなく、参加者の皆様ご自身に体験していただくことを大切にしています。

3-1-2.本日の構築対象「Data Analytics Agent(D360)」のご紹介

ハンズオンの題材は、Discovery360(以下、D360)と呼んでいるデータ分析AIエージェントです。「データ分析の依頼を、SQL もダッシュボード操作もなしに、日本語の対話だけで完結させる」ことを目指して開発されました。

たとえば「先月、関東で離反した顧客の特徴を教えて。対策を検討して。」と入力すれば、その場で答えが返ってきます。さらに管理者向けの画面では、どのデータを使うか、どのような目的で使用するか等、自社の業務に合わせて調整することができます。

D360ユーザ画面1 2026-05-26 14.06.01.webp


D360ユーザ画面22026-05-26 14.03.20.webp

構成はシンプルで、Web の画面、AI エージェント、データベースの 3 層で成り立っています。ユーザーは聞きたいことを日本語で入力するだけ。AI が裏でデータを取得・集計し、答えを返してくれます。SQL も、複雑なダッシュボード操作も必要ありません。

Data Analytics Agent の構成

スクリーンショット 2026-06-01 18.54.05.webp

D360 はオープンソースとして公開されており、構築方法は GitHub からご確認いただけます。Discovery360 (D360) - AWS Samples

ワークショップでは、Observe で業務を見て Shift で AI に任せる範囲を決めていく思考の流れを通じて、この D360 をご自身の業務・目的に合った仕様に仕上げていく、という流れで進めました。

3-2.第2部:D360構築とAI BPRハンズオン

後半は、実際に D360 を構築しながら AI BPR の 4 ステップを体験するハンズオンを実施しました。

IMG_8952.webp
IMG_8955.webp
IMG_8956_ぼかし.webp


参加企業ごとに部屋を分け、それぞれのチームで議論を深めながら構築を進めていただきました。

今回のワークショップで大切にしたことは、「自社の環境で、自社のデータで、AI エージェントが応答する」体験を、説明や事例紹介ではなくご自身の手で作っていただくことです。実データをインポートし、AI に質問を送り、精度を調整する、というサイクルを実際に回していただきました。

各社の議論を覗いていると、印象的な変化が起こっていました。最初は「本当にうまく答えてくれるのか」と慎重に質問を送っていた方が、何回かのやりとりの後に「これなら現場で使える」と表情が変わる。ある参加企業では、Shift のステップで「これは AI に任せたいけど、ここは絶対に人が判断したい」という線引きが、メンバー間でその場で言語化されていく場面も見られました。

ハンズオンの最後には、ご自身の業務に AI エージェントを実装するための展開計画を、AI との対話を通じてその場で作成いただきました。


4.参加者の声

ワークショップ終了後、ご参加いただいた 30名の皆様にアンケートをご協力いただきました。

  • ワークショップ全体満足度:90% 以上
  • 進行・フォロー満足度:85%以上
  • 「AI エージェント前提への業務変換は実現可能」:85%
  • 「この内容を経営層に報告したい」:63% 

特に注目したい部分は、最後の結果です。参加者の6割以上が「この内容を経営層に報告したい」と回答されました。自分のチームで試すだけでなく、会社全体で取り組む価値があると感じていただけたことが、このワークショップの一つの成果だと考えています。

アンケートでは以下の声をいただきました。(一部抜粋)

  • 他業務でも転用できるノウハウを伺えた。
  • 根本的な課題がかなりクリアになった。
  • 質問に答えるだけで AI エージェントの作成・動作確認ができるのが新鮮だった。
  • 言語化や可視化のスピードが、人手とは比にならない。
  • AI で業務整理を進める過程そのものがとても参考になった。


「業務の言語化に AI が想像以上に役立つ」「自社の業務にどう AI を組み込むかが具体的にイメージできた」といった感想もあり、AI BPR の考え方が実体験を通して伝わっていたことがうかがえました。

また、「次のステップで必要な支援」としては、自社データを使った検証が 56%、他社事例の共有が 41%、業務部門向けデモ・勉強会が 30% と、当日の気づきを自社の実データへの適用に進めたいというご意向を、多くの方から頂戴しました。

5.組織への展開と本格導入

​​「ワークショップ当日は盛り上がったが、結局その後動かなかった」――AI 活用の取り組みでよく聞く話です。なぜそうなるのか。多くの場合、原因は次のどちらかに偏ってしまうことにあります。

  • 計画偏重:きれいな展開計画はできたが、実装が伴わず絵に描いた餅で終わる
  • 実装偏重:技術的には動くものができたが、業務との接続が弱く現場で使われない

AUCでは、AI BPRの実践とAWS上での構築、どちらも行っています。当日作成された展開計画をお客様の状況に合わせて実装フェーズに接続し、業務での定着まで伴走することができます。今回のD360はもちろん、他テーマでも同様にご支援が可能です。

一例ですが、3か月程度の PoC を通して、

  • 自社データでの検証
  • 活用シナリオの検討
  • 業務への定着化

を進めることも可能です。

AI の活用が重要であることは感じていても、

  • どこから始めればよいのか分からない
  • 何を作ればよいのか分からない
  • 経営層や現場を巻き込んでどう進めればいいか分からない

という方も多いと思います。

そうした段階でも、課題整理から一緒に検討させていただきます。本記事をきっかけに、AI BPR や D360 にご関心をお持ちいただけましたら、ぜひお気軽にご相談ください。

AUCへのお問い合わせ

目次


結局、何が違うのか?

こんにちは、アクセルユニバース株式会社、データサイエンティストの世古大樹です。

クラウド環境にデータウェアハウス(DWH)を構築しようとする場合、まず候補に上がるのはAWSの Redshift と、Google Cloud(旧GCP)の BigQuery でしょう。Redshiftが選ばれるのは、主に組織がすでにAWS上にシステムを構築しており、DWHも同じAWS上に作りたいからだと思います。AWSのシェアは国内トップであり(出典1)、「クラウドサービスならAWS」と考える企業も多いです。

一方、データ分析基盤に詳しい人は、Google Cloud のBigQueryを好みがちかもしれません。AWSがクラウドサービス全体をリードしているのに対し、Google Cloud はデータ分析や機械学習の領域に特別な強みがあると知られています。2019年に「DWHにBigQueryを採用しないことは大罪である」と主張するブログ記事(出典2)が公開され、このような意見は今も根強く残っています。

とはいえ、この記事の時からは月日が流れ、データウェアハウスを取り巻く環境は大きく変わりました。上のブログでRedshiftがBigQueryより劣っているとされた理由は「自前のクラスタ管理が必要など、フルマネージドでない」「スケールしない」という運用性の面でしたが、2022年に Redshift Serverless がリリースされ、BigQuery同様のフルマネージド・サーバーレスサービスとして使えるようになりました(今でもBigQueryの長所に「サーバーレス」を挙げる記事は多いですが、Redshiftもとっくにサーバーレスなのです)。

今は、単純に「RedshiftよりBigQueryの方が良い」と言えない状況です。両者の差は縮まっており、ほとんど同じ物のように見えます。RedshiftとBigQueryで共通している主な機能を、筆者が調査できた限りで表にしてみます。1つ1つ読む必要はありませんが、「これほど多くの機能が共通しているならどちらでもいいのでは?」と感じられるのではないでしょうか。

機能ジャンル 機能 備考
処理最適化 自動スケーリング
処理最適化 ストアドプロシージャ
処理最適化 クエリ結果のキャッシュ
処理最適化 マテリアライズドビュー
運用効率化 クエリのスケジューリング
運用効率化 ウェブベースのクエリエディタ Redshift:Redshift クエリエディタ v2
BigQuery:Google Cloud コンソール、BigQuery data canvas
運用効率化 クエリプランの取得 実行中のクエリを細分化したステージや進捗状況を表示する機能
SQLのEXPLAINステートメント
運用効率化 クエリの待ち行列(キュー) Redshift:WLM
BigQuery:クエリキュー
Redshiftはショートクエリを自動で判断して
待ち行列をスキップさせる機能もある。(後述)
運用効率化 手動のテーブル設計・
自動のテーブル最適化
拡張性 フェデレーテッドクエリ・
外部テーブル
DWH外部のストレージや関係データベースにクエリを
かける機能と、そのクエリに必要な便宜上のテーブル
拡張性 半構造化データへのクエリ Redshift:SUPERデータ型
BigQuery:JSONデータ型
拡張性 ユーザー定義関数(UDF) Lambda/Cloud Function を呼び出せるのも同じ。
拡張性 ほぼ全てのBIツールとの円滑な統合
拡張性 SQLでの機械学習モデル構築 Redshift:Redshift ML
BigQuery:BigQuery ML
セキュリティ 保存時暗号化 Redshift:KMSキーでの暗号化(オプション)
BigQuery:Google管理のAESキーでの暗号化(デフォルト)
レジリエンス デフォルトの地理的分散 Redshift:複数AZへの分散
BigQuery:複数ゾーンへの分散
レジリエンス 自動バックアップ Redshift:30分毎の復旧ポイントを0〜35日の設定期間にわたり保持
BigQuery:全時点のタイムトラベルを7日間保持
レジリエンス リージョンレベルの災害復旧 Redshift:スナップショット(手動バックアップ)と
復旧ポイント(自動バックアップ)のクロスリージョンレプリケーション
BigQuery:クロスリージョン・データセットレプリケーション
(プレビュー中の機能)

実際のところ、「どちらを使ってもあまり変わらない」というのも一つの見識だと思います。自社や担当者が詳しい方のクラウドを選択するのも手、です。

しかし、より詳しく見ていくと、両者には明確な違いもあることがわかります。このブログでは、それを 「運用性ならRedshift、コスパならBigQuery」 という視点に整理し、2回に分けて説明します。1回目の今回はRedshiftの運用性を解説します。読者の方が適切なクラウドサービスを選べる助けになれば幸いです。


Redshiftの運用性

Redshiftの運用性が高いと言える根拠は、「1. AWS上の対向システムとの統合容易性」「2. 機械学習ベースの管理タスク自動化」「3. Glue/Lake Formation による一元的なメタデータ・アクセス権限管理」の3つです。


1. AWS上の対向システムとの統合容易性

前述した通り、AWSは国内シェアが高く、クラウドサービス業界を牽引する存在です。そのため多くの組織のITシステムはすでにAWS上に構築されており、オンプレミスでホストされていても、AWSへの移行が検討される場合が多いです。DWHには、これらのITシステムからデータを受け取るための連携が必要なので、DWHも同じAWS上に立てると簡単になります。構成としては、データソース・データレイク・データウェアハウス・データマートをはっきり分けて構築する、以下のようなものが考えられます(あくまで一例です)。

スライド1.jpeg (筆者作成)

Glueが複数種類のデータソースを横断して、簡単にデータ収集・データレイク構築をしてくれるのが見て取れると思います。一方、AWSから Google Cloud へのデータ連携は複雑です。一般的なマルチクラウド構成の複雑さ(クラウド相互の認証認可、学習コスト、運用体制等)はもちろんのこと、RDSから Google Cloud へのデータ転送など、構築自体が大きく複雑になります。

AWS上のシステムと容易に統合できるRedshiftには、 運用簡素性・開発工数・自由度 の面でメリットがあるのです。


2. 機械学習ベースの管理タスク自動化

先ほどRedshiftとBigQueryで共通している機能の表を載せましたが、どちらかにしかない機能もあります。それらの機能の一つ一つは細々としたものですが、「機械学習ベースの管理タスク自動化」とまとめると、塵も積もれば山となる、Redshiftの強みと言って差し支えないものになります。

このことをより具体的に言うと、Redshiftは、 利用しているだけでデータやクエリの状況を自動的に判定し、DWHを最適化してくれる のです。他方のBigQueryは自動最適化の機能が限定的で、BIツールによるクエリを最適化する BI Engine がある程度です。基本的には、モニタリングを行って、その結果を判定し、手動でメンテナンスを行わなければなりません(出典3参照)。Redshiftであれば、機械学習などによって監視・判定・メンテナンスを自動化し、放っておくだけで処理を効率的にしてくれる様々な機能が提供されています。詳しく見ていきましょう。

バラしてしまうと、この節の元ネタは Black Belt(出典4) の、主に pp.20-33 です。この Black Belt 資料自体、Redshiftを理解するうえで必読のものなのでぜひ読んでいただきたいのですが、それはそれとして、この記事では Black Belt をもとにRedshift独自の(=BigQueryにはない)管理機能をまとめ、筆者の観点から補足を行います。


Redshift独自の自動管理機能のまとめ

  • カラム(列データ)の最適化
    • ゾーンマップとソートキー:カラムをあらかじめソートし、ディスクの物理ブロックごとに最小値と最大値を記憶しておくことで、不要なブロックを読み飛ばして処理する。
    • 圧縮エンコードの自動管理:カラムごとに最適なエンコード方式を自動で判定し、圧縮する。(手動選択も可能)
  • テーブルの最適化
    • 自動テーブル最適化:利用状況を判定し、ソートキーや分散キーを必要に応じて自動設定する。(手動設定も可能)
    • Vacuum Delete 自動化:Delete・Update 後の削除領域の開放を、適切なタイミングで自動実行する。(手動実行も可能)
    • ソート自動化:テーブル全体のソートは負荷が高くなるが、機械学習によってソートが必要な部分だけをピンポイントに、適切なタイミングでソートすることで、パフォーマンスやアクセスに影響なくソートできる。(手動ソートも可能)
  • ネットワークの最適化
    • データを均等分散して並列処理に最適化するだけでなく、JOINするレコード同士を同じ場所に置くことで、I/Oを削減できる。
  • クエリ実行の最適化
    • Analyze 自動化:テーブルの統計情報を適切なタイミングで自動更新することで、最適な実行計画のもとにクエリを行える。
    • Auto WLM:ワークロードごとに、物理リソース(メモリ・CPU等)の割当を自動で最適化する。
    • ショートクエリアクセラレーション:クエリの実行時間を予測し、短時間と判断されたクエリは、特別な領域でスムーズに処理される。(オフにもできる)


「ゾーンマップとソートキー」「自動テーブル最適化」について

たとえばテーブルの列 date: DATE がちゃんと日付順に並んでいないと、普通は WHERE date >= '2024-01-01' のようなWHERE句を含むクエリに対してフルスキャンを行う必要があり、応答時間や課金額が無駄に大きくなってしまいます。そこで列をいくつかの塊(1MBのブロックごと)にまとめ、塊の中の最大値と最小値をメモリに保存しておけば、最大値が 2024-01-01 未満である部分を読み飛ばして処理することができ、効率が上がります。これがゾーンマップです。

しかし、これだけではまだ無駄があります。読み込むブロックに含まれる全てのデータが 2024-01-01 以上とは限らないからです。例えば100件中1件のみが 2024-01-01 以上で、99件のスキャンは無駄だった、ということもありえます。そのため、さらに列のデータを降順にソートしておけば、少なくとも最大値が 2024-01-01 以上である最後のブロックの1つ前のブロックまでは全て 2024-01-01 以上ということになるので、効率的な処理が可能です。このようにソートする列を指定するものがソートキーで、適切なソートキーを自動設定してくれるのが自動テーブル最適化です。

スライド2.jpeg (筆者作成)

これらの機能は検索の効率を上げるという点で、インデックスに似ています。BigQueryにはインデックスがあり、一度作成したインデックスを自動管理してくれる機能もあります。しかし、最初にインデックスを作るには手動のDDLコマンドが必要であり、適切なインデックスを作るためにクエリパターンの監視・解析も必要です。管理の簡素さという点で、Redshiftに軍配が上がるところです。


3. Glue/Lake Formation による一元的なメタデータ・アクセス権限管理

これは厳密にはDWHの機能ではありませんが、AWSで多くの場合Redshiftと併用されるサービスのメリットとして挙げます。

「メタデータ」とは何かというと、データレイクやDWHに保存されているデータ(実データ)を説明する情報です。具体的には、テーブル・列・データ型・データソース・データ作成日などの説明です。そしてメタデータは「データカタログ」というデータベースに保管されます。データカタログを調べてメタデータを見れば、どのデータが「使える」データなのか、どの列が何を意味しているのかなどがわかります。DWHに蓄積される膨大なデータを活用するためには、「どこにどんなデータがあるのか」がわかることが重要です(わからないと使えるデータも埋もれてしまいます)。

データに対するアクセス権限管理についても、メタデータが重要な役割を果たします。たとえば、メタデータに「センシティブデータであるかどうか」という列を加え、それを利用してセンシティブデータを閲覧できるユーザーを制限する、というような運用が可能になります。また、(あまり無いことですが)機密情報に関わるメタデータ自体へのアクセスを制限することも考えられます。

AWSではETLとメタデータ・アクセス権限管理が一体化している ため、このあたりの管理が大きく省力化されます。先ほどの構成例をもう一度見てみましょう。

スライド1.jpeg

まず、下の Glue Crawler がデータソースを探索し、メタデータを作って Glue Data Catalog に保管します。保管されたメタデータの使い道は2つあります。1つはGlueのJob(ETLタスク)において、取得対象のデータを特定するために使われます。もう1つは Lake Formation で、テーブルの列単位などのきめ細かい粒度でのアクセス権限を設定するために使われます。Lake Formation のアクセス権限管理はデータレイクだけでなく、データマートにも及びます。Redshiftデータ共有と統合することで、コンシューマーRedshiftのアクセス権限を Lake Formation で管理することができるのです。

要するに、AWSではETLとメタデータがGlueという1つのサービスに合体しており、さらにGlueが統合的なアクセス権限管理を行う Lake Formation と高度に接続されていることで、データの管理負担が抑えられる、ということです。これは大きな特長です。

Google Cloud ではこうは行きません。ETLサービスであるDataflowにはメタデータ管理などの機能がなく、Data Catalog というサービスで別途データカタログを作成する必要があります。また、アクセス権限管理も Data Catalog、Cloud Storage(データレイク)、BigQuery(DWH)で個別に行わなければなりません(かなり大変そうじゃありませんか?)。特に Cloud Storage できめ細かいアクセス権限管理を行うことは、煩雑なため公式に非推奨とされています。Cloud Storage のきめ細かいアクセス権限管理はACL(Amazon S3 のACLと同様のものであり、現在はAWSでも Google Cloud でも非推奨とされている方式)で行う必要があるためです。


きめ細かいアクセスでは、2 つの異なるアクセス制御システム間で調整が必要になります。このため、意図しないデータ漏洩が発生する可能性が高くなり、リソースにアクセスできるユーザーの監査は複雑になります。特に、個人を特定できる情報などの機密データを含むオブジェクトが存在する場合は、均一なバケットレベルのアクセスを有効にしてバケットにデータを保存することをおすすめします。
(出典5)


......と言われても、一部のユーザーから隠さなければならない機密データを含むバケットで均一なアクセスを有効にするとなると、

  1. 機密データを見てはいけないユーザーがそのバケット(=データレイク)全体を見られなくなるか、
  2. 機密データを保管するバケットをデータレイクから分割し、そのバケットにアクセス制限をかけるか


しかありません。1だとそのユーザーのデータ利用が制約されてしまいますし、2だと「すべての生データを1箇所に集約する」というデータレイクのコンセプトそのものに反してしまいます。Google Cloud ではデータレイクのアクセス権限管理に課題があると言わざるを得ないでしょう。


まとめと次回予告

今回の記事では、BigQueryと比較したときのRedshiftの運用性について解説しました。ポイントをまとめておきます。


  1. RedshiftもBigQueryも、多くの機能が共通しているが、詳しく見ると「運用性ならRedshift、コスパならBigQuery」という違いがある。
  2. Redshift中心の構成では、AWS上の対向システムからデータ収集を行うのが簡単。
  3. Redshiftは、自動管理機能によって設定・監視・メンテナンスの負担が小さくなる。
  4. AWSでは、ETL・メタデータ・きめ細かいアクセス権限管理が Glue/Lake Formation に一元化されており、効率的な運用が可能。


次回は、実際にクエリを投げてかかった費用と応答時間を比較しながら、「BigQueryのコスパ」について解説します。「BigQueryはコストパフォーマンスに優れている」とはよく言われますが、必ずしもRedshiftより安くて速い訳ではありません。それでもBigQueryにあるコスパ上の利点とは何でしょうか?(次回記事はこちら


出典(いずれも記事公開時点閲覧)

  1. 総務省『情報通信白書令和6年度版 データ集』「第4章第8節 7. PaaS/IaaS利用者のAWS、Azure、GCP利用率」 https://www.soumu.go.jp/johotsusintokei/whitepaper/ja/r06/html/datashu.html#f00281
  2. データエンジニアの酩酊日記『近年のデータ分析基盤構築における失敗はBigQueryを採用しなかったことに全て起因している』 https://uma66.hateblo.jp/entry/2019/10/17/012049
  3. Google Cloud『BigQuery 管理の概要』 https://cloud.google.com/bigquery/docs/admin-intro?hl=ja
  4. Amazon Web Services『Amazon Redshift 運用管理 AWS Black Belt Online Seminar』 https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt2023Amazon-Redshift-Management0331v1.pdf
  5. Google Cloud『アクセス制御の概要』 https://cloud.google.com/storage/docs/access-control?hl=ja




X(旧Twitter)・Facebookで定期的に情報発信しています!

目次


「費用対効果」で選ぶには?

こんにちは。アクセルユニバース株式会社、データサイエンティストの世古大樹です。

今回は、以前のデータウェアハウス比較記事の続編で、BigQuery と Redshift Serverless のコストパフォーマンスを比較する実験を行いました。

詳しくは後述しますが、同じデータセットに対して同じクエリを実行し、かかった料金(コスト)と応答時間(パフォーマンス)を計測しています。

データセットは、80GiB程度の構造化データと360GiB程度の半構造化データの2種類を利用します。それぞれのデータセットに対し、性質の異なる3つのクエリ(合計6つのクエリ)を書きました。クエリ1つにつき5回実行して、特有のオーバーヘッドがある1回目を除外し、2〜5回目の計測結果の平均値を取っています。

「BigQueryの方が安価で性能が良い」とよく言われているようですが、実際に比較した結果を公開しているものは見当たりません。本記事を読めば、コストとパフォーマンスに根拠を持ってデータウェアハウスの選定ができるようになります。


結果:BigQueryは「安定」している

実験の詳細は後回しにして、いきなり結果から発表したいと思います。詳細は .xlsx ファイル(こちらからダウンロード)でご確認いただけますが、ここでは概要の解説を行います。

まず、「コスト」にあたる平均課金額は以下のようになりました。(データセットやクエリの内容は後述します)

スクリーンショット 2024-10-01 15.49.13のコピー3.png

「差」「比」はともに「BigQuery 容量コンピューティング」と「Redshift Serverless」を比較したもので、BigQueryが有利な場合は青色、Redshiftが有利な場合は赤色で表現しています(以降の表も同様)。

「比」を見ると、思いのほか傾向が一定していないのが驚きです。BigQueryの方が安いクエリも、Redshiftの方が安いクエリもあります。クエリ1-1はBigQueryの方が約13倍高い一方、2-2はRedshiftの方が15倍高くなり、ばらつきが大きいようです。

次に、「パフォーマンス」にあたる平均応答時間は以下のようになりました。

スクリーンショット 2024-10-01 15.49.13のコピー2.png

BigQueryの応答時間は比較的安定している(425〜27,212ms、約64倍)のに対し、Redshiftはかなり開きがある(68〜158,411ms、約2300倍)のが目を惹きます。RedshiftのRPU(仮想的な演算リソース)は、BigQueryと近い応答時間になるように設定したのですが、実際に比を1付近にできたのは1-2と2-3だけです。1-1と2-2ではRPUを最低にしてもBigQueryより大幅に速くなり、逆に1-3と2-1ではRPUを非常に大きくしてもあまり速度が上がらず、BigQueryには遠く及びませんでした。

「BigQueryの方がパフォーマンスが高い」とよく言われていますが、むしろ正確には、「BigQueryの方がパフォーマンスが安定している(Redshift Serverless ほど極端に長い応答時間を必要としない)」傾向にありそうだ、ということが読み取れます。

また、平均課金額の表で垣間見えていた、各DWHにとって「どのクエリが得意/不得意か」の傾向は、応答時間にも共通しており、課金額の比が大きい順に平均応答時間の比も大きくなりました。つまり、どちらのDWHにも、「得意なクエリは安いし速い」「苦手なクエリは高いし遅い」という相対的な傾向があるようです。

そこで、各クエリがそれぞれのDWHにとってどの程度「得意」かを表す指標を作ります。平均応答時間の逆数を、クエリに対するDWHの「パフォーマンス」、平均課金額を「コスト」と捉え、パフォーマンスをコストで割ります。この「コストパフォーマンス指数」が高いほど、そのDWHがそのクエリを「得意」としていることを示します。

スクリーンショット 2024-10-01 15.49.13のコピー.png

まず行ごとに「比」を見ると、BigQueryがRedshiftの何倍のコストパフォーマンスを示したかがわかります。全体的にはBigQueryが上回っており、特に2-1では210倍もの差になっていますが、6個中2つのクエリではRedshiftが優勢で、1-1では83倍の差です。必ずBigQueryの方が優れているとは言い切れず、「どちらが良いかは場合による」ことになります。

実験した限りでは、Redshiftが上回った場合の共通点は、「RPUが最低の8でも非常に速いこと(数秒以内)」でした。「CTE(サブクエリ)があるとRedshiftが不利になる傾向にある」ということも言えそうですが、サブクエリはメモリサイズをボトルネックとする処理なので、「メモリサイズと比例するRPUの数を増やしても、サブクエリを含むクエリがあまり速くならないこと」の説明がつきません。事前には、BigQueryではインデックスを張れるのに対しRedshiftではソートキーの指定しか行えないため、JOINが含まれるとBigQueryが大幅に速くなると予想していたのですが、そういった傾向もありませんでした。

困ったことに、BigQueryの測定から得られる情報(スロット数・応答時間・処理データ容量)からは、Redshiftの方が良いのかどうか予想できません。言えるのは、「Redshift Serverless を使っている場合、RPU数が小さくなければBigQueryに移行した方が良くなる見込みが大きい」ということです。

次に列ごとに(縦に)表を見ると、こちらははっきりした傾向が読み取れます。値が大きいほどそのDWHにとって簡単な処理で、逆に小さいほど難しい処理だということになり、最大値と最小値の幅が狭いほど、常に安定したコストパフォーマンスを発揮できることになります。BigQueryの方が、平均課金額・平均応答時間ともに幅の開きが小さかったため、コストパフォーマンス指数も安定しました。BigQueryは 0.00012〜2.4 の20,000倍、Redshiftは 0.0000023〜200 の約90,000,000倍です。「BigQueryのコストパフォーマンスは、Redshift Serverless に比べて、(優れているというより)安定している」というのが、この実験における最も重要な結論です。

平均前の測定値、RPU数、課金額の計算方法などの詳細、および注意事項などは、 .xlsx ファイル(こちらからダウンロード)をご覧ください。


実験条件

以下では、どのような条件の下に実験を行ったのかを、「データセット」「データウェアハウス設定・計測条件」「クエリ」に分けて述べます。ただし、実際に利用したクエリ文等は非常に長いので、末尾の「コード集」にまとめて掲載しました。ここでは日本語ベースで、概要や意図だけを説明します。


データセット

利用したデータセットの1つ目は、Chicago Data Portal の Taxi Trips (2013-2023) です。シカゴ市が公開している、市内のタクシーの賃走についてのデータセットです。11年分の運行について、タクシーID、開始・終了時刻、走行距離、運賃などが記録されています。CSV形式で23列×約2億行、解凍時サイズは約80GiBの、単一テーブルのデータです。表では「小規模構造化データ」と呼んでいます。

2つ目は、Hugging Face にある McAuley-Lab の Amazon-Reviews-2023 です。1996年から2023年までに amazon.com へ投稿された商品レビューが収集されています。レビューデータの集まりである User Reviews、レビューされる商品の情報である Item Metadata の2種類のデータがあり、さらにそれぞれ商品カテゴリでデータセットを分割されています。実験では、商品カテゴリによって分割されたデータを1つにまとめ、種類に応じて2つのテーブルとしました。いずれもJSON形式で、User Reviews は約260GiB、Item Metadata は約90GiB、計360GiBほどです。表では「中規模半構造化データ」と呼んでいます。

いずれも GCS/S3 バケットに保存し、そこから BigQuery/Redshift へロードしました。


データウェアハウス設定・計測条件

今回の実験では、「実際にDWHの利用を開始した時のパフォーマンスを再現すること」を目指し、現実の利用開始時に近い設定としました。

パフォーマンス最適化の基本としては、BigQueryではインデックスの作成、Redshiftではソートキーの指定ができます。DWHの利用が進んでいけば、メトリクスに基づいて最適なインデックスを構築したり、ソートキーが自動的に最適化されたりするところですが、普通、利用開始時にはそこまで行わないと思います。とはいえ、最適化を何も行わないのもリアリティがありません。そこで今回は、「インデックスは主キーにのみ張り、ソートキーにはタイムスタンプ列を指定する」というシンプルな設定にしました。また、RedshiftのVACUUMは、各データベースでデータロード完了後の1度のみ実施しました。

計測に際しては、信頼度の高いデータとするため、リザルトキャッシュの取得を無効化した上で5回連続で同じクエリを行い、2〜5回目の計測結果の平均値を取りました。1回目の結果を除くのは、Redshiftで初回のみコードコンパイルが行われるなど、例外的なオーバーヘッドが発生しうるからです。

リージョン(ロケーション)については、AWS・Google Cloud ともに全て東京単一リージョンで実行し、料金も東京の金額で計算しました。

BigQueryの課金モデルは、予約したスロット(仮想的な演算リソース)の利用量に基づく「容量コンピューティング」と、スキャンしたデータ量に基づく「オンデマンド」の2種類があり、さらに「容量コンピューティング」には3つのエディションがあります。BigQueryの利用を開始する場合、まず「オンデマンド」で自社の利用量等を把握し、「容量コンピューティング」の予約に移行することが想定されていますが、初めから「容量コンピューティング」を使うことも十分あり得ます。そこで今回は、「オンデマンド」を利用してその課金額と処理データ量を記載しつつ、「容量コンピューティング・Standardエディション」の場合のスロット数と課金額も計算し、記録しました(こちらの数値を元に、他のエディションの金額も計算可能です)。

Redshift Serverless の課金額は、RPU(仮想的な演算リソース)の利用量に基づきます。このRPUのベース値は8〜512の間で指定することができ、大きくすると応答時間は短くなりますが、ある程度を超えると短縮効果がコストの増加に見合わなくなります。今回は、原則的にBigQueryの応答時間と近くなるように設定しつつも、コストに見合わないほど大きな値にはしないようにしました。例えばクエリ1-3の応答時間は、RPUが128のとき約160秒でしたが、最大の512に設定しても約100秒と、40%の時短に対して2.5倍のコストになります(コストパフォーマンス指数は0.64倍)。BigQueryは約20秒でしたが、それにできるだけ近づけるのではなく、現実的なラインと考えられた128RPUに設定しました。

なお、最適なRPU数を探索するためのクエリは、測定に影響を及ぼさないように、別のデータベースで行いました。また、Redshift Serverless には1分間の最低課金時間があり、1分未満のクエリであっても1分間分の料金が発生します。しかし実際の利用においては、1分未満のクエリも、他のクエリと同時に実行することで合計1分以上にできると考えられるため、測定されたままのミリ秒数に基づいて料金を計算しました。

以上の料金は、演算(クエリ)に対する費用であり、実際にはストレージ(データ保存)に対する費用も発生しますが、そちらは(通常の利用では)演算に対する費用に比べてかなり安価になることと、デモを行わなくても簡単に見積が取れることから、計測を行いませんでした。また、本記事に掲載されている料金についての情報は公開時点のものであることにご注意ください。料金の詳細について、BigQueryはこちら、Redshiftはこちらをご覧ください。

計測結果の取得には、BigQueryでは後に掲載するクエリを用いました。Redshiftでは、クエリエディタv2のリザルトに表示される Elapsed time で応答時間を計測し、応答時間にRPU数とRPU時間あたりの料金を乗じて課金額を計算しました。RPU数が指定したベースRPU数の通りであることはコンソール上で確認しました。


クエリ

DWHパフォーマンスのボトルネックとして代表的な処理は、ディスクI/OとCPUに負荷のかかるソート(結合・ORDER BY句・ウィンドウ関数)と、メインメモリを消費するサブクエリです。そのため、DWHの主なワークロードである集計(集約関数)を全クエリに含めながら、これらの句や関数の様々な組み合わせを試しました。

できるだけ標準SQLに準拠するようにしました。方言の違いにより、BigQueryとRedshiftで全く同じクエリ文にはなりませんでしたが、同じ処理を実現するものになっています。

また、クエリは集計・分析として意味を持つように作成し、コード集にはその説明も付けておきました。


コード集

前処理

Amazon-Reviews-2023 の Item Metadata は、そのままだと BigQuery の仕様でエラーが出るので、以下のPythonスクリプトで前処理しました。Redshiftの場合は前処理なしで読み込めますが、同一条件で比較するために、同じく前処理したデータをロードしています。



import json
import re

def clean_columns(obj):
    if isinstance(obj, dict):
        keys = list(obj.keys())
        for key in keys:

            # テーブル定義に合わないデータ型を修正する。
            if key == 'rating_number' and not isinstance(obj[key], int):
                del obj[key]
            elif key in ('average_rating', 'price') and not isinstance(obj[key], float):
                del obj[key]
            elif key == 'details' and not isinstance(obj[key], dict):
                del obj[key]
            elif key in ('videos', 'images', 'features', 'description', 'categories', 'bought_together') and not isinstance(obj[key], list):
                obj[key] = []
            elif key == 'images':
                images = obj[key]
                for image in images:
                    for image_size in list(image):
                        if not image_size in ('thumb', 'large', 'variant', 'hi_res'):
                            del image[image_size]
                            print(str(image_size) + 'を削除しました。')

            # BigQueryの仕様に合わせたキーの前処理:使用できない特殊文字を消去し、大文字・小文字の違いを無視する。衝突が発生するか空文字列になる場合は削除する。
            cleaned_key = re.sub(r'[!"$()*,./;?@[\]^`{}~]', '', key.lower())
            if key != cleaned_key or cleaned_key == '':
                if cleaned_key in obj or cleaned_key == '':
                    del obj[key]
                else:
                    obj[cleaned_key] = obj.pop(key)

        for value in obj.values():  # ネストされたオブジェクトに対して再帰的に処理を行う。
            clean_columns(value)
    return obj

def main():
    with open('item_metadata_before_cleaned.jsonl', 'r') as input, open('item_metadata.jsonl', 'w') as output:
        for line in input:
            obj = json.loads(line)
            obj = clean_columns(obj)
            cleaned_line = json.dumps(obj)
            output.write(cleaned_line + '\n')

if __name__ == '__main__':
    main()


データロード等

Taxi Trips (2013-2023), BigQuery

このデータセットのタイムスタンプ形式はBigQueryにサポートされていないので、一度文字列型で読み込んでから変換しています。 また、各データセット共通ですが、BigQueryでは4GB以上のファイルを圧縮形式でロードできないので、解凍したデータを読み込んでいます(Redshiftでは圧縮したまま利用しています)。



CREATE SCHEMA taxi_dataset 
  OPTIONS(
      location='asia-northeast1',
      storage_billing_model='LOGICAL'
  );

CREATE TABLE taxi_dataset.TaxiTrip (
    trip_id STRING,
    taxi_id STRING,
    trip_start_timestamp STRING,
    trip_end_timestamp STRING,
    trip_seconds INT,
    trip_miles FLOAT64,
    pickup_census_tract STRING,
    dropoff_census_tract STRING,
    pickup_community_area INT,
    dropoff_community_area INT,
    fare FLOAT64,
    tips FLOAT64,
    tolls FLOAT64,
    extras FLOAT64,
    trip_total FLOAT64,
    payment_type STRING,
    company STRING,
    pickup_centroid_latitude FLOAT64,
    pickup_centroid_longitude FLOAT64,
    pickup_centroid_location GEOGRAPHY,
    dropoff_centroid_latitude FLOAT64,
    dropoff_centroid_longitude FLOAT64,
    dropoff_centroid_location GEOGRAPHY,
    PRIMARY KEY(trip_id) NOT ENFORCED
);

LOAD DATA INTO taxi_dataset.TaxiTrip
FROM FILES (
    allow_jagged_rows=true,
    field_delimiter=',',
    format='CSV',
    null_marker='',
    skip_leading_rows=1,
    uris=['gs://my-bucket-name/TaxiTrip.csv']
);

ALTER TABLE taxi_dataset.TaxiTrip
    ADD COLUMN new_trip_start_timestamp TIMESTAMP,
    ADD COLUMN new_trip_end_timestamp TIMESTAMP;

UPDATE taxi_dataset.TaxiTrip
    SET
        new_trip_start_timestamp = PARSE_TIMESTAMP('%m/%d/%Y %I:%M:%S %p', trip_start_timestamp),
        new_trip_end_timestamp = PARSE_TIMESTAMP('%m/%d/%Y %I:%M:%S %p', trip_end_timestamp)
    WHERE true;

ALTER TABLE taxi_dataset.TaxiTrip
    DROP COLUMN trip_start_timestamp,
    DROP COLUMN trip_end_timestamp;

ALTER TABLE taxi_dataset.TaxiTrip
    RENAME COLUMN new_trip_start_timestamp TO trip_start_timestamp,
    RENAME COLUMN new_trip_end_timestamp TO trip_end_timestamp;

CREATE SEARCH INDEX tt_pk_index    -- 主キーにインデックスを張る。
    ON taxi_dataset.TaxiTrip(trip_id);


Taxi Trips (2013-2023), Redshift Serverless



CREATE DATABASE taxi_database;

CREATE TABLE IF NOT EXISTS
    taxi_database.public.TaxiTrip (
        trip_id CHAR(40),
        taxi_id CHAR(128),
        trip_start_timestamp TIMESTAMP,
        trip_end_timestamp TIMESTAMP,
        trip_seconds INT,
        trip_miles FLOAT,
        pickup_census_tract VARCHAR(255),
        dropoff_census_tract VARCHAR(255),
        pickup_community_area INT,
        dropoff_community_area INT,
        fare FLOAT,
        tips FLOAT,
        tolls FLOAT,
        extras FLOAT,
        trip_total FLOAT,
        payment_type VARCHAR(64),
        company VARCHAR(255),
        pickup_centroid_latitude FLOAT,
        pickup_centroid_longitude FLOAT,
        pickup_centroid_location GEOMETRY,
        dropoff_centroid_latitude FLOAT,
        dropoff_centroid_longitude FLOAT,
        dropoff_centroid_location GEOMETRY,
        PRIMARY KEY(trip_id)
    )
    BACKUP NO
    COMPOUND SORTKEY(trip_start_timestamp, trip_end_timestamp);    -- タイムスタンプ列をソートキーにする。

COPY taxi_database.public.TaxiTrip (
        trip_id, taxi_id, trip_start_timestamp, trip_end_timestamp, trip_seconds, trip_miles, 
        pickup_census_tract, dropoff_census_tract, pickup_community_area, dropoff_community_area, 
        fare, tips, tolls, extras, trip_total, payment_type, company, 
        pickup_centroid_latitude, pickup_centroid_longitude, pickup_centroid_location, 
        dropoff_centroid_latitude, dropoff_centroid_longitude, dropoff_centroid_location
    )
FROM 's3://my-bucket-name/TaxiTrip.csv.gz'
IAM_ROLE 'my-role-arn'
REGION 'ap-northeast-1'
CSV
DELIMITER ','
NULL AS ''
IGNOREHEADER 1
TIMEFORMAT 'MM/DD/YYYY HH:MI:SS AM'
GZIP;

VACUUM;


Amazon-Reviews-2023, BigQuery



CREATE SCHEMA amazon_reviews_dataset 
    OPTIONS(
        location='asia-northeast1',
        storage_billing_model='LOGICAL'
    );

CREATE TABLE amazon_reviews_dataset.ItemMetadata (
    main_category STRING
    , title STRING
    , average_rating FLOAT64
    , rating_number INT
    , features ARRAY
    , description ARRAY
    , price FLOAT64
    , images ARRAY>
    , videos ARRAY> 
    , store STRING
    , categories ARRAY
    , details JSON
    , parent_asin STRING 
    , bought_together ARRAY
    , PRIMARY KEY(parent_asin) NOT ENFORCED
);

/*
ItemMetadataのみ、スキーマ自動検出でエラーが出るので、スキーマ自動検出をオフにするため、SQLではなく以下のbqコマンドでロードする。

$ bq --location=asia-northeast1 load \
--noautodetect --source_format=NEWLINE_DELIMITED_JSON \
amazon_reviews_dataset.ItemMetadata \
gs://my-bucket-name/item_metadata.jsonl
*/

CREATE SEARCH INDEX im_pk_index    -- 主キーにインデックスを張る。
    ON amazon_reviews_dataset.ItemMetadata(parent_asin);

CREATE TABLE amazon_reviews_dataset.UserReviews (
    rating FLOAT64
    , title STRING
    , text STRING 
    , images ARRAY>
    , asin STRING
    , parent_asin STRING 
    , user_id STRING
    , timestamp INT -- UNIX時刻。
    , verified_purchase BOOLEAN
    , helpful_vote INT
    , PRIMARY KEY(parent_asin, user_id) NOT ENFORCED
    , FOREIGN KEY(parent_asin) REFERENCES amazon_reviews_dataset.ItemMetadata(parent_asin) NOT ENFORCED
);

LOAD DATA INTO amazon_reviews_dataset.UserReviews
FROM FILES (
    format='NEWLINE_DELIMITED_JSON',
    ignore_unknown_values=true,
    uris=['gs://my-bucket-name/user_reviews.jsonl']
);

CREATE SEARCH INDEX ur_pk_index    -- 主キーにインデックスを張る。
    ON amazon_reviews_dataset.UserReviews(parent_asin, user_id);


Amazon-Reviews-2023, Redshift Serverless

Item Metadata の description は本来SUPER型ですが、サイズが大き過ぎてCOPY時にエラーが出るデータがいくつもあります。サイズの限度で打ち切るため、COPY時に TRUNCATECOLUMNS オプションを付け、SUPER型は TRUNCATECOLUMNS の対象にならないため VARCHAR(65535) 型で定義しました。この処理を施してもデータサイズはほぼ変化せず、クエリに description を使うこともないため、測定に問題はありません。



CREATE DATABASE amazon_reviews_database;

CREATE TABLE IF NOT EXISTS
    amazon_reviews_database.public.ItemMetadata (
        main_category VARCHAR(64)
        , title VARCHAR(4096)
        , average_rating FLOAT
        , rating_number INT
        , features SUPER
        , description VARCHAR(65535)
        , price VARCHAR(64)
        , images SUPER
        , videos SUPER
        , store VARCHAR(4096)
        , categories SUPER
        , details SUPER
        , parent_asin VARCHAR(16)
        , bought_together SUPER
        , PRIMARY KEY(parent_asin)
    )
    BACKUP NO;

COPY amazon_reviews_database.public.ItemMetadata (
        main_category, title, average_rating, rating_number, features, description, price,
        images, videos, store, categories, details, parent_asin, bought_together    
    )
FROM 's3://my-bucket-name/item_metadata.jsonl.gz'
IAM_ROLE 'my-role-arn'
REGION 'ap-northeast-1'
JSON 'auto'
NULL AS ''
TRUNCATECOLUMNS
GZIP;

CREATE TABLE IF NOT EXISTS 
    amazon_reviews_database.public.UserReviews (
        rating FLOAT
        , title VARCHAR(4096)
        , text VARCHAR(65535)
        , images SUPER
        , asin VARCHAR(16)
        , parent_asin VARCHAR(16)
        , user_id VARCHAR(128)
        , timestamp BIGINT
        , verified_purchase BOOLEAN
        , helpful_vote INT
        , FOREIGN KEY(parent_asin) REFERENCES ItemMetadata(parent_asin)
    )
    BACKUP NO
    COMPOUND SORTKEY(timestamp);

COPY amazon_reviews_database.public.UserReviews (
        rating, title, text, images, asin,
        parent_asin, user_id, timestamp, verified_purchase, helpful_vote 
    )
FROM 's3://my-bucket-name/user_reviews.jsonl.gz'
IAM_ROLE 'my-role-arn'
REGION 'ap-northeast-1'
JSON 'auto'
NULL AS ''
TRUNCATECOLUMNS
GZIP;

VACUUM;


計測記録の取得(BigQuery)

Redshiftではコンソール上で記録を閲覧するため、コードがありません。



SELECT
    job_id  -- クエリの一意ID。
    , TIMESTAMP_DIFF(end_time, start_time, MILLISECOND) AS elapsed_time_milliseconds  -- 応答時間(ミリ秒)。
    , total_slot_ms / TIMESTAMP_DIFF(end_time, start_time, MILLISECOND) AS num_slot  -- 「容量コンピューティング」の場合のスロット数。
    , 0.051 * total_slot_ms / (1000 * 60 * 60) AS cost_capacity  -- 「容量コンピューティング」の場合の課金額(USD)。
    , total_bytes_billed * POW(2, -40) AS total_terabytes_billed  -- スキャンされたデータ容量(TiB)。
    , 7.5 * total_bytes_billed * POW(2, -40) AS cost_on_demand  -- 「オンデマンド」の場合の課金額(USD)。
    , start_time  -- クエリの開始時刻。
    , state  -- クエリの状態(実行中でないか確認するために出力させる)
    , LEFT(query, 50)  -- クエリ文の冒頭50文字。
FROM `my-project-name`.`region-asia-northeast1`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
ORDER BY start_time DESC;


リザルトキャッシュの無効化(Redshift Serverless)

BigQueryではコンソール上で設定を行うため、コードがありません。



SET enable_result_cache_for_session TO off;  -- 無効化。

SHOW enable_result_cache_for_session;  -- 無効化されていることの確認。


クエリ1-1:小規模構造化データ、単純集計

データセットに含まれる運行の総数と、運賃の平均値を求めます。

BigQuery



SELECT 
    COUNT(*) AS num_trips, 
    AVG(fare) AS avg_fare
FROM taxi_dataset.TaxiTrip;


Redshift



SELECT 
    COUNT(*) AS num_trips, 
    AVG(fare) AS avg_fare
FROM TaxiTrip;


クエリ1-2:小規模構造化データ、集計+ウィンドウ関数

1台のタクシーが1日に乗車された回数のTOP10と、その日付を調べます。


BigQuery



SELECT
    RANK() OVER (ORDER BY COUNT(trip_id) DESC) AS ranking,
    LEFT(taxi_id, 10) AS short_taxi_id,
    CAST(trip_start_timestamp AS DATE) AS date,
    COUNT(trip_id) AS trip_count
FROM taxi_dataset.TaxiTrip
GROUP BY
    CAST(trip_start_timestamp AS DATE), 
    taxi_id
ORDER BY ranking ASC
LIMIT 10;


Redshift



SELECT
    RANK() OVER (ORDER BY COUNT(trip_id) DESC) AS ranking,
    LEFT(taxi_id, 10) AS short_taxi_id,
    CAST(trip_start_timestamp AS DATE) AS date,
    COUNT(trip_id) AS trip_count
FROM TaxiTrip
GROUP BY
    CAST(trip_start_timestamp AS DATE), 
    taxi_id
ORDER BY ranking ASC
LIMIT 10;


クエリ1-3:小規模構造化データ、ウィンドウ関数+CTE+集計

「タクシーは、どの程度スムーズに乗客を見つけられるのか?」を知りたいとします。一つの指標として、「前の乗客を降ろした場所の近くで、すぐに次の乗客を拾うという事象」について調べます。1人目の客を降ろした直後にその近くで2人目の客を拾い、2人目の客を降ろした直後にその近くで3人目の客を拾い......と、この事象は連続することがあります。n回連続した事象を series n trip、n回以上連続した事象を series n or more trip と呼び、n = 1, 2, 3 について相対度数を算出します。


BigQuery



-- タクシーごとに、各運行とその直前の運行の、運行ID、開始/終了座標、開始/終了時刻を取得するCTE。
WITH CurrAndPrevTrips AS (
    SELECT
        trip_id AS curr_trip_id,
        LAG(trip_id, 1) OVER W AS prev_trip_id,
        pickup_centroid_location AS curr_start_point,
        ST_GeogFromText(
            LAG(ST_AsText(dropoff_centroid_location), 1) OVER W    -- 地理データに直接ウィンドウ関数を適用できないRedshiftに合わせた処理(本来BigQueryでは不要)。
        ) AS prev_end_point,
        trip_start_timestamp AS curr_start_time, 
        LAG(trip_end_timestamp, 1) OVER W AS prev_end_time
    FROM taxi_dataset.TaxiTrip
    WHERE
        ST_Distance(pickup_centroid_location, dropoff_centroid_location) 
            < trip_miles * 1609 -- 誤った位置情報を持っている可能性が高いレコードが15%程度あるので、除外する。
    WINDOW W AS (
        PARTITION BY taxi_id
        ORDER BY trip_start_timestamp, trip_end_timestamp, trip_id
    )
),
-- 客を降ろしてから、一定の時間・距離内で、新しい客を拾った運行(series 1 trip)の組を取得するCTE。
Series1Trips AS (
    SELECT
        curr_trip_id,
        prev_trip_id
    FROM CurrAndPrevTrips
    WHERE
        TIMESTAMP_DIFF(curr_start_time, prev_end_time, MINUTE) <= 15
        AND ST_Distance(curr_start_point, prev_end_point) < 500
),
Series2Trips AS (
    SELECT
        Tbl1.curr_trip_id AS curr_trip_id,
        Tbl1.prev_trip_id AS prev1_trip_id,
        Tbl2.prev_trip_id AS prev2_trip_id
    FROM 
        Series1Trips AS Tbl1
        INNER JOIN Series1Trips AS Tbl2
            ON Tbl1.prev_trip_id = Tbl2.curr_trip_id -- 連続している2つのうち前者が、別の連続の後者として存在している場合を捉えて結合する。以下同様。
),
Series3Trips AS (
    SELECT
        S2.curr_trip_id AS curr_trip_id,
        S2.prev1_trip_id AS prev1_trip_id,
        S2.prev2_trip_id AS prev2_trip_id,
        S1.prev_trip_id AS prev3_trip_id
    FROM 
        Series2Trips AS S2
        INNER JOIN Series1Trips AS S1
            ON S2.prev2_trip_id = S1.curr_trip_id
),
Series4Trips AS (
    SELECT S3.curr_trip_id
    FROM 
        Series3Trips AS S3
        INNER JOIN Series1Trips AS S1
            ON S3.prev3_trip_id = S1.curr_trip_id
),
-- 総運行数とn連続運行数を集計するCTE。
DuplicatedCounts AS (
    SELECT 
        (SELECT COUNT(*) FROM CurrAndPrevTrips) AS num_all_trips,
        (SELECT COUNT(*) FROM Series1Trips) AS s1, -- この数は、series 2 trip を重複して2回数えている(一般に series n trip を重複してn回数えている)。
        (SELECT COUNT(*) FROM Series2Trips) AS s2, -- この数は、series 3 trip を重複して2回数えている(一般に series n trip を重複してn-1回数えている)。以下同様。
        (SELECT COUNT(*) FROM Series3Trips) AS s3,
        (SELECT COUNT(*) FROM Series4Trips) AS s4
)
SELECT
    num_all_trips,
    s1 - s2 AS num_series_1_or_more_trips, -- この数は、series n trip を重複なく1回として数えている。以下同様。
    s2 - s3 AS num_series_2_or_more_trips,
    s3 - s4 AS num_series_3_or_more_trips,
    FORMAT('%.2f %%', (s1 - s2) / (num_all_trips * 1.0) * 100) AS rate_series_1_or_more_trips, -- 1.0 を掛けるのは、実数除算を行うための措置。以下同様。
    FORMAT('%.2f %%', (s2 - s3) / (num_all_trips * 1.0) * 100) AS rate_series_2_or_more_trips,
    FORMAT('%.2f %%', (s3 - s4) / (num_all_trips * 1.0) * 100) AS rate_series_3_or_more_trips
FROM DuplicatedCounts;


Redshift



-- タクシーごとに、各運行とその直前の運行の、運行ID、開始/終了座標、開始/終了時刻を取得するCTE。
WITH CurrAndPrevTrips AS (
    SELECT
        trip_id AS curr_trip_id,
        LAG(trip_id, 1) OVER (
            PARTITION BY taxi_id
            ORDER BY trip_start_timestamp, trip_end_timestamp, trip_id
        ) AS prev_trip_id,
        pickup_centroid_location AS curr_start_point,
        ST_GeomFromText(LAG(ST_AsText(dropoff_centroid_location), 1) OVER (  -- Redshift では、地理データに直接ウィンドウ関数を適用できない。
            PARTITION BY taxi_id
            ORDER BY trip_start_timestamp, trip_end_timestamp, trip_id
        )) AS prev_end_point,
        trip_start_timestamp AS curr_start_time, 
        LAG(trip_end_timestamp, 1) OVER (
            PARTITION BY taxi_id
            ORDER BY trip_start_timestamp, trip_end_timestamp, trip_id
        ) AS prev_end_time
    FROM TaxiTrip
    WHERE
        ST_Distance(pickup_centroid_location, dropoff_centroid_location) 
            < trip_miles * 1609 -- 誤った位置情報を持っている可能性が高いレコードが15%程度あるので、除外する。
),
-- 客を降ろしてから、一定の時間・距離内で、新しい客を拾った運行(series 1 trip)の組を取得するCTE。
Series1Trips AS (
    SELECT
        curr_trip_id,
        prev_trip_id
    FROM CurrAndPrevTrips
    WHERE
        (EXTRACT(EPOCH FROM curr_start_time) - EXTRACT(EPOCH FROM prev_end_time)) / 60 <= 15
        AND ST_Distance(curr_start_point, prev_end_point) < 500
),
Series2Trips AS (
    SELECT
        Tbl1.curr_trip_id AS curr_trip_id,
        Tbl1.prev_trip_id AS prev1_trip_id,
        Tbl2.prev_trip_id AS prev2_trip_id
    FROM 
        Series1Trips AS Tbl1
        INNER JOIN Series1Trips AS Tbl2
            ON Tbl1.prev_trip_id = Tbl2.curr_trip_id -- 連続している2つのうち前者が、別の連続の後者として存在している場合を捉えて結合する。以下同様。
),
Series3Trips AS (
    SELECT
        S2.curr_trip_id AS curr_trip_id,
        S2.prev1_trip_id AS prev1_trip_id,
        S2.prev2_trip_id AS prev2_trip_id,
        S1.prev_trip_id AS prev3_trip_id
    FROM 
        Series2Trips AS S2
        INNER JOIN Series1Trips AS S1
            ON S2.prev2_trip_id = S1.curr_trip_id
),
Series4Trips AS (
    SELECT S3.curr_trip_id
    FROM 
        Series3Trips AS S3
        INNER JOIN Series1Trips AS S1
            ON S3.prev3_trip_id = S1.curr_trip_id
),
-- 総運行数とn連続運行数を集計するCTE。
DuplicatedCounts AS (
    SELECT 
        (SELECT COUNT(*) FROM CurrAndPrevTrips) AS num_all_trips,
        (SELECT COUNT(*) FROM Series1Trips) AS s1, -- この数は、series 2 trip を重複して2回数えている(一般に series n trip を重複してn回数えている)。
        (SELECT COUNT(*) FROM Series2Trips) AS s2, -- この数は、series 3 trip を重複して2回数えている(一般に series n trip を重複してn-1回数えている)。以下同様。
        (SELECT COUNT(*) FROM Series3Trips) AS s3,
        (SELECT COUNT(*) FROM Series4Trips) AS s4
)
SELECT
    num_all_trips,
    s1 - s2 AS num_series_1_or_more_trips, -- この数は、series n trip を重複なく1回として数えている。以下同様。
    s2 - s3 AS num_series_2_or_more_trips,
    s3 - s4 AS num_series_3_or_more_trips,
    TO_CHAR((s1 - s2) / (num_all_trips * 1.0) * 100, 'FM999.000') || ' %' AS rate_series_1_or_more_trips, -- 1.0 を掛けるのは、実数除算を行うための措置。以下同様。
    TO_CHAR((s2 - s3) / (num_all_trips * 1.0) * 100, 'FM999.000') || ' %' AS rate_series_2_or_more_trips,
    TO_CHAR((s3 - s4) / (num_all_trips * 1.0) * 100, 'FM999.000') || ' %' AS rate_series_3_or_more_trips
FROM DuplicatedCounts;


クエリ2-1:中規模半構造化データ、CTE+集計

Amazonレビューが「参考になった」を獲得する数に関して分析したいとします。一般に、投稿された直後のレビューは「参考になった」を獲得しておらず、時間の経過とともに獲得数を増やし、十分時間が経過することでそのレビューに相応しい獲得数に漸近すると考えられます。そこで分析の際、「十分時間」が経過していないレビューをWHERE句で排除することで、獲得数が安定したデータのみを対象にできます。そのためにここでは、「十分時間」が何年であるか特定するため、レビューを経過年数の階級に分類し、階級ごとに獲得数の平均値を求めます。そして階級の平均値と全体の平均値の比を取り、その比が一定の閾値に達しているかどうかのフラグを出力させます。

このデータセットのタイムスタンプはミリ秒単位のUNIX時刻です。このデータが収集されたのは2023年9月23日で、UNIX時刻にすると1695427200000になります。


BigQuery



WITH VoteNumWithClass AS (
    SELECT
        helpful_vote
        , CASE
            WHEN timestamp > 1679529600000 THEN '0.0-0.5y'  -- 投稿から経過した時間が0年以上半年未満である階級。以下同様。
            WHEN timestamp > 1663891200000 THEN '0.5-1.0y'
            WHEN timestamp > 1647993600000 THEN '1.0-1.5y'
            WHEN timestamp > 1632355200000 THEN '1.5-2.0y'
            WHEN timestamp > 1616457600000 THEN '2.0-2.5y'
            WHEN timestamp > 1600819200000 THEN '2.5-3.0y'
            WHEN timestamp > 1584921600000 THEN '3.0-3.5y'
            WHEN timestamp > 1569196800000 THEN '3.5-4.0y'
            WHEN timestamp > 1553299200000 THEN '4.0-4.5y'
            WHEN timestamp > 1537660800000 THEN '4.5-5.0y'
            WHEN timestamp <= 1537660800000 THEN '5.0y-'
            ELSE NULL
        END AS class
        , AVG(helpful_vote) OVER () AS overall_avg
    FROM amazon_reviews_dataset.UserReviews
)
SELECT
    class
    , AVG(helpful_vote) / MAX(overall_avg) AS votes_ratio_to_overall
    , CASE
        WHEN AVG(helpful_vote) / MAX(overall_avg) < 0.7
        THEN 'Too recent to rely on.'
        ELSE 'Old enough.'
    END AS eval_usable_or_not
FROM VoteNumWithClass
GROUP BY class
ORDER BY class;


Redshift



WITH VoteNumWithClass AS (
    SELECT
        helpful_vote
        , CASE
            WHEN timestamp > 1679529600000 THEN '0.0-0.5y'  -- 投稿から経過した時間が0年以上半年未満である階級。以下同様。
            WHEN timestamp > 1663891200000 THEN '0.5-1.0y'
            WHEN timestamp > 1647993600000 THEN '1.0-1.5y'
            WHEN timestamp > 1632355200000 THEN '1.5-2.0y'
            WHEN timestamp > 1616457600000 THEN '2.0-2.5y'
            WHEN timestamp > 1600819200000 THEN '2.5-3.0y'
            WHEN timestamp > 1584921600000 THEN '3.0-3.5y'
            WHEN timestamp > 1569196800000 THEN '3.5-4.0y'
            WHEN timestamp > 1553299200000 THEN '4.0-4.5y'
            WHEN timestamp > 1537660800000 THEN '4.5-5.0y'
            WHEN timestamp <= 1537660800000 THEN '5.0y-'
            ELSE NULL
        END AS class
        , AVG(helpful_vote) OVER () AS overall_avg
    FROM UserReviews
)
SELECT
    class
    , AVG(helpful_vote) / MAX(overall_avg) AS votes_ratio_to_overall
    , CASE
        WHEN AVG(helpful_vote) / MAX(overall_avg) < 0.7
        THEN 'Too recent to rely on.'
        ELSE 'Old enough.'
    END AS eval_usable_or_not
FROM VoteNumWithClass
GROUP BY class
ORDER BY class;


クエリ2-2:中規模半構造化データ、JOIN+集計

商品のレビュー数が多くなると、「自分がレビューを書く意味がない」と感じる人が増えて、レビュー数の増加が鈍化する傾向がある、という推測を立てました。これについて調査したいと思います。しかし、単にレビュー数の時間変化率だけを見ると、その商品が人気を失ってレビューされなくなった場合と区別できません。また、サクラを利用していたり、クーポンなどの報酬を用意して購入者にレビューを書くよう促したりしている商品であれば、通常のレビュー数の鈍化パターンに当てはまらないだろうと思われます。

そこでこのクエリでは、「商品ID、商品のレビュー数、商品のレビューについた『参考になった』の総数」の組を取得します。十分なレビュー数がある商品のうち、レビュー数に対して「参考になった」総数が多いものは、鈍化傾向にある可能性が高く、逆に少ないものはサクラなどを利用している可能性が高いと考えられます。

結果セットのサイズが大きいので、Redshiftでは一時テーブルに出力して表示させることができません。UNLOADコマンドによってS3バケットに結果を出力します。


BigQuery



SELECT
    UR.parent_asin
    , MAX(IM.rating_number) AS rating_number -- 1つの parent_asin に対応する rating_number は一意。このMAX関数は形式的なもので、実際は唯一の rating_number が返される。
    , SUM(UR.helpful_vote) AS sum_vote
FROM
    amazon_reviews_dataset.UserReviews UR 
    INNER JOIN amazon_reviews_dataset.ItemMetadata IM
        ON UR.parent_asin = IM.parent_asin
GROUP BY UR.parent_asin;


Redshift



UNLOAD('
    SELECT
        UR.parent_asin
        , MAX(IM.rating_number) AS rating_number -- 1つの parent_asin に対応する rating_number は一意。このMAX関数は形式的なもので、実際は唯一の rating_number が返される。
        , SUM(UR.helpful_vote) AS sum_vote
    FROM
        UserReviews UR 
        INNER JOIN ItemMetadata IM
            ON UR.parent_asin = IM.parent_asin
    GROUP BY UR.parent_asin;
')
TO 's3://my-bucket-name/unload2-2/'
IAM_ROLE 'my-role-arn'


クエリ2-3:中規模半構造化データ、ウィンドウ関数集計+CTE+JOIN

「参考になった」を獲得しやすい、優れたレビューの特徴を分析したいとします。そのためのデータを、2-1で求めた期間の条件でフィルタリングし、適切に加工して取り出す必要があります。しかし、単純に「参考になった」の獲得数を基準とするのは不適切だと考えられます。100件以上の「参考になった」を獲得しているレビューが複数ある商品で10件の「参考になった」を獲得しているレビューと、他に「参考になった」を獲得しているレビューが存在しない商品で10件獲得しているレビューでは、後者をより「優れている」と判定すべきだからです。

そこでこの分析では、パレート分析のような方法を用いることにしました。具体的には、商品ごとに、「参考になった」獲得数の順にレビューを並べ、累積相対度数に応じてグループA、B、Cに分けます(総度数が0の場合のグループはNULLとします)。そしてレビューごとに、そのレビューがどのグループに属するかと、以下の情報をセットにして出力します(後工程で、レビューがどのグループになるかを説明できる変数の組み合わせを探るという想定):「商品ID」、「評価値(星の数)」、「商品全体の平均評価値と評価値の差」、「添付画像の有無」、「レビュータイトルの原文」、「レビュータイトルの単語数」、「レビュー本文の原文」、「レビュー本文の単語数」。

結果セットのサイズが大きいので、一時テーブルに出力して表示させることができません。BigQueryでは別のテーブルを作成した上でそこに結果を保存する設定を行い、RedshiftではUNLOADコマンドによってS3バケットに結果を出力します。


BigQuery



WITH UserReviewWithWindow AS (
    SELECT
        *
        , 1.0 * SUM(UR.helpful_vote) OVER (
            PARTITION BY UR.parent_asin
            ORDER BY UR.helpful_vote DESC
        ) -- 商品ごとに、「参考になった」数の多い順にレビューを並べたときの、「参考になった」の累積度数。1.0を掛けるのは商を浮動小数点値にするため。
            / NULLIF(SUM(UR.helpful_vote) OVER (PARTITION BY UR.parent_asin), 0) -- 商品ごとの「参考になった」合計数。これで割ることで、「参考になった」の累積相対度数になる。
            AS crf
        , RANK() OVER (
            PARTITION BY UR.parent_asin
            ORDER BY UR.helpful_vote DESC
        ) AS rank -- 商品ごとに、「参考になった」数の多い順にレビューを並べたときの順位。
    FROM amazon_reviews_dataset.UserReviews UR
    WHERE UR.timestamp < 1584921600000 -- 2−1で決まったUNIX時刻(3年以上経過)。
)
SELECT
    UR.parent_asin -- 商品ID。
    , CASE -- パレート分析のグループを割り当てる。70%までをAグループ、90%までをBグループ、以降をCグループとする。
        WHEN UR.rank = 1 AND UR.crf IS NOT NULL THEN 'A' -- 相対度数が0.7以上のレビューをAグループに割り振るための処理。
        WHEN UR.crf < 0.7 THEN 'A'
        WHEN UR.crf < 0.9 THEN 'B'
        WHEN UR.crf <= 1.0 THEN 'C'
        ELSE NULL
    END AS pareto_group
    , UR.rating
    , IM.average_rating - UR.rating AS diff_avg_rating
    , CASE
        WHEN ARRAY_LENGTH(UR.images) = 0
        THEN false
        ELSE true
    END AS has_images
    , UR.title AS raw_title
    , LENGTH(UR.title) - LENGTH(REPLACE(UR.title, ' ', '')) + 1 AS num_words_title
    , UR.text AS raw_text
    , LENGTH(UR.text) - LENGTH(REPLACE(UR.text, ' ', '')) + 1 AS num_words_text
FROM
    UserReviewWithWindow UR 
    INNER JOIN amazon_reviews_dataset.ItemMetadata IM 
        ON UR.parent_asin = IM.parent_asin;


Redshift



UNLOAD('
    WITH UserReviewWithWindow AS (
        SELECT
            *
            , 1.0 * SUM(UR.helpful_vote) OVER (
                PARTITION BY UR.parent_asin
                ORDER BY UR.helpful_vote DESC
                ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING  -- Redshift では集約ウィンドウ関数に ORDER BY 句を含める場合、フレーム句が必須。
            ) -- 商品ごとに、「参考になった」数の多い順にレビューを並べたときの、「参考になった」の累積度数。1.0を掛けるのは商を浮動小数点値にするため。
                / NULLIF(SUM(UR.helpful_vote) OVER (PARTITION BY UR.parent_asin), 0) -- 商品ごとの「参考になった」合計数。これで割ることで、「参考になった」の累積相対度数になる。
                AS crf
            , RANK() OVER (
                PARTITION BY UR.parent_asin
                ORDER BY UR.helpful_vote desc
            ) AS rank -- 商品ごとに、「参考になった」数の多い順にレビューを並べたときの順位。
        FROM UserReviews UR
        WHERE timestamp < 1584921600000 -- 2-1で決まったUNIX時刻(3年以上経過)。
    )
    SELECT
        UR.parent_asin -- 商品ID。
        , CASE -- パレート分析のグループを割り当てる。70%までをAグループ、90%までをBグループ、以降をCグループとする。
            WHEN UR.rank = 1 AND UR.crf IS NOT NULL THEN ''A'' -- 相対度数が0.7以上のレビューをAグループに割り振るための処理。
            WHEN UR.crf < 0.7 THEN ''A''
            WHEN UR.crf < 0.9 THEN ''B''
            ELSE ''C''
        END AS pareto_group
        , UR.rating
        , IM.average_rating - UR.rating AS diff_avg_rating
        , CASE
            WHEN GET_ARRAY_LENGTH(UR.images) = 0
            THEN false
            ELSE true
        END AS has_images
        , UR.title AS raw_title
        , LENGTH(UR.title) - LENGTH(REPLACE(UR.title, '' '', '''')) + 1 AS num_words_title
        , UR.text AS raw_text
        , LENGTH(UR.text) - LENGTH(REPLACE(UR.text, '' '', '''')) + 1 AS num_words_text
    FROM
        UserReviewWithWindow UR
        INNER JOIN ItemMetadata IM 
            ON UR.parent_asin = IM.parent_asin;
')
TO 's3://my-bucket-name/unload2-3/'
IAM_ROLE 'my-role-arn'




X(旧Twitter)・Facebookで定期的に情報発信しています!

データ分析基盤構築とは、大量のデータを蓄積・変換・分析するためのインフラを開発することです。主軸となるデータレイク・データウェアハウス・BIツールの他、NoSQLデータベース、データパイプラインツール、ETLツールなど様々な要素があり、それぞれ様々なベンダーから多種多様な製品が出ています。

比較項目は膨大で、複雑です。性能・機能・セキュリティ・コスト......一体何を基準に選べばよいのでしょうか。当社には一つの戦略があります。それは、

" 分析ツールはコスト最適性で、データ基盤は運用性で選ぶ。 "


これがデータ利活用を成功させるための鍵です。以下のように、2つのポイントに分けられます。

データ分析基盤の構成.jpg

詳しく解説していきます。

秘訣①BIツールの二極化と、「コスト最適」な配分

まずは実際に、分析ツールの費用をご紹介しましょう。データ利活用の最初に導入するデータ分析ツールとしては、BIツールが一般的です。TableauとQuickSightという2つのBIツールを比較します。1,000人の従業員が利用する場合、支払額は次のようになります。

[ケース1]

  • Tableau:約3,000万円/年
  • QuickSight:約800万円/年


3〜4倍の差です。

極端な例ではありません。BIツールの価格は二極化しており、このくらい高いか、安いか、どちらかです。最初に適切な製品を導入できないと、その後毎年不必要なコストがかかり続けてしまいます。

とはいえ高価なツールには、独自の機能や、高度な利用をする際の使いやすさがあります。例えばTableauはドラッグ&ドロップのような直感的な操作によって、簡単・迅速にデータ分析ができ、分析を主務とするユーザーに向いています。ただ一律に安価なものを導入すると、結局機能が不足してデータ利活用が進まないという本末転倒の事態になるでしょう。

「安ければいい」でも「高機能であればいい」でもなく、「必要な利用者にだけ必要な機能を」。それが「コスト最適化」の観点です。具体的には、

  • 自社のニーズを洗い出し、
  • 「誰が・どんなタスクを・どの程度」できる必要があるかを見極め、
  • それぞれに必要十分なツールを用意すること。


これによってコスト最適な利活用推進を図ることが重要なのです。

ツール配分の例を考えましょう。先ほどの従業員1,000人のうち、データからインサイトを得るための分析業務を行うのは経営戦略部と店舗開発部の計100人で、その他の900人はあらかじめ作成された可視化ダッシュボードを日々の業務で参照するだけだとします。分析を行う100人(とツールを管理するエンジニアチーム)がTableauを利用し、残りの900人はQuickSightを利用するのが適切です。費用は以下の通りです。

[ケース2]

  • Tableau:約700万円/年
  • QuickSight:約800万円/年
  • 合計:約1,500万円/年


さらに、従業員に対する細かいアクセス権限の設定などが不必要である場合、QuickSightの代わりに無料の Looker Studio を利用できます。この場合、費用はさらに安くなります。

[ケース3]

  • Tableau:約700万円/年
  • Looker Studio:0円/年
  • 合計:約700万円/年


これはあくまで簡単な例ですが、「分析か/可視化か」「細かいアクセス権限管理が必要か/不要か」という観点は一般的なものです。自社のニーズをこのような形で整理し、それに合った適切なツールを選んでいくというイメージをつけましょう。

では具体的に、どのようなツールがあるのでしょうか。高価・高機能なものでは、Board、Domo、Qlikといった有名製品にそれぞれ特長がありますが、AIによる自然言語処理で直感的に使えるThoughtSpotのような製品も生産性を向上し、使い始めのハードルを下げるでしょう。安価なものはGoogleの Looker Studio が代表的ですが、OSSのMetabaseも、自動でダッシュボードを生成してくれるX-rayという機能に魅力があります。

秘訣②利活用に必要な「運用性の高いデータ基盤」

「データ基盤」とは、データを利用可能な形で蓄積するためのインフラストラクチャのことです。と一口に言っても、実際にはデータパイプライン・データレイク・ETL・データウェアハウスと、多くの構成要素があります。それぞれの構成要素を動かすのに必要なプログラムや設定の項目は非常に多く、開発や運用の工数が大きくなります。分析ツールが費用の勘所であるのに対して、データ基盤はいわば工数の勘所です。

しかし本当に工数を割くべきなのは、開発や運用ではありません。要件定義とそれに基づく継続的デリバリーに力を入れてこそ、将来の成功があります。データ基盤の構築はDXやクラウド移行と似ており、現行のシステムや業務自体の変化を伴うものです。様々なステークホルダーへの説明・ヒアリング・調整が不足していれば、プロジェクトは失敗へ向かいます。一方で、プロジェクトのエンジニアチームが主体となって調整を行い、使う人たちにとって使いやすいデータ基盤を構築することができれば、利活用は順調な滑り出しを迎えられます。データ基盤は一度出来上がった後でも現場から様々な要望が舞い込んできますが、それに応じられるコミュニケーション体制も確立できるはずです。

そのため、エンジニアチームがコミュニケーションに時間を割けるように、データ基盤では保守運用性の高い構成を選択し、開発工数も抑えることが重要になります。

その視点で一般的に優れているのは、基盤をAWSサービスだけで構築することです。AWSでは、データウェアハウスのメンテナンス自動化、アクセス権限やメタデータの管理一本化などの恩恵を得られます。また、AWS上に構築されることの多い対向業務システムとの連携もスムーズです。

もちろん、どのような場合でもAWSがベストな訳ではありません。Google Cloudは先進的な性能によって、データ基盤市場に確固たる地位を築いています。データウェアハウスについては、独自の機能と料金システムで支持を伸ばしているSnowflakeも検討の余地があります。

データ利活用のスタートダッシュ

"分析ツールはコスト最適性で、データ基盤は運用性で選ぶ。"というテーマについて解説してきました。それでは、なぜこれが望ましい戦略なのでしょうか。それは、「データ利活用/データドリブン経営を、最低限のコストで、スムーズに始められる」からです。このうち「最低限のコスト」については、分析ツールを適切に選ぶことで高すぎる負担を避けられるということが、まずあります。運用性の高いデータ基盤も、保守・改修を含めたTCOを抑えることで、コストカットに繋がります。

では、「スムーズに始められる」とはどういうことでしょうか。データ利活用/データドリブン経営の「スムーズな開始」を阻む要因は、主に2つ考えられます。1つ目は「利用者のリテラシー不足」で、「ツールの操作がわからない」「データが何に使えるのかわからない」といったものです。2つ目は「エンジニアの設計ミス」で、「ツールのスペックが足りない」「使い物にならないデータを生成している」などです。この2種類の失敗は、「基盤を使う人たちとエンジニアチームのコミュニケーション不足」という1つの失敗から出てくる結果に過ぎません。

基盤構築の段階から、エンジニアチームが積極的に利用者に関わっていくことが重要なのです。コミュニケーションを重ねることで、利用者がデータを理解していくことができます。エンジニアチームの側も、フィードバックから要件定義、製品選定やスキーマ設計のヒントを与えられます。

「コスト最適に・スムーズに始める」ことにゴールを置いて基盤構築をすれば、その後のデータカルチャーの浸透、データによる収益拡大といった経営目標へと繋がっていくでしょう。そのためには、分析ツールはコスト最適性で、データ基盤は運用性で選ぶことになります。

最適な製品選定と要件調整のご提供

アクセルユニバース株式会社では、当社の掲げる「顧客中心主義」に基づいて、お客様に合わせた最適な分析ツール・データ基盤を立案・開発いたします。お客様の状況をヒアリングし、適切なプランをご提案することを得意とする企業です。

また、必要に応じて、お客様部署間のファシリテーションなどの総合的なITコンサルティングを提供しております。

まずは追加でお聞きになりたい情報から、お気軽にご相談ください。

(お問い合わせはこちら)

BIツールの費用計算

いずれも記事公開時点での情報です。1ドル=150円、有効数字1桁としました。 Tableau公式の価格情報AWS公式の料金情報 をもとに計算しています。

[ケース1] Tableau:「Tableau Creator」10人、「Tableau Explorer」100人、「Tableau Viewer」890人。 QuickSight:「管理者」10人、「作成者」100人、「閲覧者」890人。

[ケース2] Tableau:「Tableau Creator」10人、「Tableau Explorer」100人。 QuickSight:「管理者」10人、「作成者」100人、「閲覧者」790人。

[ケース3] Tableau:「Tableau Creator」10人、「Tableau Explorer」100人。 Looker Studio:無料ユーザーのみ。

このアーカイブについて

このページには、過去に書かれた記事のうちデータ分析カテゴリに属しているものが含まれています。

前のカテゴリは生成AIです。

次のカテゴリはAWSです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。