テーブル結合は JOIN と FROM どちらで行うべきか。両者の違いと注意点

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

www.ritolab.com


JOIN での結合式と、FROM での結合式

テーブル結合といえば JOIN 句が一般的ですが、FROM 句に複数テーブルを指定することでもテーブルを結合できます。例えば、以下 2 つのクエリは全く同じ結果を返します。

SELECT
  *
FROM books
INNER JOIN author_book ON books.id = author_book.book_id
INNER JOIN authors ON author_book.author_id = authors.id
;
SELECT
  *
FROM
  books,
  author_book,
  authors
WHERE books.id = author_book.book_id
  AND author_book.author_id = authors.id
;

リレーショナルデータベースにおいて上記のようなテーブル結合式は、製品に関わらず広く実装されていますが、例えばこれらは、MySQLオープンソース化された ver 3.23(2001年)の時点で、両者とも存在する伝統的な結合式です。

では、両者の違いはどこにあるのでしょうか?

(以下、クエリの実行は MySQL 8.0 です)

FROM 句での結合は用途が限定的

FROM 句に複数テーブルを指定して結合する場合、前提はデカルト積になります。つまり、クロス結合です。

SELECT id, name FROM tbl_a;
+----+--------------+
| id | name         |
+----+--------------+
|  1 | tbl a name 1 |
|  2 | tbl a name 2 |
|  3 | tbl a name 3 |
|  4 | tbl a name 4 |
|  5 | tbl a name 5 |
+----+--------------+

SELECT id, tbl_a_id, name FROM tbl_c;
+----+----------+--------------+
| id | tbl_a_id | name         |
+----+----------+--------------+
|  1 |        1 | tbl c name 1 |
|  2 |        1 | tbl c name 2 |
|  3 |        1 | tbl c name 3 |
|  4 |        1 | tbl c name 4 |
|  5 |        1 | tbl c name 5 |
+----+----------+--------------+

-- FROM 句でのテーブル結合
SELECT 
   tbl_a.id, 
   tbl_a.name,
   tbl_c.tbl_a_id,
   tbl_c.id, 
   tbl_c.name 
FROM 
   tbl_a, 
   tbl_c 
ORDER BY tbl_a.id, tbl_c.id;
+----+--------------+----------+----+--------------+
| id | name         | tbl_a_id | id | name         |
+----+--------------+----------+----+--------------+
|  1 | tbl a name 1 |        1 |  1 | tbl c name 1 |
|  1 | tbl a name 1 |        1 |  2 | tbl c name 2 |
|  1 | tbl a name 1 |        1 |  3 | tbl c name 3 |
|  1 | tbl a name 1 |        1 |  4 | tbl c name 4 |
|  1 | tbl a name 1 |        1 |  5 | tbl c name 5 |
|  2 | tbl a name 2 |        1 |  1 | tbl c name 1 |
|  2 | tbl a name 2 |        1 |  2 | tbl c name 2 |
|  2 | tbl a name 2 |        1 |  3 | tbl c name 3 |
|  2 | tbl a name 2 |        1 |  4 | tbl c name 4 |
|  2 | tbl a name 2 |        1 |  5 | tbl c name 5 |
|  3 | tbl a name 3 |        1 |  1 | tbl c name 1 |
|  3 | tbl a name 3 |        1 |  2 | tbl c name 2 |
|  3 | tbl a name 3 |        1 |  3 | tbl c name 3 |
|  3 | tbl a name 3 |        1 |  4 | tbl c name 4 |
|  3 | tbl a name 3 |        1 |  5 | tbl c name 5 |
|  4 | tbl a name 4 |        1 |  1 | tbl c name 1 |
|  4 | tbl a name 4 |        1 |  2 | tbl c name 2 |
|  4 | tbl a name 4 |        1 |  3 | tbl c name 3 |
|  4 | tbl a name 4 |        1 |  4 | tbl c name 4 |
|  4 | tbl a name 4 |        1 |  5 | tbl c name 5 |
|  5 | tbl a name 5 |        1 |  1 | tbl c name 1 |
|  5 | tbl a name 5 |        1 |  2 | tbl c name 2 |
|  5 | tbl a name 5 |        1 |  3 | tbl c name 3 |
|  5 | tbl a name 5 |        1 |  4 | tbl c name 4 |
|  5 | tbl a name 5 |        1 |  5 | tbl c name 5 |
+----+--------------+----------+----+--------------+

ここから更に、WHERE 句を指定することで、内部結合の状態を作り出します。

SELECT 
   tbl_a.id, 
   tbl_a.name,
   tbl_c.tbl_a_id,
   tbl_c.id, 
   tbl_c.name 
FROM 
   tbl_a, 
   tbl_c 
WHERE tbl_a.id = tbl_c.tbl_a_id
ORDER BY tbl_a.id, tbl_c.id;
+----+--------------+----------+----+--------------+
| id | name         | tbl_a_id | id | name         |
+----+--------------+----------+----+--------------+
|  1 | tbl a name 1 |        1 |  1 | tbl c name 1 |
|  1 | tbl a name 1 |        1 |  2 | tbl c name 2 |
|  1 | tbl a name 1 |        1 |  3 | tbl c name 3 |
|  1 | tbl a name 1 |        1 |  4 | tbl c name 4 |
|  1 | tbl a name 1 |        1 |  5 | tbl c name 5 |
+----+--------------+----------+----+--------------+

このように、FROM 句に複数テーブルを指定してテーブルを結合させるのは、クロス結合、ないし内部結合が前提となります。外部結合はできません。

JOIN 句は結合職人

JOIN 句を用いたテーブル結合は、内部結合、外部結合、そしてクロス結合と柔軟に結合式を組み立てられます。まさにテーブル結合のためのセンテンスです。

INNER JOIN tbl_b ON ... 
                     
LEFT JOIN tbl_b ON ...
                    
RIGHT JOIN tbl_b ON ...
                     
CROSS JOIN tbl_b

パフォーマンス比較

では、どちらの方がパフォーマンスが良いのでしょうか。

テーブルとデータを用意しました。本と出版の世界の一部を表現しています。

books テーブルには 100 万件のデータを収録し、あとはそれぞれ、1 対 1 または多対多のリレーションです。

books_count authors_count publishers_count managers_count author_book_count book_manager_count
1000000 100000 500 5000 1000000 1000000

実行クエリはそれぞれ以下になります。

-- JOIN 句で結合
SELECT
*
FROM books b
INNER JOIN author_book ab ON b.id=ab.book_id
INNER JOIN authors a ON ab.author_id=a.id
INNER JOIN publishers p on b.publisher_id=p.id
INNER JOIN book_manager bm ON b.id=bm.manager_id
INNER JOIN managers m ON bm.manager_id=m.id
;

-- FROM 句で結合
SELECT
  *
FROM
  books b,
  author_book ab,
  authors a,
  publishers p,
  book_manager bm,
  managers m
WHERE b.id=ab.book_id
  AND ab.author_id=a.id
  AND b.publisher_id=p.id
  AND b.id=bm.manager_id
  AND bm.manager_id=m.id
;

Explain

双方の実行計画を確認しましたが、両方とも全く同じ結果になりました。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE managers NULL ALL PRIMARY NULL NULL NULL 4845 100.00 NULL
1 SIMPLE books NULL eq_ref PRIMARY,books_publisher_id_foreign PRIMARY 8 sample.managers.id 1 100.00 NULL
1 SIMPLE publishers NULL eq_ref PRIMARY PRIMARY 8 sample.books.publisher_id 1 100.00 NULL
1 SIMPLE author_book NULL ref author_book_author_id_foreign,author_book_book_id_foreign author_book_book_id_foreign 8 sample.managers.id 1 100.00 NULL
1 SIMPLE authors NULL eq_ref PRIMARY PRIMARY 8 sample.author_book.author_id 1 100.00 NULL
1 SIMPLE book_manager NULL ref book_manager_manager_id_foreign book_manager_manager_id_foreign 8 sample.managers.id 190 100.00 NULL

JOIN 句は結合として明示的に意図を示せる句ですが、FROM 句に複数テーブルを指定した場合でも、結合として JOIN 句と同等に処理されることが確認できます。

実行時間

実行計画が同じではあったものの、念のため両者の実行時間を見てみます。

それぞれのクエリを 10 回実行し、その実行時間を確認します。

試行回数 JOIN: Execution time JOIN: Table lock wait time FROM: Execution time FROM: Table lock wait time
1 3.84146363 0.000132 3.70469371 0.000019
2 3.86136084 0.00015 3.85744995 0.000007
3 3.78419258 0.000018 3.77823229 0.000034
4 3.87785846 0.000184 3.80834454 0.00011
5 3.78526071 0.000016 3.81698154 0.000121
6 3.83468558 0.000057 3.79614567 0.000031
7 4.27876295 0.000013 3.76076388 0.000011
8 4.43237696 0.000016 3.82177008 0.000158
9 3.82524521 0.000123 3.98029196 0.000102
10 3.76645421 0.000033 3.88980984 0.000116
---------- ---------- ---------- ---------- ----------
AVERAGE 3.928766113 0.0000742 3.821448346 0.0000709

実行時間もほとんど差がないといえそうです。

どちらを使うべきか

パフォーマンスには差がないため、結果を抽出するということだけ考えたらどちらを使っても良いということになりますが、テーブル結合を行うならば、JOIN 句を使用しておくのが安全であると考えます。

なぜならば、FROM に複数のを指定した結合は、以下、可読性の面からヒューマンエラーが起こりやすいと感じているからです。

うっかりクロス結合

FROM 句で結合を行う際に最も注意しなければならないのは、結合条件の記述漏れです。

例えば、JOIN 句で結合を行う際は、JOIN 句に対して ON 句を記述することで、各 JOIN に対する結合条件を 1 行ずつ記述していくことができます。これは各テーブルの結合とその結合条件がセットで確認出来るため、条件やそれの指定漏れの確認も行いやすいです。

