- データベース
リレーショナルデータベース(RDB)とは|表・キー・更新漏れをSQLで再現
結論
リレーショナルデータベース(RDB)とは、データを「行と列の表」に分けて保存し、表どうしを「キー」でつないで(関係づけて)扱うデータベースです。同じ情報を1か所にだけ置き、必要なときに SQL の JOIN で組み合わせるため、名前の変更などで書き換え漏れが起きにくくなります。
この記事でわかること
- 表・キー(主キー・外部キー)・リレーション(1対多・多対多)の考え方と、それぞれの役割
- 1枚の表で起きる更新漏れ・削除で消える情報を SQLite で再現した出力と、表を分けて JOIN で戻した出力
- 10万人分のデータで測った容量・更新時間・読み出し時間、制約とトランザクションのエラー原文、NoSQL との違い
この記事で実際に動かしたもの: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つに決める「主キー」と、別の表の行を指す「外部キー」。キーがあるから、名前が同じ人や部署を取り違えずに済む。
例:社員番号、部署番号
リレーション
キーを手がかりにした表どうしの結びつき。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〜第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行が何を表すか(受講1件ごと・社員1人ごと)で、FROM に置く表を決める。受講ごとなら enrollments。
外部キーをたどって JOIN する
ON には「外部キー = 指す先の主キー」を書く。条件を書き忘れると全行の組み合わせになる(よくあるエラーの節)。
相手がいない行も残すか決める
社員0人の部署も出したいなら LEFT JOIN、両方にある行だけでよいなら INNER JOIN。
並べ順を指定する
行に順番はないので、画面に出す順は 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回の中央値です。
| 測ったこと | 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 に組み込まれていて追加のインストールなしで試せるため、最初の一歩に向いています。
- SQLite で
CREATE TABLE・INSERT・SELECT・WHERE・ORDER BYを書く - 表を2つに分けて主キー・外部キーを付け、
JOINとGROUP BYで集計する(この記事の範囲) - 正規化と ER図で、題材を自分で表に分けてみる
- 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日)。
- SQLite「SQLite Foreign Key Support」https://www.sqlite.org/foreignkeys.html
- SQLite「STRICT Tables」https://www.sqlite.org/stricttables.html
- PostgreSQL 16 Documentation「Sorting Rows (ORDER BY)」https://www.postgresql.org/docs/16/queries-order.html
- PostgreSQL 16 Documentation「Constraints」https://www.postgresql.org/docs/16/ddl-constraints.html
- PostgreSQL 16 Documentation「Transactions」https://www.postgresql.org/docs/16/tutorial-transactions.html
- E.F. Codd「A Relational Model of Data for Large Shared Data Banks」Communications of the ACM, 1970 https://dl.acm.org/doi/10.1145/362384.362685
- Google Cloud「リレーショナル データベース(RDBMS)とは」https://cloud.google.com/learn/what-is-a-relational-database
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「リレーショナルデータベース(RDB)とは|表・キー・更新漏れをSQLで再現」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。