デザインパターン Stateについて

Stateパターンとは

ステートパターンは状態をクラスとして表現して、クラスを切り替えることで状態の変化を表すことができます。

サンプルプログラム

状態をクラスとして表現する例として、ATM使用時に時刻ごとに手数料が異なるシステムを考えてみましょう。
以下を時刻ごとの手数料として、画面に表示するとします。

9:00~17:59
・手数料が0円

18:00~8:59
・手数料が200円

これを単純にif文で表現すると以下のようになります。

   ATM使用時に呼ばれるメソッド () {
       if (9:00~17:59) {
           手数料0円を画面表示
       } else if (18:00~8:59) {
           手数料200円を画面表示
       }
   }

Stateパターンを使わない場合は、状態のチェックにif文を使用します。
Stateパターンではif文を使用しません。
9:00~17:59と18:00~8:59のATMの状態をクラスとして表現します。
作成したサンプルプログラムを見てみましょう。

・サンプルプログラムで登場するクラスとインターフェース

f:id:xxyoko10:20200412230513p:plain

Stateインターフェース

ATMの状態を表しています。
引数のContextで状態の管理を行っており、
useATMメソッドは状態に応じて、処理が変化するメソッドです。

public interface State {
    public abstract void doClock(Context context, int hour);    // 時刻設定
    public abstract void useATM(Context context);               // ATM使用
}

DayState1クラス

DayState1クラスは9:00~17:59の状態を表すクラスです。
doClockメソッド内で、状態の遷移をしています。
また、インスタンスは一つしか作れないように、Singletonパターンを使用しています。

public class DayState1 implements State {
    private static DayState1 singleton = new DayState1();
    private DayState1() {                                // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (hour < 9 || 18 <= hour) {
            context.changeState(DayState2.getInstance());
        }
    }
    public void useATM(Context context) {               // ATM使用
        context.showScreen("手数料は0円です。");
    }
}

DayState2クラス

DayState2クラスは18:00~8:59の状態を表すクラスです。
DayState1と同様にdoClockメソッド内で、状態の遷移をしています。

public class DayState2 implements State {
    private static DayState2 singleton = new DayState2();
    private DayState2() {                              // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (9 <= hour && hour < 18) {
            context.changeState(DayState1.getInstance());
        }
    }
    public void useATM(Context context) {               // ATM使用
        context.showScreen("手数料は200円です。");
    }
}

Contextクラス

状態の管理をしているインターフェースです。

public interface Context {

    public abstract void setClock(int hour);                // 時刻設定
    public abstract void changeState(State state);          // 状態変化
    public abstract void showScreen(String msg);            // 手数料表示
    public abstract void recordLog(String msg);             // 記録
}

ATMFrameクラス

GUIを使ってATMシステムを実現するクラスです。
コンストラクタで背景色やボタンの配置を決めています。
ここで注目すべきは、ATM使用ボタンを押したときに呼ばれる、useATMメソッドです。
if文で時刻の状態チェックをするわけでなく、いきなりメソッドを呼び出しています。
ここでは現在の状態を表すクラスをchangeStateメソッドに引数で渡し、
状態を表すフィールドに設定することで、遷移を行っています。

public class ATMFrame extends Frame implements ActionListener, Context {
    private TextField textClock = new TextField(60);        // 現在時刻表示
    private TextArea textScreen = new TextArea(10, 60);     // ATM出力
    private Button buttonUse = new Button("ATM使用");       // ATM使用ボタン
    private Button buttonExit = new Button("終了");         // 終了ボタン

    private State state = DayState1.getInstance();           // 現在の状態

    // コンストラクタ
    public ATMFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        setLayout(new BorderLayout());
        // textClockを配置
        add(textClock, BorderLayout.NORTH);
        textClock.setEditable(false);
        // textScreenを配置
        add(textScreen, BorderLayout.CENTER);
        textScreen.setEditable(false);
        // パネルにボタンを格納
        Panel panel = new Panel();
        panel.add(buttonUse);
        panel.add(buttonExit);
        // そのパネルを配置
        add(panel, BorderLayout.SOUTH);
        // 表示
        pack();
        show();
        // リスナーの設定
        buttonUse.addActionListener(this);
        buttonExit.addActionListener(this);
    }
    // ボタンが押されたらここに来る
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        if (e.getSource() == buttonUse) {              // ATM使用ボタン
            state.useATM(this);
        } else if (e.getSource() == buttonExit) {      //終了ボタン
            System.exit(0);
        } else {
            System.out.println("?");
        }
    }
    // 時刻の設定
    public void setClock(int hour) {
        String clockstring = "現在時刻は";
        if (hour < 10) {
            clockstring += "0" + hour + ":00";
        } else {
            clockstring += hour + ":00";
        }
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this, hour);
    }
    // 状態変化
    public void changeState(State state) {
        System.out.println(this.state + "から" + state + "へ状態が変化しました。");
        this.state = state;
    }
    // ATMの画面に手数料を出力
    public void showScreen(String msg) {
        textScreen.append(msg + "\n");
    }
    // ATM記録
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}

