達人に学ぶSQL徹底指南書第2版の練習問題
paiza.ioのMySQLだとさっと試すのが便利
練習問題1-1
自分の回答
create table Greatests(id varchar(128), x integer, y integer, z integer);
insert into Greatests(id, x, y, z) values
("A", 1, 2, 3),
("B", 5, 5, 2),
("C", 4, 7, 1),
("D", 3, 3, 8);
select
id,
(case when x > y then x else y end) as greatest
from
Greatests;
select
id,
(case
when x > y and x > z then x
when y > z then y
else z
end) as greatest
from
Greatests;
回答との比較
WHENの中でCASE文も使うことができる
ただ、これは解説にもあるけど可読性は良くないなあ。。
行変換してMAX関数使うという手もある。
練習問題1-2
自分の回答
create table PopTbl2(pref_name varchar(128), sex integer, population integer);
insert into PopTbl2 values
("徳島", 1, 60),
("徳島", 2, 40),
("香川", 1, 100),
("香川", 2, 100),
("愛媛", 1, 100),
("愛媛", 2, 50),
("高知", 1, 100),
("高知", 2, 100),
("福岡", 1, 100),
("福岡", 2, 200),
("佐賀", 1, 20),
("佐賀", 2, 80),
("長崎", 1, 125),
("長崎", 2, 125),
("東京", 1, 250),
("東京", 2, 150);
select
case sex when 1 then "男" else "女" end as 性別,
sum(population) as 全国,
sum(case pref_name when "徳島" then population else 0 end) as 徳島,
sum(case pref_name when "香川" then population else 0 end) as 香川,
sum(case pref_name when "愛媛" then population else 0 end) as 愛媛,
sum(case pref_name when "高知" then population else 0 end) as 高知,
sum(case pref_name
when "徳島" then population
when "香川" then population
when "愛媛" then population
when "高知" then population
else 0
end) as 四国(再掲)
from PopTbl2
group by 性別;
回答との比較
IN
を使うのは頭から抜けてた。。
最初、ORでやろうとしたけど
sum(case pref_name when "徳島" or "香川" or "愛媛" or "高知" then population else 0 end)
こんな書き方して、うまくいかなかった。単純CASE式と検索CASE式がごちゃごちゃになってしまっていた。。
練習問題1-3
自分の回答
select id
from Greatests
order by
case id
when "B" then 1
when "A" then 2
when "D" then 3
when "C" then 4
end;
回答との比較
- SELECT句にソート列として定義する方法もある
- ただし、非推奨
- 最初からソート用の列をテーブルに持たせた方がいい
- SQLの仕事の基本はデータ検索
練習問題2-1
create table ServerLoadSample(
server varchar(128),
sample_date date,
load_val integer
);
insert into ServerLoadSample values
("A", "2018-02-01", 1024),
("A", "2018-02-02", 2366),
("A", "2018-02-05", 2366),
("A", "2018-02-07", 985),
("A", "2018-02-08", 780),
("A", "2018-02-12", 1000),
("B", "2018-02-01", 54),
("B", "2018-02-02", 39008),
("B", "2018-02-03", 2900),
("B", "2018-02-04", 556),
("B", "2018-02-05", 12600),
("B", "2018-02-06", 7309),
("C", "2018-02-01", 1000),
("C", "2018-02-07", 2000),
("C", "2018-02-16", 500);
select
server,
sample_date,
sum(load_val) over () as sum_load
from ServerLoadSample;
予想
- load_valと同じ値がsum_loadになる
- カーソルの位置を移動させていないから
結果
server sample_date sum_load
A 2018-02-01 74448
A 2018-02-02 74448
A 2018-02-05 74448
A 2018-02-07 74448
A 2018-02-08 74448
A 2018-02-12 74448
B 2018-02-01 74448
B 2018-02-02 74448
B 2018-02-03 74448
B 2018-02-04 74448
B 2018-02-05 74448
B 2018-02-06 74448
C 2018-02-01 74448
C 2018-02-07 74448
C 2018-02-16 74448
全部の合計だったかあ。。
1行か、合計のどちらかになるかなとは思ってけど、オプションがないと全部が1つのパーティションの対象になるのか。
練習問題2-2
select
server,
sample_date,
sum(load_val) over (partition by server) as sum_load
from ServerLoadSample;
予想
- Aの行ならAの合計値、Bの行ならBの合計値、Cの行ならCの合計値が表示される
- serverの種類の単位でパーティションが区切られるから
結果
server sample_date sum_load
A 2018-02-01 8521
A 2018-02-02 8521
A 2018-02-05 8521
A 2018-02-07 8521
A 2018-02-08 8521
A 2018-02-12 8521
B 2018-02-01 62427
B 2018-02-02 62427
B 2018-02-03 62427
B 2018-02-04 62427
B 2018-02-05 62427
B 2018-02-06 62427
C 2018-02-01 3500
C 2018-02-07 3500
C 2018-02-16 3500
予想通り♪
ウィンドウ関数、今まで全然使ってこなかったから、使えそうな機会は積極的に使っていきたいな。
練習問題 3-1
create table Products(
name varchar(128),
price integer
);
insert into Products values
("りんご", 100),
("みかん", 50),
("バナナ", 80);
select * from Products;
自分の回答
select
P1.name as name_1,
P2.name as name_2
from Products as P1
inner join Products as P2 on P1.name >= P2.name;
回答と同じ♪
練習問題 3-2
create table Products(
name varchar(128),
price integer
);
insert into Products values
("りんご", 100),
("みかん", 50),
("みかん", 50),
("みかん", 50),
("バナナ", 80);
select
name,
price,
count(*) over tmp_id_w as tmp_id
from Products
window tmp_id_w as (order by name rows between unbounded preceding and current row);
delete P1 from (
select name, price, count(*) over (order by name rows between unbounded preceding and current row) as tmp_id
from Products
) as P1
where P1.tmp_id < (
select name, price, count(*) over (order by name rows between unbounded preceding and current row) as tmp_id
from Products P2
where P1.name = P2.name and P1.price = P2.price
);
こんな感じのSQLを考えたけど、The target table P1 of the DELETE is not updatable
となってしまった。。
降参して回答との比較
- create tableで実テーブル作ってた。。この考えが漏れてた。。
-
row_number()
関数を使えばよかった、この関数知らなかった。。 - ウィンドウ関数は
partition by
を使えばよかった
演習問題 4-1
MySQLをよく使うので、MySQLのORDER BY
句のソート順
ORDER BY を実行する場合、NULL 値は ORDER BY ... ASC では最初に表示され、ORDER BY ... DESC では最後に表示されます。
試しに、以下のSQLの実行してみた
create table Test(id integer, title varchar(100));
insert into Test(id, title) values(1, "Hello");
insert into Test(id, title) values(1, "");
insert into Test(id, title) values(1, null);
insert into Test(id, title) values(1, "World");
select * from Test order by title;
id title
1 NULL
1
1 Hello
1 World
4-2
引数のいずれかかが NULL である場合、CONCAT() は NULL を返します。
演習問題 4-3
リストの最初の非 NULL 値を返します。非 NULL 値がない場合は、NULL を返します。
COALESCE() の戻り型は、引数型の集計型です。
SELECT COALESCE("Hello","", null, "World");
-> Hello
expr1 = expr2 が true の場合は NULL を返し、それ以外の場合は expr1 を返します。 これは、CASE WHEN expr1 = expr2 THEN NULL ELSE expr1 END と同じです。
演習問題 5-1
create table ArrayTbl(id char(1), i integer, val integer);
insert into ArrayTbl values
("A", 1, null),
("A", 2, null),
("A", 3, null),
("A", 4, null),
("A", 5, null),
("A", 6, null),
("A", 7, null),
("A", 8, null),
("A", 9, null),
("A", 10, null),
("B", 1, 3),
("B", 2, null),
("B", 3, null),
("B", 4, null),
("B", 5, null),
("B", 6, null),
("B", 7, null),
("B", 8, null),
("B", 9, null),
("B", 10, null),
("C", 1, 1),
("C", 2, 1),
("C", 3, 1),
("C", 4, 1),
("C", 5, 1),
("C", 6, 1),
("C", 7, 1),
("C", 8, 1),
("C", 9, 1),
("C", 10, 1);
select distinct AT1.id
from ArrayTbl AT1
where not exists (
select AT2.val
from ArrayTbl AT2
where AT1.id = AT2.id
and (AT2.val <> 1 or AT2.val is null)
);
回答とほぼ同じ♪
havingやallだと簡潔に書けるんだなあ。。
演習問題 5-2
select distinct P1.project_id
from Projects P1
where "完了" = all (
select P2.status
from Projects P2
where P1.project_id = P2.project_id
and P2.step_nbr <= 1
)
and "待機" = all (
select P3.status
from Projects P3
where P1.project_id = P3.project_id
and P3.step_nbr > 1
);
- 回答との比較
- サブクエリのcaseで、
step_nber <= 1 and status = "完了"
のときと、step_nber > 1 and status = "待機"
のときをそれぞれ定義してあげればよかった - 回答の方がサブクエリ1つだけで済むから、こっちのほうが良いね
- サブクエリのcaseで、
演習問題 5-3
select N1.num as prime_number
from Numbers N1
where N1.num > 1
and not exists (
select N2.num
from Numbers N2
where N1.num > N2.num
and N1.num % N2.num = 0
);
これでいけるかなと思ったけど、結果は何も返らず。。
回答と比較したら、サブクエリで、N2.num > 1
するのが抜けていた。。
演習問題 6-1
select
case
when count(*) = max(seq) then '歯抜けなし'
else '歯抜けあり'
end as gap
from SeqTbl;
回答との比較
- UNION ALL使うのは思い浮かばなかった
演習問題 6-2
全員が9月中に提出済みの学部(経済学部のみ)
create table Students(student_id integer, dpt varchar(16), sbmt_date date);
insert into Students values
(100, "理学部", "2018-10-10"),
(101, "理学部", "2018-09-22"),
(102, "文学部", null),
(103, "文学部", "2018-09-10"),
(200, "文学部", "2018-09-22"),
(201, "工学部", null),
(202, "経済学部", "2018-09-25");
select dpt
from Students
group by dpt
having count(*) = sum(
case
when sbmt_date < "2018-10-01" then 1
else 0
end
);
回答との比較
- between使って9月に限定する
- extract使って、年、月をそれぞれ確認する方法だと、日付気にせず確認できる
演習問題 6-3
create table Items(item char(16));
insert into Items values ("ビール"),("紙オムツ"),("自転車");
create table ShopItems(shop char(16), item char(16));
insert into ShopItems values
("仙台", "ビール"),
("仙台", "紙オムツ"),
("仙台", "自転車"),
("仙台", "カーテン"),
("東京", "ビール"),
("東京", "紙オムツ"),
("東京", "自転車"),
("大阪", "テレビ"),
("大阪", "紙オムツ"),
("大阪", "自転車");
select
SI.shop,
count(I.item) as my_item_cnt,
(select count(*) from Items) - count(I.item) as diff_cnt
from ShopItems SI left join Items I on SI.item = I.item
group by SI.shop;
回答との比較
- INNER JOINを使った方がよかった
演習問題 7-1
create table Accounts(prc_date date, prc_amt integer);
insert into Accounts values
("2018-10-26", 12000),
("2018-10-28", 2500),
("2018-10-31", -15000),
("2018-11-03", 34000),
("2018-11-04", -5000),
("2018-11-06", 7200),
("2018-11-11", 11000);
select
A1.prc_date,
A1.prc_amt,
(
select avg(A2.prc_amt) from (
select A3.prc_amt
from Accounts A3
where A1.prc_date >= A3.prc_date
order by A3.prc_date desc limit 3
) A2
) prc_avg
from Accounts A1;
回答との比較
- betweenでA2とA1の期間をずらして、それが3以下のものを取得していた、なるほど
演習問題 7-2
select
prc_date,
prc_amt,
(case
when (count(prc_amt) over w) < 3 then null
else avg(prc_amt) over w
end)
from Accounts
window w as (
order by prc_date
rows between 2 preceding and current row
);
select
A1.prc_date,
A1.prc_amt,
(
select
case
when count(A2.prc_amt) < 3 then null
else avg(A2.prc_amt)
end
from (
select A3.prc_amt
from Accounts A3
where A1.prc_date >= A3.prc_date
order by A3.prc_date desc limit 3
) A2
) prc_avg
from Accounts A1;
回答との比較
- window関数の方はfromの中で平均とcountを取得するようにしていた。なるほど。
- 相関サブクエリの方はhavingを使っていた
演習問題 8-1
create table TblAge(age_class integer, age_range varchar(16));
insert into TblAge values(1, "21~30歳"),(2, "31~40歳"),(3, "41~50歳");
create table TblSex(sex_cd char(8), sec varchar(16));
insert into TblSex values("m", "男"),("f", "女");
create table TblPop(pref_name char(8), age_class integer, sex_cd char(8), population integer);
insert into TblPop values
("秋田", 1, "m", 400),("秋田", 3, "m", 1000),("秋田", 1, "f", 800),("秋田", 3, "f", 1000),
("青森", 1, "m", 700),("青森", 1, "f", 500),("青森", 3, "f", 800),
("東京", 1, "m", 900),("東京", 1, "f", 1500),("東京", 3, "f", 1200),
("千葉", 1, "m", 900),("千葉", 1, "f", 1000),("千葉", 3, "f", 900);
select
age_class,
sex_cd,
sum(
case
when pref_name in ("青森", "秋田") then population
else null
end
) as pop_tohoku,
sum(
case
when pref_name in ("東京", "千葉") then population
else null
end
) as pop_kanto
from (
select
TblAge.age_class as age_class,
TblSex.sex_cd as sex_cd,
TblPop.pref_name as pref_name,
TblPop.population as population
from TblAge
cross join TblSex
left outer join TblPop
on TblAge.age_class = TblPop.age_class
and TblSex.sex_cd = TblPop.sex_cd
) as DATA
group by age_class, sex_cd;
回答と一緒♪
演習問題 8-2
create table Personal(employee char(16), child_1 char(16), child_2 char(16), child_3 char(16));
insert into Personal values
("赤井", "一郎", "二郎", "三郎"),
("工藤", "春子", "夏子", null),
("鈴木", "夏子", null, null),
("吉田", null, null, null);
create view Children(child)
as (
select child_1 as child from Personal
union all
select child_2 as child from Personal
union all
select child_3 as child from Personal
);
select
A.employee,
sum(
case
when A.child is null then 0
else 1
end
) child_cnt
from (
select distinct EMP.employee, Children.child
from Personal EMP
left outer join Children
on Children.child in (EMP.child_1, EMP.child_2, EMP.child_3)
) A
group by A.employee;
回答との比較
- count(Children.child)にすれば良かったのか。。
- distinctしたテーブル使わないと重複行が集計されてしまうような。。?
- viewを作るときに
union all
しちゃってたからだった。。
- viewを作るときに
演習問題 8-3
create table Class_A(id integer primary key, name varchar(16));
insert into Class_A values
(1, "田中"),
(2, "鈴木"),
(3, "伊集院");
create table Class_B(id integer primary key, name varchar(16));
insert into Class_B values
(1, "田中"),
(2, "内海"),
(4, "西園寺");
insert into Class_A(id, name)
select Class_B.id, Class_B.name
from Class_B
on duplicate key update Class_A.name = Class_B.name;
回答との比較
- MERGE 使えって書いてあったのを無視してinsert on duplicate key にしてしまっていた。。
- MERGE構文知らなかった
演習問題 9-1
create table tbl_A(
key char(16) primary key,
col_1 integer,
col_2 integer,
col_3 integer;
insert into tbl_A values
("A", 2, 3, 4),
("B", 0, 7, 9),
("C", 5, 1, 6);
create table tbl_B(
key char(16) primary key,
col_1 integer,
col_2 integer,
col_3 integer;
insert into tbl_A values
("A", 2, 3, 4),
("B", 0, 7, 9),
("C", 5, 1, 6);
select count(*) as union_row_cnt
from (
select * from tbl_A
union
select * from tbl_B
) TMP
union all
select count(*) as union_row_cnt from tbl_A
union all
select count(*) as union_row_cnt from tbl_B;
回答との比較
- case使って、各テーブルのcountと一致しているか判定しているのか。なるほど。