INNER JOIN author_book ab  ON b.id = author_book.book_id -- 結合テーブルと結合条件がセットで記述できる
INNER JOIN authors a       ON ab.author_id = a.id
INNER JOIN publishers p    ON b.publisher_id = p.id
INNER JOIN book_manager bm ON b.id = bm.manager_id
INNER JOIN managers m      ON bm.manager_id = m.id

対して、FROM 句で結合を行う際は、WHERE 句にて結合条件を記述していくことになります。

FROM
  -- 結合テーブル
  books b,
  author_book ab,
  authors a,
  publishers p,
  book_manager bm,
  managers m
WHERE b.id = ab.book_id     -- books, author_book の結合条件
  AND ab.author_id = a.id   -- author_book, authors の結合条件
  AND b.publisher_id = p.id -- books, publishers の結合条件
  AND b.id = bm.manager_id  -- books, book_manager の結合条件
  AND bm.manager_id = m.id  -- book_manager, managers の結合条件

この、結合したいテーブルと結合条件がセットにならない状況は、結合テーブルが増えた際に結合条件の記述漏れを引き起こす可能性が JOIN 句よりもかなり高いといえます。

前述の通り、結合条件を指定しない場合はクロス結合となるため、例えば今回用意したデータであれば、books, author_book の結合条件が未記述の場合、100 万件 × 100 万件 = 1 兆レコードの結果を返そうとすることになり、レコードの多いテーブルではなかなかリスクがあるなといえます。

(ちなみに JOIN 句であっても、結合条件を指定しなければクロス結合になるので注意)

不明瞭な境界線

JOIN 句では、結合後の絞り込みは、例えば以下のように記述するでしょう。

SELECT
    *
FROM tbl_a
INNER JOIN tbl_b ON tabl_a.id = tbl_b.tabl_a_id
WHERE tbl_b.score > 5 -- 結合後の絞り込み

結合テーブルとその条件があり、結合したデータに対して WHERE 句で絞り込みを行っています。

ON で使用される search_condition は、WHERE 句で使用できるフォームの条件式です。 通常、ON 句はテーブルの結合方法を指定する条件に使用され、WHERE 句は結果セットに含める行を制限します。

JOIN 句 - MySQL 8.0 リファレンスマニュアル

この表現を FROM 句での結合で行う場合は、以下のようなクエリになります。

SELECT
    *
FROM 
    tbl_a,
    tbl_b
WHERE tabl_a.id = tbl_b.tabl_a_id -- 結合条件
  AND tbl_b.score > 5  -- 結合後の絞り込み

結合条件と、結合後の絞り込みが同じ WHERE 句に混在することになります。シンプルなクエリなら良いかもしれませんが、結合数が多くなると可読性は一気に下がるはずです。

結合表現の一貫性

冒頭で述べた通り、FROM 句に複数テーブルを指定して結合する場合、クロス結合ないし内部結合が前提です。

外部結合のときだけ JOIN 句を用いるくらいなら、内部結合もクロス結合も JOIN を使っておけば、テーブル結合の表現は全て JOIN 句で統一されるため、メンテナンスもしやすいはずです。

SQL ステートメントは、意図の分かる作文でありたい。

どちらを使うにせよ、業務で SQL 文を作成するのであれば、欲しい結果を得る為だけでなく、意図が明確に伝わるようにクエリを組み立てていく意識は持ちたいものです。

いつかそのクエリをメンテナンスするであろう、チームの誰か、そして自分のために。

SQL 文は、パフォーマンスを大切にしながらも、生み出す自身の意図が簡単かつ適切に相手に伝わる作文でありたいですね。

参考:


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

Laravelを使用したStripeのCheckoutの決済済みかどうかの判定の実装

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

Laravelを使用したStripeのCheckoutの決済済みかどうかの判定の実装


前回と前々回の記事の続きです。 https://zenn.dev/ota_rg/articles/5bb03b17198f58 https://zenn.dev/ota_rg/articles/836c891b481c23

概要

今回はCheckoutで決済した後の決済、未決済の判定について紹介していきたいと思います。 バックエンドはLaravelを使用しています。

決済、未決済の判定をする方法は以下のように2種類あり、今回はそれらについて解説しています。 ・StripeのPaymentIntentの検索APIを使用し判定する方法Webhookを使用して判定する方法

結論、2種類の方法のうちWebhookのほうが早いので、そちらをお勧めします。

StripeのPaymentIntentの検索APIを使用し判定する方法

PaymentIntentの検索APIを使用し、PaymentIntentのstatusがsucceededだと決済が完了していることがわかります。 https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status

この方法は、Webhookに比べると遅いです。 決済して10秒くらい経ってもstatusがかわらないことがあるので、リトライ処理を作ったほうがいいです。

PaymentIntentとは

「支払いされたか、支払い方法はどうしたか」など支払いに関する情報が保存されているオブジェクトです。 これはCheckoutを使用した場合、決済をした後に作成されます。

https://stripe.com/docs/api/payment_intents

なぜPaymentIntentの検索APIを使用したか

Checkoutのセッションでも決済、未決済判定ができますが、 セッションIDをDBに保存しておくと考えたときに、 「決済前に2人セッションを作成したら、決済されるセッションと決済されないセッションができるので、どちらをDBに保存すればいいのかわからない」 という懸念点があります。

また、セッションの検索APIは、セッションIDでしか検索することができません。 よって、別の方法を考える必要がありました。

そこでMetadataという、自由に構造化情報を設定できるパラメーターを使用して検索できるPaymentIntentの検索APIを使用した。

https://stripe.com/docs/api/payment_intents/search

実装

metadataの設定

以下のようにCheckoutセッション作成時のpayment_intent_data['metadata'] のパラメーターに自由にmetadataを設定することができます。

$checkout = $stripe->checkout->sessions->create([
            'mode'                   => 'payment', // 支払いモード
            'payment_method_types'   => ['card'],
                ・
        ・
        ・
        ・
            // PaymentIntentのMetadata設定
            'payment_intent_data'    => [
                'metadata' => [
                    'reference_report_request_id' => 2,
            
                ],
            ],
        ],[
            'idempotency_key' => 'test',
        ]);

サンプルコード

支払いが完了していて、顧客とメタデータのitem_idで絞り込んだPaymentIntentを検索するコードです。 PaymentIntentのstatusがsucceededだと支払いが完了しています。

public function getSession(Request $request): JsonResponse
    {
        $config = config('define.stripe.token');
        $stripe = new StripeClient($config);
        //PaymentIntentsの検索
        $paymentIntents=$stripe->paymentIntents->search([
            'query' => 'status:\'succeeded\' AND customer:\'customer_id\' AND metadata[\'item_id\']:\'1\'',
        ]);
        return response()->json($paymentIntents);
}

重要なところの解説

PaymentIntentsの検索は$stripe->paymentIntents->search()で行っていますが、以下のクエリの書き方に注意する。

  • =が:になる
  • 'が「\'」になる

Webhookを使用した方法

StripeのWebhookを使用し、支払い完了イベントである「payment_intent.succeeded」を受け取ったら支払いが完了していることがわかります。

この方法はPaymentIntent検索APIを使う方法より、早く支払い完了であることがわかるのでお勧めです。

準備

ローカル環境 https://dashboard.stripe.com/test/webhooks/create?endpoint_location=local

  • 受信するイベントを絞り込む 本番または開発環境のみで可能で、エンドポイント作成の時の「リッスンするイベントの選択」で 「payment_intent.succeeded」を選択することで、支払い成功以外のイベントを受信しなくなる

実装

サンプルコード

public function webhook()
    {
        // 参考:https://stripe.com/docs/payments/checkout/fulfill-orders

        Stripe::setApiKey(env('STRIPE_SECRET'));

        // StripeのWebhookのendpoint_secret
        $endpoint_secret = 'endpoint_secret';

        $payload = @file_get_contents('php://input');
        $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
        $event = null;

        try {
            $event = Webhook::constructEvent(
                $payload, $sig_header, $endpoint_secret
            );
        } catch(\UnexpectedValueException $e) {
            // Invalid payload
            http_response_code(400);
            exit();
        } catch(\Stripe\Exception\SignatureVerificationException $e) {
            // Invalid signature
            http_response_code(400);
            exit();
        }

        if($event->type==='payment_intent.succeeded'){
        $payment_intent = event.data.object
            //支払いが完了した後に行いたい処理をかく
        }

        http_response_code(200);
    }

重要なところの解説

以下のようにpayment_intent.succeededのイベントを受け取るとevent.data.objectでPaymentIntentを取得することができる。 PaymentIntentに事前にメタデータを設定することで、自前DBの商品IDやユーザーIDで誰のものかを特定することができる。

 if($event->type==='payment_intent.succeeded'){
        $payment_intent = event.data.object
            //支払いが完了した後に行いたい処理をかく
        }

終わりに

今回はLaravelを使用したStripeのCheckoutでの決済済みかどうかの判定の解説をしました。 PaymentIntentのステータスが変わる時間にはムラがあるということが、実装してから分かったので、その対応策を考えたり、リトライ処理を仕込むのが大変でした。 皆さんは最初からWebhookを使いましょう。


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

Nuxt.js+LaravelでStripeのCheckoutの実装をやってみた【決済フォーム埋め込み編】

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

Nuxt.js+LaravelでStripeのCheckoutの実装をやってみた【決済フォーム埋め込み編】


前回の記事の続きです。 https://zenn.dev/ota_rg/articles/5bb03b17198f58

概要

LaravelとNuxt.jsでStripeの決済処理を実装しました。 今回はCheckoutの「自前のページに決済フォームを埋め込む方法」の紹介をしていきたいと思います。

https://stripe.com/docs/payments/checkout/how-checkout-works?payment-ui=embeddable-payment-form

実装したもの

モーダルにStripeの決済フォームを埋め込み、顧客が1つの商品を購入して完了するまでの実装をしました

https://youtu.be/Oz1fp_TcQZE

事前準備

前回説明したので、詳細は割愛しますが、以下の準備をしてください。

  • StripeのAPIキーの取得
  • 商品の追加
  • 顧客アカウントの作成

