Software Design 202406
Bun
Node.js は以下の問題を抱えていた。
package.json
やnode_modules
などによるパッケージ管理の複雑さ- CJS/ESM の混在による複雑さ
- TypeScript の非サポート
これらを解消すべく Deno が生み出された。
package.json
やnode_modules
、CJS 廃止- テストランナー・バンドラーの同梱
- TS の標準サポート
しかし、Deno は Node.js との互換性がなく、移行はあまり進んでいない。これを踏まえ、Bun は Node.js と Deno の中間的な特徴を持って誕生した。
- パッケージマネージャー、テストランナー、バンドラー、TS トランスパイラをすべて同梱しており追加作業が不要
- npm/yarn/jest/webpack/esbuild/babel などがすべて不要
- とにかく速い
- Node.js/Jest と互換性を持つ
- ドロップインリプレイスメントになることを目指している
Bun CLI というツールが含まれており、この CLI を使って様々な作業を行う。
bun run <file path>
ファイルを実行bun run <script name>
package.json スクリプトを実行bun x <package name
npx の代わり。bunx
でもよい。bun exec "echo hello"
シェルスクリプトを実行bun install
パッケージのインストールbun add <package name>
パッケージの追加bun remove <package name>
パッケージの削除bun update
semver に沿って最新化bun link/unlink
外部パッケージのリンク/アンリンク- yarn link と同じ
- dependencies に
"pkg_a": "link:pkg_a"
のように書かれる
bun pm
パッケージマネージャーの使い方bun pm bin (-g)
bin フォルダのパスを表示bun pm ls
パッケージを一覧表示bun pm cache (rm)
キャッシュフォルダを表示/削除bun test
テストを実行するbun test -t <filename>
特定の名前を含むテストファイルを実行bun test <filepath>
特定のテストファイルを実行bun build ./index.ts
バンドルして JS ファイルとして出力する(ベータ版)bun compile ./index.ts
実行可能バイナリとしてコンパイルする
実行計画インデックスの仕組み
RDBMS と SQL の基礎概念
リレーショナルモデルとは
リレーショナルモデル(関係モデル)とは、データモデルの一種である。
データモデルとは、データの表現方法と、演算の定義の組み合わせからなる。
リレーショナルモデルが扱うデータの単位はリレーション/関係と呼ばれる。特別な構造をもった、数学的な意味での「集合」の一種であり、順序も重複もない。リレーショナルモデルでは、このリレーションを値として演算を行う。
つまり、リレーショナルモデルとは、リレーションという集合がどのようなものであるか、そしてそれに対する演算はどのようなものかを定義したものである。
リレーションの構造は以下のようになっている。
- リレーション
- ヘッダ/見出しと、ボディ/本体からなる。
- 単に「リレーション」といった場合は、一般的に「ボディの全部」を指す。
- ヘッダ
- 属性の集合である。
- 属性は名前とデータ型からなる。
- データ型もまた集合である。
- ボディ
- タプル/組の集合である。
- タプルは属性値の集合である。
- 含まれる属性値はヘッダに定義されているため、全てのタプルは同じ構造を持つ。
※以下は便宜上テーブルとして表現しているものの、リレーションは集合であるため、順序はない。本来はベン図などで表すべきである。
学生 ID | 名前 | 年齢 | 学年 |
---|---|---|---|
1 | 田中太郎 | 20 | 2 年 |
2 | 鈴木花子 | 19 | 1 年 |
- 属性: 学生 ID(int), 名前(string), 年齢(int), 学年(int)
- 属性値: 「田中太郎」, 「20」など
- ヘッダ: 属性の集合
- タプル: 属性値の集合(田中太郎さんのデータ、鈴木花子さんのデータなど)
- ボディ: タプルの集合
リレーショナルモデルの演算
演算は、1 つ又は複数のリレーションを入力として、1 つのリレーションを出力するものである。これにより、リレーションの演算はその結果をまた別の演算の入力として利用することができる。以下のような種類がある。
射影 / Project
ヘッダから特定の属性のみを取り出し、タプルの属性を絞り込む演算。
重複は取り除かれる(集合には重複は存在しないため)。
e.g. 従業員のモデルから職位を重複なく抽出する
制限 / Restrict
特定の条件を持つタプルだけをリレーションから選出する演算。
e.g. 従業員のモデルから入社日が 2018 年以降の社員を選出する
和 / Union
集合和、つまり足し合わせたものを求める演算。同じヘッダを持つリレーション同士でしか行えない演算。重複は取り除かれる。
積 / Intersect
集合積、つまり共通部分を求める演算。同じヘッダを持つリレーション同士でしか行えない演算。重複は取り除かれる。
結合 / Join
SQL にはいろいろな結合があるが、リレーショナルモデルにおける結合は自然結合の一種類しかない。
自然結合は 2 つのリレーションを入力とし、まずは 2 つのリレーションに共通して存在する属性を見つけ、それらの属性の値が等しいタプル同士を見つけて合体させて新たなタプルとしていき、最終的にそのタプルの集合を出力する演算。
ヘッダが全く同じ 2 つのリレーションを入力とする場合、結果は積と同じになる。積は結合の特殊パターンといえる。
ヘッダに共通部分が全くない場合、結果は直積/Product となり、総当たり(タプルの総数 x タプルの総数)の組み合わせとなる。
SQL とリレーショナルモデルの関係性
SQL においてリレーションの概念を体現している要素は以下の通り。
- テーブル: リレーション
- カラム: 属性
- レコード: タプル
SQL の演算の入力はテーブルであり、出力もまたテーブルに相当するものであるという点が非常に重要である。
SQL の演算とリレーショナルモデルでの演算は以下のように対応する。実は SELECT は 3 つの演算を同時に行っているのである。
SELECT employee_name -- 射影
FROM employees -- 結合 (ここではJOINはしていないけど)
WHERE hire_date > '2018-01-01'; -- 制限
和は UNION で、積は JOIN で代用できる。
テーブルは以下の特徴もち、つまりリレーションではないため、演算の用語はリレーショナルモデルと SQL では異なっている。
- 行に順序がある
- 行が重複できる
- NULL を持てる
逆に、これらの特徴を可能な限りテーブルから取り除いておけば、リレーショナルモデルの利点を最大限引き出すことができる。たとえば、宣言的にクエリを記述することが可能になる。とはいえデータがリレーショナルモデルの範疇に収まるかどうかは、データの性質によるため、見極めが大事である。
リレーショナルモデルとインデックス
リレーショナルモデルとインデックスの親和性は低い。
そもそもインデックスや、インデックスを使って実装されている主キーというものは、リレーショナルモデルには存在しない。インデックスは RDBMS のベンダーごとに異なる実装の話である。キーも、リレーショナルモデルに存在するのは「候補キー」という「タプルを一意に特定するための属性の組み合わせのうち、属性の数が最小になるもの」が定義されているのみである。
SQL にあってリレーショナルモデルにないもの
- インデックス
- 詳細前述
- 更新
- リレーションは値であり、更新できない。1 を別の値にするなんでできないでしょ?
- トランザクション
- 更新がないのでトランザクションもない
- クエリ以外の SQL コマンド
- GRANT, REVOKE, BEGIN, COMMIT,,,
- NULL
- 未知の値は集合の要素になることはできない
- ソート
- 集合に順序はない
SQL 実行の仕組みと実行計画
SQL 実行の流れ
- パーサーが SQL 文を解析し、AST(抽象構文木)を作成する。
- オプティマイザが AST を解析し、実行計画を作成してエグゼキュータに渡す。
- コストベースオプティマイザを使用して最適化を行う。
- 同じ結果を生み出す複数の AST をヒューリスティックに洗い出し、その中から最もコストが低いものを選ぶ。
- 見積もりが常に正確であるわけではない。
- ルールベースオプティマイザも存在するが、これは過去の遺物である。
- 実行計画はクエリを効率的に実行するためのステップを具体化したもので、AST の形式で表現される。
- コストベースオプティマイザを使用して最適化を行う。
- エグゼキュータが実行計画に基づいてテーブルからデータを読み取り、必要な加工を行った後、結果をクライアントに返す。
EXPLAIN コマンド
- id
- 結合(JOIN)に対して振られる番号
- MySQL ではクエリの実行単位は結合である
- 1 つの FROM 句でいくつ JOIN しようと、1 つの結合としてカウントされる
- select_type
- そのテーブルへのアクセスがなぜ必要だったかを示す
- 単純な JOIN なら SIMPLE になる。そのほか、PRIMARY, SUBQUERY, DERIVED, UNION などがある。
- id と select_type を見ていけば、全体の構造がわかる。苦手なら TREE 形式での出力にするとよい。
- table
- どのテーブルへのアクセスか
- テーブル名よりもエイリアス名が優先して表示される
- partitions
- アクセスするパーティション
- パーティショニングが有効になっている場合にのみ意味をもつ項目
- パーティションの刈り込み(Partition pruning)が行われているかどうかを確認できる
- 必要なパーティションだけをアクセスし、不要なパーティションを無視することでクエリの効率を高める技術
- type
- テーブルアクセスの方法
- Join type や Access type と呼ばれる
- 効率が良いかどうかが一目瞭然でわかる
const
,eq
,eq_ref
あたりは効率よしALL
,index
あたりは効率悪しindex
はインデックス全体がスキャンされるという意味であり、語感に反して効率は悪いので注意
- possible_keys
- インデックスの候補一覧
- key
- 実際に使われたインデックス
- key_len
- 実際に使われたインデックスのサイズをバイトで表したもの(bigint なキーなら 8 バイトなど)
- ref
- インデックスを使うときに、なんの値で検索されるかを示す
- インデックスが使用されない場合は空欄になる
- リテラル値の場合は
const
、 別のテーブルのカラムの場合はtable_name.col_name
の形式で表示される。
- rows
- 駆動表(ひとつ前のステップで抽出されたテーブルにフィルタを適用したあと状態)の 1 行につき、どれだけの行が返されるかの見積もり
- オプティマイザが見誤りやすい項目である
- filtered
- WHERE 句などによりフィルタリングが発生した場合に、残ったデータの割合
- 最大値は 100 で、これはフィルタリングが行われなかった(全て必要なデータだった)ことを表す
- 最小値は 0 で、これはフィルタリングにより全てのデータが除外された(全て不要なデータだった)ことを表す
rows * (filtered / 100)
が、次のステップでテーブルと結合されるデータの件数となる。- 前のステップで意味のあるデータ群に絞り込めているかという指標とも言える。
- Extra
- その他ヒントになりそうな情報など
- よくあるのは
Using where
で、これは「ここで Where 句が実行されましたよ」という意味- Where 句の実行はフルテーブルスキャンなどを発生させる可能性があるので、type と合わせてよく見ておくとよい
コストの把握
コストの把握にはなんといっても行数を見るのが大事。
例えば、t1,t2,t3,t4 の 4 つのデーブルを順に取得し、フィルタ後に残った件数(rows * (filtered / 100)
)を t1_filtered, t2_filtered, t3_filtered, t4_filtered とすると、コストは以下のように計算できる。
t1 +
t1_filtered * t2 +
t1_filtered * t2_filtered * t3 +
t1_filtered * t2_filtered * t3_filtered * t4
これを見ると、後になるほどコストがかかることがわかる。コスト改善のポイントは以下の 2 つ。
- 内部表の抽出作業がどれだけ効率的に行われるか
- そのステップで抽出するテーブル(前述の t1, t2, t3, t4)が内部表である。
- 前のステップから与えられるテーブル or 次のステップに与えるテーブルが駆動表である。
- インデックスを使って効率的にアクセスすることが重要
- filtered の値は内部表の抽出のパフォーマンスと関係がないので注意
- フィルタリングはテーブルから行を取得したあとに行われる
- このため、いくらフィルタされたとしても自身のテーブルへのアクセスにおいてはパフォーマンスとは関係がない
- あくまで後段の処理のパフォーマンス(≒ 駆動表の件数)に影響する
- 駆動表においてどれだけ行数を絞り込めるか
- 前述の
t*_filterd
の部分 - ここではじめて filtered の値が意味を持つ
- 前述の
表形式以外の EXPLAIN
- TREE 形式
EXPLAIN FORMAT=TREE
とすると、テーブル形式ではなくツリー形式で説明を出力できる- コストの値が表示される。特に単位はなく、あくまでも抽象的・相対的な値である点に注意。
- EXPLAIN ANALYZE
- 実際の時間を計測する
- TREE 形式の上位互換
インデックスの機能とアルゴリズム
B+ツリーインデックスの基本
インデックスにはいろいろ種類がある。MySQL の InnoDB では、B+ツリーインデックス(B ツリーインデックスの亜種)、空間インデックス、フルテキストインデックスの 3 種類が利用できる。B+ツリーインデックスは等価比較、範囲検索、スキャンで利用でき、汎用性が高い。
B+ツリーインデックスは木構造を持つ。
- ルートノード: それ以上親を持たないノード
- リーフノード: それ以上子を持たないノード。実際のデータはすべてここに保持される。
- 内部ノード(ノンリーフノード): ルートノードでもリーフノードでもないノード。ポインタとして機能する。
B+ツリーの高さは比較的低く抑えられており、ルートノードから順にノードをたどっていくことで効率的にデータを検索できる。bigint な主キーを持つ 1 億行のレコードなら、本来 19 万ページを探索しなければならないところ、たった 3 階層の探索ですむ。キーが 100 バイトなら 4 階層、1K バイトなら 7 階層ですむ。
クラスタードインデックス
InnoDB が採用しているテーブルの構造の名前。テーブルのデータがインデックス自体の順序に従って物理的に並べられる特徴を持つ。他の製品では索引構成表(Index Organized Table)という。範囲クエリや連続したデータの取得が高速という特徴がある。
インデックスのキーには以下が上から順に優先して採用される。
- 主キー
- ユニークな非 NULL 列
- 自動生成された内部的な 6 バイトの ROW ID
マルチカラムインデックス
マルチカラムインデックスにおいては、インデックスの列の並び順を意識することが非常に重要。
マルチカラムインデックスは、より左にある列を優先して昇順ソートして行として並べた状態で、内部的に一つの B+ツリーとして保持されている。
このため、左にある列の範囲を指定せずにそれ以降の列に対してのみクエリを行うと、フルスキャンになる。探しようがないから。必ず左から順に条件を指定すること。
場合によっては、カラムのセットは同じだけど、並び順だけが異なるインデックスを作成することも検討すること。
DI / 依存性の注入
DI とは、あるコンポーネントがほかのコンポーネントを使うときに、そのインスタンスを自ら生成するのではなく外部から受け取ることで、コンポーネント間の結合を疎結合にする設計手法である。
DI のメリットは、共通処理を切り出しやすくなる点と、オブジェクトの生成と使用を分離できる(OOP の原則)ことで保守やテストが容易になる点である。
以下のように、使用する側のコードが直接依存オブジェクトを注入する場合と、DI コンテナやフレームワークがその役割を担う場合がある。
- コンストラクタインジェクション(推奨)
- コンストラクタのパラメータを使ってセットする方法
- 依存関係が必須であることが明示されて良い一方、コンストラクタがごちゃつきがち
- メソッドインジェクション
- メソッドの引数を使ってセットする方法
- 柔軟性がある一方、管理が複雑になる面も
- フィールドインジェクション
- アノテーションなどを使ってフィールドに依存関係を自動でセットする方法
- コードが簡潔で読みやすい一方、テストが困難になる場合も
- セッターインジェクション
- セッターを使ってセットする方法
- 依存がオプショナルである場合に適している
- 依存が設定されていない状態でオブジェクトが使用される可能性があるため注意
関連用語
Service Locator パターンという似た仕組みもあるが、再利用性が低いので、DI のほうが優れている。
DI コンテナは、設定ファイルやアノテーションに基づき、オブジェクトの生成と依存オブジェクトの注入を自動的に行う便利なしくみのこと。
Inversion of Control (IoC) は、プログラムの制御の流れを反転させる原則のこと。アプリケーションがライブラリやフレームワークを呼ぶのではなく、逆になること。たとえば、GUI ではフレームワーク側にイベントループがあり、必要に応じてアプリケーション側のコードを呼び出す。DI はその原則を実現するための具体的な技術である(ただし同じものではないので注意)。多くの責務をフレームワーク側に任せることで、開発者はより重要な部分に集中できるメリットがある。
Dependency Inversion Principle / DIP / 依存性逆転の原則は、高レベルのモジュールは低レベルのモジュールに依存すべきではなく、どちらも抽象に依存すべきであるという原則である。DI はその原則を実現するための具体的な技術である。高レベルモジュールにおいて「俺がほしいのはこれだ」と主導権を握ることで、技術の詳細に振り回されるのを防ぎ、保守や変更を容易にすることができる。
所感: 要は自身の関心ではない部分のコードを外部から受け取ることができればいいだけの話である。JavaScript の世界だと、コールバック関数なりカリー化なりを使うことでもっとシンプルに問題を解決できる気がする。
Cloudflare Workers / Rust
Worksers では JavaScript や Wasm(Rust)を動かすことができる。axum という人気の web フレームワークも利用可能。
ドメイン解体新書 / DNS への攻撃とセキュリティ
DNS への攻撃には 2 種類ある。
まずはドメインを偽装するタイプ。DNS キャッシュポイズニングとは、DNS キャッシュに偽の情報を入れ込む攻撃のこと。
つぎにドメインを利用不可能にするタイプ(サービス拒否型)。DNS 水責め攻撃とは、DNS サーバーに対して大量のリクエストを送りつける攻撃のこと。DDoS 攻撃とは、大量のリクエストを送りつける攻撃のこと。いろんな種類がある。
サービス拒否型の攻撃は近年増えている。
DNS を守る技術
SSL/TLS(https 通信)と PKI(証明書)があれば、DNS キャッシュポイズニングされたとしても偽装は失敗する。
DNS over (HTTPS|TSL|QUIC)があれば、クライアントとキャッシュサーバ間の暗号化と改ざん防止ができる。
DNSSEC は名前解決応答の真正性を担保するための仕組み。これがあれば、キャッシュサーバと(ルートサーバ|TLD 権威サーバ|権威サーバ)間の通信において、情報が真正であることを担保できる。しかし諸々のハードルがあり、利用率は低迷している。
データベースリファクタリング / 意図を忘れ去られたテーブル
いらなくなったテーブルはすぐ消せ。認知不可が高まるし変な使い方をするやつも出てくるから。すぐに消せない場合は以下を試みよ。
- テーブルやカラムのコメントに削除予定日をあらかじめ書いて周知しておく
- 統計情報を見て未利用であることを確認しておく
- 削除の前段階として、まずはリネームしておく
- そのテーブルだけをバックアップしておき、いざというとき戻せるようにしておく