0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

可視化SQL | 現在の給料と昇給前の給料 を1行で表示する(その4) | CROSS JOIN と MAX

Last updated at Posted at 2022-03-24

初めに

この問題は、SQLパズル #15 『現在の給料と昇給前の給料 』 を参考にしています
手元に『SQL パズル』があれば問題の詳細が記載されているのでよりわかりやすいです

下記に表示するテーブルとデータを見ると、社員の昇給日と給料は1行づつ追加されていて
複数昇給している社員とまだ昇給していない社員がいます

この問題は、社員のお給料を保持しているテーブルから
『現在の給料』と『昇給前の給料』を取出して、1行で表示させる問題です

『その4』は『その2』ととても似ています・・・が人によってアプローチの方法が違います
違う記述で同じ結果を出力するSQLを見るのはとても勉強になると思います

下記画像の様な出力結果となります
各社員に対して、今のお給料の昇給日とお給料、前回の昇給日とお給料が
1行で表示されています

rep_1.PNG

PostgreSQL で動作確認しています

テーブル と データ

SQL
CREATE TABLE Salaries (
	emp_name CHAR(10) NOT NULL,
	sal_date DATE NOT NULL,
	sal_amt DECIMAL(8,2) NOT NULL,
	PRIMARY KEY (emp_name, sal_date)
)

INSERT INTO Salaries
VALUES ('Tom',   '1996-06-20', 500.00),
       ('Tom',   '1996-08-20', 700.00),
       ('Tom',   '1996-10-20', 800.00),
       ('Tom',   '1996-12-20', 900.00),
       ('Dick',  '1996-06-20', 500.00),
       ('Harry', '1996-07-20', 500.00),
       ('Harry', '1996-09-20', 700.00);

▼ Salaries テーブルの中身

rep_2.PNG

CROSS JOIN を使って集計元になるデータを作る

SQL
SELECT S0.*, S1.*
FROM Salaries AS S0, Salaries AS S1
WHERE S0.emp_name = S1.emp_name

Salaries テーブルには3人の社員のデータが有り、全部で7件登録されています
Salaries テーブルを自己結合して、全組合せのデータを作ります
条件の WHERE句で、同じ社員名を追加しています

Tomは4件のデータが有るので 4 x 4 = 16件
Marryは2件のデータが有るので 2 x 2 = 4件
Dickは1件しかデータが無いので 1 x 1 = 1件

下記に Salaries テーブルを CROSS JOIN した後のイメージを置きます

rep_3.PNG

ここまでは、『その2』と同じ・・・

組合せを作る

SQL
SELECT S0.*, S1.*
FROM Salaries AS S0, Salaries AS S1
WHERE S0.emp_name = S1.emp_name AND S0.sal_date <= S1.sal_date

▼ 出力(画像の右側)

WHERE句に S0.sal_date <= S1.sal_date を追加して日にちの組合せを絞り込む
S0.sal_date を今,S1.sal_date を過去の給料昇給日と見た時
S0.sal_date が過去になってしまう組合せは必要ありません
 ⇒ S0.sal_date より大きな S1.sal_dateを持つ行を取出す
 ⇒ テーブル同士を自己結合して組合せを作ります
 ⇒ 組合せを作る場合よく使う手法です。画像をご確認ください

rep_5.PNG

S0.Salaries のデータで GROUP BY

SQL
SELECT S0.emp_name AS emp_name, 
       S0.sal_date AS sal_date,
       MAX(S0.sal_amt) AS sal_amt,
       COUNT(*)
FROM Salaries AS S0, Salaries AS S1
WHERE S0.sal_date <= S1.sal_date AND S0.emp_name = S1.emp_name
GROUP BY S0.emp_name, S0.sal_date

▼ 出力(画像の左側)

rep_6.PNG

COUNT(*) が 2以下のデータを取出す

SQL
SELECT S0.emp_name AS emp_name, 
       S0.sal_date AS sal_date,
       MAX(S0.sal_amt) AS sal_amt,
       COUNT(*)
FROM Salaries AS S0, Salaries AS S1
WHERE S0.sal_date <= S1.sal_date AND S0.emp_name = S1.emp_name
GROUP BY S0.emp_name, S0.sal_date
HAVING COUNT(*) <= 2  -- 追加

▼ 出力

emp_name sal_date sal_amt count
Tom 1996-10-20 800 2
Tom 1996-12-20 900 1
Harry 1996-07-20 500 2
Harry 1996-09-20 700 1
Dick 1996-06-20 500 1

rep_7.PNG

なぜ、COUNT(*) の数が2以下のデータをとりだすのか?
Tomの場合、行数が4や3となるデータが存在する。それは昇級した回数が多いからです