処理の簡単な流れ

  1. フロントがバックエンドのCheckoutセッション作成APIを叩く(フロント)
  2. バックエンドがstripeのセッション作成APIを叩き、その結果をフロントに返す(バックエンド)
  3. フロントがモーダルを表示する
  4. フロントがレスポンスのclient_secretを使用し、stripeの決済フォームを作成、モーダルにマウントする(フロント)
  5. 決済をし、任意のページにリダイレクト(フロント)

バックエンドの実装(Laravel)

前回同様にstripe-phpという、 Stripe公式のphp用のライブラリを使用して実装しました。 ライブラリを使用し、StripeのCheckoutセッション作成APIを叩き、その結果を返すAPIの作成をしました。

https://stripe.com/docs/api/checkout/sessions/create https://github.com/stripe/stripe-php

重要なところの解説

$stripe->checkout->sessions->create()でStripeのCheckoutセッション作成APIを叩いています。 前回も説明したので、今回は埋め込み式にする上で必要なパラメーターのみ解説します。

ui_mode

このパラメーターをembeddedにすることで、埋め込み式に変わります。 デフォルトはhostedとなっており、Stripeの決済ページに移動する方法になっています。

return_url

埋め込み式の場合のみ使用し、決済成功後のリダイレクト先のURLを指定する。 Stripeの決済ページに移動する方の時は、success_urlとcancel_urlとなっていて、少し違うので注意する

redirect_on_completion

if_requiredに設定することで、決済が成功した後にreturn_urlで指定したURLにリダイレクトしなくなり、 フロントエンドでstripe.initEmbeddedCheckoutの時のonCompleteに自由に処理を書くことができる。

https://stripe.com/docs/payments/checkout/custom-redirect-behavior

ソースコード

public function createCheckoutSession(): JsonResponse
    {
        /** @var string $config */

        $config = config('define.stripe.token');
        $stripe = new StripeClient($config);
        //Checkoutセッション作成
        $checkout = $stripe->checkout->sessions->create([
            // 商品
            'line_items'             => [[
                'price'    => 'price_id',//商品ID
                'quantity' => 1,//個数
            ],
            ],
            'mode'                   => 'payment', // 支払いモード
            'customer'               => 'customer_id', // 顧客ID 
            'ui_mode' => 'embedded',// 埋め込み式にするため
            //支払い成功時のリダイレクト先 ({CHECKOUT_SESSION_ID}とするとセッションIDが取得できる)
            'return_url' => 'http://localhost:8080/success',
            // 税金を自動徴収するかどうか(3万の商品だったら、決済ページで3万3千円になる)
            'automatic_tax'          => [
                'enabled' => true,
            ],
            //フロントのstripe.initEmbeddedCheckoutの時にonCompleteを使用することができる。ただし、return_urlには行かなくなる
            //参考:https://stripe.com/docs/js/embedded_checkout/init#embedded_checkout_init-options-onComplete
            'redirect_on_completion'=> 'if_required',
            // 支払い方法を保存するかどうか
            'payment_method_options' => [
                'card' => [
                    'setup_future_usage' => 'on_session',
                ],
            ],
            // 支払い方法
            'payment_method_types'   => ['card']
        ]);
        return response()->json($checkout);
    }

フロントエンドの実装

ボタンを押したら、バックエンドのセッション作成のAPIを叩き、モーダルを表示し、レスポンスのclientSecretを使用し、Stripeの決済フォームを作成し、モーダルにマウントするようになっています。

前回同様に、stripe-jsという、Stripeの公式のJavaScript用のライブラリを使用しました。 モーダルは、vue-js-modalというライブラリを使用しました。 https://www.npmjs.com/package/vue-js-modal

重要なところの解説

決済フォームの作成

バックエンドからもらったclientSecretを使用し、ライブラリのinitEmbeddedCheckout()を呼べば、 Stripeの決済フォームを作成します。

const checkout = await stripe?.initEmbeddedCheckout({
            clientSecret: res.data.client_secret,
          })
// Mount Checkout
checkout?.mount('#checkout')

マウントしたフォームの削除

Stripeの決済フォームを一度マウントした後に、そのままもう一度フォームを作成し、マウントしようとするとエラーになる。 なので、以下のようにモーダルを閉じるたびにマウントしたフォームを削除する必要があります。 vue-js-modalを使用しているので、hide()に処理を書くだけでモーダルが閉じた時の処理を書くことができる。

async hide() {
      // 一度destroyしないとcheckoutが残ってしまうので削除
      await this.checkout?.destroy()
      await this.$modal.hide('modal-content')
    },

ソースコード

<template>
  <div>
    <div>
      <button @click="submit">支払いをする</button>
    </div>

    <modal
      name="modal-content"
      height="auto"
      :scrollable="true"
      :click-to-close="false"
    >
      <button @click="hide">閉じる</button>
      <div id="checkout">
        <!-- Checkout will insert the payment form here -->
      </div>
    </modal>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { StripeEmbeddedCheckout, loadStripe } from '@stripe/stripe-js'
import axios from 'axios'

export default Vue.extend({
  name: 'StripeTest',
  head: () => ({
    title: 'StripeTest | back check',
  }),
  components: {},
  data() {
    return {
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
      loading: false,
      clientSecret: '',
      checkout: undefined as StripeEmbeddedCheckout | undefined,
    }
  },

  methods: {
    async submit() {
      const stripe = await loadStripe(
        process.env.STRIPE_PUBLISHABLE_KEY
          ? process.env.STRIPE_PUBLISHABLE_KEY
          : '',
      )

      await axios
        .post('http://localhost:8080/api/create_checkout_session')
        .then(async (res) => {
          this.$modal.show('modal-content')
          this.clientSecret = res.data.client_secret
          // StripeのCheckoutの作成
          const checkout = await stripe?.initEmbeddedCheckout({
            clientSecret: res.data.client_secret,
          })
          this.checkout = checkout

          // Mount Checkout
          checkout?.mount('#checkout')
        })
    },


    async hide() {
      // 一度destroyしないとcheckoutが残ってしまうので削除
      await this.checkout?.destroy()
      await this.$modal.hide('modal-content')
    },
  },
})
</script>

終わりに

今回は、StripeのCheckoutの実装で決済フォームを自前のページに埋め込む方法で行いました。 前回の決済フォームに遷移しての決済と比べると、モーダルなどを使用した分少し工数はかかりましたが、そこまで工数がかかるわけではないので、UIを重視したい方はこちらをお勧めします。 Stripeには、決済フォーム自体を自前で作る方法もあるので、さらにUIにこだわりたい方はそちらを使用してください。ただし工数がかなりかかると思います、、、


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

Nuxt.js+LaravelでStripeのCheckoutの実装をやってみた【Stripe決済ページ遷移編】

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

Nuxt.js+LaravelでStripeのCheckoutの実装をやってみた【Stripe決済ページ遷移編】


概要

LaravelとNuxt.jsでStripeの決済処理を実装しました。 Stripeにはいくつか決済方法があるのですが今回はCheckoutと呼ばれる、Stripeが用意してくれた決済フォームを使用しました。 また、Checkoutにも2種類存在し、「決済時にStripeの決済ページに遷移する方法」と「自前のページに決済フォームを埋め込む方法」があるのですが、今回は「決済時にStripeの決済ページに遷移する方法」の紹介になります。

https://stripe.com/docs/payments/checkout

実装したもの

顧客が1つの商品を購入して完了するまでの実装をしました

https://youtu.be/ovSFHPwcZGY

事前準備

StripeのAPIキーの取得

以下を参考にAPIキーを取得してください。

https://stripe.com/docs/keys?locale=ja-JP#reveal-an-api-secret-key-for-test-mode

公開可能キーはフロントエンド側で使用するキー。 シークレットキーはバックエンド側で使用するAPIキー。

商品の追加

ダッシュボードの商品カタログから、購入してもらいたい商品を追加し、そのAPI IDをメモしておく。(バックエンドで使用する)

顧客アカウントの作成

誰が購入したかがわかるように、Stripeの顧客アカウントを作成する。 今回は記載していないが、Stripe APIを使用して顧客アカウントを作成も可能。

ダッシュボードの顧客より作成し、顧客IDをメモしておく。(バックエンドで使用する)

決済の簡単な流れ

  1. フロントがバックエンドのCheckoutセッション作成APIを叩く(フロント)
  2. バックエンドがstripeのセッション作成APIを叩き、その結果をフロントに返す(バックエンド)
  3. フロントがレスポンスのsessionIdを使用し、stripeの決済ページにリダイレクトする(フロント)
  4. 決済をし、任意のページにリダイレクト(フロント)

詳しくは以下をチェック↓

https://stripe.com/docs/payments/checkout/how-checkout-works?locale=ja-JP#lifecycle

バックエンドの実装(Laravel)

ライブラリを使用し、StripeのCheckoutセッション作成APIを叩き、その結果を返すAPIの作成をしました。

https://stripe.com/docs/api/checkout/sessions/create

ライブラリについて

stripe-phpという、 Stripe公式のphp用のライブラリを使用して実装しました。 採用理由としては、公式で信頼できるという点とドキュメントや使用例がたくさんあった点です。

https://github.com/stripe/stripe-php

Laravel CahierというLaravelが提供している別のライブラリも存在したのですが、こちらはドキュメントや使用例が少なく、サブスク支払がメイン機能そうだったので、今回は使用しませんでした。

https://readouble.com/laravel/9.x/ja/billing.html

重要なところの解説

$stripe->checkout->sessions->create()でStripeのCheckoutセッション作成APIを叩いています。以下にパラメーターについて解説します。

パラメーター 説明
line_items->price 顧客に購入してほしい商品のID。事前準備の商品IDを記載する
line_items->quantity 顧客に購入してもらう商品の個数
mode 今支払うか、後払いにするか、サブスクにするかを選択できる
customer Stripeの顧客アカウントID。誰が購入したかの把握ができる。事前準備の顧客IDを記載する
success_url 支払いに成功した時のリダイレクト先
cancel_url Stripeの決済ページの戻りボタンを押した時のリダイレクト先
automatic_tax->enable 税金の自動徴収をするかどうか。オンにすると価格1万円に設定した商品が、 税込の1万1千円で決済される
payment_method_options->card->setup_future_usage クレジットカードの情報を保存する。off_sessionにするとずっと保存してくれる
payment_method_types 支払い方法をどうするか。カード決済、ApplePayでの支払いなど選択できる
idempotency_key 多重決済を防ぐためのキー。 ユニークである必要がある。顧客が商品を購入する時の初回に作成し、それ以降はDBに保存されたこのキーを参照する

