製薬ITとは関係ないのですが、おまけです。

躓きやすいところと、勘違いしやすいところなど

Outer Join で注意すべき3つの
おまけです。テーブルとか作らずに、簡単に
明けましておめでとうございます。滅多に更
select文を投げたら帰ってこない。i
NULLがややこしいという記事を書きまし
SQLでややこしくて、よく理解しないまま

新人SEのためのSQL 外部結合

Outer Join で注意すべき3つの点

SQLを書くようになって、割と最初に引っかかりやすいのが『外部結合』ではないでしょうか? この記事では、『外部結合( outer join )』について解説したいと思います。

JOINの種類は全部で5つ

JOINの種類は全部で5つ。以下の図を参照してください。

画像に alt 属性が指定されていません。ファイル名: 20211202-5つのjoin-1024x576.png

この記事で取り上げるのは、outer joinです。
Left outer join、Right outer join 、Full outer joinの3種類があります。
それぞれがどういうものかは、上の図を見れば理解できると思います。
3種類ありはしますが、実際に使うのは99%がLeft outer join ではないかと思います。

Outer Join で注意すべき点は3つです。

・3つ以上のテーブルが絡むときの書き方
・外部結合先のテーブルに対して、固定値の条件を指定する時
・外部結合先のテーブルの項目はNULLを意識する必要がある

3つ以上のテーブルが絡むときの書き方

テーブルが2つの時は迷いはありません。問題は3つ以上の時です。
[親1人子2人]なのか、[親-子-孫]なのか、きちんとイメージする必要があります。

上の図を見て下さい。上のクエリーと下のクエリーは赤字のONの箇所のみの違いです。結果も似ていますが、構造は異なります。

外部結合先のテーブルに対して、固定値の条件を指定する時

JOIN のON句とWHERE句のどちらに書くかで結果は大きく異なりますWHERE句に記載すると、外部結合で結合された状態に対して絞り込みが発生します。

外部結合先のテーブルの項目はNULLを意識する必要がある

外部結合は、結合先のテーブルに該当の行がなかったとしても、行が返されます。その場合、その項目はNULLとなります。したがって、その項目に対して、「=」や「<>」を指定しても正しい結果は返りません。

NULLなので当然ですが、ウッカリしがちなところなので注意してください。

という事で、外部結合で注意すべきことになります。

(2021.12.02 )

簡単に直積演算を体感するスクリプト

おまけです。
テーブルとか作らずに、簡単に直積演算を体感するスクリプトです。

標準SQL編


select concat(t1.decitem,'×',t2.decitem,' = ',t1.decitem * t2.decitem ) from
(
select 1 as decitem
union
select 2
union
select 3
union
select 4
union
select 5
union
select 6
union
select 7
union
select 8
union
select 9
) t1
cross join
(
select 1 as decitem
union
select 2
union
select 3
union
select 4
union
select 5
union
select 6
union
select 7
union
select 8
union
select 9
) t2
on 1=1
order by 1;

結果です。

1×1 = 1
1×2 = 2
1×3 = 3
1×4 = 4
1×5 = 5
1×6 = 6
1×7 = 7
1×8 = 8
1×9 = 9
2×1 = 2
2×2 = 4
2×3 = 6
2×4 = 8
2×5 = 10
2×6 = 12
2×7 = 14
2×8 = 16
2×9 = 18
3×1 = 3
3×2 = 6
3×3 = 9
3×4 = 12
3×5 = 15
3×6 = 18
3×7 = 21
3×8 = 24
3×9 = 27
4×1 = 4
4×2 = 8
4×3 = 12
4×4 = 16
4×5 = 20
4×6 = 24
4×7 = 28
4×8 = 32
4×9 = 36
5×1 = 5
5×2 = 10
5×3 = 15
5×4 = 20
5×5 = 25
5×6 = 30
5×7 = 35
5×8 = 40
5×9 = 45
6×1 = 6
6×2 = 12
6×3 = 18
6×4 = 24
6×5 = 30
6×6 = 36
6×7 = 42
6×8 = 48
6×9 = 54
7×1 = 7
7×2 = 14
7×3 = 21
7×4 = 28
7×5 = 35
7×6 = 42
7×7 = 49
7×8 = 56
7×9 = 63
8×1 = 8
8×2 = 16
8×3 = 24
8×4 = 32
8×5 = 40
8×6 = 48
8×7 = 56
8×8 = 64
8×9 = 72
9×1 = 9
9×2 = 18
9×3 = 27
9×4 = 36
9×5 = 45
9×6 = 54
9×7 = 63
9×8 = 72
9×9 = 81

