あえて今ここでSQLを一旦学習してみる-SQLの基礎-
昨今AIの急速な普及に伴い、コードをほぼ自分で書かずにアプリを作成するという方も増えてきているのではないでしょうか?
もちろんこのコードの中にはDBに対して扱うSQLも当てはまります。
さらに最近のWebアプリでは、ORM(SQLを直接書かずにプログラミング言語からデータベースを操作する仕組み)を利用するケースが一般的になってきています。
斯くいう私も『Prisma』やCloudflareのD1を使う時は『Drizzle』を使うことが多いです。
これらの理由から、生のSQLはおろかそれぞれの用途にあったSQLの記述の設計を自分で考えたりする機会があまりありませんでした。
しかし、いくらAIの精度が上がっていると言っても、構成によっては脆弱性やパフォーマンス低下に大きく関係するDBと接続するロジック(コード)は自分で確認できるようにしておきたいし、たまに作成段階でコマンドラインからDBに直接アクセスして確認したいときは基本的にSQLで記述する必要があります。
そこで今回、改めてSQLを学び直そうという考えに至りました。
本記事では、主にSQLの基本的な内容を整理しつつ、最後に自分が体験した実務的なことについてまとめられたらと思います。
SQLとは
SQL(Structured Query Language)はデータベースでデータを扱う際に用いられる言語です。
もっと詳しくいうと、関係データベース(RDBMS)でデータを操作、定義、制御するための ISO(国際標準化機構)と米国規格協会(ANSI)によって標準化された国際標準言語です。
見方によっては「一番廃れにくい言語」と言えますね。
・プログラミング言語との違い(命令型 vs 宣言型)
javascriptやreactを学習した際に「命令型」と「宣言型」について学んだ方が多いと思います。
「命令型」とは手順を書くを詳しく書く形です。
たとえば「20歳以上のユーザー名を集めたい」とき、JavaScriptやPythonのような普通のプログラミング言語では、だいたい次のような発想になります。
for (const user of users) {
if (user.age >= 20) {
const li = document.createElement('li');
li.textContent = user.name;
ul.appendChild(li);
}
}
ここで書いているのは、
1.配列を最初から最後まで見る
2.条件に合うか判定する
3.合えば追加する
という処理手順そのものです。
これが命令型の感覚です。
「宣言型」は目的だけを明示し手順は指定しない形のことです。
{users
.filter(user => user.age >= 20)
.map(user => <li key={user.id}>{user.name}</li>)
}
ここでは「20歳以上のユーザーをリストとして表示したい」という結果の形(UI)をそのまま書いています。
「こういう結果が欲しい」を宣言しているのが宣言型の感覚です。
「これjsでも宣言型にできるんじゃね?」というツッコミはさておき、あくまで考え方はこの通りです。
これはある意味 AGENTS.mdなどを使わない対話型AIも同じ概念と言えるのかもしれません。
そして、今回のSQLも宣言型ということができます。
SELECT name
FROM users
WHERE age >= 20;
「どの表から」「どの列を」「どんな条件で」取り出したいかは書いていますが、
「1行ずつどうループするか」「どの順で比較するか」「どのインデックスを使うか」といった細かな実行手順は普通は書きません。
方法は、それぞれのDBが選んで実行します。
もちろん、SQLには WHERE、GROUP BY、ORDER BY などの命令型っぽい記述もあることは確かですが、その選択方法や並べ替え方法はDB側に委ねることになります。...という少し言い訳じみたことも言いましたが、あくまでこのような考え方ができると思ってください。
この違いを意識できると、SQLの読み方が変わります。
初心者のうちは、SQLを見て
「これ、内部でfor文みたいに回ってるのかな?」
と考えがちです。
もちろん内部では何らかの処理が行われますが、
SQLを書く人が主に考えるべきなのは “ループの書き方” ではなく “必要な結果をどう表現するか” です。
そしてその上で、性能が必要な場面では応用的に実行計画やインデックスを見て、「DBがその宣言をどう実行したか」を確認していく、という流れになります。
データベースの基本概念
1. 関係データベース
先ほどの章の最初で「関係データベース(RDB)でデータを扱う言語」をSQLと言いました。
なぜ、厳密には「データベース」ではなく「関係データベース(RDB -> リレーショナルデータベース)」なのかというと、最初からこの形だったわけではないからです。
代表的なものとしては以下のような種類があります。
階層型データベース:ツリー構造でデータを管理する
ネットワーク型データベース:複雑な関係を持つデータを表現できる
関係データベース(RDB):テーブル(表)形式でデータを扱う
これらの中で、現在主流となっているのが関係データベースです。
関係データベースが広く使われるようになった理由は、データをシンプルな表として扱える点にあります。
「データベース」と思い浮かべればわかる通り、データは「テーブル」という単位で管理され、
・行(レコード):1件のデータ
・列(カラム):データの属性
・主キー:データを一意に識別するための値
という非常にわかりやすいシンプルな形です。
このようにデータが整理されていることで、SQLによって「どのデータが欲しいか」を宣言的に記述し、柔軟に取得できるようになっています。
これが、前の章で言った「欲しいデータを宣言的に取得できる」という意味です。
2. データベースの信頼性(ACID特性)
関係データベースが広く使われているもう一つの理由に、「データの整合性と信頼性を強く保証できる」という点があります。その代表的な考え方が「ACID特性」です。
ACIDとは以下の4つの性質の頭文字を取ったものです。
- Atomicity(原子性)
処理はすべて成功するか、すべて失敗するかのどちらかになる - Consistency(一貫性)
データベースの整合性が常に保たれる - Isolation(独立性)
同時に実行される処理同士が互いに影響を与えない - Durability(永続性)
一度確定したデータは、障害が発生しても失われない
よく「銀行の振込処理」の例えが使われます。
- 送金元から減算される
- 送金先に加算される
この2つは必ずセットで実行される必要があります。
もし途中で処理が失敗した場合でも、どちらかだけが反映されることはなく、処理全体が取り消されます。
このような仕組みによって、データの信頼性が保たれています。
基本情報技術者の学習範囲だったと思います。
当時しっくりこなかったことでも改めて学習すると、自分が少しだけ成長しているように感じるのと同時に基本情報技術者が意外と今に活きていることを感じますね。
なお、近年ではNoSQLと呼ばれるデータベースも広く使われるようになっており、これらはACID特性の一部を緩めることでスケーラビリティを重視する設計になっている場合があります。
しかし、銀行のシステムなどの常に正確な情報が求められる場合では使えません。
そのため、用途に応じて「強い整合性」と「スケーラビリティ」を使い分けることが重要になります。
NoSQLの広まった背景や設計思想がとても面白いのですが、本記事の趣旨とズレるのでここでは扱いません。
基本クエリの理解
ここでは、データ取得に使用するSELECT文を中心に、実務でよく使われるSQLの基本的な操作をまとめて紹介します。
なお、INSERTやUPDATEなどのデータ操作についてもあわせて扱いますが、これらは厳密にはクエリというより「データ操作言語(DML)」に分類されます。
SQLは役割ごとにDMLやDDLといった分類がありますが、実際の開発ではまず「データを取得する(SELECT)」ことを中心に理解し、その後にデータの更新やテーブル設計へと理解を広げていく方が分かりやすいと感じ、本記事では明確にこれらの分類をせずにまとめています。
補足: SQLは役割ごとにいくつかの種類に分類されます。
DQL(Data Query Language):データ取得・検索(SELECT)
DML(Data Manipulation Language):データ操作(追加・INSERT / 更新・UPDATE / 削除・DELETE)
DDL(Data Definition Language):テーブル定義(作成・CREATE / 削除・DROP / 変更・ALTER )
DCL(Data Control Language):権限管理(GRANT / REVOKE)詳しくは、SQLとDBMSの理解:DDL、DML、DQL、DCL、TCLコマンドの概要 がわかりやすくまとまっています。
本記事では主にDQLとDMLを中心に扱います。
1. データ取得(SELECT)
SQLの中で最も基本となるのが「SELECT文」です。 これは、データベースから必要なデータを取得するための命令です。
まずは最もシンプルな形を見てみましょう。
SELECT name
FROM users;
このSQLは、「usersテーブルからnameカラムの値をすべて取得する」という意味になります。
ここで重要なのは、SQLは単にデータを取り出しているのではなく、 新しい結果テーブルを作っているという考え方です。
たとえば、元のテーブルが以下のようになっているとします。
| id | name | age |
|---|---|---|
| 1 | 太郎 | 18 |
| 2 | 花子 | 25 |
このとき、上記のSELECT文を実行すると、次のような結果になります。
| name |
|---|
| 太郎 |
| 花子 |
このように、元のテーブルから必要な列だけを取り出し、 新しい表(結果セット)を作るのがSELECT文の役割です。
さらに、条件をつけることもできます。
SELECT name
FROM users
WHERE age >= 20;
これは「20歳以上のユーザーの名前を取得する」という意味になります。
結果は以下のようになります。
| name |
|---|
| 花子 |
また、取得したデータに対して並び替えや件数制限を行うこともできます。
-
ORDER BY: 取得した結果を指定したカラムで並び替えるために使用します。昇順(ASC)や降順(DESC)を指定することができます。
SELECT * FROM products ORDER BY price DESC; -
LIMIT: 取得する件数を制限するために使用します。主に「先頭から何件取得するか」といったページネーションなどの用途で使われます。
SELECT * FROM users ORDER BY age DESC LIMIT 3;
SELECT * FROM users LIMIT 5 OFFSET 5;;
// 6~10件目を取得
これらは、SELECT文によって取得した結果に対して、最後に適用される処理と考えると理解しやすいでしょう。
一般的にSQLの処理は、FROM → WHERE → SELECT → ORDER BY → LIMIT のような流れで適用されると考えることができます。
このように、SQLでは
- どのテーブルから(FROM)
- どの列を(SELECT)
- どの条件で(WHERE)
- どの順番で(ORDER BY)
- 何件崇徳するか(LIMIT)
データを取得するかを記述します。
また、繰り返しになりますが、重要なのは「どのように処理するか」ではなく、 「どのような結果が欲しいか」を書いているという点です。
これは前の章で説明した「宣言的な書き方」の典型的な例です。
このようにSELECT文は、テーブルからデータを取得するだけでなく、条件や集計、結合などを組み合わせることで、さまざまな形にデータを変換することができます。
2. 集計とグループ化
これまでのSELECT文では、「1行ずつのデータを条件で絞り込む」という操作を見てきました。 一方で、SQLでは複数のデータをまとめて扱う「集計」も非常に重要な機能です。
集計関数
SQLでは、複数の行に対して計算を行うための「集計関数」が用意されています。
代表的なものとしては以下のようなものがあります。
- COUNT:件数を数える
- SUM:合計を求める
- AVG:平均を求める
- MAX / MIN:最大値・最小値を求める
これらの関数を使うことで、データ全体に対して統計的な情報を取得することができます。
SELECT COUNT(*) FROM users;
GROUP BY(グループ化)
集計関数は、単にテーブル全体に対して使うだけでなく、「特定の単位ごとにまとめる」こともできます。 そのために使われるのが「GROUP BY」です。
GROUP BYを使うことで、データを特定のカラムの値ごとにグループ化し、 それぞれのグループに対して集計処理を行うことができます。
たとえば、「国ごとにユーザー数を数える」といった処理は、GROUP BYによって実現できます。
以下は、「同じ名前ごとに年齢の平均を出す」という少し変な例です。
SELECT name, AVG(age) AS avg_age //平均結果を「avg_age」という新しいカラムに格納
FROM users
GROUP BY name; //name カラムが同じものをグループに
WHEREとの違い
ここで少し注意が必要なのが、WHEREとの違いです。
- WHERE:各行に対して条件を適用する(グループ化の前)
- GROUP BY:データをグループ単位にまとめる
つまり、SQLでは
「まず行を絞り込み(WHERE)、その後にグループ化して集計する」
という流れになります。
HAVING(グループへの条件)
GROUP BYで作られたグループに対して条件を指定したい場合は、「HAVING」を使用します。
WHEREが「行」に対する条件であるのに対し、
HAVINGは「グループ」に対する条件を指定するためのものです。
先ほどの、「同じ名前ごとに年齢の平均を出す」というものに「平均が30以上のみ」という条件をつけたものになります。
SELECT name, AVG(age) AS avg_age
FROM users
GROUP BY name
HAVING AVG(age) >= 30;
正直、ひよっこの私はまだGROUP BYやHAVINGは使う場面に直面したことはありません。
このように、集計とグループ化を組み合わせることで、 単なるデータの取得だけでなく、より高度な分析を行うことができるようになります。
3. テーブル結合(JOIN)
これまでの例では、1つのテーブルに対してデータを取得してきました。 しかし実際のデータベースでは、データは複数のテーブルに分割して管理されることが一般的です。
たとえば、
- usersテーブル:ユーザー情報
- ordersテーブル:注文情報
のように、役割ごとに分けて管理されます。
具体的にテーブルを使って考えてみます。
usersテーブル
| id | name |
|---|---|
| 1 | 太郎 |
| 2 | 花子 |
| 3 | 次郎 |
ordersテーブル
| id | user_id | amount |
|---|---|---|
| 1 | 1 | 1000 |
| 2 | 1 | 2000 |
| 3 | 2 | 1500 |
このような分かれたテーブル同士を組み合わせてデータを取得するために使用するのが「JOIN」です。
JOINは、共通のキーをもとにテーブル同士を結びつけ、 1つの結果テーブルとしてまとめるための仕組みです。
- INNER JOIN(内部結合)
INNER JOINは、両方のテーブルに存在するデータのみを結合します。
つまり、結合条件に一致するデータだけが結果に含まれます。
たとえば「ユーザーとその注文情報を取得する」場合でも、 注文が存在しないユーザーは結果に含まれません。
先ほどの、具体的なテーブルで確認してみましょう。
SELECT users.name, orders.amount //最終的に作成するテーブルの構成
FROM users
INNER JOIN orders
ON users.id = orders.user_id; //対応する行の指定
INNER JOINでは、両方のテーブルに存在するデータのみが結合されます。
| name | amount |
|---|---|
| 太郎 | 1000 |
| 太郎 | 2000 |
| 花子 | 1500 |
この場合、「次郎」はordersテーブルに対応するデータが存在しないため、結果に含まれません。
- LEFT JOIN(外部結合)
LEFT JOINは、左側のテーブルのデータをすべて残しつつ結合します。
結合条件に一致するデータが右側に存在しない場合でも、 左側のデータは結果に含まれます。
その場合、右側の値はNULLとして扱われます。
具体的なテーブルで確認してみましょう。
SELECT users.name, orders.amount
FROM users
LEFT JOIN orders
ON users.id = orders.user_id;
LEFT JOINでは、左側のテーブルのデータはすべて残されます。
| name | amount |
|---|---|
| 太郎 | 1000 |
| 太郎 | 2000 |
| 花子 | 1500 |
| 次郎 | NULL |
「次郎」は注文データを持っていませんが、usersテーブルには存在するため結果に含まれます。 このとき、orders側の値はNULLとして扱われます。
- INNER JOINとLEFT JOINの違い
両者の違いは、「どのデータを残すか」にあります。
- INNER JOIN:両方に存在するデータだけ残す
- LEFT JOIN:左側のデータはすべて残す
この違いを意識すると、用途が分かりやすくなります。
- 使い分けの考え方
- 「対応するデータがあるものだけ欲しい」場合 → INNER JOIN
- 「基準となるデータはすべて表示したい」場合 → LEFT JOIN
たとえば、
- 注文があるユーザーだけ見たい → INNER JOIN
- すべてのユーザーと、その注文状況を見たい → LEFT JOIN
というように使い分けます。
JOINは最初は難しく感じますが、 「テーブルを結合する」というよりも
「どの行を残すかを決める操作」
として捉えると理解しやすくなります。
補足:
RIGHT JOINやFULL OUTER JOINといった種類も存在しますが、実務ではLEFT JOINを理解しておけば多くのケースに対応できます。
また、このJOINについてもとてもわかりやすい最適な解説動画があります。
SQL初心者のためのテーブル結合:INNER JOINとOUTER JOINの違い - YouTube
4. データ操作(CRUD)
ここまでで、複数のテーブルを組み合わせてデータを取得する方法を見てきました。次は、データの追加や更新といった操作について見ていきます。
タイトルに「CRUD」とあります。
これは、
- Create:データの作成(追加)
- Read:データの取得
- Update:データの更新
- Delete:データの削除
の略で『クラッド』と言います。
これは、データの基本の4操作のことでdbの話だけでなくシステムの話の中でも使われることがよくあります。
IT意識の高い人が「クラッド」と多用してきたら、SQLのことを「シークェル」と言って威嚇しましょう。
これらに対応するSQLは以下の通りです。
- Create → INSERT
- Read → SELECT
- Update → UPDATE
- Delete → DELETE
1. INSERT(データの追加)
新しいデータをテーブルに追加するために使用します。
INSERT INTO users (name, age) VALUES ('佐藤', 25);
2. UPDATE(データの更新)
既存のデータを更新するために使用します。
UPDATE users SET age=65 WHERE id=10;
3. DELETE(データの削除)
データを削除するために使用します。
DELETE FROM users WHERE id=10;
これまで紹介してきたSELECTやJOINと組み合わせることで、データの取得から更新まで一通りの操作をSQLで行うことができます。
終わり
本記事では、SQLの基礎として以下の内容を整理しました。
- SELECTによるデータ取得
- 条件指定(WHERE)や並び替え(ORDER BY)、件数制限(LIMIT)
- COUNT, MAXなどの集計関数とGROUP BYによるデータの集約
- INNER JOINやLEFT JOINによる複数テーブルの結合
- INSERT / UPDATE / DELETEによるデータ操作(CRUD)
SQLは単なる「データを取得するための言語」ではなく、
データをさまざまな形に変換し、必要な情報を取り出すための強力なツールです。
また、Reactなどで触れた「宣言的な考え方」とも共通しており、
「どのように処理するか」ではなく「どのような結果が欲しいか」を記述する点が特徴です。
今回改めて基礎を整理することで、これまで曖昧に扱っていた部分がクリアになり、
SQLに対する理解が一段深まったと感じています。
こた、本記事が同じような駆け出しエンジニアの方の参考に少しでもなれば幸いです。
なお、実務ではこれらの基礎に加えて、
- インデックス設計
- クエリのパフォーマンス最適化
- データベースの選定(RDB / NoSQL)
といった観点が非常に重要になります。
これらについては、本記事とは切り分けて、
実践的なSQLの書き方やデータベース設計の考え方として、別の記事でまとめる予定です。
※付録:初学者におすすめ参考資料
-
【70年代の遺産】なぜ『SQL』は半世紀経っても現役なのか?IBMの天才が産んだ「データの革命」と、Googleすら勝てなかった数学的完成度【ゆっくり解説】 - YouTube
SQLの歴史からわかりやすく説明されており学習の導入に最適です。というかこの記事の内容はこの動画で全て網羅できます。 -
SQLの練習ができる学習サービス | SQLab
どちらも手を動かしながら学習できます。 -
ChatGPT
わからないことを満足するまで聞いてください。
-以上
Author
主にシステム面で学習したことをまとめています。 フロント、サーバー、インフラ、AIなど細かい分野に絞らずに広く発信していきます。