【輪読会振り返り】「A Philosophy of Software Design」と「失敗から学ぶRDBの正しい歩き方」

# はじめに

こちらはアドベントカレンダー25日目の記事です。ついにラストです！（僕の19日目の記事はどこに行ってしまったのでしょうか）

https://qiita.com/advent-calendar/2024/ctoa-24

先日、輪読会でJohn Ousterhout著の「A Philosophy of Software Design」と[@soudai1025](https://x.com/soudai1025)さん著の「失敗から学ぶRDBの正しい歩き方」について輪読会をしました。その際の資料があったので雑にGPT4oにブログ化してもらいました。

ちなみに輪読会自体についてのブログも書いてるので良ければクリスマスプレゼントだと思ってはてブ押していただけると嬉しいです。

https://product.plex.co.jp/entry/circle-reading

# 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つのアプローチを挙げています：

1. **コードをシンプルかつ明確にする**
2. **コードをカプセル化する（モジュラーデザイン）**

これらを組み合わせることで、システム全体の設計を簡潔で保守しやすいものにできます。

## 複雑性の本質

複雑性とは、システムの理解や修正を困難にする要素のことを指します。システム全体の複雑さは、各部分の複雑さとその使用頻度に依存します。そのため、複雑性を見えない場所に隔離する「カプセル化」が有効です。

> "Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system."

複雑性によって生じる3つの症状：
1. **変更の増幅**：単純な変更でも多くの箇所の修正を要する
2. **認知負荷**：開発者がタスクを完了するために知っておくべき情報量が多い
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."

情報隠蔽の実践例として、デザイン決定をモジュール内に閉じ込め、他の部分に影響を与えない設計が挙げられます。一方で、複数のモジュールに同じデザイン決定が散らばる「情報漏洩」は避けるべきです。

:::note info
「深いモジュール」と「情報隠蔽」について[@kohii00](https://x.com/kohii00)さんの記事が大変有益なので一読してみることをお勧めします。

https://zenn.dev/kohii/articles/2c1126389d914a
:::

# 失敗から学ぶRDBの正しい歩き方

## JOINの特性

### 代表的なINNER JOIN

```sql
SELECT
    会員.名前 AS 会員名,
    都道府県.名前 AS 出身県
FROM
    会員
    INNER JOIN 都道府県
      ON 会員.出身県id = 都道府県.県id;
```

### その他のJOIN

Webアプリケーションでは、主体が明確なデータを表示する場合が多いため、`LEFT OUTER JOIN`で十分なことが多いです。集計機能などでは、`FULL OUTER JOIN`が必要になるケースもあります。

## JOINのアルゴリズム(NLJ)

- **Nested Loop Join (NLJ)**
  駆動表（外部表）をループの外側に配置し、効率的に結合を行います

以下は疑似コードによるイメージです。

```text
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が効かなくなることがあります。

```sql
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%'`) は利用不可

### 統計情報の影響

統計情報が実データと乖離している場合、異なる実行計画が選択されます。データ追加後のサンプリングや固定化で改善を図ります。

## 知らないロック
### ロックの基本
:::note info
ロックやデットロックについては[@tekihei2317_](https://x.com/tekihei2317_)さんの記事が大変有益でした。

https://zenn.dev/gibjapan/articles/1d8dfb7520dabc
:::

### ギャップロックやネクストキーロック

- **ギャップロック**や**ネクストキーロック**によるデッドロックの例:

```sql
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の動作例

1. トランザクションAがデータを更新中でも、トランザクションBはトランザクションAの変更前のデータを参照できます
2. コミットが行われると、変更内容が可視化されます

#### MVCCとロックの関係

- MVCCを使用すると、読み取り処理に対するロックを削減でき、並列性が向上します
- 書き込み処理には引き続きロックが必要です

## ロックの功罪

### トランザクション分離レベル

- **Atomicity (原子性)**: すべてのタスクが完全に実行されることを保証
- **Consistency (一貫性)**: データの整合性を満たす
- **Isolation (分離性)**: 他の操作から独立
- **Durability (永続性)**: 完了通知後の結果が失われない

### 分離レベルと問題発生のリスク
それぞれの現象を深ぼっても良いですが、ここでは割愛します。

| レベル              | ダーティリード | ファジーリード | ファントムリード | ロストアップデート |
| ---------------- | ------- | ------- | -------- | --------- |
| Read Uncommitted | 発生      | 発生      | 発生       | 発生        |
| Read Committed   | 発生しない   | 発生      | 発生       | 発生        |
| Repeatable Read  | 発生しない   | 発生しない   | 発生       | 発生        |
| Serializable     | 発生しない   | 発生しない   | 発生しない    | 発生しない     |

# 最後に
輪読会について振り返っていきました。特に「A Philosophy of Software Design」は今後のキャリアにおいてもバイブルとして読み返していきたいですね。

本日でアドベントカレンダーが最後なのですが、当初の予定よりもカレンダーが埋まって本当に嬉しかったです。CTO協会24新卒のメンバーありがとうございました！