こんな感じです。

Oracleの書き方がこちら。

select t1.dec||'×'||t2.dec||' = '||t1.dec * t2.dec from
(
select 1 as dec from dual
union
select 2 from dual
union
select 3 from dual
union
select 4 from dual
union
select 5 from dual
union
select 6 from dual
union
select 7 from dual
union
select 8 from dual
union
select 9 from dual
) t1,
(
select 1 as dec from dual
union
select 2 from dual
union
select 3 from dual
union
select 4 from dual
union
select 5 from dual
union
select 6 from dual
union
select 7 from dual
union
select 8 from dual
union
select 9 from dual
) t2
;

( 2021.7.11 、2024.08.22 標準SQL編追加)

直積演算でSQLを理解する(2)

明けましておめでとうございます。
滅多に更新しないブログですが、本年もよろしくお願いいたします。

今回は直積シリーズの第二弾。直積をイメージする助けになりそうな、とある使い方を紹介します。

まずは以下のテーブルを用意します。
超簡単なテーブルです。

DROP TABLE crossjoin_test;

CREATE TABLE crossjoin_test (
id number(1)
);

中に入れるのは以下のデータです。

TRUNCATE TABLE crossjoin_test;

INSERT INTO crossjoin_test VALUES( 1 );
INSERT INTO crossjoin_test VALUES( 2 );
INSERT INTO crossjoin_test VALUES( 3 );
INSERT INTO crossjoin_test VALUES( 4 );
INSERT INTO crossjoin_test VALUES( 5 );
INSERT INTO crossjoin_test VALUES( 6 );
INSERT INTO crossjoin_test VALUES( 7 );
INSERT INTO crossjoin_test VALUES( 8 );
INSERT INTO crossjoin_test VALUES( 9 );

COMMIT;

超簡単なテーブルが用意出来ました。
次に、以下のSQLを実行してみます。

SELECT * FROM crossjoin_test t1
INNER JOIN crossjoin_test t2 ON t1.id=t2.id
ORDER BY 1
;

/* Oracle的に書くと */
SELECT * FROM crossjoin_test t1,crossjoin_test t2
WHERE t1.id = t2.id
ORDER BY 1
;

こちらが実行結果です。

ID ID
-- --
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9

どうという事はない結果です。

ちなみに、一つのテーブルをこのように2回登場させることができます。

では次に直積を試してみます。

SELECT * FROM crossjoin_test t1
CROSS JOIN crossjoin_test t2
ORDER BY 1,2
;

/* Oracle的に書くと */
SELECT * FROM crossjoin_test t1,crossjoin_test t2
ORDER BY 1,2
;

結果は以下の様になります。

ID ID
-- --
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
2 1
2 2
2 3
:
( 省略 )
:
8 6
8 7
8 8
8 9
9 1
9 2
9 3
9 4
9 5
9 6
9 7
9 8
9 9

81行が選択されました。

当たり前の結果です。

次は少し遊んでみましょう。

SELECT concat(t1.id , ' × ' , t2.id , ' = ' , t1.id * t2.id )
FROM crossjoin_test t1
CROSS JOIN crossjoin_test t2
ORDER BY 1
;

/* Oracle的に書くと */
SELECT t1.id||' × '||t2.id||' = '||t1.id*t2.id
FROM crossjoin_test t1,crossjoin_test t2
ORDER BY 1
;

実行結果が以下です。

T1.ID||'×'||T2.ID||'='||T1.ID*T2.ID
----------------------------------------
1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
1 × 4 = 4
1 × 5 = 5
1 × 6 = 6
1 × 7 = 7
1 × 8 = 8
1 × 9 = 9
2 × 1 = 2
2 × 2 = 4
2 × 3 = 6
:
( 省略 )
:
8 × 6 = 48
8 × 7 = 56
8 × 8 = 64
8 × 9 = 72
9 × 1 = 9
9 × 2 = 18
9 × 3 = 27
9 × 4 = 36
9 × 5 = 45
9 × 6 = 54
9 × 7 = 63
9 × 8 = 72
9 × 9 = 81