Mainクラス

時刻の設定を1時間ごとに行っています。

public class Main {
    public static void main(String[] args) {
        ATMFrame frame = new ATMFrame("State Sample");
        while (true) {
            for (int hour = 0; hour < 24; hour++) {
                frame.setClock(hour);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}

実行結果

18:00~8:59にATM使用ボタン押下 f:id:xxyoko10:20200413003324p:plain

9:00~17:59にATM使用ボタン押下 f:id:xxyoko10:20200413003430p:plain

まとめ

システムの各状態を個別のクラスで表現するStateパターンを学びました。
Stateパターンで押さえておくべきポイントは以下の2点です。

・抽象メソッドとして宣言し、インターフェースとする
・具象メソッドとして実装し、個々のクラスとする

これがStateパターンの状態に依存した処理の表現方法となります。

無駄なソートを回避する

ソートでSQLが遅くなる

SQLを早くするテクニックについて勉強したので、
備忘録として記事を書きます。
タイトルにあるように、GROUP BY 、ORDER BY、DISTINCTなどを使用するとソートが
発生するので、メモリを多く必要とします。
結果としてSQLが遅くなってしまいます。
パフォーマンス向上のために、ソートを回避する方法を紹介します。

UNIONを使う

集合演算子UNIONは二つの検索結果を足し合わせた和集合を求めます。
UNIONは重複行を1行にまとめます。
f:id:xxyoko10:20200221204138p:plain

実際にテーブルを作って試してみます。
データが同じクラステーブルを二つ作りました。

CREATE TABLE Class_A
(id VARCHAR(10) PRIMARY KEY,
 name CHAR(10) NOT NULL);
 
 CREATE TABLE Class_B
(id VARCHAR(10) PRIMARY KEY,
 name CHAR(10) NOT NULL);
 
 
INSERT INTO Class_A VALUES('1', 'たなか');
INSERT INTO Class_A VALUES('2', 'さとう');
INSERT INTO Class_A VALUES('3', 'いとう');

INSERT INTO Class_B VALUES('1', 'たなか');
INSERT INTO Class_B VALUES('2', 'さとう');
INSERT INTO Class_B VALUES('3', 'いとう');

■Class_A

SELECT * FROM Class_A

f:id:xxyoko10:20200221205858p:plain

■Class_B

---Class_B
SELECT * FROM Class_B

f:id:xxyoko10:20200221205932p:plain

この二つのテーブルの重複を排除した結果を求めます。

SELECT * FROM Class_A
UNION
SELECT * FROM Class_B

■結果
f:id:xxyoko10:20200221210705p:plain

重複を排除した結果を得ることができました。

EXISTSを理論的に学ぶ

はじめに

ブログでEXISTSについて、取り上げましたが理論的なことには触れていませんでした。 本に解説があったので、整理するために今回の記事を書きます。

■参考書籍

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

  • 作者:ミック
  • 出版社/メーカー: 翔泳社
  • 発売日: 2018/10/11
  • メディア: 単行本(ソフトカバー)

SQLの述語とは

まず、EXISTSの特徴を取り上げる前に、SQLの述語について理解しておく必要があります。
SQLの述語は戻り値が真理値になる関数のことを指します。
BETWEEN、LIKE、IN、=、>、<とかです。
すべて戻り値はtrue、false、unknowになります。
SUMやAVGは、含まれないです。
EXISTSは述語ですが、BETWEENやINなど他の述語とは異なります。
それは『引数』です。

SELECT * FROM TABLE
 WHERE Colum  BETWEEN 100 AND 200;
SELECT * FROM TABLE
 WHERE Colum IN ('100','150');
SELECT * FROM TABLE_A A
  WHERE EXISTS
              (SELECT * FROM TABLE_B B
                 WHERE A.id = B.id);

上のSQLを見ていただければわかると思います。
BETWEEN とIN は引数は「150」とか単一の値をとります。
しかしEXISTSの引数はSELECT文です。
EXISTSはサブクエリがどういう列を選択するかは気にしません。
EXISTSのサブクエリのSELECT句のリストには3通りの書き方があります。

1.ワイルドカード:SELECT *
2.定数:SELECT 'ここは何でもいいんだよ'
3.列名:SELECT col

EXISTSは階層が異なる

以上のことを踏まえて、以下の図を見てください。
■引数は1行 f:id:xxyoko10:20200204215037p:plain

■引数は行の集合 f:id:xxyoko10:20200204215057p:plain

1行を入力としているLIKEやBETWEENなどの述語を「1階の述語」、
行の集合を入力としている述語を「2回の述語」と言います。

一階の述語 = 「1行」を入力に取る述語
二階の述語 = 「行の集合」を入力に取る述語

EXISTSは他の関数とは異なり、複数行を一単位とみなした高度な条件を記述することができるのです。

述語論理について

SQLの基礎理論には述語論理があります。

述語論理には、文を書くための道具である量化子という述語を用いた、量化理論があります。
EXISTS述語は量化理論で表現することができます

量化子には、全称量化子と存在量化子があり、まとめると以下のようになります。

「すべてのxが条件Pを満たす」           ・・・全称量化子
「条件Pを満たすxが(少なくとも1つ)存在する」  ・・・存在量化


ここで押さえておかなくてはならないのは、
EXISTSは存在量化子を実装したものであるということです。
SQLは全称量化子に対応する述語を導入しませんでした。
そのため、全称量化子を表現するためには、「すべてのxが条件Pを満たす」を
「条件Pを満たさない行が存在しない」に変換する必要があります。

量化理論の実践編

量化理論がEXISTS述語でどう利用されているか具体例を用いて考えます。

CREATE TABLE TestScores
(student_id INTEGER,
 subject    VARCHAR(32) ,
 score      INTEGER,
  PRIMARY KEY(student_id, subject));
INSERT INTO TestScores VALUES(100, '算数',100);
INSERT INTO TestScores VALUES(100, '国語',80);
INSERT INTO TestScores VALUES(100, '理科',80);
INSERT INTO TestScores VALUES(200, '算数',80);
INSERT INTO TestScores VALUES(200, '国語',95);
INSERT INTO TestScores VALUES(300, '算数',40);
INSERT INTO TestScores VALUES(300, '国語',90);
INSERT INTO TestScores VALUES(300, '社会',55);
INSERT INTO TestScores VALUES(400, '算数',80);

■TestScores
f:id:xxyoko10:20200209193947p:plain

作成した TestScoresテーブルを用いて、「すべての行について~」という全称量化の表現を、
「~でない行が一つも存在しない」という二重否定文へ変換する技術を学びます。

「すべての教科について50点以上を取っている生徒を選択してください」という問題を、
全称量化を用いて表現すると、

すべての教科が50点以上である

となり、これを存在量化に変換すると、

50点未満である教科が1つも存在しない

になります。

これをNOT EXISTSで表現します。

SELECT DISTINCT student_id
  FROM TestScores TS1
 WHERE NOT EXISTS -- 以下の条件を満たす行が存在しない
        (SELECT *
           FROM TestScores TS2
          WHERE TS2.student_id = TS1.student_id
            AND TS2.score < 50); --50 点未満の教科

■結果
f:id:xxyoko10:20200209201136p:plain

このように、肯定 ⇔ 二重否定の変換を自然に頭の中で
できるようにしたいです。

NOT INとNOT EXISTSは結果が一致しないことがある

はじめに

INとEXISTSの使い分けについて、過去の記事で紹介しましたが、 今回はNOT INとNOT EXISTSについて取り上げます。 タイトルに書いてあるとおりINとEXSITとは異なり、 必ずしも結果が一致しません。誤った使い方をしてしまわないようにここでポイントを 押さえておきたいと思いました。

どうしてNOT INとNOT EXISTSは結果が異なるのか

NOT INとNOT EXISTSで結果が異なるパターンがあることを説明するために、 例として、2つの動物園を表現するテーブルを作りました。

CREATE TABLE Zoo_1
(animal CHAR(20) PRIMARY KEY,
 type CHAR(20) NOT NULL,
 num INTEGER);
    
INSERT INTO Zoo_1 VALUES('ペンギン','鳥類',10);   
INSERT INTO Zoo_1 VALUES('キツネ','ほ乳類',5);
INSERT INTO Zoo_1 VALUES('シマウマ','ほ乳類',3);
CREATE TABLE Zoo_2
(animal CHAR(20) PRIMARY KEY,
 type CHAR(20) NOT NULL,
 num INTEGER);
 
INSERT INTO Zoo_2 VALUES('アヒル','鳥類',8);   
INSERT INTO Zoo_2 VALUES('トラ','ほ乳類',3);
INSERT INTO Zoo_2 VALUES('クマ','ほ乳類',5);
INSERT INTO Zoo_2 VALUES('シマウマ','ほ乳類',NULL);

Zoo_1とZoo_2の2テーブルができました。

Select * From Zoo_1

f:id:xxyoko10:20200201193812p:plain

Select * From Zoo_2

f:id:xxyoko10:20200201194259p:plain

Zoo_2のシマウマの数がNULLになっている点に注目してください。
ここで、「Zoo_2のほ乳類の動物と数が一致しないZoo_1の動物」を選択したいとします。
NOT INを使用すると次のようなSQLになります。

SELECT * FROM Zoo_1
 WHERE num NOT IN 
       ( SELECT num FROM Zoo_2
          WHERE type = 'ほ乳類' );

SQLを実行するとZoo_1のペンギンのデータが抽出されると考えられます。
しかし、結果は1行も選択されませんでした。

f:id:xxyoko10:20200202221611p:plain

SQLが正しく実行されないのは、Zoo_2のシマウマのNULLが原因です。

NOT INのサブクエリで使用されるテーブルの選択列にNULLが存在する場合

なぜ1行も結果が選択されなかったのでしょうか。
それは選択列にNULLが含まれているからです。

SELECT * FROM Zoo_1
 WHERE NOT (num = 3) AND NOT(num = 5) AND NOT(num = NULL);

SQLを実行したとき、このように比較の際NULLが使用される動きをしています。
NULLは値ではないため、「=」で比較するのは正しい使い方ではありません。

そのためNOT INのサブクエリで使用されるテーブルの選択列にNULLが存在する場合は、
1行も選択されないという結果になります。

EXISTSを使ってみる

EXISTSを使用することで、正しい結果を得ることができます。

SELECT * FROM Zoo_1 A 
 WHERE NOT EXISTS 
       ( SELECT * FROM Zoo_2 B
          WHERE A.num = B.num
            AND B.type = 'ほ乳類' );

f:id:xxyoko10:20200202232645p:plain

サブクエリで使用されるテーブルの選択列にNULLが存在するのは、
NOT INの時と、変わりません。

SELECT * FROM Zoo_1 A 
 WHERE NOT EXISTS 
       ( SELECT * FROM Zoo_2 B
          WHERE A.num = NULL
            AND B.type = 'ほ乳類' );

なぜこのSQLが正しく実行されるかは、EXISTSがtrueかfalseしか返さないからです。
数がNULLのシマウマは「数が一致しない動物」として、
「Zoo_1 には存在しない」 = 「NOT EXISTS がtrueになる」ということです。
このようにNOT INとNOT EXISTS の結果が異なるパターンもあります。
NOT EXISTSがtrueかfalseしか返さないということも頭に入れておかなくてはいけないと
思いました。

参考書籍

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

  • 作者:ミック
  • 出版社/メーカー: 翔泳社
  • 発売日: 2018/10/11
  • メディア: 単行本(ソフトカバー)

異なる条件の集計を1つのSQLでおこなう ~CASE式~

異なる条件の集計をCASE式を使用して行う

異なる条件で集計をするときに、CASE式を使用すると便利です。
例として、クラスごとに生徒が所属している部活動のタイプを集計したテーブルを作成して、考えてみます。
club_type が1が運動部、2が文化部、3が帰宅部とします。

CREATE TABLE ClubTbl
(class_name CHAR(3),
 club_type CHAR(1) NOT NULL,
 student_number INTEGER NOT NULL,
    PRIMARY KEY(class_name, club_type));
INSERT INTO ClubTbl VALUES('1組', '1',   15 );
INSERT INTO ClubTbl VALUES('1組', '2', 10 );
INSERT INTO ClubTbl VALUES('1組', '3',   5);
INSERT INTO ClubTbl VALUES('2組', '1', 20);
INSERT INTO ClubTbl VALUES('2組', '2', 5);
INSERT INTO ClubTbl VALUES('2組', '3', 5 );
INSERT INTO ClubTbl VALUES('3組', '1', 10);
INSERT INTO ClubTbl VALUES('3組', '2', 12);
INSERT INTO ClubTbl VALUES('3組', '3', 8); 

■集計元のテーブル
f:id:xxyoko10:20200123224105p:plain

このテーブルを元に、クラス別、部活動のタイプ別に集計した結果を求める場合を考えてみます。
■集計結果
f:id:xxyoko10:20200126182618p:plain

CASE式を使ってみる・・・

では、実際にCASE式を使用して、、クラス別、部活動別の集計を行います。
f:id:xxyoko10:20200126201050p:plain

1つのSQLで結果を求めることができました。

CASE式を使わないやり方だと・・・

CASE式を使わずにSELECT文のSQLを3回発行して集計するとします。

--運動部の生徒数
SELECT class_name,
       student_number
 FROM ClubTbl      
WHERE club_type = '1'   

--文化部の生徒数
SELECT class_name,
       student_number
 FROM ClubTbl      
WHERE club_type = '2'     

--帰宅部の生徒数
SELECT class_name,
       student_number
 FROM ClubTbl      
WHERE club_type = '3'     

無駄に長いSQLになり、実行コストもかかります。

SUM関数は必要なのか??

集計結果を見たら分かるように、合計をしているわけではありません。
しかしSUM関数は必要です。
なぜならレコードを集約するには、CASE式自身に集約機能がないので、集約関数が必要となるからです。
GROUP BYを使っているのに、集約関数が存在していなかったら、集約が行われません。
だから今回は、実際合計はしていませんがSUM関数を使いました。

参考にした書籍

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

達人に学ぶSQL徹底指南書 第2版 初級者で終わりたくないあなたへ (CodeZine BOOKS)

  • 作者:ミック
  • 出版社/メーカー: 翔泳社
  • 発売日: 2018/10/11
  • メディア: 単行本(ソフトカバー)

条件ごとにグループ化して集計したいときのSQL ~CASE式~

グループ化して集計をするには?

既存のデータをグループ化して集計する方法について、学んだことをまとめました。 今回の記事は『達人に学ぶSQL徹底指南書』を参考にしています。

都道府県ごとの人口の情報を持った、「PopTbl」というテーブルがあるとします。 これを都道府県ごとの人口でなく、地方ごとの人口として集計したいというときに、CASE式を しようしたグループ化が有効になります。

f:id:xxyoko10:20200119140852p:plain

上のグループ化後のPopTblように集計するには、 以下のSQLで出来ます。

SELECT CASE pref_name                        --------①
          WHEN '徳島' THEN '四国'
          WHEN '香川' THEN '四国'
          WHEN '愛媛' THEN '四国'
          WHEN '高知' THEN '四国'
          WHEN '福岡' THEN '九州'
          WHEN '佐賀' THEN '九州'
          WHEN '長崎' THEN '九州'
          ELSE 'その他' END AS district,
       SUM(population)         --------③
  FROM PopTbl
 GROUP BY CASE pref_name                   --------②
             WHEN '徳島' THEN '四国'
             WHEN '香川' THEN '四国'
             WHEN '愛媛' THEN '四国'
             WHEN '高知' THEN '四国'
             WHEN '福岡' THEN '九州'
             WHEN '佐賀' THEN '九州'
             WHEN '長崎' THEN '九州'
             ELSE 'その他' END;

集計の流れ

①SELECT句で指定している列、行の絞り込み、検索処理を行う  f:id:xxyoko10:20200119153417p:plain

②検索結果に対して、GROUP BYで分類する(グループ化)
③SUMで各グループを集計する

気を付けたほうがいいこと

GROUP BY句にSELECT句と全く同じSQLがコピーされています。
少し面倒なように感じるので、ためしにSQLをこのように書いて実行します

SELECT CASE pref_name
          WHEN '徳島' THEN '四国'
          WHEN '香川' THEN '四国'
          WHEN '愛媛' THEN '四国'
          WHEN '高知' THEN '四国'
          WHEN '福岡' THEN '九州'
          WHEN '佐賀' THEN '九州'
          WHEN '長崎' THEN '九州'
          ELSE 'その他' END AS district,
       SUM(population)
  FROM PopTbl
 GROUP BY  pref_name

結果

f:id:xxyoko10:20200119151322p:plain

正しい結果が得られませんでした。
理由は変換前の列(pref_name)を指定しているからです。
ただし、エラーは起きないので気を付けなくてはならないです。

SELECT句でつけた変換後の列名、「district」を GROUP BY 句で
使用するとうまくいきます。

SELECT CASE pref_name
          WHEN '徳島' THEN '四国'
          WHEN '香川' THEN '四国'
          WHEN '愛媛' THEN '四国'
          WHEN '高知' THEN '四国'
          WHEN '福岡' THEN '九州'
          WHEN '佐賀' THEN '九州'
          WHEN '長崎' THEN '九州'
          ELSE 'その他' END AS district,
       SUM(population)
  FROM PopTbl
 GROUP BY district;

結果

f:id:xxyoko10:20200119171828p:plain

ただし、MySQLPostgreSQLでは上の書き方で実行できますが、
OracleやDB2ではエラーとなります。

INとEXISTSの使い分け

はじめに

SQLのINとEXISTSの特徴を掴んで使い分け出来ていなかったので、ここで抑えておこうと思いました。

 

INとEXISTSの使い分けのポイント  

 

具体例を示して考えます。

以下、音楽テーブルがあります。

f:id:xxyoko10:20200102191309p:plain f:id:xxyoko10:20200102191334p:plain

ここでは、曲の基本情報が入っているソングテーブルを主表と考え、主表に紐づくアーティスト情報を持つアーティストテーブルを従属表とします。

「2013年以降のアメリカの曲」で、INとEXISTSの2パターンで検索するとします。

 

IN
SELECT * FROM ソング s   
WHERE A_ID IN ( 
      SELECT ID FROM アーティスト 
      WHERE 国 = 'アメリカ'    
      ) 
      AND s.発表年 > 2013;    
EXISTS
SELECT * FROM ソング s   
WHERE s.発表年 > 2013
AND EXISTS (    
      SELECT 1FROM アーティスト a
      WHERE 
    s.A_ID = a.ID 
            AND 国 = 'アメリカ'    
      ) ;   

両社とも得られる結果は同じですが、処理が早いのはINになります
処理の流れを見てみます。

IN

①アーティストテーブル(従属表)を検索 ⇒ 1件抽出   
②ソング テーブル(主表)を検索   

EXISTS

①ソング テーブル(主表)を検索     ⇒ 4件抽出   
②アーティストテーブル(従属表)を検索

INの時は、アーティストテーブルの検索で大幅に件数が減っています。
たった1件に対して主表の処理を行っているため、EXISTSと比較して処理が速くなります。

これを踏まえて、次は「2012年以前のイギリスの曲」という条件でINとEXISTSの2パターンで検索すると、 どちらが早くなるでしょう。

IN
SELECT * FROM ソング s   
WHERE A_ID IN ( 
      SELECT ID FROM アーティスト 
      WHERE 国 = 'イギリス'    
      ) 
      AND s.発表年 < 2012;    
EXISTS
SELECT * FROM ソング s   
WHERE s.発表年 < 2012
AND EXISTS (    
      SELECT 1FROM アーティスト a
      WHERE 
    s.A_ID = a.ID 
            AND 国 = 'イギリス'    
      ) ;   

こちらはEXISTSのほうが早くなります。
こちらも処理の流れを見ていただきます。

IN

①アーティストテーブル(従属表)を検索 ⇒ 3件抽出
②ソング テーブル(主表)を検索    

EXISTS

①ソング テーブル(主表)を検索     ⇒ 1件抽出  
②アーティストテーブル(従属表)を検索
  

今回はEXISTSのソングテーブル検索の際に件数が大きく減りました。

つまり、INとEXISTSの使い分けのポイントとして以下のことが言えます。

従属表で件数を絞れるとき ⇒  IN を使用すると早くなる 

主表で件数をしぼれるとき ⇒  EXISTS を使用すると早くなる 

INは従属表⇒主表の順で抽出を行い、   
EXISTSは主表⇒従属表の順で抽出するので、早い段階で件数を一気に減らしたほうが良いということでした。

参考書籍