情弱エンジニアのなかのblog

一人前のエンジニアになる為のブログです

SQL JOINの注意点について

初回購入商品ごとに顧客を出力して顧客たちの初回購入月の総売上を出力するSQLを作っていたのですが 問題点があったので記載いたします。

まず今回このようなデータが入っているものとします。

customersテーブル
     顧客名   
+---+-------+
|id |name   |
+---+-------+
|1  |tanaka |
|2  |yamada |
+---+-------+

customer_historiesテーブル
      顧客id         注文日          購入回数         注文id
+---+------------+----------------+------------------+---------+
|id |customer_id |date_of_purchase|number_of_purchase|order_id |
+---+------------+----------------+------------------+---------+
|1  | 1          |2017-05-01      | 1                |1        |
|2  | 1          |2017-05-15      | 2                |2        |
|3  | 1          |2017-05-20      | 3                |3        |
|4  | 2          |2017-05-02      | 1                |4        |
+---+------------+----------------+------------------+---------+

ordersテーブル
      顧客id      注文日          金額
+---+------------+--------------+--------------+
|id |customer_id |order_date    | total_amount |
+---+------------+--------------+--------------+
|1  | 1          |2017-05-01    | 100          |
|2  | 1          |2017-05-15    | 500          |
|3  | 1          |2017-05-20    | 300          |
|4  | 2          |2017-05-02    | 100          |
+---+------------+--------------+--------------+

order_detailsテーブル
      注文id    商品id
+---+---------+---------+
|id |order_id |item_id  |
+---+---------+---------+
|1  | 1       |1        |
|2  | 2       |2        |
|3  | 2       |3        |
|4  | 3       |3        |
|5  | 4       |1        |
+---+---------+---------+

itemsテーブル(商品テーブル)
      商品名  値段
+---+----------+-----+
|id |name      |price|
+---+----------+-----+
|1  |Pepper    |100  |
|2  |salt      |200  |
|3  |mayonnaise|300  |
|4  |ketchup   |400  |
+---+----------+-----+

まず初めに以下のようなSQLを作りました サブクエリやDATE_FORMATやFORMATの使い方に関しては以前の記事を参考にしてください。

MySQLで特定期間のデータを抽出する - 情弱エンジニアのなかのblog

MySQLで日付に関する処理 - 情弱エンジニアのなかのblog

SQL小数点の表示とtimestamp - 情弱エンジニアのなかのblog

SELECT
    ch.name AS '商品名',
    FORMAT(count(distinct orders.customer_id),0) AS '顧客数',
    FORMAT(sum(orders.total_amount ),0) AS '1月間の総売上'
FROM
    orders
INNER JOIN
    (
    SELECT
        customer_histories.customer_id,
        customer_histories.date_of_purchase,
        items.id,
        items.name
    FROM
        customer_histories
    WHERE
        date_of_purchase between '2017-05-01' and '2017-05-31'
        AND
        number_of_purchase = 1
    ) AS ch
ON
    orders.customer_id = ch.customer_id
INNER JOIN
    order_details
ON
    orders.id = order_details.order_id
INNER JOIN
    items
ON
    order_details.item_id = items.id
WHERE
    DATE_FORMAT(orders.order_date , '%Y%m') <= DATE_FORMAT(ch.date_of_purchase , '%Y%m')
GROUP BY
    items.id

こちらですと問題点として まず初回購入以外の商品データもGROUP BYの集計に入ってしまいます。

あとordersテーブルとorder_detailsテーブルを見るとわかるのですが、 order.id = 2で[tanaka]さんが同時に2つの商品を購入しているのでordersとorder_detailsの結合時に order.id = 2が2行分作られます。その状態で

select
    sum(orders.charged_amount)

をするとorder.id = 2 の total_amount = 500が2回集計されて間違ったデータになってしまします。

以下が正しい修正バージョンです。

SELECT
    ch.name AS '商品名',
    FORMAT(count(distinct orders.customer_id),0) AS '顧客数',
    FORMAT(sum(orders.total_amount),0) AS '1月間の総売上'
FROM
    orders
INNER JOIN
    (
    SELECT
        customer_histories.customer_id,
        customer_histories.date_of_purchase,
        items.id,
        items.name
    FROM
        customer_histories
    INNER JOIN
        order_details
    ON
        customer_histories.order.id = order_details.order_id
    INNER JOIN
        items
    ON
        order_details.item_id = items.id
    WHERE
        date_of_purchase between '2017-05-01' and '2017-05-31'
        AND
        number_of_purchase = 1
    ) AS ch
ON
    orders.customer_id = ch.customer_id
WHERE
    DATE_FORMAT(orders.order_date , '%Y%m') <= DATE_FORMAT(ch.date_of_purchase , '%Y%m')
GROUP BY
    ch.id

今回の問題点は無駄な結合を行っていたことと、集計の軸を間違えていたことです。

それでは