Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

WrenAI で手軽に Generative BI を実現

こんにちは。大橋です。

3月も終わりを迎え、近くの川辺では桜がちょうど見ごろになりました。
春のやわらかな空気に包まれながら、新しい季節の訪れをしみじみ感じる頃ですね。

ところで、みなさんはビジネスデータを分析する際、SQLを書いたり、BIツールの集計条件を何度も調整したりする作業に時間を取られたことはないでしょうか。

このようなデータ分析も、生成AIを利用して、自然言語でいい感じにデータを分析できたら良いですよね。

そのような内容を、最近では、生成AIとBIツールを掛けて「Generative BI」という言い方をします。 今回、OSSのGenerative BIツールである「WrenAI」を使い、RDBのデータを日本語で分析できるかを検証しました。

1. はじめに

1-1. Generative BIとは

Generative BIとは、自然言語でデータに関する質問を行うことで、生成AIがSQL生成・集計・可視化までを支援してくれる仕組みのことを指します。 Generative BIを利用することで、BIツールの操作やSQLの細かい文法を知らなくても、分析を進めることができます。

1-2. 「WrenAI」とは

WrenAI」はGenerative BIを実現するサービスの1つです。

以下のようなデータベースに接続してデータを取得し、ユーザーの質問に答えることができます。

商用版もありますが、今回はOSS版を使い、できることを検証していきます。

1-3. 「WrenAI」の仕組み

「WrenAI」が自然言語の質問に回答する際、まずテーブル定義・リレーション・カラム説明を読み込み、データについて理解します。

理解した内容を基に、ユーザーの質問に答えられるよう内部でSQLを生成します。生成されたSQLを実行してデータベースからデータを取得し、ユーザーの質問に対する回答を生成します。

例えば以下のようなSQLが生成されます。 以下は後述の「支払い方法別の売上合計を算出してください。」という質問に対して生成されたSQLです。 支払い方法ごとに、売り上げの合計を算出するSQLを生成してくれています。

WITH
  "payments_agg" AS (
    SELECT
      "public_payments"."payment_type" AS "payment_type",
      SUM("public_payments"."payment_value") AS "total_sales"
    FROM
      "public_payments"
    GROUP BY
      "public_payments"."payment_type"
  )
SELECT
  "payments_agg"."payment_type" AS "payment_type",
  "payments_agg"."total_sales" AS "total_sales"
FROM
  "payments_agg"

分析対象のデータを理解した上で質問をSQLに変換し、データを検索・集計することで、ユーザーの質問に回答できるというわけです。

また、「WrenAI」では、必要に応じてグラフ化した結果の表示や、グラフのダッシュボード化まで行うこともできます。

1-4. 本記事でやること

本記事では、PostgreSQLにロードしたEC注文データを「WrenAI」から参照し、日本語の質問に対してどのような分析・回答を行うことができるか確認します。

基本集計、ランキング、複雑条件、時系列、複数テーブル分析、データ品質確認まで、「WrenAI」 がどこまで扱えるかを見ていきます。

2. セットアップ

2-1. データの準備

今回は、Kaggleで配布されている、e-commerce order dataset を使いました。

Kaggleとは、世界最大級のデータ分析・機械学習のオンラインプラットフォームです。 他にも様々なデータの配布や、データ分析コンペが開催されているので、興味がある方はこちらをご覧ください。

e-commerce order datasetは、一般的なeコマースプラットフォームのデータです。 Customers / Orders / OrderItems / Products / Payments の5テーブルが提供されており、注文や顧客、および商品に関する包括的な情報を得ることができます。

今回はローカルで起動したPostgreSQLに取り込みました。「WrenAI」 では接続時に選択したテーブルがそのままモデルとして扱われるため、この段階で「今回の分析に使うテーブル」を絞っておくと後の設定がかなり楽になります。

この段階で、customer_idorder_id などの主キー・外部キーに相当する列を把握しておくと、「WrenAI」 側のリレーション設定をスムーズに行うことが可能です。

