結論

リレーショナルデータベース(RDB)とは、データを「行と列の表」に分けて保存し、表どうしを「キー」でつないで(関係づけて)扱うデータベースです。同じ情報を1か所にだけ置き、必要なときに SQL の JOIN で組み合わせるため、名前の変更などで書き換え漏れが起きにくくなります。

この記事でわかること

  • 表・キー(主キー・外部キー)・リレーション(1対多・多対多)の考え方と、それぞれの役割
  • 1枚の表で起きる更新漏れ・削除で消える情報を SQLite で再現した出力と、表を分けて JOIN で戻した出力
  • 10万人分のデータで測った容量・更新時間・読み出し時間、制約とトランザクションのエラー原文、NoSQL との違い

2026年10月29日更新|SQLite 3.45.1・PostgreSQL 16.15 で実行確認

この記事で実際に動かしたもの:SQLite 3.45.1 で「社員・部署・研修講座」の1枚の表と、4つに分けた表を作り、更新漏れの再現・JOIN・制約違反のエラーを実行しました。PostgreSQL 16.15 を作業用のディレクトリに立てて 127.0.0.1 だけで待ち受けさせ、同じ表で制約・トランザクション・ロック待ち・行の並び順を確かめ、10万人・受講30万件のデータで容量と時間を測りました。

リレーショナルデータベースは、銀行の口座・ネットショップの注文・社内の勤怠など、多くの業務システムの土台です。最初に全体の流れを図で示します。

flowchart TD
    A[1枚の表<br>部署名が何度も出る] -->|分ける| B[部署・社員<br>講座・受講の4表]
    B -->|キーでつなぐ| J[JOIN で1枚に戻す]
    B --> U[名前の変更は1行]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
リレーショナルデータベースとは 表にデータを入れ、キーで表どうしを関係づけるデータベース。略して RDB リレーショナルデータベースとは(意味と3つの部品)
RDB とは(RDBMS との違い) RDB はデータの持ち方、RDBMS はそれを動かすソフト(PostgreSQL・MySQL など) RDBMSの主な製品とSQLの位置づけ
「リレーショナル」の意味 「関係(relation)」。数学では表そのものを relation と呼ぶ リレーショナルデータベースとは(意味と3つの部品)
主キーと外部キーの違い 主キーは行を1つに決める列、外部キーは別の表の主キーを指す列 リレーショナルデータベースのキー
なぜ表を分けるのか 同じ情報を何度も書くと、1か所の直し忘れで矛盾が生まれるため 表を分けないと何が起きるか
分けた表をどう見るのか SQL の JOIN でキーを手がかりに1枚に組み立てる 表を分けてJOINで戻す
NoSQL との違い RDB は表と制約で正しさを守る。NoSQL は形を決めずに速く・大きく扱う リレーショナルデータベースとNoSQLの違い
自分の DB がどう関係づけられているか見たい SQLite は PRAGMA foreign_key_list、PostgreSQL は \d 表名 自分で確かめる