この問題では『今の給料』と『1つ前の給料』を取り出したいので
社員名と昇給日 で GROUP BY した後の COUNT が2以下となる組合せが
『今の給料』と『1つ前の給料』のデータとなります

この出力結果でほぼデータは取れています・・・が、もう一息です

『今の給料』と『1つ前の給料』が縦に並んでいるので横並びにする必要が有ります
その為にもう一度 CROSS JOIN して、組み合わせを作ります

組合せを作る2

SQL
WITH Salaries2 AS (
  SELECT S0.emp_name AS emp_name, 
         S0.sal_date AS sal_date,
         MAX(S0.sal_amt) AS sal_amt
  FROM Salaries AS S0, Salaries AS S1
  WHERE S0.sal_date <= S1.sal_date AND S0.emp_name = S1.emp_name
  GROUP BY S0.emp_name, S0.sal_date
  HAVING COUNT(*) <= 2
)

SELECT S1.emp_name, S1.sal_date, S1.sal_amt,
       S2.sal_date, S2.sal_amt
FROM Salaries2 AS S1, salaries2 AS S2
WHERE S1.emp_name = S2.emp_name

▼ 出力

rep_8.PNG

縦並びに表示されていたお給料を取出したSQLを CTE で定義して( Salaries2 )
このテーブルデータを CROSS JOIN して組み合わせを作っています

CROSS JOIN しているので各社員の給料情報の全組み合わせがあります
S1.sal_date を 『今の給料の昇給日』、S2.sal_date を『1つ前の昇給日』とすると
縦に表示されていた給料情報が、横に並んでいるように見えます

ここから必要な行を取出します

給料データの取出し

SQL
WITH Salaries2 AS (
  SELECT S0.emp_name AS emp_name, 
         S0.sal_date AS sal_date,
         MAX(S0.sal_amt) AS sal_amt
  FROM Salaries AS S0, Salaries AS S1
  WHERE S0.sal_date <= S1.sal_date AND S0.emp_name = S1.emp_name
  GROUP BY S0.emp_name, S0.sal_date
  HAVING COUNT(*) <= 2
)

SELECT S1.emp_name, S1.sal_date, S1.sal_amt,
       S2.sal_date, S2.sal_amt
FROM Salaries2 AS S1, salaries2 AS S2
WHERE S1.emp_name = S2.emp_name AND S1.sal_date > S2.sal_date

▼ 出力

S1.emp_name S1.sal_date S1.sal_amt S2.sal_date S2.sal_amt
Tom 1996-12-20 900 1996-10-20 800
Harry 1996-09-20 700 1996-07-20 500

WHERE条件に、S1.sal_date > S2.sal_date を追加しています
S1.sal_date は『今の給料の昇給日』なので S2.sal_date はそれより過去となる条件です
『=』 が無いので S1.sal_date と S2.sal_date が同じ日は有りません

しかし、Dick がいなくなりました・・・

理由は WHERE句の条件が 2つあって AND となっているので両方を満たす必要が有ります
S1.emp_name = S2.emp_name AND S1.sal_date > S2.sal_date
 ⇒ Dick は この条件を満たしません S1.sal_date > S2.sal_date

その為、別途 Dick のデータを取出す必要が有ります

rep_9.PNG

回答 SQL

SQL
WITH Salaries2 AS (
  SELECT S0.emp_name AS emp_name, 
         S0.sal_date AS sal_date,
         MAX(S0.sal_amt) AS sal_amt
  FROM Salaries AS S0, Salaries AS S1
  WHERE S0.sal_date <= S1.sal_date AND S0.emp_name = S1.emp_name
  GROUP BY S0.emp_name, S0.sal_date
  HAVING COUNT(*) <= 2
)

SELECT S1.emp_name, S1.sal_date, S1.sal_amt,
       S2.sal_date, S2.sal_amt
FROM Salaries2 AS S1, salaries2 AS S2
WHERE S1.emp_name = S2.emp_name AND S1.sal_date > S2.sal_date

UNION ALL  -- Dick の給料情報

SELECT emp_name, MAX(sal_date), MAX(sal_amt), NULL, NULL
FROM Salaries2
GROUP BY emp_name
HAVING COUNT(*) = 1  -- Dick のデータは1行しかなり

▼ 出力

S1.emp_name S1.sal_date S1.sal_amt S2.sal_date S2.sal_amt
Tom 1996-12-20 900 1996-10-20 800
Harry 1996-09-20 700 1996-07-20 500
Dick 1996-06-20 500 空白 空白

Dick の給料情報を取出すために、Salaries2 の出力結果から
社員名で GROUP BY した結果が1行だけのデータを取出しています

集約する変数を emp_name だけなので
sal_date や sal_amt を MAX関数で表示させています

rep_10.PNG

参考文献

SQLパズル 第2版~プログラミングが変わる書き方/考え方 | Joe Celko, ミック

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?