2-2. 「WrenAI」をインストール・起動する

簡単に流れを説明すると、以下のような流れで、インストールして起動できるかを確認します。

  • Docker Desktopをインストールする
  • OpenAIのAPIキーを準備する
  • 「WrenAI Launcher」を使って「WrenAI OSS」を起動する
  • ブラウザで http://localhost:3000 にアクセスする

詳細は、公式ドキュメントを参照してください。

2-3. 「WrenAI」とPostgreSQLを接続する

「WrenAI」 でPostgreSQLを追加する場合は、ホーム画面の Connect a data source からPostgreSQLを選び、Display nameHostPortUsernamePasswordDatabase name を入力します。公式ドキュメントでは、WindowsでローカルPostgreSQLに接続する場合は host.docker.internal を使うよう案内されています。

あわせて、接続に使う PostgreSQL ユーザーには、最低でも SELECT / CREATE TEMPORARY VIEW / DROP VIEW の権限が必要です。「WrenAI」はクエリ実行時に一時ビューを作成・削除するため、この権限が不足していると接続後の問い合わせでエラーになりやすいです。

接続できたら、今回使用する 5 テーブルを選択して次へ進みます。

接続後は、「WrenAI」側で customers / orders / order_items / products / payments の5テーブルを選択し、関係性を確認します。 今回のデータでは、主に次のリレーションを前提にしました。

  • customers.customer_id = orders.customer_id
  • orders.order_id = order_items.order_id
  • products.product_id = order_items.product_id
  • orders.order_id = payments.order_id

この設定で、顧客単位のランキング、州別カテゴリ分析、時系列集計までを一つの接続先で確認できます。

2-4. カラム説明を設定する

接続後には、「WrenAI」側で各カラムの説明も設定しました。

カラムの説明は、画面上部の「Modeling」から遷移したモデリング画面で設定できます。

モデリング画面に表示されているテーブルをクリックすると、編集ダイアログが表示されます。

編集後は、デプロイを忘れないようにしましょう。 カラム名を日本語で指定しても回答できるよう、全カラムに説明を入れたうえで、特に pricepayment_value には補足説明を付けています。

Customers

カラム名 日本語説明
customer_id 顧客ID
customer_zip_code_prefix 郵便番号プレフィックス
customer_city 顧客都市
customer_state 顧客州

Orders

カラム名 日本語説明
order_id 注文ID
customer_id 顧客ID(Customersと関連)
order_status 注文ステータス
order_purchase_timestamp 注文日時
order_approved_at 注文承認日時
order_delivered_timestamp 配送完了日時
order_estimated_delivery_date 配送予定日

OrderItems

カラム名 日本語説明
order_id 注文ID(Ordersと関連)
product_id 商品ID(Productsと関連)
seller_id 販売者ID
price 商品価格(商品価格+送料で請求金額)
shipping_charges 送料

Products

カラム名 日本語説明
product_id 商品ID
product_category_name 商品カテゴリ
product_weight_g 重量(g)
product_length_cm 長さ(cm)
product_height_cm 高さ(cm)
product_width_cm 幅(cm)

Payments

カラム名 日本語説明
order_id 注文ID(Ordersと関連)
payment_sequential 支払い連番
payment_type 支払い方法
payment_installments 分割回数
payment_value 支払金額(売上金額)

3. 日本語で質問してデータ分析

ここでは、「WrenAI」でどこまで自然に分析できるかが伝わりやすい6問を取り上げます。モデルはgpt-5-miniを使い、検証しました。 単純な集計だけでなく、複数条件の絞り込み、時系列、複数テーブル分析、データ品質チェックまで幅を持たせました。