確認環境:Ubuntu 24.04.5 LTS(2 CPU・メモリ 8GB の共用マシン)の上で、SQLite は Ubuntu の sqlite3 パッケージ(3.45.1)を作業用ディレクトリに展開して使い、Python 3 に組み込みの SQLite(3.53.1)でも動きを照らし合わせました。PostgreSQL 16.15 は作業用ディレクトリに initdb で新しいクラスタを作り、127.0.0.1 の専用ポートだけで起動し、確認用のデータベースを作って終了後に消しました。時間の測定は psql の \timing で5回測った中央値で、ほかの作業と同居するマシンのため数値は目安です。SQLite の外部キーの既定値は SQLite 公式ドキュメント、行の並び順・制約・トランザクションは PostgreSQL 16 の公式ドキュメント、リレーショナルモデルの由来は E.F. Codd の1970年の論文の書誌情報で確かめました。図は mermaid-cli で描画を確かめています(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「リレーショナルデータベース(RDB)とは|表・キー・更新漏れをSQLで再現」

リレーショナルデータベースとは(意味と3つの部品)

この節では、リレーショナルデータベースという言葉の意味と、表・キー・リレーションという3つの部品の役割が分かります。

リレーショナルデータベース(Relational Database、略して RDB)は、データを表(テーブル)の形で持ち、表と表を共通の値(キー)で関係づけて扱うデータベースです。日本語では「関係データベース」とも呼びます。1970年に IBM の研究者 E.F. Codd が論文「A Relational Model of Data for Large Shared Data Banks」で示した「リレーショナルモデル」がもとになっています。

「リレーショナル」という言葉は、場面によって受け取り方が変わります。最初に並べておきます。

場面 relation / リレーショナルの意味 この記事との関係
ふだんの英語 関係・つながり(relationship に近い) 「表どうしがつながる」というイメージは合っている
数学(集合論) いくつかの集合の組み合わせの集まり=表そのもの Codd が使った意味。表1枚を relation と呼ぶ
データベースの用語 表=リレーション、行=タプル、列=属性 試験や教科書で出てくる言い方
設計の会話 表と表の結びつき(1対多など)を「リレーション」と呼ぶ ER図で線を引く対象

つまり、もともとの意味では「表そのもの」が relation ですが、実務の会話では「表と表のつながり」を指すことが多い、という二重の意味があります。どちらの意味でも、RDB を形づくる部品は次の3つです。

1

表(テーブル)

1行が1件のデータ、1列が1つの項目。列ごとに型(整数・文字列・日付)が決まっていて、決まった形以外のデータは入らない。

例:社員の表、部署の表

2

キー

行を1つに決める「主キー」と、別の表の行を指す「外部キー」。キーがあるから、名前が同じ人や部署を取り違えずに済む。

例:社員番号、部署番号

3

リレーション

キーを手がかりにした表どうしの結びつき。1対1・1対多・多対多の3種類があり、SQL の JOIN で1枚の結果に組み立てる。

例:社員は1つの部署に所属

Excel の表との違いは、「列の型や重複の禁止をデータベースが強制する」「複数の表をキーでつなぐ前提で作る」「たくさんの人が同時に書き込んでも矛盾しないしくみ(トランザクション)がある」の3点です。データベース全体の入口(DBMS とは何か、Excel との違い、SQL の最初の一歩)は、関連記事「データベースとは|種類・Excelとの違い・SQLの基本を実例で解説」で扱っています。この記事は、その中のリレーショナルデータベースに絞り、表を分ける理由とキーの働きを実行結果で深掘りします。

リレーショナルデータベースの表(テーブル)の決まり

この節では、RDB の表が Excel のシートとどう違うのか、とくに「列の型」と「行に順番がない」ことが分かります。

RDB の表には、次の決まりがあります。

  • 列は名前と型を先に決める:CREATE TABLE で「emp_id は整数、emp_name は文字列」と宣言してからデータを入れます。この決まりの集まりを「スキーマ」と呼びます。
  • 1つのマスには1つの値:「SQL入門, Git入門」のようにカンマで並べて1マスに入れません。複数あるなら行を分けるか、別の表にします。
  • 同じ行が2つあってはいけない:主キーで行を区別します(次の節)。
  • 行にも列にも順番の意味がない:表は「行の集合」で、上から何番目という情報を持ちません。

最後の「行に順番がない」は、初めて学ぶ人がつまずきやすい点です。PostgreSQL で、3人の社員の表を ORDER BY を付けずに読み、1行だけ更新してからもう一度読みました。

SELECT emp_id, emp_name FROM employees;
UPDATE employees SET emp_name = '佐藤' WHERE emp_id = 1;   -- 同じ値で上書き
SELECT emp_id, emp_name FROM employees;
SELECT emp_id, emp_name FROM employees ORDER BY emp_id;
 emp_id | emp_name
--------+----------
      1 | 佐藤
      2 | 鈴木
      3 | 高橋
(3 rows)

UPDATE 1
 emp_id | emp_name
--------+----------
      2 | 鈴木
      3 | 高橋
      1 | 佐藤
(3 rows)

 emp_id | emp_name
--------+----------
      1 | 佐藤
      2 | 鈴木
      3 | 高橋
(3 rows)

名前を同じ値で上書きしただけで、佐藤さんの行が最後に回りました。PostgreSQL は更新した行を新しい場所に書くため、読み出しの順番が変わります。PostgreSQL 16 の公式ドキュメントには「並べ替えを指定しない場合、行は不特定の順序で返され、その順序に頼ってはならない」と書かれています。画面に出す順番が決まっているなら、必ず ORDER BY を書く、が RDB の基本です。

型の扱いは製品で差があります。SQLite は既定では型に寛容で、整数の列に文字列が入ります。STRICT を付けた表と PostgreSQL は拒否しました。

$ sqlite3 test.db "CREATE TABLE t(n INTEGER); INSERT INTO t VALUES ('abc'); SELECT n, typeof(n) FROM t;"
abc|text
$ sqlite3 test.db "CREATE TABLE s(n INTEGER) STRICT; INSERT INTO s VALUES ('abc');"
Error: stepping, cannot store TEXT value in INTEGER column s.n (19)

-- PostgreSQL
ERROR:  invalid input syntax for type integer: "十"
LINE 1: INSERT INTO employees VALUES (7,'山本','十');
                                               ^

リレーショナルデータベースのキー(主キー・外部キー・複合キー)

この節では、キーの種類と役割、そして主キー・外部キーに反したときに実際に出るエラーが分かります。

キーは「どの行のことか」を間違いなく指すための列です。名前は同姓同名がありえますし、部署名は変わることがあります。そこで、変わらず重ならない値(社員番号など)を行の目印にします。

キーの種類 意味 この記事の例
主キー(PRIMARY KEY) 表の中で行を1つに決める列。重複も空(NULL)も不可 employees の emp_id
外部キー(FOREIGN KEY) 別の表の主キーを指す列。指す先に無い値は入れられない employees の dept_id → departments
複合キー 2つ以上の列の組み合わせで1つに決まるキー enrollments の (emp_id, course_id)
候補キー 主キーになれる列(重ならない列)。主キーに選ばなかったものは UNIQUE にする departments の dept_name
代理キー(サロゲートキー) 業務の意味を持たない連番や UUID を主キーにしたもの dept_id(10・20・30)
自然キー(ナチュラルキー) 業務で使う値そのものを主キーにしたもの 社員番号・商品コードなど

この記事で使う4つの表と、キーによる結びつきを図にします。PK が主キー、FK が外部キーです。

flowchart TD
    D[部署 departments<br>PK dept_id] -->|1対多| E[社員 employees<br>PK emp_id / FK dept_id]
    E -->|1対多| N[受講 enrollments<br>PK emp_id + course_id]
    C[講座 courses<br>PK course_id] -->|1対多| N

表は次の SQL で作りました。SQLite でも PostgreSQL でもほぼ同じ書き方で動きます(PostgreSQL では INTEGER を integer と小文字で書いても同じです)。

CREATE TABLE departments(
  dept_id   INTEGER PRIMARY KEY,
  dept_name TEXT NOT NULL UNIQUE,
  floor     TEXT NOT NULL);
CREATE TABLE employees(
  emp_id   INTEGER PRIMARY KEY,
  emp_name TEXT NOT NULL,
  dept_id  INTEGER NOT NULL REFERENCES departments(dept_id));
CREATE TABLE courses(
  course_id INTEGER PRIMARY KEY,
  title     TEXT NOT NULL UNIQUE,
  hours     INTEGER NOT NULL CHECK(hours > 0));
CREATE TABLE enrollments(
  emp_id    INTEGER NOT NULL REFERENCES employees(emp_id),
  course_id INTEGER NOT NULL REFERENCES courses(course_id),
  PRIMARY KEY(emp_id, course_id));

主キー・外部キーに反するデータを入れると、PostgreSQL は次のように止めます。エラー文の DETAIL の行に、どの値が問題かが出ます。

-- 存在しない部署 99 に社員を入れる
ERROR:  insert or update on table "employees" violates foreign key constraint "employees_dept_id_fkey"
DETAIL:  Key (dept_id)=(99) is not present in table "departments".

-- 社員が所属している部署 20 を消す
ERROR:  update or delete on table "departments" violates foreign key constraint "employees_dept_id_fkey" on table "employees"
DETAIL:  Key (dept_id)=(20) is still referenced from table "employees".

-- 社員番号 1 をもう一度入れる
ERROR:  duplicate key value violates unique constraint "employees_pkey"
DETAIL:  Key (emp_id)=(1) already exists.

2つ目のエラーが外部キーの大事な働きです。社員が所属している部署を消そうとすると、データベースが止めます。これを「参照整合性」と呼びます。消したときに社員側をどうするか(一緒に消す・NULL にする・止める)は外部キーに ON DELETE で指定でき、関連記事「SQL制約一覧|主キー・UNIQUE・CHECK・NOT NULLの追加と削除」で制約の追加と削除の書き方をまとめています。主キーに連番を使うか業務の値を使うかの判断は、設計の話としてこの記事では深入りしません。

リレーション(関係)の種類:1対1・1対多・多対多

この節では、表どうしのつながり方の3種類と、それぞれをどの表のどの列で表すかが分かります。

11対多(いちばん多い)

1つの部署に何人もの社員。社員は1つの部署にだけ所属。「多」の側(employees)に、「1」の側の主キー(dept_id)を外部キーとして持たせる。

2多対多

1人が複数の講座を受け、1つの講座を複数人が受ける。どちらかの表に列を足すと1マスに複数の値が入ってしまうため、間に「受講(enrollments)」という中間の表を置き、2つの外部キーを持たせる。

31対1

社員1人に社員証1枚のように、両側が1つずつ。片方の表に外部キーを置き UNIQUE を付ける。めったに読まない列や、見せる人を絞りたい列(給与など)を分けたいときに使う。

多対多は、そのままでは表に書けません。中間の表を挟むと「1対多」が2本になる、と覚えると設計しやすくなります。中間の表の主キーや名前の付け方は、関連記事「多対多のテーブル設計|中間テーブルの主キー・命名・属性の決め方」で詳しく扱っています。

表とつながりを図に描いたものが ER図です。カラスの足(三本線)などの記号で「1」と「多」を描き分けます。描き方と記号の読み方は、関連記事「ER図の書き方|記号の意味・読み方の早見表とMermaid・DDL実例」にあります。

表を分けないと何が起きるか(更新漏れの再現)

この節では、すべてを1枚の表に入れたときに起きる「更新漏れ」「削除で消える情報」を、SQLite で実際に起こした結果で確かめられます。

Excel で研修の記録を付けると、次のような1枚の表になりがちです。社員・部署・階・講座・時間を1行に並べます。

CREATE TABLE training_flat(
  emp_id INTEGER, emp_name TEXT, dept_name TEXT, dept_floor TEXT,
  course TEXT, course_hours INTEGER);
INSERT INTO training_flat VALUES
 (1,'佐藤','開発部','5F','SQL入門',6),
 (1,'佐藤','開発部','5F','Git入門',3),
 (2,'鈴木','開発部','5F','SQL入門',6),
 (3,'高橋','営業部','3F','Git入門',3);

「開発部」が3回、「SQL入門は6時間」が2回、同じ情報が繰り返し書かれています。ここで、開発部が「開発本部」に名前を変えたとします。佐藤さんの行だけを直してしまった場合の結果です。

sqlite> UPDATE training_flat SET dept_name='開発本部' WHERE emp_id=1;
sqlite> SELECT emp_name, dept_name, dept_floor FROM training_flat;
emp_name | dept_name | dept_floor
佐藤 | 開発本部 | 5F
佐藤 | 開発本部 | 5F
鈴木 | 開発部 | 5F
高橋 | 営業部 | 3F

sqlite> SELECT dept_name, COUNT(DISTINCT emp_id) AS 人数 FROM training_flat GROUP BY dept_name;
dept_name | 人数
営業部 | 1
開発本部 | 1
開発部 | 1

同じ部署なのに名前が2種類になり、部署ごとの人数を数えると「開発本部1人・開発部1人」と別の部署として集計されました。講座の時間も同じことが起きます。

sqlite> UPDATE training_flat SET course_hours=8 WHERE emp_id=1 AND course='SQL入門';
sqlite> SELECT course, GROUP_CONCAT(DISTINCT course_hours) AS 時間の値 FROM training_flat GROUP BY course;
course | 時間の値
Git入門 | 3
SQL入門 | 8,6

「SQL入門は何時間か」に、8 と 6 の2つの答えが入りました。さらに、営業部の唯一の社員である高橋さんの行を消すと、営業部が3階にあるという事実まで消えます。

sqlite> DELETE FROM training_flat WHERE emp_id=3;
sqlite> SELECT DISTINCT dept_name, dept_floor FROM training_flat;
dept_name | dept_floor
開発本部 | 5F
開発部 | 5F

この3つは教科書で「更新時異常」「削除時異常」と呼ばれるものです。もう1つ「挿入時異常」もあり、この表には「社員がまだいない新しい部署」を登録できません(社員と講座の列が空の行になってしまうため)。

同じ情報を何か所にも書く

やってはいけない部署名・講座の時間のように、1つの事実を行ごとに繰り返し書く。直すときに全部の行を見つけて直さないと、矛盾したデータが残る
→
代わりに1つの事実は1つの表の1行にだけ書き、ほかの表からはキー(部署番号・講座番号)で指す。変更は1行で済む

この「事実を1か所に置く」ように表を分ける手順を「正規化」と呼びます

正規化には第1〜第3正規形という段階があり、どの順で表を分けるかを決める手順になっています。手順そのものは関連記事「第一正規化から第三正規化までわかりやすく|データベースの正規化を図とSQLで」で図と SQL つきで説明しています。

表を分けてJOINで戻す(SQLiteで実行)

この節では、4つに分けた表を SQL の JOIN で1枚の結果に組み立てる方法と、名前の変更が1行で済む様子が分かります。

前の節の1枚の表を、部署・社員・講座・受講の4つに分けました(表の定義は「キー」の節の SQL)。データは次のとおりです。総務部は、まだ社員がいない部署として入れてあります。

PRAGMA foreign_keys = ON;   -- SQLite では外部キーの確認を毎回オンにする(後の節)
INSERT INTO departments VALUES (10,'開発部','5F'),(20,'営業部','3F'),(30,'総務部','2F');
INSERT INTO employees   VALUES (1,'佐藤',10),(2,'鈴木',10),(3,'高橋',20);
INSERT INTO courses     VALUES (100,'SQL入門',6),(200,'Git入門',3);
INSERT INTO enrollments VALUES (1,100),(1,200),(2,100),(3,200);

JOIN で、キーが一致する行どうしを横につなげると、もとの1枚の表と同じ見た目に戻ります。

SELECT e.emp_name, d.dept_name, d.floor, c.title, c.hours
FROM enrollments AS en
JOIN employees   AS e ON e.emp_id    = en.emp_id
JOIN departments AS d ON d.dept_id   = e.dept_id
JOIN courses     AS c ON c.course_id = en.course_id
ORDER BY e.emp_id, c.course_id;
emp_name | dept_name | floor | title | hours
佐藤 | 開発部 | 5F | SQL入門 | 6
佐藤 | 開発部 | 5F | Git入門 | 3
鈴木 | 開発部 | 5F | SQL入門 | 6
高橋 | 営業部 | 3F | Git入門 | 3

ここで開発部の名前を変えます。直すのは departments の1行だけです。

sqlite> UPDATE departments SET dept_name='開発本部' WHERE dept_id=10;
sqlite> SELECT changes() AS 更新した行数;
更新した行数
1
sqlite> SELECT e.emp_name, d.dept_name FROM employees e JOIN departments d USING(dept_id) ORDER BY e.emp_id;
emp_name | dept_name
佐藤 | 開発本部
鈴木 | 開発本部
高橋 | 営業部

1行の更新で、佐藤さんにも鈴木さんにも新しい名前が反映されました。「社員がいない部署」も消えずに残り、LEFT JOIN で0人として数えられます。

sqlite> SELECT d.dept_name, COUNT(e.emp_id) AS 人数
   ...> FROM departments d LEFT JOIN employees e USING(dept_id)
   ...> GROUP BY d.dept_id ORDER BY d.dept_id;
dept_name | 人数
開発本部 | 2
営業部 | 1
総務部 | 0

JOIN を書くときの手順をまとめます。

1

出したい列を決める

「社員名・部署名・講座名」のように、結果に並べたい列と、それがどの表にあるかを書き出す。

SELECT

2

起点の表を決める

結果の1行が何を表すか(受講1件ごと・社員1人ごと)で、FROM に置く表を決める。受講ごとなら enrollments。

FROM

3

外部キーをたどって JOIN する

ON には「外部キー = 指す先の主キー」を書く。条件を書き忘れると全行の組み合わせになる(よくあるエラーの節)。

JOIN … ON

4

相手がいない行も残すか決める

社員0人の部署も出したいなら LEFT JOIN、両方にある行だけでよいなら INNER JOIN。

INNER / LEFT

5

並べ順を指定する

行に順番はないので、画面に出す順は ORDER BY で必ず書く。

ORDER BY

INNER JOIN と LEFT JOIN の結果の違いは、関連記事「SQLのJOINの違い|INNER JOINとLEFT JOINの使い分け」で、件数が増える・減る例と一緒に説明しています。

10万人のデータで測る:容量・更新・読み出し

この節では、表を分けたときと1枚にまとめたときで、容量・名前変更の時間・読み出しの時間がどれだけ違うかを、PostgreSQL で測った数値で確かめられます。

部署50・社員10万人・講座20・受講30万件(1人3講座)のデータを PostgreSQL 16.15 に作り、4つに分けた表と、JOIN の結果をそのまま保存した1枚の表(30万行)を比べました。部署名は「部署7(システム開発第7グループ)」のような長さ、講座名は「講座3:データベース設計の基礎と応用」のような長さにしています。容量は索引を含む pg_total_relation_size、時間は5回の中央値です。

27MB4つに分けた表の合計1枚の表は 50MB(約1.8倍)
1行部署名の変更で書き換えた行1枚の表は 6,000 行
0.48ms部署名の変更にかかった時間1枚の表は 65.6ms
測ったこと 4つに分けた表 1枚の表(30万行)
容量(索引を含む) 28,401,664 バイト(27MB) 52,445,184 バイト(50MB)
部署名の変更:書き換えた行 1行 6,000行
部署名の変更:時間 0.48ms 65.6ms
講座の時間の変更:書き換えた行 1行 15,000行
社員1,000人分(3,000行)を読む 3.04ms(JOIN あり) 2.09ms
30万行すべてを数える 130.3ms(JOIN あり) 83.4ms

結果は、容量と書き換えは分けた表が有利、読み出しは1枚の表がやや速い、でした。JOIN は表をつなぐ手間がかかるぶん、読み出しでは1〜1.6倍ほど時間がかかります。それでも社員1,000人分なら 3ms 程度で、多くの画面では差を感じない大きさです。一方で1枚の表は、部署名を変えるたびに 6,000 行を確実に見つけて書き換える必要があり、1行でも漏れれば前の節の矛盾が起きます。

集計や分析のように「読むことがほとんどで、書き換えない」用途では、あえて1枚にまとめた表(非正規化した表)を別に作ることもあります。正しいデータは分けた表で持ち、速さが要る読み出し用に1枚の表を作る、という役割分担が一般的です。

PostgreSQL が JOIN をどう処理したかは、EXPLAIN ANALYZE で見られます。社員1,000人分の JOIN では、社員と受講を主キーの索引で引き、部署は同じ値を覚えておく Memoize で50回だけ読み、講座はハッシュ表にしてつないでいました。

Hash Join (actual rows=3000 loops=1)
  Hash Cond: (n.course_id = c.course_id)
  ->  Nested Loop (actual rows=3000 loops=1)
        ->  Nested Loop (actual rows=1000 loops=1)
              ->  Index Scan using b_emp_pkey on b_emp e (actual rows=1000 loops=1)
                    Index Cond: ((emp_id >= 1) AND (emp_id <= 1000))
              ->  Memoize (actual rows=1 loops=1000)
                    Cache Key: e.dept_id
                    Hits: 950  Misses: 50  Evictions: 0  Overflows: 0  Memory Usage: 8kB
                    ->  Index Scan using b_dept_pkey on b_dept d (actual rows=1 loops=50)
        ->  Index Only Scan using b_enr_pkey on b_enr n (actual rows=3 loops=1000)
  ->  Hash (actual rows=20 loops=1)
        ->  Seq Scan on b_course c (actual rows=20 loops=1)

主キーには自動で索引が作られるため、キーでつなぐ JOIN は速く動きます。外部キーの列(この例の dept_id)には PostgreSQL は自動で索引を作らないので、JOIN や削除の確認でよく使う外部キーには自分で CREATE INDEX を付けます。

制約が守るデータの正しさ(SQLiteの外部キーは既定でオフ)

この節では、制約(データの決まり)に反したときの SQLite と PostgreSQL のエラー原文と、SQLite の外部キーが既定では確かめられない点が分かります。

リレーショナルデータベースは、表を作るときに書いた決まり(制約)を、データを入れるたびにデータベース自身が確かめます。アプリの作りが変わっても、誰がどこから書き込んでも、決まりに反するデータは入りません。

ただし SQLite には注意点があります。SQLite の外部キーの確認は、互換性のために既定でオフです。実際に、オフのまま存在しない部署 99 に社員を入れると、エラーなしで入ってしまいました。

$ sqlite3 company.db "PRAGMA foreign_keys; INSERT INTO employees VALUES (4,'田中',99); SELECT changes(); PRAGMA foreign_key_check;"
0
1
employees|4|departments|0

$ sqlite3 company.db "PRAGMA foreign_keys=ON; INSERT INTO employees VALUES (5,'伊藤',99);"
Error: stepping, FOREIGN KEY constraint failed (19)

1行目の 0 が「外部キーの確認はオフ」、1 が「1行入った」です。PRAGMA foreign_key_check で、外部キーに反した行(employees の4行目が departments を指せていない)が見つかりました。SQLite 公式ドキュメントには、この設定は接続ごとに有効にする必要があると書かれています。アプリから SQLite を使うときは、接続した直後に毎回 PRAGMA foreign_keys = ON; を実行するのが定番です。

同じデータで、ほかの制約に反したときの原文を並べます。

反した決まり SQLite 3.45.1 の原文 PostgreSQL 16.15 の原文(1行目)
主キーの重複 UNIQUE constraint failed: employees.emp_id duplicate key value violates unique constraint "employees_pkey"
複合キーの重複 UNIQUE constraint failed: enrollments.emp_id, enrollments.course_id duplicate key value violates unique constraint "enrollments_pkey"
外部キー(無い先を指す) FOREIGN KEY constraint failed insert or update on table "employees" violates foreign key constraint "employees_dept_id_fkey"
NOT NULL NOT NULL constraint failed: employees.emp_name null value in column "emp_name" of relation "employees" violates not-null constraint
CHECK(0時間の講座) CHECK constraint failed: hours>0 new row for relation "courses" violates check constraint "courses_hours_check"
UNIQUE(同じ部署名) UNIQUE constraint failed: departments.dept_name duplicate key value violates unique constraint "departments_dept_name_key"

SQLite は主キーの重複も UNIQUE constraint failed と表示します。「UNIQUE を付けた覚えがないのに」と迷ったら、主キーの重複を疑います。SQLite の外部キーのエラーは、どの値が問題かを表示しないため、PRAGMA foreign_key_check と組み合わせて探します。

トランザクションとACID(途中で失敗したら全部取り消す)

この節では、複数の書き込みをひとまとめにするトランザクションと、ACID という4つの性質を、PostgreSQL で失敗させた結果で確かめられます。

トランザクションは「全部成功するか、全部なかったことにするか」の単位です。例として、講座の申し込みを「受講の表に1行足す」と「講座の残り席を1つ減らす」の2つの書き込みで行います。残り席が0の講座(Git入門)に申し込むと、席を減らすところで CHECK 制約(席は0以上)に反して失敗します。

BEGIN
INSERT 0 1
psql:p2.sql:6: ERROR:  new row for relation "courses" violates check constraint "courses_seats_check"
DETAIL:  Failing row contains (200, Git入門, 3, -1).
psql:p2.sql:7: ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

-- COMMIT のあとで確かめる
 emp_id | course_id
--------+-----------
      2 |       100
(1 row)

受講の1行目の追加(INSERT 0 1)は成功していましたが、席の更新が失敗したため、最後に COMMIT を送っても PostgreSQL は ROLLBACK と返し、受講の追加も取り消しました。鈴木さんの受講は SQL入門の1件のままで、「申し込みはあるのに席が減っていない」という半端な状態は残りません。失敗のあとに送った文は current transaction is aborted で無視されます。

次に、2つの接続から同じ行を同時に書き換えました。接続Aがトランザクションの中で講座100の席を5に変えて4秒待つ間に、接続Bが同じ行を9に変えようとし、1秒で待つのをやめる設定にしています。

-- 接続B(lock_timeout = 1s)
ERROR:  canceling statement due to lock timeout
CONTEXT:  while updating tuple (0,1) in relation "courses"

-- 接続Aが COMMIT する前に、別の接続から読む
 course_id | seats
-----------+-------
       100 |     1

-- 接続Aの COMMIT 後
 course_id | seats
-----------+-------
       100 |     5

接続Aが確定するまで、ほかの接続には変更前の値(1)が見え、同じ行の書き換えは待たされました。これらの性質をまとめたのが ACID です。

性質 意味 この節の実験で見えたこと
Atomicity(原子性) 全部成功か、全部取り消しか 席の更新が失敗し、受講の追加も取り消された
Consistency(一貫性) 制約に反する状態で確定しない 席が -1 になる更新は CHECK で止まった
Isolation(独立性・分離性) 確定前の変更がほかから見えない 接続Aの確定前、ほかの接続には 1 が見えた
Durability(永続性) 確定した変更は障害が起きても失われない COMMIT 後の 5 が残った(電源断の試験は行っていない)

お金の振替や在庫の引き当てのように「半端な状態が1秒でもあると困る」処理で、RDB が長く選ばれている理由がこのしくみです。

RDBMSの主な製品とSQLの位置づけ

この節では、RDB と RDBMS と SQL の関係、主な RDBMS 製品の特徴、学ぶ順番が分かります。

RDB(リレーショナルデータベース)

データの持ち方・考え方(表とキー)

データそのものの集まり

例:研修管理のデータベース

RDBMS(管理システム)

RDB を作り、読み書きし、守るソフト

制約の確認・トランザクション・権限・バックアップ

例:PostgreSQL・MySQL・SQLite

RDBMS に「何がほしいか」を伝える言葉が SQL です。SQL は「どう探すか」ではなく「何がほしいか」を書く言語(宣言的な言語)で、JOIN の順番や索引の使い方は RDBMS が決めます(前の節の EXPLAIN がその結果です)。SQL は ISO・JIS で規格になっていて、基本の文は製品が違ってもほぼ同じように書けます。

製品 形態・ライセンス よく使われる場面
PostgreSQL サーバー型。オープンソース(PostgreSQL License) Web サービス・業務システム。JSON 型や拡張機能が多い
MySQL サーバー型。Oracle 社が開発。GPL 版と商用版 Web サービス。WordPress などの CMS
MariaDB サーバー型。MySQL から分かれたオープンソース MySQL と同じ用途
Oracle Database サーバー型。商用 大規模な基幹システム
SQL Server サーバー型。Microsoft の商用(無料の Express 版あり) Windows・.NET の業務システム
SQLite ファイル1つに入る組み込み型。パブリックドメイン スマホアプリ・ブラウザ・試作・テスト

クラウドでは、Amazon RDS・Google Cloud SQL・Azure SQL Database のように、RDBMS のインストールやバックアップ・更新をクラウド事業者が受け持つ形(マネージドサービス)も広く使われています。中身は上の表の製品か、その互換品です。

学ぶ順番の目安は次のとおりです。SQLite は Python に組み込まれていて追加のインストールなしで試せるため、最初の一歩に向いています。

  1. SQLite で CREATE TABLE・INSERT・SELECT・WHERE・ORDER BY を書く
  2. 表を2つに分けて主キー・外部キーを付け、JOIN と GROUP BY で集計する(この記事の範囲)
  3. 正規化と ER図で、題材を自分で表に分けてみる
  4. PostgreSQL か MySQL をサーバーとして動かし、トランザクション・索引・EXPLAIN を確かめる

リレーショナルデータベースとNoSQLの違い

この節では、RDB と NoSQL の違いを、同じデータを JSON の文書で持ったときの実測と判断表で確かめられます。

NoSQL は「表とSQLだけに限らないデータベース」の総称で、キーバリュー型(Redis など)・ドキュメント型(MongoDB など)・列指向型・グラフ型があります。4つの種類の違いや Redis での実物は、別記事「NoSQLとは|RDBとの違い・4種類をRedis/PostgreSQL実測で解説」で扱っているため、ここでは「表を分けるか、1つの文書にまとめるか」の違いに絞ります。

10万人のデータを、社員1人を1つの JSON 文書にして PostgreSQL の jsonb 列に入れました。部署と講座を文書の中に埋め込む、ドキュメント型の典型的な持ち方です。社員7の文書は次のとおりです(jsonb_pretty の出力を、講座の部分だけ1行ずつに詰めて載せています)。

{
    "dept": {
        "name": "部署8(システム開発第8グループ)",
        "floor": "9F"
    },
    "name": "社員7",
    "courses": [
        { "hours": 6, "title": "講座8:データベース設計の基礎と応用" },
        { "hours": 6, "title": "講座9:データベース設計の基礎と応用" },
        { "hours": 6, "title": "講座10:データベース設計の基礎と応用" }
    ]
}

社員1人の情報が1回の読み出しで全部そろうのが、この持ち方の強みです。一方で、部署名を変えるときは、その部署の社員の文書を全部書き換えることになりました。

持ち方 容量(10万人) 部署名の変更で書き換えた件数
RDB:4つに分けた表 27MB 1行
文書:社員ごとの JSON(jsonb) 46MB 2,000件(その部署の社員の数)
1枚の表(受講ごと) 50MB 6,000行

どちらが優れているかではなく、何を優先するかで選びます。

比べる点 リレーショナルデータベース NoSQL(ドキュメント型の例)
データの形 表。列と型を先に決める(スキーマ) JSON など。文書ごとに形が違ってもよい
関係のある情報 表を分けてキーでつなぎ、JOIN で組み立てる 1つの文書に埋め込む。JOIN は苦手か、無い製品もある
書き換え 1つの事実は1か所。変更が1行で済む 埋め込んだ情報は、コピーの数だけ書き換える
正しさの守り方 制約とトランザクションをデータベースが強制 アプリ側で守る部分が多い(製品によって差がある)
大きくするとき 1台を強くする(スケールアップ)が基本 台数を増やす(スケールアウト)を前提にした製品が多い

判断の目安を表にします。

こんなとき 向く方 理由
お金・在庫・予約など、矛盾が許されない RDB トランザクションと制約で半端な状態を残さない
同じ情報(部署・商品・顧客)をあちこちから参照する RDB 1か所を直せば全体に反映される
集計の切り口が後から増える RDB SQL で表の組み合わせを自由に変えられる
セッション・キャッシュなど、キーで1件を速く取りたい NoSQL(キーバリュー) 形を決めずにメモリ上で速く読み書きできる
項目が文書ごとに大きく違い、まとめて1件で読む NoSQL(ドキュメント) 埋め込めば1回の読み出しでそろう
迷う・まだ決まっていない RDB から始める PostgreSQL・MySQL には JSON 型もあり、部分的に文書の形でも持てる

実際のシステムでは、注文や会員は RDB、セッションは Redis、のように組み合わせて使うことが多く、「RDB か NoSQL か」の二択になるとは限りません。

自分で確かめる(ターミナルで1行・SQLで1行)

この節では、手元のデータベースがどんな表とキーで関係づけられているかを、その場で確かめる方法が分かります。どれも、この記事の環境で実際に動かしたものです。

確かめ方1:SQLite の表の定義と外部キーを見る(ターミナルで1行)

$ sqlite3 company.db ".schema employees" "PRAGMA foreign_key_list(employees);"
CREATE TABLE employees(
  emp_id INTEGER PRIMARY KEY, emp_name TEXT NOT NULL,
  dept_id INTEGER NOT NULL REFERENCES departments(dept_id));
0|0|departments|dept_id|dept_id|NO ACTION|NO ACTION|NONE

2行目の結果は「dept_id 列が departments の dept_id を指している。更新・削除時の動きは NO ACTION(指されている行は消せない)」という意味です。外部キーが1行も出なければ、その表はどこも指していません。

確かめ方2:SQLite の外部キー確認がオンか、反した行が無いか

$ sqlite3 company.db "PRAGMA foreign_keys; PRAGMA foreign_key_check;"
0
employees|4|departments|0

1行目が 0 ならオフです。2行目以降に何か出たら、外部キーに反した行が残っています(表名・行番号・指す先の表)。

確かめ方3:PostgreSQL で表の定義とつながりを見る(psql で1行)

t230db=# \d employees
              Table "public.employees"
  Column  |  Type   | Collation | Nullable | Default
----------+---------+-----------+----------+---------
 emp_id   | integer |           | not null |
 emp_name | text    |           | not null |
 dept_id  | integer |           | not null |
Indexes:
    "employees_pkey" PRIMARY KEY, btree (emp_id)
Foreign-key constraints:
    "employees_dept_id_fkey" FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
Referenced by:
    TABLE "enrollments" CONSTRAINT "enrollments_emp_id_fkey" FOREIGN KEY (emp_id) REFERENCES employees(emp_id)

Foreign-key constraints がこの表から指している先、Referenced by がこの表を指している表です。1つの表の上流と下流が一度に分かります。

確かめ方4:どの RDBMS でも使える SQL で外部キーを一覧にする

SELECT tc.table_name, kcu.column_name,
       ccu.table_name AS ref_table, ccu.column_name AS ref_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY 1, 2;
 table_name  | column_name |  ref_table  | ref_column
-------------+-------------+-------------+------------
 employees   | dept_id     | departments | dept_id
 enrollments | course_id   | courses     | course_id
 enrollments | emp_id      | employees   | emp_id

information_schema は SQL の規格にある「データベースの目次」で、PostgreSQL・MySQL・SQL Server で使えます(SQLite には無いため、確かめ方1を使います)。この結果を見ながら ER図を描けば、既存のデータベースの全体像をつかめます。

表を作る前の確認項目も挙げておきます。

よくあるエラー・つまずきと直し方

この節では、RDB を使い始めたときに出やすいエラーの原文と、原因・直し方が分かります。原文はすべてこの記事の環境で出したものです。

症状・エラー原文 原因 直し方
JOIN したら件数が増えた(3行×3行で 9 件) ON の条件が無く、全行の組み合わせ(直積)になった ON に「外部キー = 主キー」を書く。条件ありでは 3 件
ambiguous column name: dept_id(SQLite)/column reference "dept_id" is ambiguous(PostgreSQL) 同じ名前の列が2つの表にある e.dept_id のように表の別名を付けるか、USING(dept_id) でつなぐ
no such table: employee/relation "employee" does not exist 表の名前の綴り違い(employees の s 抜け)、別のスキーマ・別のファイル SQLite は .tables、PostgreSQL は \dt で正しい名前を見る
存在しない部署を指す社員が入ってしまう(SQLite) SQLite の外部キー確認が既定でオフ 接続ごとに PRAGMA foreign_keys = ON;。残った行は PRAGMA foreign_key_check で探す
UNIQUE constraint failed: employees.emp_id SQLite では主キーの重複もこの文言になる 既にある番号か確かめる。連番に任せるなら主キーの値を書かずに INSERT する
current transaction is aborted, commands ignored until end of transaction block トランザクションの中で1つの文が失敗し、以降の文が無視されている 最初のエラーを直し、ROLLBACK してからやり直す
canceling statement due to lock timeout 別の接続が同じ行を書き換え中で、待ち時間の上限を超えた 相手のトランザクションを早く終える。長いトランザクションを作らない
前は上から順に出ていたのに、順番が変わった ORDER BY が無い。更新すると並びが変わる 表示順が要る SELECT には必ず ORDER BY を書く
NULL = NULL が真にならず、条件に当てはまらない NULL は「値が分からない」で、比べた結果も NULL になる IS NULL・IS NOT NULL で比べる

最後の行は SQLite で確かめると、SELECT NULL = NULL, NULL IS NULL; の結果が「空(NULL)」と「1」になりました。NULL を含む列で = や NOT IN を使うと、思った件数にならないことがあります。

もう1つ、JOIN してから合計を出すときの注意です。部署ごとに「受講時間の合計」と「人数」を同時に出すと、社員が受講の数だけ行に増えるため、人数は COUNT(DISTINCT e.emp_id) で数える必要があります。実行した結果は「開発本部:受講時間の合計 15・人数 2」「営業部:3・1」で、DISTINCT を外すと開発本部の人数が受講件数の 3 になります。

よくある質問

この節では、リレーショナルデータベースについてよく聞かれる質問への短い答えが分かります。

Q. リレーショナルデータベースを一言でいうと何ですか。

A. データを表に分けて入れ、表どうしを共通の番号(キー)で結びつけて扱うデータベースです。必要なときに SQL で表を組み合わせて取り出します。

Q. RDB と RDBMS は同じ意味ですか。

A. 会話では混ぜて使われますが、RDB はデータの持ち方とデータそのもの、RDBMS は PostgreSQL・MySQL などの管理ソフトを指します。

Q. Excel の表とは何が違いますか。

A. 列の型・重複の禁止・外部キーなどの決まりをデータベースが強制すること、複数の表をキーでつなぐ前提で作ること、同時に書き込んでも矛盾しないトランザクションがあることです。

Q. 表を分けると JOIN で遅くなりませんか。

A. 10万人の実測では、社員1,000人分の読み出しが 2.09ms(1枚の表)と 3.04ms(JOIN)で、差は1ms ほどでした。主キーに索引があるため、キーでつなぐ JOIN は速く動きます。

Q. 主キーには何を選べばよいですか。

A. 重ならず、空にならず、変わらない値を選びます。名前やメールアドレスは変わることがあるため、連番や UUID の代理キーを使う設計が多く見られます。

Q. 外部キーは必ず付けるべきですか。

A. 付けると、存在しない相手を指す行や、指されている行の削除をデータベースが止めます。アプリ側だけで確かめる設計もありますが、書き込み口が増えると漏れが出やすくなります。

Q. SQLite もリレーショナルデータベースですか。

A. はい。ファイル1つに入る組み込み型の RDBMS です。外部キーの確認が既定でオフなので、接続ごとに PRAGMA foreign_keys = ON; を実行します。

Q. NoSQL が広まったら RDB は使われなくなりますか。

A. 矛盾が許されない業務データでは RDB が使われ続けており、多くのシステムは RDB と NoSQL を用途ごとに組み合わせています。PostgreSQL や MySQL には JSON 型もあります。

Q. 「リレーション」は表のことですか、表のつながりのことですか。

A. もとの理論では表そのもの(relation)を指し、実務の会話では表と表のつながり(relationship)を指すことが多い言葉です。文脈で見分けます。

Q. 正規化は最後まで進めるべきですか。

A. 業務システムでは第3正規形までを目安にすることが多く、集計用には読み出しを速くするため、あえて1枚にまとめた表を別に作ることもあります。

まとめ

この節では、この記事の要点を5つにまとめて確かめられます。

  • リレーショナルデータベースは、データを表に入れ、主キーと外部キーで表どうしを関係づけるデータベースです。1970年の Codd のリレーショナルモデルがもとになっています。
  • 1枚の表に同じ情報を繰り返し書くと、1か所の直し忘れで「同じ部署に2つの名前」「講座の時間が2つ」が起きることを SQLite で再現しました。
  • 表を分けると名前の変更は1行で済み、10万人のデータでは容量 27MB 対 50MB、部署名の変更 0.48ms 対 65.6ms でした。読み出しは JOIN のぶん1〜1.6倍かかります。
  • 制約とトランザクションが正しさを守ります。PostgreSQL は途中で失敗したトランザクションの COMMIT を ROLLBACK にし、SQLite は外部キーの確認を自分でオンにする必要があります。
  • NoSQL とは「表を分けてつなぐか、文書にまとめるか」で選びます。迷ったら RDB から始め、表示順には ORDER BY を必ず書きます。

参考・出典

この節では、この記事で確かめた公式の資料が分かります(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「リレーショナルデータベース(RDB)とは|表・キー・更新漏れをSQLで再現」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?