81行が選択されました。

九九の表が出来ました。

直積でコードを生成する

上記の九九の様な使い方、あまりしないと思いますが、便利なときもあります。

上記の例は、9行のテーブルを2回使いましたが、これに0の行を追加します。

INSERT INTO crossjoin_test VALUES( 0 );
COMMIT;

これで、0~9の10行のデータが入りました。

そして、一つのテーブルを3回使用する、以下のようなSQLを書いて実行します。

SELECT concat(t1.id , t2.id , t3.id )
FROM crossjoin_test t1
CROSS JOIN crossjoin_test t2
CROSS JOIN crossjoin_test t3
ORDER BY 1
;

/* Oracle的に書くと */
SELECT t1.id||t2.id||t3.id
FROM crossjoin_test t1,crossjoin_test t2,crossjoin_test t3
ORDER BY 1
;

実行結果が以下となります。

T1.ID||T2.ID||T3.ID
----------------------
000
001
002
003
004
005
006
:
(省略)
:
997
998
999

1000行が選択されました。


例えば、トランザクションを全てUPDATE文で処理したいような場合、テーブルの中にあらかじめすべてのデータを入れておくという事をすることがあります。

そのような場合に、この方法で簡単にコード値を作成して、これを基にINSERTしておくことができます。

あらかじめテーブルにデータを入れておくというのは、SQL文がシンプルになるという事もありますし、ストレージ内でデータのフラグメンテーションが生じるのを抑制したい場合などが考えられます。

最後は少し話がそれましたが、直積をきちんと理解すると、RDBの理解につながると思います。

( 2021.01.20 )


直積演算でSQLを理解する

select文を投げたら帰ってこない。
insertのバッチ処理が容量オーバーになった。

誰しもそういう経験があると思います。

Joinのキーが不足していたために、直積で膨大な件数が発生するという原因です。

直積とは

以下のケースで考えてみます。

社員テーブル(syain_tb)と銀行テーブル(bank_tb)から、社員ごとの給与振込口座一覧表を作成するというものです。

まずは準備のスクリプトです。

CREATE TABLE syain_tb (
syain_id CHAR(2),
syain_name VARCHAR(10),
bank_cd CHAR(4),
branch_cd CHAR(3)  );

CREATE TABLE bank_tb (
bank_cd CHAR(4),
branch_cd CHAR(3),
branch_name VARCHAR(30)  );

INSERT INTO syain_tb VALUES( '01','山田','0001','002');
INSERT INTO syain_tb VALUES( '02','鈴木','0001','003');
INSERT INTO syain_tb VALUES( '03','田中','0002','001');

INSERT INTO bank_tb VALUES( '0001','001','りんご銀行本店');
INSERT INTO bank_tb VALUES( '0001','002','りんご銀行青森支店');
INSERT INTO bank_tb VALUES( '0001','003','りんご銀行長野支店');
INSERT INTO bank_tb VALUES( '0002','001','みかん銀行本店');
INSERT INTO bank_tb VALUES( '0003','001','いちご銀行本店');

COMMIT;

社員ごとの給与振込口座一覧を出力するには、
syain_tbの bank_cd と、bank_tbの bank_cd
syain_tbの branch_cd と、bank_tbの branch_cd
をJOINします。

SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s
INNER JOIN bank_tb b ON s.bank_cd = b.bank_cd
      AND s.branch_cd = b.branch_cd
ORDER BY 1 ;

/* Oracle的に書くと */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s,bank_tb b
WHERE s.bank_cd = b.bank_cd
AND s.branch_cd = b.branch_cd
ORDER BY 1 ;

いずれも結果は以下となります。

01 山田 0001 002 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行長野支店
03 田中 0002 001 みかん銀行本店

ここで、JOINのキーが足りないとどうなるでしょうか?
branch_cd をJOINのキーから外してみます。

/* branch_cdのJOINをコメントアウトしました */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s,bank_tb b
WHERE s.bank_cd = b.bank_cd
--AND s.branch_cd = b.branch_cd
ORDER BY 1
;

/* Oracleの書き方 */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s
INNER JOIN bank_tb b ON s.bank_cd = b.bank_cd
-- AND s.branch_cd = b.branch_cd
ORDER BY 1
;

以下が検索結果です。