No. 質問 この質問で見たいこと ---
1 総支払金額が高い顧客トップ10を表示してください。 顧客単位で支払金額を集計し、必要に応じてテーブルを JOIN したうえで、LIMIT または DENSE_RANK を使って上位10件を取得できるか。
2 支払い方法別の売上合計を算出してください。 基本集計と GROUP BY を素直に組み立てられるか。
3 2018年かつクレジットカード支払いで、分割回数が6回以上の注文を表示してください。 年、支払い方法、分割回数といった複数条件を正しく解釈し、複数の WHERE 条件で適切に絞り込めるか。
4 月別の売上合計推移を表示してください。 日付から月単位の粒度に変換して売上を集計し、月別の推移として可視化しやすい形にまとめられるか。
5 各州で最も売上が高い商品カテゴリを表示してください。 複数テーブルを JOIN して州別・カテゴリ別に売上を集計し、DENSE_RANK などで各州の最上位カテゴリを特定できるか。
6 注文承認日時がNULLの注文件数を集計してください。 注文承認日時の欠損を IS NULL で判定し、COUNT で対象件数を集計できるか。

3-1. 総支払金額が高い顧客トップ10を表示する

顧客粒度のJOINとランキングを確認する

質問: 総支払金額が高い顧客トップ10を表示してください。

生成されたSQL:

WITH
  payment_totals AS (
    SELECT
      "public_orders"."customer_id" AS "customer_id",
      SUM(COALESCE("public_payments"."payment_value", 0)) AS "total_payment"
    FROM
      "public_payments"
      JOIN "public_orders" ON "public_payments"."order_id" = "public_orders"."order_id"
    GROUP BY
      "public_orders"."customer_id"
  ),
  ranked AS (
    SELECT
      pt."customer_id" AS "customer_id",
      pt."total_payment" AS "total_payment",
      DENSE_RANK() OVER (
        ORDER BY
          pt."total_payment" DESC
      ) AS "rank"
    FROM
      payment_totals pt
  )
SELECT
  r."rank" AS "rank",
  c."customer_id" AS "customer_id",
  c."customer_city" AS "customer_city",
  c."customer_state" AS "customer_state",
  r."total_payment" AS "total_payment"
FROM
  ranked r
  LEFT JOIN "public_customers" c ON r."customer_id" = c."customer_id"
WHERE
  r."rank" <= 10
ORDER BY
  r."rank" ASC,
  r."total_payment" DESC

まず、「WrenAI」からは総支払金額が高い顧客を上から並べた回答が返ってきました。

この回答があると、売上が大きい顧客をすぐに把握できます。 売上が一部の顧客に集中しているか、どの顧客を重点的に見ればよいかを考える入口として使いやすい結果でした。

今回の回答では、支払いデータを注文・顧客にひも付けて、顧客ごとに支払金額を合計するところまでできていました。 そのため、「誰が大きく買っているか」を見るためのランキングとしては、そのまま読める形になっています。

一方で、順位付けには DENSE_RANK が使われていました。 DENSE_RANK は同率を同じ順位として扱う方法なので、10位タイが複数いると結果が10件を超えることがあります。 そのため、「必ず10件に絞りたい」用途では注意が必要です。 この点は後半の注意点でも取り上げます。

3-2. 支払い方法別の売上合計を算出する

基本集計とGROUP BYを確認する

質問: 支払い方法別の売上合計を算出してください。

生成されたSQL:

SELECT
  "public_payments"."payment_type" AS "payment_type",
  SUM("public_payments"."payment_value") AS "total_payment_value"
FROM
  "public_payments" AS "public_payments"
GROUP BY
  "public_payments"."payment_type"
ORDER BY
  "total_payment_value" DESC

まず、「WrenAI」からは支払い方法ごとの売上合計をまとめた回答が返ってきました。

「支払い方法別の売上合計を算出してください。」の結果を見ると、どの決済手段が売上の中心なのかをひと目で確認できます。 支払い方法の偏りが分かれば、決済導線の見直しやキャンペーン設計を考えるときの材料にもなります。

今回の回答では、支払い方法ごとに支払金額を集めて、その合計を並べてくれました。 そのため、「どの支払い方法がどれだけ売上を作っているか」を素直に読み取れる結果になっています。