ソースコード

public function createCheckoutSession(): JsonResponse
    {
        /** @var string $config */
        $config = config('define.stripe.token');
        $stripe = new StripeClient($config);

        $checkout = $stripe->checkout->sessions->create([
            // 商品
            'line_items'             => [[
                'price'    => 'price_id',//商品ID
                'quantity' => 1,//個数
            ],
            ],
            'mode'                   => 'payment', // 支払いモード
            'customer'               => 'customer_id',//顧客ID 
            'success_url'            => 'http://localhost:8080/success,
            'cancel_url'             => 'http://localhost:8080/cancel', 
            // 税金を自動徴収するかどうか
            'automatic_tax'          => [
                'enabled' => true,
            ],
            // 支払い方法を保存するかどうか
            'payment_method_options' => [
                'card' => [
                    'setup_future_usage' => 'on_session',
                ],
            ],
            // 支払い方法
            'payment_method_types'   => ['card'],
        ],[
            'idempotency_key' => 'G5pobWIO8Q0ODYK1313333333',
        ]);
        return response()->json($checkout);
    }

フロントエンドの実装

ボタンを押したら、バックエンドのセッション作成のAPIを叩き、レスポンスのsessionIdを使用し、Stripeの決済ページにリダイレクトさせるような実装になっています。

stripe-jsでの実装

stripe-jsという、Stripeの公式のJavaScript用のライブラリを使用した実装方法の紹介です。

https://github.com/stripe/stripe-js

重要なところの解説

バックエンドからもらったclientSecretを使用し、ライブラリのredirectToCheckout()を呼べば、 Stripeの決済ページにリダイレクトします。

// Stripeのページにリダイレクトする
await stripe?.redirectToCheckout({
  sessionId: res.data.id,
})

ソースコード

<template>
  <div>
    <div>
      <button @click="submit">支払いをする</button>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { StripeEmbeddedCheckout, loadStripe } from '@stripe/stripe-js'
import axios from 'axios'

export default Vue.extend({
  name: 'StripeTest',
  head: () => ({
    title: 'StripeTest | back check',
  }),
  components: {},
  data() {
    return {
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
      loading: false,
      sessionId: '',
      checkout: undefined as StripeEmbeddedCheckout | undefined,
    }
  },

  methods: {
    async submit() {
      const stripe = await loadStripe(
        process.env.STRIPE_PUBLISHABLE_KEY
          ? process.env.STRIPE_PUBLISHABLE_KEY
          : ''
      )

      await axios
        .post('http://localhost:8080/api/create_session')
        .then(async (res) => {
          // Stripeのページにリダイレクトする
          await stripe?.redirectToCheckout({
            sessionId: res.data.id,
          })
        })
    },
})
</script>

vue-stripeでの実装

vue-stripeとは、Stripeの認定パートナーのライブラリです。 ドキュメントが充実している。Checkoutで、Stripeの決済ページに遷移する方は実装できるが、自前のページに決済フォームを埋め込む方法は実装できません。 stripe-jsの方がコード量少ないので、個人的にはstripe-jsをお勧めします。

https://vuestripe.com

重要なところの解説

以下のように、ライブラリのコンポーネントを使用し、バックエンドからのsessionIdをコンポーネントに設定し、this.$refs.checkoutRef.redirectToCheckout();を呼び出すだけで、Stripeの決済ページに遷移する。

<stripe-checkout
      ref="checkoutRef"
      mode="payment"
      :pk="publishableKey"
      :session-id="sessionId"
      @loading="(v) => (loading = v)"
/>

ソースコード

<template>
  <div>
    <stripe-checkout
      ref="checkoutRef"
      mode="payment"
      :pk="publishableKey"
      :session-id="sessionId"
      @loading="(v) => (loading = v)"
    />
    <div>
      <button @click="submit">支払いをする</button>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { StripeCheckout } from '@vue-stripe/vue-stripe'
import axios from 'axios'

export default Vue.extend({
  name: 'StripeTest',
  head: () => ({
    title: 'StripeTest | back check',
  }),
  components: {
    StripeCheckout,
  },
  data() {
    return {
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
      loading: false,
      sessionId: '',
    }
  },

  methods: {
    async submit() {
      await axios
        .post('http://localhost:8080/create_checkout_session')
        .then((res) => {
          this.sessionId = res.data.id
        })
        .then(() => {
          ;(this.$refs.checkoutRef as any).redirectToCheckout()
        })
    },
  },
})
</script>

終わりに

今回はStripeを使用し、決済ページに遷移するCheckoutの実装をやってみました。 Stripeの使用は初めてだったのですが、実装もそこまで難しなく、簡単に決済処理が作れて驚きでした。 ダッシュボードも使いやすかったので、決済の導入にはStripeをお勧めします。


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

Node.js サーバーアプリケーションにおけるトークンバケットを用いたレートリミットの実現

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

Node.js サーバーアプリケーションにおけるトークンバケットを用いたレートリミットの実現

はじめに

こんにちは、株式会社 ROXX で back check というサービスを開発しているぐっきー(@Area029S)です。

リクエストを受け付けるごとに重い処理が実行される機能の実装などにおいては一定の時間内に大量に処理を実行するとシステムのリソースを圧迫してしまう可能性があるため注意が必要です。 このような処理をリソースを圧迫させずに処理する方法の1つとして、レートリミット(時間あたりの実行数の制御)の導入があげられます。

今回はネットワークのトラフィックシェービングなどに利用されることの多いトークバケットアルゴリズムを導入することで、サーバー側のアプリケーションでレートリミットを実現しようと思います。

レートリミットとは?

レートリミットとは主に時間あたりのネットワークトラフィックを制御するために用いられる戦略です。

一般的には下記のようなことに役立ちます。

  • アプリケーションリソースの枯渇を防ぐ
    • 特定のエンドポイントへのリクエスト数を制限する
  • 運用コストの超過を防ぐ
    • ChatGPT など利用ごとに課金される外部 API を利用したサービスのリクエスト数を制限することにより出費を管理する
  • 悪意のあるユーザーから API を保護する

参照: The Fundamentals of Rate Limiting: How it Works and Why You Need it

外部 API を利用したサービスのリクエスト数を制限する

今回のやりたいことであるリクエスト毎に重い処理を実行したい要件においては、上記の文脈でレートリミットを適用することでシステムリソースの負荷を分散させる用途に利用できそうです。

レートリミットを実装するための一般的なアルゴリズムには下記のようなものが存在します。

  • token bucket
  • leaky bucket
  • fixed window counter
  • sliding window log
  • sliding window counter

参照:様々な rate limit アルゴリズム

今回は高負荷の掛かるリクエストが大量に来たときは処理量を制限しつつ、制限の範囲内においては短期間に多数のリクエストを処理(バースト)することの可能な方式を利用したいため token bucket がよさそうです。そこで token bucket の特徴について説明します。

token bucketトークバケットアルゴリズム)とは?

token bucket

画像引用:様々な rate limit アルゴリズム より

概要

トークバケットアルゴリズムは、リクエストのレートを一定の制限内に保ちつつ、短期的なトラフィックの増加(バースト)も許容するための手法です。このアルゴリズムは、トークンの生成速度とバケットのサイズを利用してリクエストの平均処理量に制限をかけます。柔軟かつ効率的なレートリミットの手段として、ネットワーク環境からアプリケーションまで幅広く採用されています。

主要用語

トークバケットアルゴリズムを理解する上で重要な用語は以下の通りです。

  1. トーク
  2. バケット
  3. トークンの生成速度
  4. トークンの消費
  5. バースト
    • バーストは、短期間に多数のリクエストを許可する能力です。バケットが満杯の場合、短時間で多数のリクエストを処理できますが、その後トークンが不足するとトークンの再生成を待たなければなりません。

仕組み

トークバケットアルゴリズムでは、定められた速度でトークンがバケットに追加されます。バケットはこれらのトークンを保持し、そのサイズには上限があります。バケットが満杯の場合、新たに生成されるトークンは失われます。

リクエストが行われると、バケットからトークンが消費されます。処理するためには、必要な数のトークンがバケット内に存在する必要があります。十分なトークンがない場合、リクエストは待機状態に入ります。

このアルゴリズムによるレートリミットは、トークンの生成速度とバケットのサイズによって決定されます。これにより、長期的には一定のレートが保たれる一方で、バケットが満杯の状態では短期間のバーストが可能になります。

もっとわかりやすく説明してほしい!という方には下記のブログをおすすめします。

トークンバケットアルゴリズムではない、MP アルゴリズムと呼べ

Node.jsにおける実装例

Node.js での実装例を簡単にまとめます。

ライブラリの活用

トークバケットアルゴリズムを自前で実装するとそこそこの工数はかかりそうだということで、ライブラリがないか探してみます。 npm trends をみてみるとレートリミットを実装するものがいくつか見つかり、 limiter というライブラリが最も利用されてることがわかりました。 (比較的レートリミット系ライブラリ自体あまり出回っていないようです)

参照: npm trends - bottleneck vs leaky-bucket vs limiter vs tokenbucket

そこで今回は limiter を利用します。

実装例

コードの概要としては、リクエストを受け付けるごとに重い処理が非同期に実行される機能を想定しています。 そのため express でサーバーをたて、 bull というライブラリを用いて local 環境で動作するキューワーカーを立ち上げます。

キューに追加されたジョブを処理する箇所でレートリミットを設定することで、クライアントからのリクエストは受け取りつつ、ミッションクリティカルな処理の実行はトークバケットを利用したレートリミット制限下で実行できる実装となっています。

github.com

const express = require("express");
const Queue = require("bull");
const { TokenBucket } = require("limiter");