01 山田 0001 002 りんご銀行長野支店
01 山田 0001 002 りんご銀行本店
01 山田 0001 002 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行本店
02 鈴木 0001 003 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行長野支店
03 田中 0002 001 みかん銀行本店

bank_cdしかJOINのキーに入っていないので、同一銀行で複数の支店のあるりんご銀行に口座をもつ山田さんと鈴木さんが3行ずつ出ています。

これが直積です。

上の例では、3行出力すべきところが、7行になっているだけですが、JOINしているテーブルによっては、本来なら100件の筈が10億件になって、バッチ処理が止まるという事になります。

くれぐれも、お気を付けください。


SQLが間違っていても、データによっては発生しない

そうなのです。
たとえ、SQL文に問題があったとしても、データによっては、出力結果の増殖は発生しません。

上記の出力結果で、田中さんについては正しい結果が表示されています。

たとえば、リリースしたばかりで、テーブルにデータが1日分しかないとか、そういう場合は日付の指定が漏れていると、リリース日の夜間ではなくて、翌日の夜間処理で悲惨な結果が発生します。
あるいは、期の指定がないと、リリースして半年たって、期が変わって世代データが発生した時に夜間処理が終わらない事になります。

くれぐれもお気を付けください。

直積こそ、リレーショナルDBを理解する肝

さて、これはぜひ言いたい。この記事で一番言いたいのはこれです。

そもそも、JOINの指定が何もなくて、完全に直積になっている状態。
その状態に対して条件を指定していく。

そう考えると、SQLはイメージしやすくなります。

フルに直積された状態から、
syain_tbのbank_cd列とbank_tb のbank_cd列の等しいものを抽出する。
さらに、
syain_tbのbranch_cd列とbank_tb のbranch_cd列の等しいものを抽出する。
というイメージです。

これは断然Oracleの書き方の方が分かりやすいです。

/* WHERE句なし */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s,bank_tb b
ORDER BY 1
;

01 山田 0001 002 りんご銀行青森支店
01 山田 0001 002 りんご銀行本店
01 山田 0001 002 りんご銀行長野支店
01 山田 0001 002 みかん銀行本店
01 山田 0001 002 いちご銀行本店
02 鈴木 0001 003 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行本店
02 鈴木 0001 003 りんご銀行長野支店
02 鈴木 0001 003 いちご銀行本店
02 鈴木 0001 003 みかん銀行本店
03 田中 0002 001 りんご銀行本店
03 田中 0002 001 りんご銀行青森支店
03 田中 0002 001 みかん銀行本店
03 田中 0002 001 りんご銀行長野支店
03 田中 0002 001 いちご銀行本店
15行が選択されました。

/* bank_cdのみWHERE句指定 */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s,bank_tb b
WHERE s.bank_cd = b.bank_cd /* ← 追加 */
ORDER BY 1
;

01 山田 0001 002 りんご銀行長野支店
01 山田 0001 002 りんご銀行本店
01 山田 0001 002 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行本店
02 鈴木 0001 003 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行長野支店
03 田中 0002 001 みかん銀行本店
7行が選択されました。

/* bank_cdとbranch_cdにWHERE句指定 */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s,bank_tb b
WHERE s.bank_cd = b.bank_cd /* 追加 */
AND s.branch_cd = b.branch_cd /* 追加 */

ORDER BY 1
;

01 山田 0001 002 りんご銀行青森支店
02 鈴木 0001 003 りんご銀行長野支店
03 田中 0002 001 みかん銀行本店

と、こんな感じになります。

Oracleだと、where句ですべて書いて行く書き方が出来ますので、完全な直積が発生します。通常の書き方だと、わざわざ"CROSS JOIN" と指定することになります。

SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s
CROSS JOIN bank_tb b
ORDER BY 1
;

ちなみに、以下の2つの書き方の、結果は同じです。

/* INNERT JOIN */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s
INNER JOIN bank_tb b ON s.bank_cd = b.bank_cd
AND s.branch_cd = b.branch_cd
ORDER BY 1
;

/* CROSS JOIN した上で、WHERE */
SELECT s.syain_id,s.syain_name,s.bank_cd,s.branch_cd,branch_name
FROM syain_tb s
CROSS JOIN bank_tb b
WHERE s.bank_cd = b.bank_cd
AND s.branch_cd = b.branch_cd
ORDER BY 1
;

