FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

ISBNに関しては、13桁のISBNについては考慮せず、かつ0で始まらない適当な10桁の数字を入れています。

書籍テーブル

主キー

出版社ID,ISBN10

出版社ID 出版社名 著者ID 著者名 ISBN10 書籍名 価格
100 A社 001A 鈴木さん 9987654321 入門C言語 111
200 B社 001B 加藤さん 1111111111 入門JAVA 222
200 B社 001B 加藤さん 2222222222 入門Scala 333
300 C社 001C 高橋さん 1111111111 入門Ruby 444
100 A社 002A 鈴木さん 1234567890 入門HTML 555

第一正規形create文

CREATE TABLE BOOK_N1
(
    COMPANY_ID INTEGER NOT NULL,
    COMPANY_NAME VARCHAR(255) NOT NULL,
    AUTHOR_ID CHAR(10) NOT NULL,
    AUTHOR_NAME VARCHAR(255) NOT NULL,
    ISBN10 BIGINT NOT NULL,
    BOOK_NAME VARCHAR(255) NOT NULL,
    BOOK_PRICE NUMERIC,

    PRIMARY KEY(COMPANY_ID, ISBN10)
);

insert文

INSERT INTO BOOK VALUES (100, 'A社', '001A', '鈴木さん', 9987654321, '入門C言語', 111);
INSERT INTO BOOK VALUES (200, 'B社', '001B', '加藤さん', 1111111111, '入門JAVA', 222);
INSERT INTO BOOK VALUES (200, 'B社', '001B', '加藤さん', 2222222222, '入門Scala', 333);
INSERT INTO BOOK VALUES (300, 'C社', '001C', '高橋さん', 1111111111, '入門Ruby', 444);
INSERT INTO BOOK VALUES (100, 'A社', '002A', '鈴木さん', 1234567890, '入門HTML', 555);

第一正規形

テーブルが第一正規形を満たす条件は以下の通りです。
【1つのセルの中には1つの値しか取らない】
この条件を満たしているセルの値のことをスカラ値と呼ぶ

ということで、この時点で書籍テーブルは第一正規形化されていることが確認できます。

なぜ1つのセルの中には1つの値しか取らないといけないのかというと、
それは関数従属性という正規形を理解するための概念と結びついているからみたいです。

関数:Y = f(X)
このとき、YはXに従属すると表現します。
この考え方をテーブルに当てはめると、X列の値を決めれば、Y列の値が1つに決まるという意味になります。
つまり、主キーの列の値から特定の列の値が1つに決められることがポイント。
よって先の書籍テーブルは関数従属性を満たしているといえる。

第二正規形

第一正規形の書籍テーブルでは、運用していく上でいくつかの問題が考えられます。
まず、出版社名の変更があった時、【出版社ID, 出版社名】が【100, A社】の他に【100, AA社】も入力ができてしまいます。
また、主キーの一部にISBN10を使用してしまっているため、ISBN10がわからないと出版社を登録することができないことがあります。

これらの問題を引き起こさないために、第二正規化を行っていきます。
ここでも関数従属性の概念がポイントになっています。
第一正規化が済んだ先ほどの書籍テーブルは、関数従属が完全ではないので、第二正規形ではありません。

どういうことかというと、
このテーブルの主キーは【出版社ID、ISBN10】であり、その他の列はこのキーに従属するのですが、よく見ると、出版社名だけは主キーの一部である出版社IDに従属しています。
このような関係は部分関数従属と呼ばれています。

これに対して主キーを構成するすべての列【出版社ID、ISBN10】に従属する関係は完全関数従属と呼ばれます。

第二正規形のテーブルは、部分関数従属がすべて排除され、完全関数従属のみで構成されているものをいいます。

以下に第二正規化された書籍テーブルと、新しく作った出版社テーブルを載せます。
どちらのテーブルも完全関数従属のみで構成されていることが見てわかると思います。

書籍テーブル

主キー

出版社ID,ISBN10

外部キー

出版社ID

出版社ID 著者ID 著者名 ISBN10 書籍名 価格
100 001A 鈴木さん 9987654321 入門C言語 111
200 001B 加藤さん 1111111111 入門JAVA 222
200 001B 加藤さん 2222222222 入門Scala 333
300 001C 高橋さん 1111111111 入門Ruby 444
100 002A 鈴木さん 1234567890 入門HTML 555

出版社テーブル

主キー

出版社ID

出版社ID 出版社名
100 A社
200 B社
300 C社

第二正規形書籍テーブルの作成

CREATE TABLE BOOK_N2
(
    COMPANY_ID INTEGER NOT NULL,
    AUTHOR_ID CHAR(10) NOT NULL,
    AUTHOR_NAME VARCHAR(255) NOT NULL,
    ISBN10 BIGINT NOT NULL,
    BOOK_NAME VARCHAR(255) NOT NULL,
    BOOK_PRICE NUMERIC,

    PRIMARY KEY(COMPANY_ID, ISBN10)
);

INSERT INTO BOOK_N2 VALUES (100, '001A', '鈴木さん', 9987654321, '入門C言語', 111);
INSERT INTO BOOK_N2 VALUES (200, '001B', '加藤さん', 1111111111, '入門JAVA', 222);
INSERT INTO BOOK_N2 VALUES (200, '001B', '加藤さん', 2222222222, '入門Scala', 333);
INSERT INTO BOOK_N2 VALUES (300, '001C', '高橋さん', 1111111111, '入門Ruby', 444);
INSERT INTO BOOK_N2 VALUES (100, '002A', '鈴木さん', 1234567890, '入門HTML', 555);

