『プログラマのためのSQL』DB読書会参加レポ
この記事は MICIN Advent Calendar 2025 の 21 日目の記事です。
前回はKotaroさんの、クラウド上でコンテンツ管理ができ、完全無料で動画再生もできるサイネージアプリを作ってみた でした。
はじめに
株式会社MICINのオンライン医療事業部の山崎です。
今年、私はMICINのOBであるdonsanさん(@donsan), まじまっちょさん(@majimaccho_)が主催の『プログラマのためのSQL 第4版』読書会に参加しました!
本記事では、1年という長い期間をかけて完走したことを通して得た技術書の読書会についての知見と、SQLの古典である本書をどのように読んでいったかの記録を共有します。
自分について
内容に入る前に、自身の前提知識について触れておきます。
- 経験:アプリケーション開発がメイン業務。普段はReact+TypeScript,Ruby on Railsをよく利用しています。
- SQLスキル:Redashなどでデータ抽出をする際にSELECT文を書く程度。あまり複雑なクエリは扱っていません。
- 参加の動機:最近では業務内でもDB設計に携わる機会が増えてきました。そして、それに伴いデータアナリティクスチームとの連携も増えてきました。そんな中でRDBMSやSQLについて体系的な知識を持ちたい、しっかり学び直したいと考えるようになりました。
読んだ本: 『プログラマのためのSQL 第4版』
今回扱ったのは、SQLの大家ジョー・セルコによる『プログラマのためのSQL 第4版』です。
訳者であるミックさんがSQLを勉強していた時代に読み込んだ本としても知られており、いわば「SQL/RDBMS関連書の古典」です。
ご参加いただいた坂井さん(本書の翻訳監修等、尽力なされた功労者のお一人)が「SQL大好きなセルコが飲み屋で語っているイメージ」とおっしゃっていた本書は、機能を全網羅する勢いで詳細に書かれている大作で、DB初心者には少々ハードルの高い一冊でした。
読書会の進め方
開催頻度と形式
- 頻度:隔週1回、平日19:00〜20:00の1時間
- 場所:オンライン(Discord + Cosense)
- 範囲:章の長さや難易度にあわせて1〜3章ずつ
隔週開催が良いペースだったように思います。週1回だと予習が大変ですが、逆に月1回だと内容を忘れてしまったり予定調整がルーズになりがちです。隔週の頻度は社会人が継続的に集まるうえで適切でした。
基本はオンライン開催で時間的ハードルが低くなり気軽に集まれる状態を作りつつ、数ヶ月に一度オフライン会(+懇親会)を挟むような形をとりました。オンラインとオフラインを組み合わせることで、モチベーションの維持とメンバー間の親睦を深めることにつながりました。
ツール: Cosense(旧Scrapbox)の活用
レジュメや議事録にはCosenseを利用しました。こちらが実際のCosense Projectです。パブリックで公開していますので、ご興味のある方はぜひ覗いてみてください!
このCosenseは会の進め方と非常に相性のよいツールでした。
- 箇条書きベースである:思考の整理がしやすい
- リアルタイム共同編集:議論の内容を書き留めたり、関連URLを貼ったりの作業を同時に行う際も、だれかの編集が消えてまうなどのトラブルが少なくスムーズ
- リンク機能:過去の議論や関連用語へのリンクが容易で、知識が蓄積されていく感覚がある
- 検索性:曖昧検索によって議論の内容をあとから簡単に探せる
独自記法に慣れてしまえば簡単で気軽です。シンプルでありながら、知識の蓄積はしっかりできるツールでした。
勉強会の具体的なフロー
参加メンバーが全員ある程度本を読んできた状態を担保するためにも、以下の流れで進めました。
a. 事前準備
各自で該当範囲を読み、事前にCosenseのページに以下の3点を記入します。
- わかったこと
- わからなかったこと
- 感想:率直なコメントや連想した話題など自由に書く欄
私の場合、予習は以下のようなペースで行っていました。
- まずはさらっと読みつつ、不明点をメモしたり、単語などちょっとしたことは調べておく
- 寝かせておき、読書会の前日などにもう一度読み直して理解度をチェックし、細かいところに立ち入る
- 複雑なSQL例は、ChatGPTなどのAIアシスタントにコードを渡して解説してもらう
利点は以下のようなものでした。
- 「わかったこと」を整理する過程で理解をチェックできたこと
- 「感想」欄があることで、少し脱線した話題や実務での悩みなども話しやすく、議論が活発になったこと
b. 当日の議論
当日は、事前に記入した内容を画面共有しながら進めます。
わからなかったことを中心に理解を進めつつ、感想やざっくばらんな内容も含めて議論をしていきました。
読書を通じての知見
この章では少しだけ内容に立ち入った話として、実際にこの『プログラマのためのSQL 第4版』を読んで大変だったこと、学んだことをご紹介します。
1. 集合・位相の知識
RDBMSは、エドガー・F・コッドが1970年代に提唱した関係データモデルがベースとなっています。関係データモデルは、数学の集合の考え方に深く影響を受けています。そのため、SQL/RDBMSを使いこなすためには、集合指向の考え方が必要になってきます。セルコは「手続き型ではなく、宣言型で記述せよ」という点を繰り返し強調します。掲載されているSQLでの問題解決も、集合論的な考え方をうまくSQLに落とし込んだものが多いです。
アプリケーションのコーディングをすることが多く手続き型の考え方に慣れている私にとっては、頭の切り替えが大変でした。
この本を読むうえでも序盤から理論的な話が展開されます。数学的な知識をある程度持っていると読み進む際にも面食らわず気が楽ですし、実際に数学の理論はSQLやRDBMSの特性を考えるうえでもベースとなっているので、理数系のバックグラウンドを持たない人はさらっと知っておくとよいかもしれません。
私は、松坂和夫氏の『集合・位相入門』 という本をベースに集合論を学習しました。
技術書だとミックさんの『達人に学ぶSQL徹底指南書』 で理論的な部分はていねいにわかりやすく説明されているので、読書会では『達人』が副読本として読まれていました。私も理解を深めるために何度も参照しました。
2. 自然キー至上主義のセルコ?
白熱したトピックがいくつかありましたが、特に「ユニークキーの選び方」については実務での実感や現実での事例をもとに活発な意見交換が行われました。
セルコは、キーにはできるかぎり自然キーを使うべきと主張しています。
私としても、業務上のデータを区別するうえでのユニークとなる意味を持った値をデータはすでに持っているはずなので(そうでないと運用できていないはず)、それをキーとして使うのが一番よいのでは?という考えでした。
この読書会では、自然キーにこだわる必要はないのではという意見が主流でした。
自然キー関連で出てきたトピックは以下のようなものでした。
a. 自然キーになりうる値・自然キーとできる値が複数ある場合にはどの値を採用するのか
私たちは医療系プロダクトを扱っていますが、では我々のプロダクトで医療機関(施設)の情報を管理しますとなった場合、DCFの施設コード(日本アルトマーク社)と厚労省の医療機関コードのどちらを採用するべきなのでしょうか?
セルコは「業界標準コードをキーとして使うよう注意する(べき)」と本書で言及しています。しかし、"標準"とは何かと考えていくと、単純にその原則を適用することが難しいケースも現実には存在します。
政治的な思惑がかかわってくることもあるでしょうし、国などプロダクトの適用範囲によっては"標準"が一意に定まらないこともあり得ます。また、どのような外部データと連携したいか、最終的にどういう使い方をしたいかによって採用すべきキーが変わることは十分ありそうです。
b. 自然キーも見方によっては人工キーではないか
本書では、人工キーはシステム設計の範囲内で決定するもの(システムで機械的に作成したもの)という前提がおかれています。その前提であればたしかに医療機関コードやISBN、社員番号などは「自然キー」ということになります。実際、一般に自然キー/人工キーという場合、定義上はこのような切り分け方をされているようです。
今回の読書会では、結局のところ自然キーそれ自体も、多くは人間が恣意的に定めたものであり、本質的には"人工キー"であるのでは?という意見がありました。たとえば注文番号や会員番号などは自然キーとして業務で利用されますが、定義的には人工キー寄りです。
c. 自然キーの変更に対応できるか
自然キーには、変更や拡張の可能性があります。
社員番号は一意であるはずですが、実際には、退職があった場合に番号を詰めるようなケースもあるようです。こうなるとデータ上ではキーがコンフリクトしてしまいます。
ISBN(国際標準図書番号)のようなケースもあります。ISBNはセルコの言う「業界標準コード」なので自然キーとして適切な対象です。この値は、当初10桁であったものが2007年から13桁に変わっています。ISBNを自然キーに採用していた場合、このような変更があった場合、データベースでもスキーマやレコードの変更といった大きな改修が必要です。
d. 純粋な自然キーの不便さ
単純な例では同姓同名問題があります。同姓同名の人がいた場合ユニークにならないので、氏名だけをキーとすることはできません。日頃の業務運用では、氏名を確認し、複数の同姓同名のものがいれば生年月日、電話番号を確認する……というように段階的に個人を特定していくことがありますが、データベースで運用をそっくりそのまま反映することはできないですし、やろうとしても複雑になってしまいます。
また、面白い事例だとConfluenceでは同じタイトルの記事は同スペース内では作成できません。Confluenceの場合、おそらく、タイトルがキーの少なくとも一部になっているものと思われます。タイトルを工夫するかプレフィックス、サフィックスを付けるなど運用での対応が必要になります。
一人で読んでいたら「今後は自然キーを使えるところは使っていこう」という理解に留まっていましたが、議論を通じて知見を得ることで、理想としての理論と現実的な解のバランスを強く意識するようになりました。
3. 複雑なSQL
本書には、こんなことまでSQLでやるのか、というレベルのSQL演算の事例が大量に載っています。とくに29章の集約関数については、n個の要素を返す極値関数、LIST関数、PRD関数などで難しいものが多かった印象です。
一生懸命読み解きながら、実務で参考になるかならないかで言うと複雑な演算はアプリケーション側でやったほうが良いな……と感じることもありましたが、「SQLでここまでできる」という引き出しが増えたことや「ここはSQLに任せる、こっちはアプリでやる」という判断基準の解像度が上がったことは大きな成果でした。
高度なSQLプログラミングについては、セルコ著・ミック訳の『SQLパズル 第2版~プログラミングが変わる書き方/考え方』という書籍も出版されており、こちらにより詳しいです。
4. いろいろなNULL
NULLひとつをとってもいろいろなトピックが掲載されています。
その成り立ちの紆余曲折についても面白かったですし、日頃SQLを書いていてつい間違えてしまうNULLの扱いかたについても、なぜそうなるか?を知っていくことが生きた理解につながりました。
特に参考になったトピックを以下にピックアップしています。
NULLの比較
NULLは値ではありません。あくまで、データがないことの目印です。
なので、NULLと数値を比較した場合UNKNOWNになります。これは、SQL初心者はよく間違えがちなケースではないでしょうか。私もよく間違えてエラーを出してしまっていました。
NULLとNULLを比較すると、これまたUNKNOWNになります。理由としては、NULLはいわば「失われた値」であるというところがあります。[1]なので、NULLとNULLの比較は本質的に成り立ちません。
「失われた値」には基本的には「未知」「適用不能」の二つがあります。「未知」と「適用不能」の比較は意味としてそもそも違っているので不可能ですし、「未知」= よくわからないものとよくわからないもの同士比べるというのも不可能です。適用不能に関しては、「適用可能」な値以外のすべては適用不能となるので、これを比較して同値かどうかを検討することもできません。
よって、以下のような書き方をした場合、SomeColumnがnullの場合は抽出結果から除外されてしまいます。
SELECT * FROM SomeTable WHERE SomeColumn = 2;
SELECT * FROM SomeTable WHERE SomeColumn <>2;
「NULLの比較ができない」ことを「お約束」としてわかってはいても背景知識は持っていなかったので、勉強になりました。NULLの奥深さの一端に触れた気持ちです。背景知識を理解することで業務で扱う際のうっかりミスも減らせそうです。
NULLとunknownの違い
NULLはあくまで、値がないということを示すマーカーです。
対してUNKNOWNは、真理値の一つです。
SQLは3値論理を採用しているので、TRUE/FALSE/UNKNOWNという3つの真理値をとります。
このことが、NULLのカラムが存在する場合の演算に大きな影響を及ぼすことになります。
以下のような例が掲載されていました。
CREATE TABLE Table1 (coll INTEGER);
INSERT INTO Table1 (coll) VALUES (1), (2);
CREATE TABLE Table2 (coll INTEGER);
INSERT INTO Table2 (coll) VALUES (1), (2), (3), (4), (5);
SELECT coll
FROM Table2
WHERE coll NOT IN (SELECT coll FROM Table1)
上の例だと、意図どおりに3,4,5を正しく抽出してくることができます。
しかし、Table1にNULLを登録して同じクエリを実行すると、結果は空になってしまいます。
NOT INの計算過程は以下のようになるからです。
SELECT coll
FROM Table2
WHERE NOT (coll IN (SELECT coll FROM Table1));
--- IN述語は、ANYに置換できる
SELECT coll
FROM Table2
WHERE NOT (coll = ANY (SELECT coll FROM Table1));
--- ANY述語は、ORと同値
SELECT coll
FROM Table2
WHERE NOT ( (coll = 1)
OR (coll = 2)
OR (coll = NULL));
coll = NULLは、NULLの比較なので、UNKNOWNとなります。
ORはド・モルガンの法則を使えば以下のようになります。
SELECT coll
FROM Table2
WHERE ((coll <> 1)
AND (coll <> 2)
AND UNKNOWN);
3値論理の論理演算で、UNKNOWNがAND演算に含まれていた場合、結果は必ずUNKNOWNまたはFALSEになります。[2]
WHERE句の検索条件においてそれらはかならず棄却されるため、結果が空になるというわけです。
SQLの3値論理の考え方も、数学の多値論理の知識と深くつながっています。[3]
AVG(x)と(SUM(x)/COUNT(*))が同値でないことにもNULLが影響している
AVG(x)と(SUM(x)/COUNT(*))は同値ではないです。
なぜかというと、SUM(x)関数はNULLを除外しますが、COUNT(*)はそうではないからです。同様に、AVG(DISTINCT x)とAVG(x), (SUM(DISTINCT x)/COUNT(*))も同値ではありません。SUM(DISTINCT x)は重複値とNULLを除外しますが、COUNT(*)はそうではないからです。
どうでしょうか?NULLがSQLで演算をする際に非常に厄介な存在であることを私は本書を読みながらしみじみと感じたのですが、みなさんにも伝わってきたでしょうか。
ダミー値でNULLは避けられるか
NULLは上記の通り、人間にとって直感的ではないものです。本書ではNULLはできるだけ避けるように、という方向性で話が進んでいきます。[4]
もちろん、セルコもNULLを使わざるを得ないケースではNULLを使うより仕方がないということは記述しています(たとえば日付、保険の失効日が不明のケースや、誰かの誕生日など)。
ここまで読みながら、そして実務で意図しない挙動をさせてしまった経験から、できるだけNULLは避けたいという考えは個人的にもかなり共感しました。
しかし、実務的な観点だと、NULLを避けようとしすぎることによって別の問題が発生することもありそうです。
例えばセルコは「人名にはNULLではなくダミー値を使うほうがよい」と記述しています。
読書会では、人名には基本的にはダミー値を使うべきではないのでは?という点が議論になりました。アプリケーションでの利用という観点では、たしかに以下のようなケースで課題がありそうです。
会員氏名がまだ登録されていない場合は「マイシンモンスター+(会員番号)」などと入れておくとします。この場合、アプリケーション側で参照する際、システムが入れたダミーの値なのか、そうでないかを判別するのが不可能になってしまいます。
では、「マイシンモンスター」がダミー値であることを十分に表さないのであれば「Dummy」としておけばダミーということが明確になるでしょうか。
この場合も、顧客が自分であえてDummyと登録したケースなのか本物のダミー値なのかが区別できないのでやはりこの問題は残ってしまいます。あえて登録したのであれば、その値は入力された値として正しく表示されているべきです。しかし、完全なダミー値は、顧客への表示の際は、「表示をそもそもしない」ほうが適当であることもあります。
みなさんも、メールを受け取ったとき、送り先に"名字 名前 さん"と書かれたまま送られてきた経験はないでしょうか?そのようなメールの送信元がDBにダミー値を入れているのか、カラムがnullのケースではアプリケーション側でプレースホルダーを入れるようにしているのかはわかりませんが、これはちょっとかっこ悪いですよね。
セルコが「人名にダミー値を使う」という話をするのは、先にしていた自然キーの議論と繋がっていると思われます。もし氏名を純粋な自然キーとして採用する場合、氏名がNULLのケースは存在できないことになるので登録時に必ずダミー値で埋めることが必要になります。
外部キーはNULLにもできる
当然ながら主キーはNULLにできないので、外部キーもNULLにできないものと思い込んでしまっていましたが、外部キーはNULLは禁止されていないようです[5]
セルコは外部キーについてもNULLを使うのは避けるべきとしていますが、「未知」のNULLが存在するケース、たとえば社員と仕事、そのマッピングテーブルの間に成り立つ関係についてはまだ割り振られていない仕事について担当社員はNULLとするのが適当だと思うかもしれない、との話も同時にしています。議論では、STIでは外部キーをNULLにするよね、という話も出ました。
5. (NOT……)INの書き換え
IN述語は、AND, OR, NOTなどを使って展開することができます。
しかし、サブクエリを伴うINの場合、書き換えによってインデックスを活用したほうがパフォーマンスがよくなるケースがあります。
プログラマにとってINを使うほうが意味的にわかりやすいとのことです。たしかに、下にあげた例などでも、IN述語を利用したほうが慣れてしまえば可読性は高いし意味的にも明確であると個人的には思います。
なので必ずしも書き換えなければならないわけでもないのですが、特にデータ量が大きいときには注意してみてもよいかもしれません。
以下はその一例になります。
結合演算で置き換える
--- レストランの電話番号から、ミシュランで三つ星をとったものだけを選択する
--- michelin_stars列にはインデックスがないだろうから、QualityGuideテーブルをシーケンシャルに検索するか、semi-joinで検索するかのどちらかになる
SELECT R.restaurant_name, R.phone_nbr
FROM Restaurants AS R
WHERE E.restaurant_name
IN (SELECT Q.restaurant_name
FROM QualityGuide AS Q
WHERE Q.michelin_stars = 3);
--- 結合で置き換えると、レストラン名のインデックスで高速に検索できる
SELECT R.restaurant_name, R.phone_nbr
FROM Restaurants AS R, QualityGuide AS Q
WHERE michelin_stars = 3
AND R.restaurant_name = Q.restaurant_name;
NOT INをNOT EXISTSで置き換える
--- 独自の名店リストから、ガイドにはない店を見つけたい
SELECT J.*
FROM JohnsBook AS J
WHERE J.restaurant_name
NOT IN (SELECT Q.restaurant_name
FROM QualityGuides AS Q);
--- NOT EXISTSを使うと結果が同じなうえに早い
SELECT J.*
FROM JohnsBook AS J
WHERE NOT EXISTS
(SELECT *
FROM QualityGuide AS Q
WHERE Q.restaurant_name = J.restaurant_name);
IN述語の中で使われるサブクエリがWHERE句を持っている場合に一時テーブルが作られる実装の場合にインデックスがないことでフルスキャンになってしまうことがパフォーマンスの違いの大きな要因のようです。
また、NOT INとNOT EXISTSに関しては、サブクエリにNULLが含まれるかどうかによって挙動が変わるので注意が必要です。
NOT EXISTSは、EXISTSがサブクエリがUNKNOWNの場合FALSEを返す関係上、すべてをTRUEに評価します。
一方、NOT INは、サブクエリにNULLが含まれている場合は1行も返しません。
SELECT *
FROM Table1
WHERE x NOT IN (SELECT x FROM Table2);
--- サブクエリを実行
SELECT *
FROM Table1
WHERE x NOT IN (1, NULL, 2);
--- NOT INを変換
SELECT *
FROM Table1
WHERE NOT (x IN (1, NULL, 2));
--- IN述語の展開
SELECT *
FROM Table1
WHERE NOT ((x = 1) OR (x = NULL) OR (x = 2));
--- ド・モルガンの法則
SELECT *
FROM Table1
WHERE ((x <> 1) AND (x <> NULL) AND (x <> 2));
--- 式を定数に評価する
SELECT *
FROM Table1
WHERE ((x <> 1 ) AND UNKNOWN AND (x <> 2));
--- ANDの演算を定数に評価
SELECT *
FROM Table1
WHERE UNKNOWN またはFALSE;
--- 結果は常に空である
おまけ ベンダー依存を防げるか?
ベンダー独自の機能に依存すべきでないということも本書では折に触れて語られています。たしかに、移行の可能性を考えた際やSQLを操作する人のキャッチアップという観点では、確かにベンダー機能への依存を避けることが理想的でしょう。
とはいえ、ベンダー依存を完全に排除することは現実問題としては困難と言えます。
たとえば中央値を求める関数は、以下のような状況になっています。
- Oracleには
MEDIAN()として実装されている - PostgreSQLでは
PERCENTILE_CONT(0.5)を使うことで比較的簡単に算出が可能 - MySQLの場合は、ウィンドウ関数などで自前で実装するか、ORDER BY + LIMITなどで対応するしかない
Oracleを採用しているのであれば、MEDIAN()を利用すれば自前で実装する必要はありません。利用したほうが可読性も高いですし、ベンダーの最適化の恩恵も受けやすいでしょう。
もう一つ、ストアドプロシージャやトリガーはベンダーによって実装差が大きい機能の一つです。[6]
実装依存が顕著な機能を実務上いくら使わないようにといっても使わざるをえないことはあるでしょう。ここでも、原則として依存性を下げることを理解しながら、既存の技術スタックを踏まえて判断していくことが重要と理解しました。
まとめ: 1年を振り返って
1年(以上?)かけて本書を読み切れたことは、大きな自信になりました。
一人では挫折してしまいそうな本も読書会というある種の強制力があることで読み切ることができました。また、困った時に相談できる相手がいるということはとても励みになりました。議論でほかの人の知見も聞くことができることで深い理解につなげることができました。
そして、この会を通じて、坂井さん(@sakaik)、ミックさん(@copinemickmack)の出版記念イベントにも参加するきっかけになったりと、貴重な機会を持つことができました。
せっかくこうしてデータベースについて勉強する期間を持てたので、次のステップとして、今年はデータベーススペシャリスト試験に挑戦してみたいです。
また、設計面にフォーカスした本やサイトでの勉強も取り入れて、知見を実践や業務に繋げていきたいです。
この記事を読んでくださった方で、ぜひDBの本を読んでみたい・読書会をやってみたいという方がいらっしゃれば嬉しいです。ぜひ、一緒に勉強しましょう!
MICIN ではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
-
このあたりの話も、ミックさんの『達人』に詳しいので、深掘りしたい方はぜひ読んでみてください ↩︎
-
3値論理の論理演算で、TRUEとUNKNOWNを比較するとUNKNOWN、UNKNOWNとFALSEを比較するとFALSEです。 ↩︎
-
ミックさんも、『NULL撲滅委員会』という興味深い記事を執筆しています。https://mickindex.sakura.ne.jp/database/db_getout_null.html ↩︎
-
読んでいる当時はこのことに驚きましたが最近はDB設計もしているので、まあそれはそうだよなと思うのですが…… ↩︎
-
セルコは極力使わないようにと太字で強調しています. ↩︎
Discussion