こうした単純な比較集計は特に分かりやすく、「WrenAI」の使い始めとして相性が良いと感じました。 そのまま棒グラフにして見比べやすい点も、Generative BIらしさが伝わりやすいポイントです。

3-3. 2018年かつクレジットカード支払いで、分割回数が6回以上の注文を表示する

複数条件の解釈を確認する

質問: 2018年かつクレジットカード支払いで、分割回数が6回以上の注文を表示してください。

生成されたSQL:

WITH
  "o" AS (
    SELECT
      "public_orders"."order_id" AS "order_id",
      "public_orders"."customer_id" AS "customer_id",
      "public_orders"."order_purchase_timestamp" AS "order_purchase_timestamp",
      "public_payments"."payment_sequential" AS "payment_sequential",
      "public_payments"."payment_type" AS "payment_type",
      "public_payments"."payment_installments" AS "payment_installments",
      "public_payments"."payment_value" AS "payment_value"
    FROM
      "public_orders"
      INNER JOIN "public_payments" ON "public_orders"."order_id" = "public_payments"."order_id"
    WHERE
      CAST(
        "public_orders"."order_purchase_timestamp" AS TIMESTAMP
        WITH
          TIME ZONE
      ) >= CAST(
        '2018-01-01 00:00:00' AS TIMESTAMP
        WITH
          TIME ZONE
      )
      AND CAST(
        "public_orders"."order_purchase_timestamp" AS TIMESTAMP
        WITH
          TIME ZONE
      ) <= CAST(
        '2018-12-31 23:59:59' AS TIMESTAMP
        WITH
          TIME ZONE
      )
      AND lower("public_payments"."payment_type") = lower('credit_card')
      AND "public_payments"."payment_installments" >= 6
  )
SELECT
  "o"."order_id" AS "order_id",
  "o"."customer_id" AS "customer_id",
  "o"."order_purchase_timestamp" AS "order_purchase_timestamp",
  "o"."payment_sequential" AS "payment_sequential",
  "o"."payment_type" AS "payment_type",
  "o"."payment_installments" AS "payment_installments",
  "o"."payment_value" AS "payment_value"
FROM
  "o"

まず、「WrenAI」からは条件に合う注文候補を返す回答が返ってきました。

今回の回答では、次の3条件をまとめて扱えていました。

  • 2018年の注文であること
  • 支払い方法が credit_card であること
  • payment_installments >= 6 であること

「2018年かつクレジットカード支払いで、分割回数が6回以上の注文を表示してください。」の結果があると、特定条件の注文だけをすぐ洗い出せます。 たとえば、分割回数が多い注文の確認や、特定の支払い条件に偏りがないかを見るときの入口として使いやすいです。

今回の回答では、注文日時・支払い方法・分割回数という別々の条件をまとめて扱い、条件に合う注文候補を抽出してくれました。 そのため、「条件に合うデータをまず確認したい」という実務に近い使い方にもつなげやすいです。

単純な集計だけでなく、複数条件を組み合わせたデータ抽出まで自然言語で進められることが分かりました。

3-4. 月別の売上合計推移を表示する

時系列集計を確認する

質問: 月別の売上合計推移を表示してください。

生成されたSQL:

WITH
  payment_per_order AS (
    SELECT
      "public_payments"."order_id" AS "order_id",
      SUM(COALESCE("public_payments"."payment_value", 0)) AS "total_payment_value"
    FROM
      "public_payments"
    GROUP BY
      "public_payments"."order_id"
  ),
  order_with_month AS (
    SELECT
      ppo."order_id" AS "order_id",
      TO_CHAR (
        CAST(
          ppo."order_purchase_timestamp" AS TIMESTAMP
          WITH
            TIME ZONE
        ),
        'YYYY-MM'
      ) AS "year_month",
      ppo."order_status" AS "order_status"
    FROM
      "public_orders" AS ppo
  )
SELECT
  owo."year_month" AS "year_month",
  SUM(poo."total_payment_value") AS "monthly_total_sales"
FROM
  order_with_month AS owo
  JOIN payment_per_order AS poo ON owo."order_id" = poo."order_id"
