第 11 回 SQL コンテストに参加しました。最終結果は 4 完 100 点で 6 位でした。
近況報告
例によって SQL を書くのはほぼ 2 ヶ月振りになります。前回からの進歩としては SQL の本を一冊概ね読んだので、ウィンドウ関数などの文法や SQL の思想についての理解が少し上がったという点があります。
コンテストの振り返り
問題 1
1 問目から表示ではなく削除を要求される問題で意表を突かれました。基本的な DELETE
文を使えるかが問われている問題ですが、日付範囲を 2024-01-01 から 2024-01-03 までと読み違えて 1 回 WA を出してしまいました。
開始約 3 分後に AC。幸先の悪いスタートとなりました。
DELETE FROM STOCK WHERE LAST_DELIVERY_DATE BETWEEN '2023-01-01' AND '2023-01-31' AND ACTUAL_AMT = 0
問題 2
こちらはメールアドレスの '@' 以降から '.co.jp' を除いた部分を表示することに主眼を置いた問題です。SUBSTR
関数を使って部分文字列を抽出したいですが、'@' が何番目にあるかがわからないと使えません。調べてみると、INSTR
関数で部分文字列の位置を検索できることがわかりました。具体的には、文字列 について INSTR(S,T)
は となる最小の (存在しなければ 0)を出力します。今回の場合は INSTR(EMAIL, '@')
で '@' の index を取得できます。'@' がもし複数あったら面倒なことになっていましたが、出してみると無事に通って安心しました。
開始約 12 分後に AC。
SELECT MEMBER_CODE AS MEMBER, EMAIL AS EMAIL, SUBSTR(EMAIL, INSTR(EMAIL, '@') + 1, LENGTH(EMAIL) - INSTR(EMAIL, '@') - 6) AS DOMAIN FROM MEMBER_MST ORDER BY DOMAIN DESC, MEMBER DESC
問題 3
各月の総売上を計算し、直近 12 ヶ月の総和と年内の総和を計算する問題です。累積和の問題なので、ウィンドウ関数が効力を発揮します。まず 2023 年以外のデータも残したまま直近 12 ヶ月の和を SUM(MONTH_AMT) OVER(ORDER BY YEAR_MONTH ASC ROWS BETWEEN 11 PRECEDING AND 0 PRECEDING)
で計算し、その後 2023 年のデータのみを残して今度は SUM(MONTH_AMT) OVER(ORDER BY YEAR_MONTH ASC)
でシンプルに累積和を計算します。
サクッと書けて開始約 26 分後に AC。私が先月書いたウィンドウ関数の記事が参考になりました。
WITH T1 AS( SELECT SUBSTR(SALES_DATE, 1, 7) AS YEAR_MONTH, SUM(SALES_AMT) AS MONTH_AMT FROM SALES WHERE UPDATED_NO IS NULL GROUP BY SUBSTR(SALES_DATE, 1, 7) ), T2 AS( SELECT YEAR_MONTH, MONTH_AMT, SUM(MONTH_AMT) OVER( ORDER BY YEAR_MONTH ASC ROWS BETWEEN 11 PRECEDING AND 0 PRECEDING ) AS YEAR_MOVE_AMT FROM T1 ), T3 AS( SELECT * FROM T2 WHERE YEAR_MONTH BETWEEN '2023-01' AND '2023-12' ), T4 AS( SELECT YEAR_MONTH, MONTH_AMT, SUM(MONTH_AMT) OVER( ORDER BY YEAR_MONTH ASC ) AS CUML_AMT, YEAR_MOVE_AMT FROM T3 ORDER BY YEAR_MONTH ) SELECT * FROM T4 WITH T1 AS( SELECT SUBSTR(SALES_DATE, 1, 7) AS YEAR_MONTH, SUM(SALES_AMT) AS MONTH_AMT FROM SALES WHERE UPDATED_NO IS NULL GROUP BY SUBSTR(SALES_DATE, 1, 7) ), T2 AS( SELECT YEAR_MONTH, MONTH_AMT, SUM(MONTH_AMT) OVER( ORDER BY YEAR_MONTH ASC ROWS BETWEEN 11 PRECEDING AND 0 PRECEDING ) AS YEAR_MOVE_AMT FROM T1 ), T3 AS( SELECT * FROM T2 WHERE YEAR_MONTH BETWEEN '2023-01' AND '2023-12' ), T4 AS( SELECT YEAR_MONTH, MONTH_AMT, SUM(MONTH_AMT) OVER( ORDER BY YEAR_MONTH ASC ) AS CUML_AMT, YEAR_MOVE_AMT FROM T3 ORDER BY YEAR_MONTH ) SELECT * FROM T4
問題 4
ここまで特にエラーに苦しむこともなく、実際 3 問目までのスピードは過去最速だったので全完のチャンスだと感じていました。問題文は長くてかなりわかりにくいですが、全ての商品の組 について
- を両方買った人数
- を買った人数
- を買った人数
- 全体の人数
がわかれば求めたい値を全て計算できます。まず商品の組を列挙しなければいけませんが、これには交差結合 (cross join) が使えます。さらに、交差結合した後に 2 つの SESSION_ID
が一致する行のみを残し、DISTINCT
文を使うことで を両方買った人数を求めることができます。同じく DISTINCT
文を使うことで、各商品を買った人数、全体の人数を求めることもできるので、これらの結果を内部結合 (inner join)、外部結合 (outer join) を駆使して商品の組の表に適切に組み込むことで、欲しかった 4 つの値を全てまとめることができます。
実装は少し長くなりましたが開始約 52 分後に AC。余裕を持って正解することができました。
WITH T1 AS( SELECT COUNT(DISTINCT SESSION_ID) AS CNT_ALL FROM PURCHASE_HISTORY ), T2 AS( SELECT *, COUNT(DISTINCT SESSION_ID) AS CNT_A FROM PURCHASE_HISTORY GROUP BY ITEM_CODE ), T3 AS( SELECT P1.ITEM_CODE AS ITEM_A, P2.ITEM_CODE AS ITEM_B, COUNT(*) AS CNT_AB FROM PURCHASE_HISTORY AS P1 CROSS JOIN PURCHASE_HISTORY AS P2 WHERE P1.SESSION_ID = P2.SESSION_ID GROUP BY P1.ITEM_CODE, P2.ITEM_CODE ), T4 AS( SELECT ITEM_A, ITEM_B, CNT_AB, TA.CNT_A AS CNT_A, TB.CNT_A AS CNT_B FROM T3 INNER JOIN T2 AS TA ON ITEM_A = TA.ITEM_CODE, T2 AS TB ON ITEM_B = TB.ITEM_CODE WHERE ITEM_A != ITEM_B ), T5 AS( SELECT * FROM T4 CROSS JOIN T1 ), T6 AS( SELECT ITEM_A, ITEM_B, ROUND(100.0 * CNT_AB / CNT_ALL, 5) AS SUPPORT, ROUND(100.0 * CNT_AB / CNT_A, 5) AS CONFIDENCE, ROUND(1.0 * CNT_AB * CNT_ALL / (1.0 * CNT_A * CNT_B), 5) AS LIFT FROM T5 ORDER BY LIFT DESC, SUPPORT DESC, CONFIDENCE DESC, ITEM_A DESC ) SELECT * FROM T6
感想
初全完でかなり嬉しいです。次もまた頑張ります。