最後の書き方をする方はいないと思いますが、こう書くと、Oracleと似ていますね。

新人SEのためのSQL 外部結合とNULL

NULLがややこしいという記事を書きました。

単体でもNULLを扱う事はもちろんありますが、出会う場面は外部結合の際が多いように思います。

今回もOracleを使って説明します。

まず、以下のテーブルを準備します。

create table student (
id char(3) ,
name varchar2(20)
) ;
create table result (
id char(3) ,
season char(1),
score number(3)
) ;

insert into student values('001','山田');
insert into student values('002','田中');
insert into student values('003','鈴木');
insert into student values('004','佐藤');

insert into result values('001',1,100);
insert into result values('002',1,80);
insert into result values('003',1,70);
insert into result values('001',2,100);
insert into result values('002',2,85);
insert into result values('004',2,80);

commit;

以下の図の様なシチュエーションを想定します。

5人の生徒がいて、1学期2学期それぞれテストを受けました。
ただし、佐藤さんは1学期、鈴木さんは2学期のテストを、高橋さんは1、2学期共にテストを受けることができませんでした。

テーブル間の関係が1:nの場合

まず、70点以下(未受験含む)の人を追試対象として抽出してみます。

resultテーブルには受験しなかった場合にデータはありません。内部結合だと、未受験のデータが取れませんので、外部結合を使います。

/* 外部結合にしただけでは駄目な例 */
SELECT s.id,s.name,r.season,r.score
FROM student s
LEFT Join result r on s.id = r.id
WHERE r.score IS NULL OR r.score <= 70
ORDER BY 1,3
;

(結果)
ID NAME SE SCORE
----- --------- ------- -----------
003 鈴木 1 70
005 高橋

高橋さんは抽出されましたが、学期の情報がありません。また、1学期未受験の佐藤さんも出て来ませんし、鈴木さんも2学期未受験の情報が出て来ません。

これは、whereを外して検索すると分かりやすくなります。

(whereを外して実行した結果)
ID NAME SE SCORE
----- --------- ------- -----------
001 山田 1 100
001 山田 2 100
002 田中 2 85
002 田中 1 80
003 鈴木 1 70
004 佐藤 2 80
005 高橋
7行が選択されました。

Left joinによって取り出された上記のデータから、70点以下と、NULLのデータを抽出しているという事になります。基準となる表と外部結合先の関係が、1:1ならいいのですが、1:nの場合にはうまく取り出せません。

1:1の関係にする

この様なケースでは、1学期と2学期を分けてSQLを書く必要があります。

resultテーブルからひとつの学期のデータのみ取り出し、studentテーブルと1:1の関係にします。

SELECT s.id,s.name,r.season,r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 1
UNION
SELECT s.id,s.name,r.season,r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 2
ORDER BY 1,3;

別々に取り出してUNIONしてみました。

(結果)
ID NAME SE SCORE
----- ------- ------- -----------
001 山田 1 100
001 山田 2 100
002 田中 1 80
002 田中 2 85
003 鈴木 1 70
003 鈴木
004 佐藤 2 80
004 佐藤
005 高橋
9行が選択されました。

なんとなくよさそうには見えますが、まだ問題があります。
高橋さんのデータが1件しかありません。さらに、鈴木さん佐藤さんの未受験の学期も空白になっています。
高橋さんが1件なのは、UNION ALLを使えば解消しますが、ここでは別の手を使います。

1学期、2学期と、それぞれ狙い撃ちで取得していますから、結果に固定値として置いても問題ありません。

SELECT s.id,s.name,1 AS gakki,r.season ,r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 1
UNION
SELECT s.id,s.name,2 AS gakki,r.season, r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 2
ORDER BY 1,3 ;

ID NAME GA SE SCORE
----- -------- --- ---- -----------
001 山田 1 1 100
001 山田 2 2 100
002 田中 1 1 80
002 田中 2 2 85
003 鈴木 1 1 70
003 鈴木 2
004 佐藤 1
004 佐藤 2 2 80
005 高橋 1
005 高橋 2
10行が選択されました。

これで5人×2学期=10行のデータを取り出すことが出来ました。
後は、where 句を追加すれば完成です。
この10行の中から、必要なデータを取り出すというイメージです。

