『SQLパズル 第2版』の別解など その2
『SQLパズル 第2版』の別解など その1 - 不思議の備忘録
の続き
パズル27
■答え2を正しく動作するように改良
SELECT S1.sno, S2.sno FROM Supparts AS S1 , Supparts AS S2 WHERE S1.sno < S2.sno AND S1.pno IN (SELECT S22.pno FROM Supparts AS S22 WHERE S22.sno = S2.sno) AND S2.pno IN (SELECT S11.pno FROM Supparts AS S11 WHERE S11.sno = S1.sno) GROUP BY S1.sno, S2.sno HAVING COUNT(*) = (SELECT COUNT(*) FROM Supparts AS S3 WHERE S3.sno = S1.sno) * (SELECT COUNT(*) FROM Supparts AS S4 WHERE S4.sno = S2.sno);
答え5の"COUNT(SP1.sno || SP2.sno)"は"COUNT(CASE WHEN SP1.sno = SP2.sno THEN 1 ELSE NULL END)" に置き換えるとうまくいかない
パズル29
答え4について
check_amt | cnt -----------+----- 100.00 | 3 300.00 | 3
のように表示したい
SELECT DISTINCT check_amt, cnt FROM (SELECT check_amt ,cnt ,MAX(cnt) OVER() AS maxcnt FROM (SELECT check_amt ,COUNT(*) OVER(PARTITION BY check_amt) AS cnt FROM Payroll) AS TMP) AS TMP2 WHERE cnt = maxcnt;
WHERE句でWindow関数やSELECT句のエイリアスを使えたらネストが減るのになあ
パズル30
■Window関数を使った別解
SELECT customer_name ,AVG(gap) FROM (SELECT customer_name ,sale_date - MAX(sale_date) OVER(PARTITION BY customer_name ORDER BY sale_date ROWS BETWEEN 1 PRECEDING AND 1PRECEDING) AS gap FROM Sales) AS TMP GROUP BY customer_name;
パズル31
1つのクエリで全部買った顧客、一部買った顧客、1つも買ってない顧客の売掛金残高の平均を出す
SELECT customer_type ,AVG(balance) FROM (SELECT C.customer_id ,CASE COUNT(DISTINCT item_id) WHEN (SELECT COUNT(DISTINCT item_id) FROM Products) THEN '全部買った' WHEN 0 THEN '何も買ってない' ELSE '一部買った' END AS customer_type ,MAX(acct_balance) AS balance FROM Customers C LEFT OUTER JOIN (Orders O INNER JOIN OrderDetails OD ON O.order_id = OD.order_id) ON O.customer_id = C.customer_id GROUP BY C.customer_id) AS TMP GROUP BY customer_type;
パズル32
答え3について、結合でT2のほうを使っていて個人的には以下のようにスカラサブクエリを使ったほうが直感的にわかりやすい気がする
SELECT SUM(tax_rate) AS total_taxes FROM TaxRates AS T1 WHERE (SELECT DISTINCT lft FROM TaxRates AS T2 WHERE T2.tax_authority = 'city2') BETWEEN T1.lft AND T1.rgt AND '1994-11-01' BETWEEN T1.start_date AND COALESCE(T1.end_date, CURRENT_DATE);
パズル33
■Window関数を使った別解
Window関数の中に集約関数を入れ子にすることができる
SELECT machine_name ,manu_date ,(accum_cost + accum_depreciation) / accum_hrs AS total_cost_per_hour FROM (SELECT M.machine_name ,manu_date ,(manu_date + 1 - M.purchase_date) * (initial_cost / lifespan) AS accum_depreciation ,SUM(SUM(manu_hrs)) OVER(PARTITION BY M.machine_name ORDER BY manu_date) AS accum_hrs ,SUM(SUM(manu_cost)) OVER(PARTITION BY M.machine_name ORDER BY manu_date) AS accum_cost FROM ManufactHrsCosts AS MHC INNER JOIN Machines AS M ON M.machine_name = MHC.machine_name GROUP BY M.machine_name, manu_date) AS TMP;
SELECT M.machine_name ,manu_date ,( (manu_date + 1 - M.purchase_date) * (initial_cost / lifespan) + SUM(SUM(manu_cost)) OVER(PARTITION BY M.machine_name ORDER BY manu_date) ) / SUM(SUM(manu_hrs)) OVER(PARTITION BY M.machine_name ORDER BY manu_date) AS total_cost_per_hour FROM ManufactHrsCosts AS MHC INNER JOIN Machines AS M ON M.machine_name = MHC.machine_name GROUP BY M.machine_name, manu_date;
パズル34
スカラサブクエリがスカラになる保証がないんじゃないかと思っていましたが
SELECT C1.emp_id, C1.emp_name, SUM (bill_hrs * (SELECT bill_rate FROM Billings AS B1 WHERE bill_date = (SELECT MAX(bill_date) FROM Billings AS B2 WHERE B2.bill_date <= H1.work_date AND B1.emp_id = B2.emp_id AND B1.emp_id = H1.emp_id))) FROM HoursWorked AS H1, Consultants AS C1 WHERE H1.emp_id = C1.emp_id GROUP BY C1.emp_id, C1.emp_name;
SELECT bill_rate FROM Billings AS B1 WHERE bill_date = (SELECT MAX(bill_date) FROM Billings AS B2 WHERE B2.bill_date <= '1990-07-01' AND B1.emp_id = B2.emp_id AND B1.emp_id = 3);
一番最後の行でMAX(bill_date)がnullになって外側のWHEREが通らないためスカラであることが保証されますね
あと答え2の最後の条件は不要なのではないでしょうか
SELECT emp_name, SUM(H1.bill_hrs * B1.bill_rate) FROM Consultants AS C1, Billings AS B1, Hoursworked AS H1 WHERE C1.emp_id = B1.emp_id AND C1.emp_id = H1.emp_id AND bill_date = (SELECT MAX(bill_date) FROM Billings AS B2 WHERE B2.emp_id = C1.emp_id AND B2.bill_date <= H1.work_date) --AND H1.work_date >= bill_date GROUP BY emp_name;
パズル41
これについても一番外側の集約は要らないのではないでしょうか
SELECT I1.item_nbr, item_descr, actual_tot, estimated_tot, check_nbr FROM ITEMS AS I1 LEFT OUTER JOIN (SELECT item_nbr, SUM(actual_amt) AS actual_tot, CASE COUNT(check_nbr) WHEN 0 THEN NULL WHEN 1 THEN MAX(check_nbr) ELSE 'Mixed' END AS check_nbr FROM Actuals GROUP BY item_nbr) AS A1 ON I1.item_nbr = A1.item_nbr LEFT OUTER JOIN (SELECT item_nbr, SUM(estimated_amt) AS estimated_tot FROM Estimates GROUP BY item_nbr) AS E1 ON I1.item_nbr = E1.item_nbr WHERE actual_tot IS NOT NULL OR estimated_tot IS NOT NULL;
パズル44
答え1は同一ペアが2重にカウントされてしまうので正しくない。さらにちょっとした修正を加えたものが以下
SELECT S0.item_a ,S0.item_b ,CASE WHEN S0.item_a = S0.item_b THEN SUM(S0.pair_tally + S1.pair_tally) / 2 ELSE SUM(S0.pair_tally + S1.pair_tally) END AS pair_tally FROM SalesSlips AS S0, SalesSlips AS S1 WHERE S0.item_a <= S0.item_b AND S0.item_a = S1.item_b AND S0.item_b = S1.item_a GROUP BY S0.item_a, S0.item_b, S1.item_a, S1.item_b;