SQLそれ自体は一番最後にあります。
天才的なソリューションだった。驚いた。日付から締日を引き算すれば、何月締めであるかを判定できるとは。シカクいアタマをマルくする必要性を痛感した。
締日集計のめんどいところは、締日がN件あること。5日、10日、15日、20日、25日、月末の6パターンある場合、2017年12月の締日別の合計は以下の期間を元に算出する必要がある。
2017年12月の締日別集計期間
締日 | 集計期間 |
---|---|
5日 | 2017/11/06〜2017/12/05 |
10日 | 2017/11/11〜2017/12/10 |
15日 | 2017/11/16〜2017/12/15 |
20日 | 2017/11/21〜2017/12/20 |
25日 | 2017/11/26〜2017/12/25 |
月末 | 2017/12/01〜2017/12/31 |
当たり前だが、月末の末日は変化する。28、29、30、31の4通りある。締日別で売上を見たいというのは、上記の集計期間別に売上総額を見て、入金予定の算段を立てる目安として使われる事が多い。
この締日別の集計期間をどう作るべきか悩んでいた。最低最悪のソリューションだと締日別にSQLを投げてUNIONするのはありえへんし、CASE-WHENで締日を見てクロス集計するのも無駄に複雑でだるい。そこに彗星の如く現れたのが「日付から締日を引き算」するというSQLだ。実際にやってみた。
伝票日付から締日を引くSQL(サンプル)
select customers.duedate, earnings.billdate, (earnings.billdate - interval customers.duedate DAY) as monthly from earnings JOIN customers ON customers.id = earnings.customer_id where year(earnings.billdate) = 2017 group by earnings.billdate,customers.duedate
MySQLの場合、日付型のデータにおいてはINTERVALを与えると引き算(足し算)してくれる。実際に引いてみたら、たしかにこれで集計できることがわかった。あとは簡単。DATE_FORMATで年月を割り出し、そいつでGroupByするだけだ。
締日別売上集計SQL
select duedate, DATE_FORMAT((billdate - interval duedate DAY),'%Y年%m月') as monthly, sum(subtotal) from earnings JOIN customers ON customers.id = earnings.customer_id group by monthly,duedate order by monthly,duedate
手元のMacで8万行の売上データが507msで集計できた。
追記(2017.12.26 17:30)
duedate(締日)に31が登録されている場合(うちのシステムは、末締めの場合31という数値を入力して表現することになっている)月末が31日じゃないものについてはバグになってしまう。11/30から31を引いたら、10/30になってしまい11月伝票が10月伝票になってしまうことに気がついたので、このSQLはクソ。駄目です。
というか、末締めの場合は締日の引き算が不要。その日付の年月が締め対象になる。というわけで、そこをCASE-WHENで場合分けすればOKだった。以下のSQLなら大丈夫なはず。
select duedate, case when duedate = 31 then DATE_FORMAT(billdate,'%Y年%m月') else DATE_FORMAT((billdate - interval duedate DAY),'%Y年%m月') end as monthly, sum(subtotal) from earnings JOIN customers ON customers.id = earnings.customer_id group by monthly,duedate order by monthly,duedate
追記(2018.03.23 14:40)
バグが有ることが判明しました。
20日締で5/25付の顧客は6月付にならなければなりません。5/21〜6/20 が期間なので。でも、5/25 で 締日が20日の場合、単純に日付から締日を引き算してしまうと、5/5になり、5月付になってしまいます。5/25は6月付やん。アカンわ。
「日付の日にち」>「締日」だったら日付の年月を1ヶ月加算、「日付の日にち」<=「締日」はその日付の年月を請求月とする。これが正しいようです。末締の場合は何も加算する必要が無いので注意が必要です。
締日5日で検証
締日 | 日付 | 請求月 |
---|---|---|
5日(2017/10/06〜2017/11/5) | 2017/10/25 | 11月締 |
5日(2017/10/06〜2017/11/5) | 2017/10/4 | 10月締 |
締日10日で検証
締日 | 日付 | 請求月 |
---|---|---|
10日(2017/10/11〜2017/11/10) | 2017/10/9 | 10月締 |
10日(2017/10/11〜2017/11/10) | 2017/10/31 | 11月締 |
締日15日で検証
締日 | 日付 | 請求月 |
---|---|---|
15日(2017/10/16〜2017/11/15) | 2017/10/9 | 10月締 |
15日(2017/10/16〜2017/11/15) | 2017/10/31 | 11月締 |
締日20日で検証
締日 | 日付 | 請求月 |
---|---|---|
20日(2017/10/21〜2017/11/20) | 2017/10/9 | 10月締 |
20日(2017/10/21〜2017/11/20) | 2017/10/24 | 11月締 |
締日25日で検証
締日 | 日付 | 請求月 |
---|---|---|
25日(2017/10/26〜2017/11/25) | 2017/11/10 | 11月締 |
25日(2017/10/26〜2017/11/20) | 2017/11/28 | 12月締 |
良さそうですね。締日よりも先の日付(締日は含まない)だったら、1ヶ月先が請求月。正しい気がします。
というわけで、締日集計のSQLの改訂版です。
SELECT CASE WHEN duedate = 31 THEN DATE_FORMAT(billdate,'%Y年%m月') WHEN duedate < 31 AND DAY(billdate) <= duedate THEN DATE_FORMAT(billdate,'%Y年%m月') WHEN duedate < 31 AND DAY(billdate) > duedate THEN DATE_FORMAT(DATE_ADD(billdate, INTERVAL 1 MONTH),'%Y年%m月') END AS monthly, duedate, sum(subtotal) FROM earnings JOIN customers ON customers.id = earnings.customer_id WHERE duedate > 0 GROUP BY monthly,duedate ORDER BY monthly,duedate
念のため、締日での請求月の算出動作確認用SQLも貼り付けておきます。
SELECT billdate, -- 伝票日付 duedate, -- 顧客の締日(1〜31) CASE WHEN duedate = 31 THEN DATE_FORMAT(billdate,'%Y年%m月') -- 末締は伝票月=請求月 WHEN duedate < 31 AND DAY(billdate) <= duedate THEN DATE_FORMAT(billdate,'%Y年%m月') -- 末締以外で、伝票日 <= 締日なら、伝票月=請求月 WHEN duedate < 31 AND DAY(billdate) > duedate THEN DATE_FORMAT(DATE_ADD(billdate, INTERVAL 1 MONTH),'%Y年%m月') -- 末締以外で、伝票日 > 締日なら、伝票月=請求月の翌月 END AS monthly FROM earnings JOIN customers ON customers.id = earnings.customer_id WHERE duedate > 0 GROUP BY billdate,duedate