// トークンバケットによるレートリミッターを設定
const bucket = new TokenBucket({
  bucketSize: 2, // トークンの最大保持可能数
  tokensPerInterval: 1, // 1分間に生成されるトークン数
  interval: "minute", // トークン生成の間隔
});

// キューの作成
const myQueue = new Queue("myQueue");

const addJobToQueue = (data) => {
  myQueue.add(data);
}

const someProcess = () => {
  const currentTime = new Date().toLocaleString();
  console.log(`Processing at: ${currentTime}`);
};

// キューでのジョブの処理
myQueue.process(async function(job, done) {
  console.log("Processing job");
  await bucket.removeTokens(1);
  someProcess();
  done();
});

const app = express();
app.use(express.json());

app.post("/job", (req, res) => {
  addJobToQueue(req.body);
  res.send("Job added to queue");
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

それでは具体的に下記の設定でトークバケットを動かしてみます。

バケットサイズ: 2 トークンの生成速度: 1 個/分 処理実行時のトークンの消費数: 1 個

下記の振る舞いが確認できれば機能していることがわかります。

  1. 3分間待機してバケットトークンが溜まるのを待つ。バケットトークンを保持できる最大数(バケットサイズ)を2に設定しているため、3つめに生成されたトークンは破棄される。
  2. curlにて連続で3回リクエストを送信する。2個は即座に処理され、1分後にバケットに空きがある状態でトークンが生成されるため、生成されたトークンを使い残りの処理が実行される。

実行結果:

output

想定した動作をすることが確認できました。

おわりに

今回は受け付けたリクエストに対して非同期に処理を行う Node.js サーバーアプリケーションにおいてトークバケットを用いたレートリミットを実装してみました。 レートリミットのアルゴリズムはとっつきづらいイメージを持ちがちですが、1度理解できると便利なので読んでいただいた方の学習の促進に少しでも繋がると幸いです。 また、本記事内でリンクを貼らせていただいている関連の記事もわかりやすいのでぜひご覧ください。

BigQuery やさしいはじめの一歩〜実際に触って理解するデータ操作ワークショップ〜

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

www.ritolab.com


BigQuery

BigQueryは、Google Cloud Platform(GCP)が提供するフルマネージドのサーバーレスデータウェアハウスです。膨大な量のデータを高速に分析することが可能で、データ分析やビジネスインテリジェンス(BI)の分野で広く利用されています。

BigQuery では、データに対してクエリを実行することで、ほしいデータに絞って取得したり、集計したりなど、データを分析することができます。

BigQuery - Google Cloud

対象

本記事は、BigQuery を用いてデータの操作を行いたい人が対象です。

  • 本記事はワークショップ(ハンズオン)形式で構成しており、実際に手を動かしながら、記事の通りにデータ操作を行っていくことで、BigQuery に関する基礎的な理解を深められます。
  • 非エンジニアで、データ操作が初めてのひとを対象にしています。

BigQuery の料金体系と無料枠

まずはじめに大切なお金のことから。BigQuery は、全てを無料で利用できるものではありません。まずは料金体系について知っておきましょう。(本記事は無料枠内で実施します)

BigQuery の料金体系は、大きく分けて以下の2つがあります。

  • ストレージ料金
    • データの保存量に対して課金されます。
  • 分析料金
    • クエリでスキャンされたデータに対して課金されます。

また、BigQuery には無料枠が用意されています。

  • ストレージ料金
    • 10GB まで無料
  • 分析料金
    • 毎月 1TB まで無料

つまり、10GB 以内のデータを保存し、合計 1TB 以内のクエリを実行する場合、無料枠の範囲内であれば、料金は発生しません。

(BigQuery は、デフォルトでオンデマンド分析料金が適用されます。オンデマンド分析料金は、クエリでスキャンされたデータに対して課金されます。)

BigQuery の料金 - Google Cloud

BigQuery サンドボックスで利用を開始する

Google アカウントにログインした状態で以下の URL にアクセスすると、サンドボックスモードで BigQuery を利用できます。(クレジットカードの登録不要)

https://console.cloud.google.com/bigquery

アクセス後の BigQuery コンソール画面です。

BigQuery でのデータ管理

BigQuery には、「プロジェクト」「データセット」「テーブル」という分類があり、それぞれが階層になりリソース(データ)を分類します。

  • プロジェクト(大分類)
    • データセット(小分類)
      • テーブル(データ)

これらの分類を以てリソースを管理します。プロジェクトとは、BigQuery の最も最上階の分類です。

例えば、以下のように分類を分けて、データの管理を分かりやすくしましょうというお話です。

  • 練習用(プロジェクト)
    • アイスクリームの売上(データセット
      • 店舗売上データ(テーブル)
      • 地域天気データ(テーブル)
      • 人件費データ(テーブル)
    • 模試の成績(データセット
      • 学校データ(テーブル)
      • 生徒データ(テーブル)
      • 模試結果データ(テーブル)

プロジェクト・データセット・テーブルは複数作成できます。

なお、プロジェクトやデータセットは必須のため、データを操作する前に、これから以下の作業を行います。

  1. プロジェクトを作成する
  2. データセットを作成する
  3. テーブルを作成する(データのインポート)

プロジェクトを作成する

プロジェクトを作成します。[プロジェクトを作成] を押下します。

プロジェクト名には任意の名前を入力し、[作成] ボタンを押下します。

プロジェクトの作成に成功すると、BigQuery API の有効化画面に遷移するので、[有効にする] ボタンを押下して有効にします。

しばらくするとコンソール画面に戻ってきます。これでプロジェクトの作成は完了です。作成したプロジェクト名が表示されています。

データセットを作成する

次に、データセットを作成します。

データセットとは、これからプロジェクトの中に作成していく、いくつかのテーブル(表データ)をグループ分けするものだと考えてください。

左メニューに表示されているデータセット名の左側に、三角のアイコンがあるので押下してみてください。

現在はまだデータセットを作成していない状態のため、プロジェクト以下を見ても「外部接続」という表示しかありません。

データセットを作成すると、ここに新しいグループが作成されます。

それでは、データセットを作成します。プロジェクト名の右側にある三点リーダーを押下し、表示されるメニューから「データセットを作成」を押下します。

データセット作成フォームが表示されるので、それぞれ以下のフォームに入力を行います。

  • データセット ID
    • データセットの名前です。英数字とアンダースコアを使って任意の名前をつけましょう。ここでは sample_ice_cream としています。
  • ロケーション タイプ
    • リージョン を選択します。
    • リージョン: あなたがもし日本で作業しているのならば asia-northeast1(東京) を選択します。

入力が完了したら [データセットを作成] ボタンを押下します。

作成が完了すると、プロジェクト配下にデータセットが表示され、新たなデータセットが作成されたことを確認できます。

これでデータセットの作成は完了です。

テーブルを作成する(データのインポート)

分析する対象となるデータをインポート(読み込み)し、テーブルを作成してみましょう。テーブルとは、例えばスプレッドシートやエクセルでいうところの表データです。

データを読み込みテーブルを作成すると、上のキャプチャ画像のスプレッドシートのように、行と列で構成されたデータがそのまま BigQuery のデータベース上に入るとイメージしてください。

練習用データ

今回使用するのは、練習用のデータとして作成した「とある店舗でのアイスクリーム販売履歴」です。

データ概要

  • データ日時範囲:
    • 2023/01/01 〜 2024/12/31 までの 2 年間
  • データ件数:
    • 10000 件
  • カラム(列)
    • purchased_at: 購入日時
    • store: 店舗
      • 店舗 A, B, C, D の 4 店舗
    • item_name: 品名
    • amount: 金額
    • member_id: ポイントメンバー会員 ID

練習用データは公開しているので誰でもアクセスできます。
BigQueryはじめの一歩サンプルデータ - 1. アイスクリーム販売履歴(スプレッドシート)

データの読み込みは CSV ファイルで行いますので、練習用データのスプレッドシートCSV ファイルとしてダウンロードしてください。

ファイル > ダウンロード > カンマ区切り形式(.csv

テーブル作成

それではデータをインポートしテーブルを作成します。

コンソール画面「独自データの追加」から「ローカル ファイル」にある [データを追加する] を押下します。

テーブル作成フォームが表示されるので、それぞれ以下のフォームに入力を行います。少し入力が多いですが頑張ってください。

  • ファイルを選択
    • ダウンロードした練習用 CSV ファイルを選択します。
  • ファイル形式
    • ファイルを選択すると自動的に CSV になります。CSV にならない場合は選択してください。
  • データセット
    • 先程作成したデータセット名を入力します。ここでは sample_ice_cream としています。
  • テーブル
    • 読み込むデータの名前です。英数字とアンダースコアを用いて任意の名前をつけましょう。アイスクリームの販売履歴なので、ここでは ice_cream_sales としています。
  • スキーマ
    • 以下を入力しましょう。モードと説明はデフォルトのままで構いません。
      • [1] フィールド名: purchased_at, タイプ: DATETIME,
      • [2] フィールド名: store, タイプ: STRING,
      • [3] フィールド名: item_name, タイプ: STRING,
      • [4] フィールド名: amount, タイプ: INTEGER,
      • [5] フィールド名: member_id, タイプ: INTEGER,
  • 詳細オプション
    • スキップするヘッター行
      • 練習用データの先頭行にはカラム(列)名が入っているため、1 を入力します。

全て入力できたら、[テーブを作成] ボタンを押下します。

テーブルの作成が完了すると、BigQuery コンソール画面にテーブルが表示されます。sample_ice_cream データセット配下に、ice_cream_sales テーブルが作成されたことが確認できます。

プレビューというタブを押下すると、ice_cream_sales テーブルに入っているデータを確認できます。

これでテーブルの作成(データのインポート)は完了です。

次の章から、実際にクエリを実行してみましょう。

データベースと会話する

これは先程作成した、アイスクリームの販売履歴のテーブルのデータです。

こういった、スプレッドシートやエクセルのように、行と列で構成されるテーブルを持つデータベースを「リレーショナルデータベース」と呼びます。

リレーショナルデータベースの詳細についてはここでは重要ではないので割愛します。

そして文字が長いので、本記事では「リレーショナルデータベース」を「データベース」と呼ぶことにします。

データベースの種別はリレーショナルデータベース以外にも存在しますが、本記事で「データベース」と言ったらリレーショナルデータベースのことだと思ってください。

さて。

BigQuery に限らず、データベースからほしいデータに絞ってを取得したり、データを集計したりなど加工したりするには、SQL という言語を使います。

日本人と会話するなら日本語、英語圏の人と会話するなら英語。のように、データベースと対話するなら SQL といったイメージです。

SQL を使えばデータベースと会話ができて、データベースが応答してくれます。


あなた: 「あのデータをちょうだい(SELECT id, name FROM my_table ORDER BY id DESC LIMIT 100)」

データベース: 「はい、これですよ(id, mane, 1, xxx, 2 ,xxx ,...)」

あなた: 「このデータをあのテーブルにしまっておいて(INSERT my_table (id, name) VALUES (123, xxx))」

データベース: 「はい、テーブルにレコードを追加しました(Query OK, 1 row affected)」


SQL

SQL とは、Structured Query Language(構造化クエリ言語)の略称で、データベースを操作するための言語です。

SQL を使えば、データベースにデータを追加したり、データを変更したり、データを検索したりすることができます。

データベースには様々な製品があり、BigQuery もその 1 つです。代表的なものだと他には、MySQL, PostgreSQL, Oracle Database, Microsoft SQL Server などがありますが、これらを操作するための言語のベースは全て SQL です。

データベースを操作する上で SQL は共通言語であり、その上で、各データベース製品によって若干文法が違ったりします。

つまり、SQL を覚えれば、あとは各データベース製品ごとの書き方のお約束を知るだけで、BigQuery でも MySQL でも操作できるようになります。

SQL を「言語」とするならば、各データベース製品ごとの文法の違いは「方言」のようなものだと思ってください。

SQL の基本

SQL の文法はシンプルです。データを取得する時の基本的な文法は以下になります。

SELECT
  見たい列の名前 1,
  見たい列の名前 2,
  見たい列の名前 3,
FROM テーブル名
;

SELECT 句は、データを取得する際の構文です。見たいカラム(行)を指定して、それらをどのテーブルから取得するのかを FROM 句で指定してあげるだけです。

SELECT
  purchased_at,
  store
FROM ice_cream_sales
;

上記のように、見やすいように改行しても良いし、1 行で書いても同じことです。

SELECT purchased_at, store FROM ice_cream_sales;

この構文をベースとして、あとは並び順を変えたり、条件によってデータを絞ったり、集計したりできます。この辺りは実際に BigQuery でクエリ(こういった SQL で作成した文)を実行していきながら見ていきましょう。

データを取得する(基本)

それでは実際に、BigQuery でクエリを実行しデータを取得してみましょう。

タブの右側に [+] のボタンがあるので押下してください。

「無題」というタブが作成されます。ここにクエリを書いて実行していきます。

データの確認

クエリを書く前に、再度、アイスクリームの販売履歴のテーブルのデータを確認をしましょう。

  • データセット名: sample_ice_cream
  • テーブル名: ice_cream_sales

それぞれのカラム(列)の意味するものは以下でしたね。

  • purchased_at: 購入日時
  • store: 店舗
  • item_name: 品名
  • amount: 金額
  • member_id: ポイントメンバー会員 ID

これからこのデータを使って、見たい情報だけに絞ったり、並び順を変えたり、集計したりをしていきます。

基本の取得

まずは、条件など無しに、シンプルにデータを取得してみましょう。BigQuery での取得のお作法は以下になります。

SELECT
  見たい列の名前,
FROM `プロジェクト名.データセット名.テーブル名`

FROM 句は、以下のどちらかで記述しテーブルを指定する必要があります。

プロジェクト名まで書くと長いので、`データセット名.テーブル名` で FROM 句を書く書き方でクエリを作成してみましょう。

SELECT
  purchased_at,
  store,
  item_name,
  amount,
  member_id,
FROM `sample_ice_cream.ice_cream_sales`
;

これを、先程の「無題」のタブにあるエディターに記述します。

記述できたら、上にある [実行] ボタンを押下してクエリを実行してみましょう。

上手くいけば、クエリ結果としてデータが返ってくるはずです。

おめでとうございます。あなたは今、BigQuery でクエリを実行してデータを取得することができました。

覚えておきたいお金のこと

さて、無事に最初のクエリを実行できました。ここで一度、BigQuery での課金についておさらいしておきましょう。

BigQuery の料金体系は、大きく分けて以下の2つがあります。

- ストレージ料金
  - データの保存量に対して課金されます。
- 分析料金
  - クエリでスキャンされたデータに対して課金されます。

また、BigQuery には無料枠が用意されています。

- ストレージ料金
  - 10GB まで無料
- 分析料金
  - 毎月 1TB まで無料

つまり、10GB 以内のデータを保存し、合計 1TB 以内のクエリを実行する場合、無料枠の範囲内であれば、料金は発生しません。

(BigQuery は、デフォルトでオンデマンド分析料金が適用されます。オンデマンド分析料金は、クエリでスキャンされたデータに対して課金されます。)

先ほど行った「クエリを実行する」という行為は、上記の「分析料金: クエリでスキャンされたデータに対して課金されます。」に該当します。

そしてそれは、毎月 1TB まで無料ということになっています。

ここで、先程のエディターに記述したクエリをカットアンドペーストして貼り付け直してみてください。エディターの右上にこんな表示が出てきます。

このクエリを実行すると、461.8 KB が処理されます。

この、461.8 KB が、今回のクエリでスキャンされたデータ量です。

1TB は 1024 GBですから、1 回の実行で 461.8KB を消費する場合、このクエリを約 2200 回程度実行出来ることになります。

そしてこれは、テーブルに収録されているデータ量が増えれば、その分スキャンするデータ量も増えていくことになります。

本記事はサンドボックスモードにて、課金されない範疇で実施するので基本的には問題はありませんが、例えば自身の所属企業の BigQuery などで大量のデータを取り扱っている場合は、特にスキャン量と課金額には注意してください。(不安な場合は管理者に聞いてみましょう)

費用の見積もりと管理 - BigQuery, Google Cloud

データを取得する(色々な取得)

ではここから、アイスクリーム販売履歴データについて、色々な側面からクエリを実行しデータを取得してみましょう。

なお、ここから紹介する文法は BigQuery に限らず、他のデータベース製品でも共通で使えるものです。(BigQuery 独自の記法は随時お知らせします)

絞り込み(WHERE)

このデータは、店舗 A, B, C, D の 4 店舗分のデータが入っています。店舗 A の売上のみを見てみたい場合は WHERE 句を使ってデータを絞り込みます。

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
WHERE store = '店舗 A' -- 店舗 A のデータのみ
;

BigQuery コンソール画面のエディターに入力して実行してみましょう。店舗 A のデータのみが取得できます。

絞り込みは複数指定できます。これに、商品名「ハーゲンダッツ」も追加してみましょう。WHERE 句に AND をつけてつけて追加します。

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
-- 店舗 A かつ ハーゲンダッツのデータのみ
WHERE store = '店舗 A'
  AND item_name = 'ハーゲンダッツ'
;

「商品は PARM とハーゲンダッツ」といった、1 つのカラムに対する複数の絞り込みには IN が使えます。

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
-- 店舗 A かつ PARM とハーゲンダッツのデータのみ
WHERE store = '店舗 A'
  AND item_name IN ('PARM', 'ハーゲンダッツ')
;

最後は、店舗 A の、単価が 100 円未満の売上データを出してみます。

「店舗 A である」は等号(=)を使うのに対して、「100 円未満である」は不等号(>, <)を用います。

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
-- 店舗 A かつ 単価が 100 円未満の売上データのみ
WHERE store = '店舗 A'
  AND amount < 100
;

このように、WHERE 句を使うと様々な絞り込みができます。(他にも WHERE を使った絞り込み方法があるので気になる場合は調べてみましょう)

並び替え(ORDER BY)

明示的に並び順を指定することで、意図した順番で結果が返ってきます。

「売り上げた日時で並べたい」「売上金額順に並べたい」などです。

店舗 A のデータに対して、売り上げた日時で並べてみましょう。並び順の指定には、ORDER BY 句を用います。

昇順(過去->未来)で並べる場合: ASC

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
WHERE store = '店舗 A'
ORDER BY purchased_at ASC -- 結果を売上日時の昇順に並べる
;

クエリ結果が売上日時の昇順で並びました。

降順(未来->過去)で並べる場合: DESC

SELECT
  purchased_at,
  store,
  item_name,
  amount,
FROM `sample_ice_cream.ice_cream_sales`
WHERE store = '店舗 A'
ORDER BY purchased_at DESC -- 結果を売上日時の降順に並べる
;

クエリ結果が売上日時の降順で並びました。

テーブル同士をつなげる

さて、データベースでは、以下のように、関連する別のデータが別のテーブルになっている状況を良く見かけます。

アイスクリームの販売履歴テーブル

  • purchased_at: 購入日時
  • store: 店舗
  • item_name: 品名
  • amount: 金額
  • member_id: ポイントメンバー会員 ID

ポイントメンバー会員テーブル

  • id: ポイントメンバー会員 ID
  • name: 氏名
  • birth_date: 生年月日
  • gender: 性別

アイスクリームの販売履歴テーブルには、ポイントメンバー会員の ID があり、ポイントメンバー会員の情報はポイントメンバー会員テーブルで持っているような、いわゆる「リレーション(関係・関連)」の構造です。 (冒頭で出てきた「リレーショナルデータベース」の醍醐味でもあります)

これらを繋げてデータを取得してみましょう。

テーブル作成

以下の練習用データをダウンロードし、テーブル作成と同じ要領で BigQuery にインポートしテーブルを作成してください。

BigQueryはじめの一歩サンプルデータ - 2. ポイントメンバー会員(スプレッドシート)

  • データセット: sample_ice_cream(アイスクリームの販売履歴テーブルと同じ)
  • テーブル名: members
  • スキーマ:
    • id: INTEGER
    • name: STRING
    • birth_date: DATE
    • gender: STRING
  • スキップするヘッダー行: 1

ポイントメンバー会員テーブルが作成できたら、テーブル同士をつなげてみましょう。

テーブルを結合する(JOIN)

テーブルとテーブルを繋げることを「結合」といい、クエリでは JOIN 句を用います。

SELECT
  sales.purchased_at, -- テーブル名.カラム名 の形式にする
  sales.store,
  sales.item_name,
  sales.amount,
  sales.member_id,
  members.name,
  members.birth_date,
  members.gender,
FROM `sample_ice_cream.ice_cream_sales` sales
INNER JOIN `sample_ice_cream.members` members ON sales.member_id = members.id -- members テーブルをつなげる
;

JOIN 句の文法は以下になっています

SELECT
  テーブル名.カラム名,
FROM 結合元テーブル名 (エイリアス)
INNER JOIN 結合したいテーブル名 (エイリアス) ON 結合元テーブル名.結合元のメンバーID = 結合したいテーブル名.メンバーID

もう一つ違う点は、「エイリアス」という短縮名を定義して使っている点です。

FROM `sample_ice_cream.ice_cream_sales` sales -- `sample_ice_cream.ice_cream_sales` を sales という短縮名にします 
INNER JOIN `sample_ice_cream.members` members -- `sample_ice_cream.members` を members という短縮名にします
        ON sales.member_id = members.id

エイリアスを定義すると、その後のテーブル名の表記を、短くしたり分かりやすくできます。(任意の名前をつけられます)

もしもエイリアスを使わないとすると、以下のようなクエリになります。

SELECT
  `sample_ice_cream.ice_cream_sales`.purchased_at,
  `sample_ice_cream.ice_cream_sales`.store,
  `sample_ice_cream.ice_cream_sales`.item_name,
  `sample_ice_cream.ice_cream_sales`.amount,
  `sample_ice_cream.ice_cream_sales`.member_id,
  `sample_ice_cream.members`.name,
  `sample_ice_cream.members`.birth_date,
  `sample_ice_cream.members`.gender,
FROM `sample_ice_cream.ice_cream_sales`
INNER JOIN `sample_ice_cream.members` ON `sample_ice_cream.ice_cream_sales`.member_id = `sample_ice_cream.members`.id
;

冗長で、パッと見てもわかりにくいですよね。エイリアスを定義するとスッキリと書けるのでオススメです。

覚えておきたい INNER JOIN と LEFT JOIN

さて、JOIN 句にはいくつか種類があります。最低限覚えておきたいのは INNER JOINLEFT JOIN です。

  • INNER JOIN
    • 結合元テーブルのレコード(行)に対して、結合したいテーブルのレコードがマッチングしたものだけを結果として返す
  • LEFT JOIN
    • 結合元テーブルのレコードに対して、結合したいテーブルのレコードがマッチングしないものでも結果として返す

アイスクリームの販売履歴のデータには member_id というカラム(列)がありますが、必ずしも会員 ID が入っているわけではありません。

ポイントメンバー会員ではない人の購入データもあるということですね。会員 ID の無い場合は null と表示されています。

この時に、INNER JOINLEFT JOIN の違いを見てみましょう。

INNER JOIN

INNER JOIN を用いると、結合元テーブルのレコード(行)に対して、結合したいテーブルのレコードがマッチングしたものだけを結果として返します。

つまり、sales.member_id に値が入っていて、なおかつ members.id と合致したレコードのみが結果として返ることになります。

SELECT
  sales.purchased_at,
  sales.store,
  sales.member_id,
  members.name,
  members.birth_date,
  members.gender,
FROM `sample_ice_cream.ice_cream_sales` sales
INNER JOIN `sample_ice_cream.members` members ON sales.member_id = members.id
ORDER BY sales.purchased_at
;

LEFT JOIN

LEFT JOIN を用いると、結合元テーブルのレコード(行)に対して、結合したいテーブルのレコードがマッチングしなかったものも結果として返します。

つまり、sales.member_id が NULL のレコードや、members.id と一致しない sales のレコードも結果として返ることになります。

SELECT
  sales.purchased_at,
  sales.store,
  sales.member_id,
  members.name,
  members.birth_date,
  members.gender,
FROM `sample_ice_cream.ice_cream_sales` sales
LEFT JOIN `sample_ice_cream.members` members ON sales.member_id = members.id
ORDER BY sales.purchased_at
;

INNER JOIN と LEFT JOIN のどちらを使うかは、データの特徴や得たい結果を考慮して選択しましょう。そのために、クエリを書く前にデータをしっかり目視で眺めるというのも大切です。

データを集計する

最後に、データを集計してみましょう。集計は、値を合計したり、平均を出したりなどすることです。

データをまとめて、集計する(GROUP BY)

データを集計するには GROUP BY 句を用います。

SELECT
  store,
  SUM(amount) as total_sales,    -- 総売上
  COUNT(amount) as number_sales, -- 販売数
  AVG(amount) as average_sales,  -- 平均売上
  MIN(amount) as max_unit_price, -- 最大単価 
  MAX(amount) as min_unit_privc, -- 最小単価
FROM `sample_ice_cream.ice_cream_sales`
GROUP BY store -- 店舗ごとに集計します
ORDER BY store
;

GROUP BY store とすることで、「店舗ごとに集計する」という意味になります。

GROUP BY を指定したら、SELECT 句で「合計」や「平均」など、どんな計算をしたいかを指定すれば、店舗ごとに集計することができます。

このクエリでは、店舗ごとの全期間の売上や販売数などを集計しています。

結果を見てみると、店舗 B は店舗 A よりも売上は多いですが、平均売上は低いです。つまり、店舗 B は店舗 A よりもお安めのアイスが売れていて、単価よりも販売数で稼いでいる。というのが相対的にわかります。

こんなふうに、集計するとこれまではただの履歴でしかなかったデータが意味を持ち、そこから発見が生まれます。データを分析することの醍醐味ですね。

GROUP BY についての、BigQuery における SUM や AVG などの集計関数は以下に一覧がありますので参考にしてください。

集計関数 / レガシー SQL 関数と演算子 - BigQuery, Google Cloud

データから意味や価値を引き出す

データ分析とは、データから意味や価値を引き出すことです。たくさんの情報を整理して意味を見つけ出すプロセスと言えるでしょう。

大量のパズルピースが散らばっている状態で、それらを組み合わせて全体の絵を作り上げるようなものです。

データ分析を行うことで、ビジネスや研究などの分野で重要な判断材料を得ることができます。例えば、商品の売れ行きや広告の効果を分析して、より効果的な戦略を立てることができます。

BigQuery を用いれば、大量のデータを蓄積できること、そしてそれらを分析することが可能です。

BigQuery でやれることはまだまだ沢山あり、今回紹介したことは一部に過ぎませんが、これを期に実際に生きたデータに触れることで、より理解も深められると思います。

ここまで実際に手を動かしてみて、なんとなくの使い方が理解いただけたら幸いです。


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

Looker Studio やさしいはじめの一歩〜実際に触って理解するレポート作成ワークショップ〜

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

www.ritolab.com


蓄積した大量のデータを収集・分析・加工し、事業戦略の意思決定を支援する。いわゆる「ビジネス・インテリジェンス(BI)」という言葉が当たり前に聞かれるようになって数十年。これまでたくさんのツールが生まれてきました。

今回は、データを視覚化するためのツール Looker Studio の使い方を解説します。

Looker Studio

Looker Studioは、Google が提供する BI ツールです。さまざまなデータソースからデータを収集し、視覚化されたレポートやダッシュボードを作成・共有することができます。

Looker Studio

Looker Studio は、Google アカウントを持っていれば誰でも無料で使うことができます。(厳密には有料の機能があります。本記事では無料の範囲で実施します。)

対象

本記事は、Looker Studio を用いてデータの可視化を行いたい人が対象です。

  • ワークショップ(ハンズオン)形式で構成しており、実際に手を動かしながら、記事の通りにレポート作成を行っていくことで、Looker Studio に関する基礎的な理解を深められます。
  • エンジニアでなくても実施できるように構成しています。

レポートの作成を開始しよう

何はともあれ、使ってみましょう。Looker Studio にアクセスし、「使ってみる」ボタンを押下します。

レポート一覧の画面に遷移します。

まだ何もレポートを作成していないため、空っぽの状態です。

「空のレポート」を押下します。

アカウント設定のモーダルが表示されるので、入力と同意を行い、「続行」ボタンを押下します。

アカウント設定が完了したら、再度「空のレポート」を押下しましょう。

レポートの作成画面に遷移しますが、「データのレポートへの追加」が開くので右上のバツを押下して閉じておきます。

これでレポートの作成を開始できました。

ここにグラフなどを設置してレポートを仕上げていくことになります。レポートの名前は、画面左上の「無題のレポート」と表示されているところを押下すれば変更できます。

データを追加しよう

チャート(グラフ)を描画するためには、元となるデータが必要です。

データを読み込んでみましょう。

今回は、練習用のデータとして「とある店舗でのアイスクリームの販売履歴」をスプレッドシートで用意しました。

データ概要

  • データ日時範囲:
    • 2023/01/01 〜 2024/12/31 までの 2 年間
  • データ件数:
    • 10000 件
  • 店舗:
    • 店舗 A, B, C, D の 4 店舗
  • アイスクリーム品目:
  • リピーターによる購入かどうか:
    • 1: YES
    • 0: NO

練習用データは公開しているので誰でもアクセスできます。適宜、ご自身のスプレッドシートにコピーするなどして利用してください。

LookerStudioはじめの一歩サンプルデータ(スプレッドシート)

1. 「データを追加」ボタンを押下します

2. 「Google スプレッドシート」を押下します。

このとき、「Looker Studioに Google スプレッドシートへのアクセス権を許可してください。」という表示が出た場合は「承認」ボタンを押下し、自身の Google アカウントにログインします。

3. Google スプレッドシートの選択画面で、追加したいデータのシートを選択します。

  1. スプレッドシートを選択
  2. シートを選択
  3. 「追加」ボタンを押下

このとき、「このレポートにデータを追加しようとしています 」というモーダルが表示されたら「レポートに追加」ボタンを押下します。

しばらく待つと、データのレポートへの追加選択画面が非表示になり、元の画面に戻ってきます。

データの追加に成功しました。

データを読み込んだ際に作成された表は利用しないため削除してしまいましょう。

これでデータの読み込みは完了です。

チャートを作成してみよう

それでは実際にチャートを作ってみましょう。手始めに「売上の推移」をチャートにしてみます。

1. チャートの設置

「グラフを追加」ボタンを押下します。

「縦棒グラフ」のボタンを押下します。

「クリックして追加するか、ドラッグして描画します」という枠が現れるので、チャートを作成したい場所を選んでクリックします。

棒グラフが追加されました。

グラフの右下をドラッグして引っ張ってあげれば、グラフ描画の領域を広げることができます。

見やすくなるようにグラフの描画領域を広げておきましょう。

2. チャートの設定

設置したチャートを操作して、売上の推移の棒グラフを作成していきます。

棒グラフを選択した状態で、右側にあるメニューを操作していきます。

01. ディメンションの指定

「ディメンション」を見ると「店舗」になっています。ここを「購入日時」に変更します。

グラフが変化したのに気づきましたか?ディメンションを「店舗」から「購入日時」に変更したことによって、これまで X 軸は「店舗」でしたが、「購入日時」になりました。

02. 指標の指定

次に、「指標」を指定します。指標には「売上」を指定します。先程と同じ要領で、「Record Count」から「売上」に変更しましょう。

グラフがまた変化しましたね。Y 軸が売上になりました。

03. チャートの並べ替え

さて、ここで棒グラフを見てみると、並び順がなんだかよくわからないことになっていることに気づきます。日時がバラバラに並んでしまっているようです。

これは、グラフの並び順が「売上の降順(大きい順)」に設定されていることが原因です。

現在作成しているチャートは「売上の推移」ですから、日時がきちんと順番に並んでほしいですね。

並べ替えを「購入日時の昇順」に変更しましょう。そうすることで、グラフの並びが購入日時の古い順になります。

ディメンションと指標

ディメンションとは、を指しています。

グラフを作成する際に、「何を軸として見るか」をここに指定します。今回は「購入日時」を指定しました。時系列を軸として見たいからです。

指標とはその名の通り、評価する値を指します。

グラフを作成する際に、「どの数字を評価したいか」をここに指定します。今回は「売上」を指定しました。

つまり、このグラフでは「購入日時を軸として、売上を見る」=「購入日時の経過に対する売上の推移」というグラフを作成したことになります。

このように、順序立てて考えると、ディメンションには何を指定するべきか、指標には何を指定するべきか。が簡単にイメージできます。

ディメンション、指標にはそれぞれ何を指定すれば良いのか迷った時は、「何を軸にして、何の数値を評価したいのか」を考えてみてください。

チャートを便利にしてみよう

1. 日次・月次・年次で集計したグラフにする

さて、ここまでで売上推移のグラフが作成できましたが、よく見てみると、日時(年/月/日 時:分:秒)ごとでグラフが並んでいます。

これは、「この日時に売上があったものを集計してグラフに表示してる」という状態になっています。

いくらなんでも、時・分・秒という細かい粒度で売上を集計して見ても何かの気づきは得られにくいですよね。せめて、日・週・月・四半期・年などで集計してグラフに表示したいところです。

この操作を行っていきましょう。

01. 購入日時を日・月・年に丸める

現在、ディメンションには「購入日時」を指定していますが、ここを操作し、購入日時を日・月・年に丸めていきます。

ディメンション「購入日時」の左にカレンダーのアイコンがあるので、そこを押下します。

すると、小さなポップアップが開くので、「データの種類」を 「年」に変更します。

ディメンションが「購入日時(年)」となり、棒グラフが年ごとの売上に集計されて表示されました。

続いて、同じ要領でディメンションを「月」に変更してみましょう。データの種類は「年、月」を選択します。

月次で集計された棒グラフになりました。

最後に日次の棒グラフを表示させましょう。

ここまでくればもうおわかりですね。ディメンションを「日付」にすれば、日次で集計されたグラフにすることができます。

日次で集計された棒グラフになりました。

2. 日次・月次・年次を切り替えられるようにする

チャートから気づきを得るためには、日次だけ、月次だけ、年次だけといったように集計単位を固定せずに見られることが大切です。

先程までは、これらのいずれかにグラフを設定しましたが、これを切り替えて見られるようにしましょう。

01. ドリルダウンの有効化

ディメンションに「ドリルダウン」のトグルスイッチがあります。

これを有効化します。すると、ディメンションに 3 つの「購入日時(日付)」 がセットされます。

02. ディメンションの指定 

この 3 つのディメンションに対して、上の 2 つをそれぞれ上からから「購入日時(年)」「購入日時(年、月)」に変更します。

03. デフォルトドリルダウンレベルの指定

ディメンション下部に「デフォルトのドリルダウンのレベル」という項目があります。

現在は「購入日時(日付)」となっていますが、ここを「購入日時(年、月)」に変更します。

棒グラフの表示が月次集計の表示に変化しました。この指定によって、このレポートを開いた時は、このグラフは月次で集計されたグラフとして表示されるようになります。

これで集計単位の設定が完了しました。

04. グラフの集計単位を切り替える

実際にグラフの集計単位を切り替えてみましょう。

グラフの右上に、いくつかのアイコンが並んでいます。

このアイコンの中の「上矢印 」と「下矢印 」が集計単位を切り替えるボタンとなります。

ドリルアップは、集計を現在より 1 つ高レベルに変更します。

ドリルダウンは、集計を現在より 1 つ低レベルに変更します。

現在、ドリルダウンのデフォルトは「月次」ですので、ドリルアップを行えば「年次」の表示に、ドリルダウンを行えば「日次」になります。

実際に押下してみてください。棒グラフの集計が年次・月次・日次で切り替わります。

年次表示

月次表示

日次表示

3. 棒グラフに表示する棒の数を調整する

さて、現在のグラフを月次で確認してみましょう。

読み込んだデータは 2023 年 1 月〜 2024 年 12 月まであるはずですが、グラフに表示されているのは 2023 年 10 月までです。

棒グラフにおいて、いくつの棒を表示するかというのはスタイルで設定できます。

現在は「棒の数」が 10 と指定されているため、グラフには 10 本の棒が表示されているという状態になっています。

例えばこれを 100 に設定すれば、最高で 100 本の棒が描画されるようになるため、より多くのデータを確認できることになります。

ただし、何事も多ければ良いというわけではありません。例えば棒の数 100 本の設定で日次集計で表示するとどうなるでしょう。

逆多すぎて見にくいですね。例えば先月分だけ見たい場合があったとすると、この細さは辛いです。

このように、ピンポイントで「この期間だけ見たい」という場合もあるでしょう。

次でその方法を紹介します。

4. 表示するグラフの期間を選択できるようにする

現在、グラフの棒の数は 10 に設定しています。

ここから、期間指定を行い、その期間のグラフを表示させていきます。

01. コントロールの追加

画面上部にある「コントロールの追加」を押下し、「期間設定」を選択します。

「クリックして追加するか、ドラッグして描画します」という枠が出てくるので、グラフを追加した時と同じ要領で、棒グラフの上部の空いているスペースを押下します。

「期間を選択」というプルダウンメニューが設置されます。これが期間指定を行う際のコントローラーになります。

02. 実際に期間指定をしてみる

日付指定のプルダウンメニューが設置されたので、実際に日付範囲を指定してみましょう。

2024 年 1 月〜 2024 年 6 月までを指定してみます。日付を指定したら、「適用」ボタンを押下します。

すると、2024 年 1 月〜 2024 年 6 月までのグラフが描画されました。

これで、見たい日付の範囲でグラフが見られるようになりました。

03. デフォルトの期間指定

データは 2024 年 12 月までありますが、デフォルトで表示されているのは 2023 年 1 月〜 10 月のグラフです。

どうせなら古いデータのグラフではなく、現在の年月データを棒グラフに含めて表示させたいので、これを設定していきます。

現在の年月から 3 ヶ月間のデータを、デフォルトで表示させるようにしましょう。

期間指定のコントローラーを選択状態にすると、右メニューに「デフォルトの日付範囲」という項目が表示されます。

デフォルトの日付範囲は 「自動期間」に指定されていますが、ここを変更していきます。

デフォルトの日付範囲のプルダウンメニューを開くとカレンダーが表示されますので、右上に「自動期間」と表示されているプルダウンメニューをさらに開きます。

プルダウンメニューより「詳細設定」を選択します。

するとカレンダーの表示が変化するので、開始日の欄で上から 3 つ目の数値を 3と入力し、 4 つ目の欄を「」に指定します。

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

これで「開始月は現在から 3 か月前」という指定を行ったことになります。

記事執筆時点は 2023 年 11 月ですので、レポートを開くと自動で 2023/08 〜 11 月までのグラフが表示されるようになります。

これでデフォルトの期間指定は完了です。

はじめの一歩、おめでとうございます。

Looker Studio で売上推移のチャートを作成しました。成果物として、売上推移のグラフをレポート上で操作し見られるまでを作成できました。

ここまで理解できたなら、はじめの一歩は十分に踏み出せたと思います。

また、今回用意した練習用データには他の項目もあります。ぜひこれらを使用して別のグラフも作成してみてください。

例えば他にも、以下のような観点でレポートが作れそうです。

  • 店舗別の売上はどうなっている?
  • 売上の中でそれぞれの品目が占める割合はどれくらいだろうか?
  • どの品物が最も売上が多い?販売数は?
  • 売上の中でリピーターはどれくらいいるだろうか?各店舗では?

LookerStudioはじめの一歩サンプルデータ(スプレッドシート)

本記事が、 Looker Studio を使い始めたい誰かの役に立てたら幸いです。


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