WHERE
  lower(owo."order_status") != lower('canceled')
GROUP BY
  owo."year_month"
ORDER BY
  owo."year_month" ASC

まず、「WrenAI」からは月別の売上合計をまとめた回答が返ってきました。

「月別の売上合計推移を表示してください。」の結果では、月ごとの売上が時系列で見えるため、増減や繁閑の流れをすぐに把握できます。 どの月で伸びたのか、どの月で落ちたのかが見えるため、季節性や施策の影響を考える出発点として扱いやすい結果でした。

今回の回答では、注文日時を月単位にまとめ直し、その月に対応する支払金額を積み上げてくれました。 そのため、単発の数字ではなく、売上の流れとして読める形になっているのが良かったです。

今回は「売上」を支払金額として素直に解釈できたため、月次推移として読みやすい回答になっています。

なお、このSQLでは注文ステータスが canceled のものを除外していました。 そのため、業務上の売上定義としてどのステータスまで含めるかは別途確認が必要です。

3-5. 各州で最も売上が高い商品カテゴリを表示する

複数テーブル分析と州内ランキングを確認する

質問: 各州で最も売上が高い商品カテゴリを表示してください。

生成されたSQL:

WITH
  "oi" AS (
    SELECT
      "public_order_items"."order_id" AS "order_id",
      "public_order_items"."product_id" AS "product_id",
      COALESCE("public_order_items"."price", 0) + COALESCE("public_order_items"."shipping_charges", 0) AS "sales"
    FROM
      "public_order_items"
  ),
  "ord" AS (
    SELECT
      "public_orders"."order_id" AS "order_id",
      "public_orders"."customer_id" AS "customer_id"
    FROM
      "public_orders"
  ),
  "cust" AS (
    SELECT
      "public_customers"."customer_id" AS "customer_id",
      "public_customers"."customer_state" AS "customer_state"
    FROM
      "public_customers"
  ),
  "prod" AS (
    SELECT
      "public_products"."product_id" AS "product_id",
      COALESCE(
        "public_products"."product_category_name",
        'UNKNOWN'
      ) AS "product_category_name"
    FROM
      "public_products"
  ),
  "state_category_sales" AS (
    SELECT
      "cust"."customer_state" AS "customer_state",
      "prod"."product_category_name" AS "product_category_name",
      SUM("oi"."sales") AS "total_sales"
    FROM
      "oi"
      JOIN "ord" ON "oi"."order_id" = "ord"."order_id"
      JOIN "cust" ON "ord"."customer_id" = "cust"."customer_id"
      JOIN "prod" ON "oi"."product_id" = "prod"."product_id"
    GROUP BY
      "cust"."customer_state",
      "prod"."product_category_name"
  ),
  "ranked" AS (
    SELECT
      "state_category_sales"."customer_state" AS "customer_state",
      "state_category_sales"."product_category_name" AS "product_category_name",
      "state_category_sales"."total_sales" AS "total_sales",
      DENSE_RANK() OVER (
        PARTITION BY
          "state_category_sales"."customer_state"
        ORDER BY
          "state_category_sales"."total_sales" DESC
      ) AS "category_rank"
    FROM
      "state_category_sales"
  )
SELECT
  "ranked"."customer_state" AS "customer_state",
  "ranked"."product_category_name" AS "product_category_name",
  "ranked"."total_sales" AS "total_sales",
  "ranked"."category_rank" AS "category_rank"
FROM
  "ranked"
WHERE
  "ranked"."category_rank" = 1

まず、「WrenAI」からは州ごとに最も売上が大きい商品カテゴリを並べた回答が返ってきました。

「各州で最も売上が高い商品カテゴリを表示してください。」の結果を見ると、地域ごとにどのカテゴリが強いのかを比較できます。 全国で一律に売れているのか、それとも州ごとに売れ筋が違うのかを把握するのに役立ち、地域別の販促や在庫の考え方にもつなげやすい分析です。