SELECT s.id,s.name,1 AS gakki,r.season ,r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 1
WHERE r.score IS NULL OR r.score <= 70
UNION
SELECT s.id,s.name,2 AS gakki,r.season, r.score
FROM student s
LEFT JOIN result r ON s.id = r.id AND r.season = 2
WHERE r.score IS NULL OR r.score <= 70
ORDER BY 1,3 ;

(検索結果)
ID NAME GA SE SCORE
----- --------- --- ---- -----------
003 鈴木   1 1 70
003 鈴木 2
004 佐藤 1
005 高橋 1
005 高橋 2

これで完成です。

各学期を横に並べてみる

次に、学期を横に並べた表を作ってみましょう。
studentテーブルに、1学期だけ、2学期だけをそれぞれ抽出したresultテーブルと外部結合します。

SELECT s.id,s.name,r1.score AS gakki1 ,r2.score AS gakki2
FROM student s
LEFT JOIN result r1 ON s.id = r1.id AND r1.season = 1
LEFT JOIN result r2 ON s.id = r2.id AND r2.season = 2
ORDER BY 1;

(検索結果)
ID NAME GAKKI1 GAKKI2
------ --------------- ------- ---------
001 山田 100 100
002 田中 80 85
003 鈴木 70
004 佐藤 80
005 高橋

ここまでは簡単です。

次に、1学期と2学期の合計を求めてみましょう。

SELECT s.id,s.name,
r1.score AS gakki1,
r2.score AS gakki2,
r1.score+r2.score AS gokei1
FROM student s
LEFT JOIN result r1 ON s.id = r1.id AND r1.season = 1
LEFT JOIN result r2 ON s.id = r2.id AND r2.season = 2
ORDER BY 1;

ID NAME GAKKI1 GAKKI2 GOKEI1
----- --------- -------- --------- --------
001 山田 100 100 200
002 田中 80 85 165
003 鈴木 70
004 佐藤 80
005 高橋

山田さんと田中さん以外のGOKEI1は空欄になっています。

NULLは何を足しても何を引いても何を掛けても何で割ってもNULLなのです。

DECODEとCASE

という事で、2種類の方法を試してみます。一つはdecode、もう一つはcaseです。それぞれnullの場合に0への変換を試みます。

SELECT s.id,s.name,
r1.score AS gakki1,
r2.score AS gakki2,
r1.score+r2.score AS gokei1,
DECODE(r1.score,NULL,0,r1.score) +

DECODE(r2.score,NULL,0,r2.score) AS gokei2,
   CASE WHEN
r1.score IS NULL THEN 0 ELSE r1.score END +
CASE WHEN r2.score IS NULL THEN 0 ELSE r2.score END AS gakki3

FROM student s
LEFT JOIN result r1 ON s.id = r1.id AND r1.season = 1
LEFT JOIN result r2 ON s.id = r2.id AND r2.season = 2
ORDER BY 1;

ID NAME GAKKI1 GAKKI2 GOKEI1 GOKEI2 GAKKI3
---- ------- -------- --------- --------- -------- ---------
001 山田 100 100 200 200 200
002 田中 80 85 165 165 165
003 鈴木 70 70
004 佐藤 80 80
005 高橋 0

いかがでしょうか?
nullに何かを足しても答えはnullなので、鈴木さん佐藤さん高橋さんのGOKEI1は空白になっています。一方、decodeで0に置き換えたGOKEI2は期待する値になっています。そしてGOKEI3もnullの影響を受けています。

CASE文の書き方に注意してください。
CASE の後にカラム名を書く方法ではNULLは判別できないし、かといってエラーにもなりません。

NULLは奥が深いですね。

新人SEのためのSQL NULL

SQLでややこしくて、よく理解しないまま過ごしてしまいがちなのがNULLではないでしょうか? しかも、DBMSによっても微妙に違いますので、尚更注意が必要です。

きっちり、NULLを押さえましょう!

押さえるべきポイントは以下の4つ。

1.NULLは、何を足しても、引いても、掛けても、割ってNULL
2.NULLには、=や<>、<、>は通用しない
3.NULLはIS NULL か IS NOT NULLで判定するかNULLを扱える
 関数を使用する
4.CASE文でNULLを使う時は注意せよ。

5.NULLは’’(長さゼロの文字値)とは違う。
でも、Oracleは「’’」とNULLは同じ

