はじめに
こちらはアドベントカレンダー25日目の記事です。ついにラストです!(僕の19日目の記事はどこに行ってしまったのでしょうか)
先日、輪読会でJohn Ousterhout著の「A Philosophy of Software Design」と@soudai1025さん著の「失敗から学ぶRDBの正しい歩き方」について輪読会をしました。その際の資料があったので雑にGPT4oにブログ化してもらいました。
ちなみに輪読会自体についてのブログも書いてるので良ければクリスマスプレゼントだと思ってはてブ押していただけると嬉しいです。
A Philosophy of Software Design
シンプルな設計の重要性
本書のテーマの一つは、「シンプルな設計が複雑性を抑え、長期的な変更容易性を高める」という点です。シンプルな設計はソフトウェアの複雑性を爆発させる前に、より大規模で強力なシステムの構築を可能にします。
"If we want to make it easier to write software, so that we can build more powerful systems more cheaply, we must find ways to make software simpler."
変更容易性のアプローチ
本書では、複雑性を抑えるために以下の2つのアプローチを挙げています:
- コードをシンプルかつ明確にする
- コードをカプセル化する(モジュラーデザイン)
これらを組み合わせることで、システム全体の設計を簡潔で保守しやすいものにできます。
複雑性の本質
複雑性とは、システムの理解や修正を困難にする要素のことを指します。システム全体の複雑さは、各部分の複雑さとその使用頻度に依存します。そのため、複雑性を見えない場所に隔離する「カプセル化」が有効です。
"Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system."
複雑性によって生じる3つの症状:
- 変更の増幅:単純な変更でも多くの箇所の修正を要する
- 認知負荷:開発者がタスクを完了するために知っておくべき情報量が多い
- 未知の未知:変更に必要な箇所や情報が明確でない
良い設計と戦略的プログラミング
本書では、短期的な成果を求める「戦術プログラミング」の危険性を指摘しています。代わりに、長期的な視点で複雑性を最小化する「戦略プログラミング」を推奨しています。
"The first step towards becoming a good software designer is to realize that working code isn’t enough."
私たちも短期的なタスク達成だけでなく、長期的な視点でコードの保守性を意識する必要があると改めて感じました。
モジュール設計と情報隠蔽
モジュラーデザインでは、モジュールのインターフェースと実装を分離し、依存関係を管理します。良いモジュールは、強力な機能を持ちながらシンプルなインターフェースを提供する「深いモジュール」であるべきです。
"The best modules are deep: they have a lot of functionality hidden behind a simple interface."
情報隠蔽の実践例として、デザイン決定をモジュール内に閉じ込め、他の部分に影響を与えない設計が挙げられます。一方で、複数のモジュールに同じデザイン決定が散らばる「情報漏洩」は避けるべきです。
「深いモジュール」と「情報隠蔽」について@kohii00さんの記事が大変有益なので一読してみることをお勧めします。
失敗から学ぶRDBの正しい歩き方
JOINの特性
代表的なINNER JOIN
SELECT
会員.名前 AS 会員名,
都道府県.名前 AS 出身県
FROM
会員
INNER JOIN 都道府県
ON 会員.出身県id = 都道府県.県id;
その他のJOIN
Webアプリケーションでは、主体が明確なデータを表示する場合が多いため、LEFT OUTER JOIN
で十分なことが多いです。集計機能などでは、FULL OUTER JOIN
が必要になるケースもあります。
JOINのアルゴリズム(NLJ)
-
Nested Loop Join (NLJ)
駆動表(外部表)をループの外側に配置し、効率的に結合を行います
以下は疑似コードによるイメージです。
for each row in t1 matching where condition {
for each row in t2 matching join and where condition {
send joined row to client;
}
}
NLJの特徴
- 内部表の結合キーにINDEXがある場合: ループ数が減少し、外部表が小さいほど高速です
- 内部表の結合キーが一意の場合: 対象レコードを絞り込むことでさらに高速化します
- INNER JOINの駆動表: オプティマイザによって決定されます
INDEXが突然利用されなくなるケース
同じクエリでも、WHERE句で指定する値や環境によってINDEXが効かなくなることがあります。
CREATE TABLE `users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`gender` tinyint(1) NOT NULL COMMENT '1 = 男性 , 2 = 女性',
`age` int(11) NOT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `index_name` (`name`),
KEY `index_age` (`age`),
KEY `index_gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
設定したINDEXが効かないケース
- 検索結果が多い、または全体の件数が少ない
- WHERE句やJOINのON句で指定していない列
- カーディナリティが低い列
- 曖昧な検索(部分一致など)
- 統計情報と実データの乖離
曖昧な検索とINDEX
-
前方一致 (
WHERE LIKE 'TEST%'
) はINDEXを利用可能 -
後方一致 (
WHERE LIKE '%TEST'
) や部分一致 (WHERE LIKE '%TEST%'
) は利用不可
統計情報の影響
統計情報が実データと乖離している場合、異なる実行計画が選択されます。データ追加後のサンプリングや固定化で改善を図ります。
知らないロック
ロックの基本
ロックやデットロックについては@tekihei2317_さんの記事が大変有益でした。
ギャップロックやネクストキーロック
- ギャップロックやネクストキーロックによるデッドロックの例:
mysql> CREATE TABLE demo2 (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
num bigint(20) unsigned NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
mysql> INSERT INTO demo2 (id, num) VALUES (1, 10), (2, 20), (3, 30), (4, 40);
-- トランザクションA
mysql> BEGIN;
mysql> DELETE FROM demo2 WHERE id= 8;
-- トランザクションB
mysql> BEGIN;
mysql> DELETE FROM demo2 WHERE id = 9;
-- トランザクションA
mysql> INSERT INTO demo2 VALUES (8, 80);
-- トランザクションB
mysql> INSERT INTO demo2 VALUES (9, 90);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction;
MVCC (Multi-Version Concurrency Control)
MVCCは、トランザクションが同時に実行されても整合性を保つための仕組みです。各トランザクションがデータのスナップショットを利用することで、以下を実現します:
- 非ブロッキング読み取り: 他のトランザクションの書き込みを待たずにデータを読み取ることができます
- 過去データの参照: 一貫性のあるビューを保持するために、トランザクション開始時点のデータを利用します
MVCCの動作例
- トランザクションAがデータを更新中でも、トランザクションBはトランザクションAの変更前のデータを参照できます
- コミットが行われると、変更内容が可視化されます
MVCCとロックの関係
- MVCCを使用すると、読み取り処理に対するロックを削減でき、並列性が向上します
- 書き込み処理には引き続きロックが必要です
ロックの功罪
トランザクション分離レベル
- Atomicity (原子性): すべてのタスクが完全に実行されることを保証
- Consistency (一貫性): データの整合性を満たす
- Isolation (分離性): 他の操作から独立
- Durability (永続性): 完了通知後の結果が失われない
分離レベルと問題発生のリスク
それぞれの現象を深ぼっても良いですが、ここでは割愛します。
レベル | ダーティリード | ファジーリード | ファントムリード | ロストアップデート |
---|---|---|---|---|
Read Uncommitted | 発生 | 発生 | 発生 | 発生 |
Read Committed | 発生しない | 発生 | 発生 | 発生 |
Repeatable Read | 発生しない | 発生しない | 発生 | 発生 |
Serializable | 発生しない | 発生しない | 発生しない | 発生しない |
最後に
輪読会について振り返っていきました。特に「A Philosophy of Software Design」は今後のキャリアにおいてもバイブルとして読み返していきたいですね。
本日でアドベントカレンダーが最後なのですが、当初の予定よりもカレンダーが埋まって本当に嬉しかったです。CTO協会24新卒のメンバーありがとうございました!