今回の回答では、顧客の州、注文された商品、商品カテゴリ、売上をつないで、州ごとにどのカテゴリが最も大きいかを比べてくれました。 こうした複数テーブルをまたぐ集計まで、自然言語で形にできると便利ですね。

一方で、州内1位の判定にはここでも DENSE_RANK が使われていました。 DENSE_RANK は同率を同じ順位として残すため、同率1位がある州では複数カテゴリが返ることがあります。 そのため、「各州1カテゴリに絞りたい」場面では注意が必要です。

3-6. 注文承認日時がNULLの注文件数を集計する

データ品質チェックを確認する

質問: 注文承認日時がNULLの注文件数を集計してください。

生成されたSQL:

SELECT
  COUNT(*) AS "null_order_approved_count"
FROM
  "public_orders"
WHERE
  "public_orders"."order_approved_at" IS NULL

まず、「WrenAI」からは注文承認日時が入っていない注文の件数を返す回答が返ってきました。

「注文承認日時がNULLの注文件数を集計してください。」の結果があると、分析の前にデータの欠損状況を手早く確認できます。 件数が多ければデータ連携や業務フローに問題がないかを疑うきっかけになり、件数が少なければそのまま分析を進めやすくなります。

今回の回答では、注文データの中から承認日時が入っていないレコードだけを拾い、その件数を数えてくれました。 そのため、細かいSQLを書かなくても「まず欠損がどれくらいあるか」をすぐ確認できます。

売上分析だけでなく、こうしたデータ品質チェックも自然言語で扱える点は便利だと思いました。

4. ダッシュボードの作成

上記の3-2~3-7の質問の結果をダッシュボード化する流れも確認しました。

生成されたグラフから、「Pin chart to dashboard」ボタンを押下するとダッシュボードに追加できます。

ホーム画面の以下のボタンを押下することで、ダッシュボードを表示できます。

行った分析の結果を一望できて便利ですね。

5. 検証で分かった注意点

  1. Top NではDENSE_RANKに注意する

    • 今回の検証では、Top N や州内1位の抽出で DENSE_RANK() が使われるケースがありました。
    • たとえば「総支払金額が高い顧客トップ10」では rank <= 10、「各州で最も売上が高い商品カテゴリ」では rank_within_state = 1 という形です。

    • この実装だと、同率がある場合に期待件数を超えて返る可能性があります。 厳密に 10件固定1州1件固定 を求める運用では、注意が必要です。

  2. 計算式を含む金額系の質問は解釈に注意する

    • 計算式を含む金額系の質問では、見たい指標を質問文の中で明示しないと、意味の近い別の金額を混在させることがあります。 特に「売上」「請求総額」「支払金額」のように似た言葉が並ぶ場合は、見た目がそれらしい結果でも定義がずれている可能性があります。

    • 今回の検証でも、「注文ごとの請求総額を計算し、上位10件を表示してください。」という質問では、本来見たい「請求総額」(price + shipping_charges)に対して、「支払金額」(payment_value)が混ざった回答になりました。 このように、計算式が必要な指標はカラム説明だけでは固定しきれないことがあります。

    • この違いは、見た目の数字がそれらしくても無視できません。 そのため、この種の質問では「請求総額は price + shipping_charges」「売上は payment_value」のように、質問文の中で指標の定義を明示しておく方が安全だと感じました。

6. まとめ

PostgreSQL に取り込んだ複数テーブルデータを 「WrenAI」から参照し、日本語で質問する流れは十分実用的でした。 今回取り上げた6問を見ると、基本集計、ランキング、複雑条件、時系列、複数テーブル分析、データ品質確認まで、かなり幅広い問いに対応できています。

一方で、Top N のような件数指定や、金額や数値の計算を伴う内容は、質問の意図と内容が合っているか、注意が必要だと感じました。 まずは代表的な質問を数問決めて「WrenAI」の解釈を確認し、それを踏まえてカラムの説明の追記や質問の明確化を行うことが現実的と思われます。

Acroquest Technologyでは、キャリア採用を行っています。
  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com