出版社テーブルの作成

CREATE TABLE COMPANY
(
    COMPANY_ID INTEGER NOT NULL,
    COMPANY_NAME VARCHAR(255),
    PRIMARY KEY (COMPANY_ID)
);

INSERT INTO COMPANY VALUES (100, 'A社');
INSERT INTO COMPANY VALUES (200, 'B社');
INSERT INTO COMPANY VALUES (300, 'C社');

第三正規形

第二正規形が終わり、すべての非キーの列は主キー列と関数従属になりました。

しかし、この第二正規形の書籍テーブルで問題になるのは、本を出版していない著者を登録できないことです。
今回は本棚の本の整理ということで話を進めてきましたが、例えば出版社名の部分が汚れで読めないので、著者だけでも登録したい。といったことができない。といったことが起こります。

ここから第三正規形を行っていきます。
第二正規化された書籍テーブルの著者IDと著者名に注目します。
主キー:出版社ID → 著者ID → 著者名
の関係が存在します。

このように段階的な関数従属があるとき、これを推移的関数従属と呼びます。
第三正規形ではこの推移的関数従属を取り除きます。

以下に第三正規化された書籍テーブルと、新しく作った著者テーブルを載せます。

書籍テーブル

主キー

出版社ID,ISBN10

外部キー

出版社ID,著者ID

出版社ID 著者ID ISBN10 書籍名 価格
100 001A 9987654321 入門C言語 111
200 001B 1111111111 入門JAVA 222
200 001B 2222222222 入門Scala 333
300 001C 1111111111 入門Ruby 444
100 002A 1234567890 入門HTML 555

著者テーブル

主キー

著者ID

著者ID 著者名
001A 鈴木さん
001B 加藤さん
001C 高橋さん
002A 鈴木さん

出版社テーブル

主キー

出版社ID

出版社ID 出版社名
100 A社
200 B社
300 C社

第三正規形書籍テーブルの作成

CREATE TABLE BOOK_N3
(
    COMPANY_ID INTEGER NOT NULL,
    AUTHOR_ID CHAR(10) NOT NULL,
    ISBN10 BIGINT NOT NULL,
    BOOK_NAME VARCHAR(255) NOT NULL,
    BOOK_PRICE NUMERIC,

    PRIMARY KEY(COMPANY_ID, ISBN10)
);

INSERT INTO BOOK_N3 VALUES (100, '001A', 9987654321, '入門C言語', 111);
INSERT INTO BOOK_N3 VALUES (200, '001B', 1111111111, '入門JAVA', 222);
INSERT INTO BOOK_N3 VALUES (200, '001B', 2222222222, '入門Scala', 333);
INSERT INTO BOOK_N3 VALUES (300, '001C', 1111111111, '入門Ruby', 444);
INSERT INTO BOOK_N3 VALUES (100, '002A', 1234567890, '入門HTML', 555);

著者テーブルの作成

CREATE TABLE AUTHOR
(
    AUTHOR_ID CHAR(10) NOT NULL,
    AUTHOR_NAME VARCHAR(255) NOT NULL,

    PRIMARY KEY(AUTHOR_ID)
);

INSERT INTO AUTHOR VALUES ('001A', '鈴木さん');
INSERT INTO AUTHOR VALUES ('001B', '加藤さん');
INSERT INTO AUTHOR VALUES ('001C', '高橋さん');
INSERT INTO AUTHOR VALUES ('002A', '鈴木さん');

可逆的

第二正規化、第三正規化でテーブルの分割を行いましたが、この分割は無損失分解のため、第二正規化と第三正規化は可逆的な操作です。

実際に分解されたテーブルをSQLで結合することにより、もとに戻してみました。

第二正規形から第一正規形

SELECT
    N2.COMPANY_ID,
    COMPANY.COMPANY_NAME,
    N2.AUTHOR_ID,
    N2.AUTHOR_NAME,
    N2.ISBN10,
    N2.BOOK_NAME,
    N2.BOOK_PRICE
FROM BOOK_N2 AS N2
INNER JOIN COMPANY ON COMPANY.COMPANY_ID = N2.COMPANY_ID;

第三正規形から第二正規形

SELECT
    N3.COMPANY_ID,
    N3.AUTHOR_ID,
    AUTHOR.AUTHOR_NAME,
    N3.ISBN10,
    N3.BOOK_NAME,
    N3.BOOK_PRICE
FROM BOOK_N3 AS N3
INNER JOIN AUTHOR ON AUTHOR.AUTHOR_ID = N3.AUTHOR_ID;

第三正規形から第一正規形

SELECT 
    N3.COMPANY_ID,
    COMPANY.COMPANY_NAME,
    N3.AUTHOR_ID,
    AUTHOR.AUTHOR_NAME,
    N3.ISBN10,
    N3.BOOK_NAME,
    N3.BOOK_PRICE
FROM BOOK_N3 AS N3
INNER JOIN COMPANY ON COMPANY.COMPANY_ID = N3.COMPANY_ID
INNER JOIN AUTHOR ON AUTHOR.AUTHOR_ID = N3.AUTHOR_ID;

正規化のメリット・デメリット

メリット

1.データの冗長性が排除され不整合が起きにくくなる
2.テーブルの持つ意味が理解しやすくなる。(エンティティ)

デメリット

1.テーブルの数が増えることによるパフォーマンスの悪化