ここではOracleを使って説明します。

1.NULLは、何を足しても、引いても、掛けても、割ってNULL

そうなのです。そしてエラーにもなりません。
以下のSQL文を実行してみてください。

SELECT NULL+1 , NULL-1 , NULL*1 , NULL/1 , LEHGTH(NULL) FROM DUAL;

答えは全てNULLです。

ただし、SUMやCOUNTの場合は集計している中にNULLの行があっても、そのデータが加算されないだけで結果がNULLにはなりません。

興味のある方は、以下も試してみてください。

SELECT COUNT(*) FROM ( SELECT NULL FROM DUAL); 
/* 答えは1 */

SELECT COUNT(col) FROM ( SELECT NULL AS col FROM DUAL);
/* 答えは0 */

SELECT SUM(col) FROM ( SELECT NULL AS col FROM DUAL);
/* 答えはNULL */

SELECT COUNT(*) FROM
( SELECT NULL FROM DUAL UNION ALL
SELECT NULL FROM DUAL );
/* 答えは2 */

SELECT COUNT(col) FROM
( SELECT NULL AS col FROM DUAL UNION ALL
SELECT 1 FROM DUAL);
/* 答えは1 */

SELECT SUM(col) FROM
( SELECT NULL AS col FROM DUAL UNION ALL
SELECT 1 FROM DUAL);
/* 答えは1 */

また、NULLには長さもありません。

LENGTH(NULL)は、ゼロではなく、NULLです。


2.NULLには、=や<>、<、>は通用しない

以下を実行してみてください。

SELECT 1 FROM DUAL WHERE NULL > 1;
SELECT 1 FROM DUAL WHERE NULL < 1;
SELECT 1 FROM DUAL WHERE NULL = 1;
SELECT 1 FROM DUAL WHERE NULL <> 1;

上記はいずれも、

>レコードが選択されませんでした。

となります。
試しに、以下も同じです。

SELECT 1 FROM DUAL WHERE NULL = NULL;

NULL とNULLは同じですが、成立しないのです。

3.NULLはIS NULL か IS NOT NULLで判定するか、NULLを扱える関数を使用する

NULLは
IS NULL または、IS NOT NULL
で判定します。

SELECT 1 FROM DUAL WHERE NULL IS NULL ;
SELECT 1 FROM DUAL WHERE 1 IS NOT NULL ;

上記はいずれもTRUEとなります。

IS NULL、IS NOT NULL以外だと、NULLを判定できるいくつかの関数があります。

DECODE、NVL、IFNULL、CASEなど。(DBMSによって異なります)

4.CASE文でNULLを使う時は注意せよ。

CASE文には2種類の書き方があります。

SELECT CASE 1 WHEN 1 THEN '1です' ELSE '1ではないです' END FROM DUAL;

SELECT CASE WHEN 1 = 1 THEN '1です' ELSE '1ではないです' END FROM DUAL;

上記はいずれも同じ結果となります。

しかし、これをNULLに置き換えると結果は違ってきます。

SELECT CASE NULL WHEN NULL THEN 'NULLです' ELSE 'NULLではないです' END FROM DUAL;

SELECT CASE WHEN NULL IS NULL THEN 'NULLです' ELSE 'NULLではないです' END FROM DUAL;

上のSQLは、"NULLではない"を返します。エラーにもなりません。

気を付けましょう。

5.NULLは「''」とは違う。
でも、Oracleは「''」とNULLは同じ

一般には、NULLと「''」は異なります。しかし、Oracleは「''」はNULLになります。くれぐれも注意してください。

/*Oracleの場合*/
SELECT CASE WHEN '' IS NULL THEN 'NULLです' ELSE 'NULLではないです' END FROM DUAL;

/*MySQLの場合*/
SELECT CASE WHEN '' IS NULL THEN 'NULLです' ELSE 'NULLではないです' END ;

Oracleは、"NULLです"が返り、MySQLは"NULLではないです"が返ります。

これは、くれぐれも注意してください。

また、LENGTH(NULL)は、NULLですが、「''」はOracleとMySQLで異なります。

/*Oracleの場合*/
SELECT LENGTH('') FROM DUAL;

/*MySQLの場合*/
SELECT LENGTH('');

OracleはNULLが返り、MySQLは0が返ります。

という事で、皆様、NULLには十分気を付けてください。