データベース
データベースの基礎、データ保存パターン、そして一般的なバックエンド用データベース選択肢のトレードオフを探る。
データベースが存在する理由
アプリケーションには、1回のプログラム実行を超えて永続するデータを、信頼できる方法で保存・取得する仕組みが必要です。
アプリ
データベース
メモリ内に作成されたデータ(一時的)
- データベースは永続的な保存を提供し、再起動や障害が発生してもデータが失われません。
- 必要なデータを素早く取得できるように、効率的なクエリを可能にします。
- 整合性を保証し、複数のユーザーやサービスからの同時アクセスをサポートします。
詳細
実際のシステムでは、アプリケーションはユーザーアカウント、トランザクション、メッセージ、ログなどのデータを常に生成し、それらに依存しています。このデータをアプリケーションのメモリに直接保存するだけでは不十分です。メモリは一時的なものであり、プログラムが停止したりクラッシュしたりすると消えてしまうからです。
データベースは、永続的な保存を提供することでこの問題を解決します。データは、再起動やシステム障害が起きても残るようにディスクへ書き込まれます。これにより、銀行システム、SNS、ECサイトのようなシステムが長期的な状態を維持できます。
保存だけでなく、データベースはデータへ構造化された方法でアクセスする手段も提供します。ファイルを手作業で順番に確認する代わりに、開発者はクエリを使って必要なデータだけを取得できます。たとえば、メールアドレスでユーザーを取得したり、過去1週間の注文をすべて取得したりできます。
データベースは整合性ルールも強制し、複数の操作が同時に発生してもデータが有効なままであることを保証します。これがなければ、同時アクセスによってシステムは簡単にデータを破損してしまいます。
高いレベルでは、データベースはシステムの記録源として機能します。アプリケーションはリクエストを処理しますが、そのリクエストが依存するデータの保存と管理はデータベースの責任です。
SQLデータベース
SQLデータベースは、定義されたスキーマとリレーションを持つ構造化されたテーブルにデータを整理し、信頼性が高く一貫したデータ管理を可能にします。
構造化されたテーブルはデータを行と列に整理する
- データは行と列を持つテーブルに保存され、構造化された形式を形成します。
- スキーマは、データをどのように保存すべきかを正確に定義することで、一貫性を強制します。
- リレーショナルクエリにより、複数のテーブルにまたがるデータを組み合わせることができます。
詳細
SQLデータベースはリレーショナルモデルに従っており、データはテーブルに整理されます。各テーブルはエンティティ(たとえばユーザーや注文)を表し、各行はそのエンティティ内の1件のレコードを表します。
重要な特徴の1つがスキーマです。データを保存する前に、列の型や制約を含む構造を定義します。これにより、すべてのデータが一貫した形式に従うことが保証され、エラーが減り、整合性が維持されます。
テーブル間の関係はSQLシステムの中心です。たとえば、user_id を通じて user テーブルを orders テーブルに関連付けることで、システムは関連するデータを効率的に結び付けることができます。
SQLデータベースは強力なクエリ機能もサポートしています。join、filter、aggregation を使うことで、開発者は複数のテーブルにまたがる複雑なデータセットを1つのクエリで取得できます。
この構造化されたアプローチは、高い一貫性と信頼性を提供します。そのため、PostgreSQL や MySQL のようなSQLデータベースは、正確性とデータ整合性が重要なシステムで広く使われています。
NoSQLデータベース
NoSQLデータベースは、固定されたリレーショナル構造の代わりにさまざまなデータモデルを許可することで、柔軟性とスケーラビリティを重視します。
柔軟なスキーマ — レコードは同一の構造である必要はない
さまざまなモデル — テーブルに限定されない
直接検索 — シンプルな key → value アクセス
水平スケーリング — データを複数のマシンに分散する
- NoSQLデータベースは柔軟なスキーマを使用し、厳密な構造なしでデータを進化させることができます。
- document、key-value、wide-column、graph など、複数のデータモデルをサポートします。
- 高いスケーラビリティと、大規模で分散されたデータセットの処理を目的として設計されています。
詳細
SQLデータベースとは異なり、NoSQLシステムでは事前定義されたスキーマは必要ありません。つまり、各レコードが異なる構造を持つことができ、データが非構造化であったり頻繁に変化したりする場合に便利です。
NoSQLデータベースは、それぞれ異なるユースケースに最適化されています。MongoDB のような document データベースは JSON に似たオブジェクトを保存し、Redis のような key-value ストアは非常に高速な検索を提供し、Cassandra のような wide-column データベースは大規模な分散データセットを扱い、graph データベースは関係性を直接モデル化します。
この柔軟性により、NoSQLシステムは多くのサーバーにわたって水平スケーリングしやすくなり、高トラフィックを処理する大規模アプリケーションにとって重要です。
その代わり、多くのNoSQLシステムは、性能とスケーラビリティを優先するために、厳密な整合性や複雑なリレーショナルクエリを犠牲にします。SQL と NoSQL のどちらを選ぶかは、データの構造とシステム要件によって決まります。
インデックス
インデックスは、データベースがテーブル全体をスキャンせずに行を見つけられるようにすることで、データ取得を高速化します。
- インデックスがない場合、データベースはすべての行をスキャンします(フルテーブルスキャン)。
- インデックスがある場合、データベースは高速な検索を行って一致するデータを見つけます。
- インデックスは読み取り性能を向上させますが、書き込みとストレージにオーバーヘッドを追加します。
詳細
クエリがインデックスなしで実行されると、データベースは一致する結果を見つけるために各行を1つずつ確認しなければなりません。これはフルテーブルスキャンと呼ばれ、データセットが大きくなるほど非常に遅くなります。
インデックスは、(本の索引のような)検索用の構造として機能します。すべてをスキャンする代わりに、データベースはデータの場所へ直接移動できます。これにより、特に大きなテーブルではクエリ時間が大幅に短縮されます。
たとえば:
CREATE INDEX idx_users_email ON users(email);
これは email カラムにインデックスを作成し、「email でユーザーを検索する」といったクエリをはるかに高速に実行できるようにします。
ただし、インデックスは無料ではありません。データが挿入、更新、削除されるたびに、インデックスも更新する必要があります。そのため書き込みは少し遅くなり、ストレージ使用量も増えます。
効果的なデータベース設計では、どのカラムにインデックスを付けるかを適切に選ぶことが重要です。通常は、検索条件、フィルター、または結合で頻繁に使われるカラムが対象になります。
トランザクション
トランザクションは複数の操作を1つの単位にまとめ、すべての変更が成功するか、または何も適用されないようにします。
- トランザクションは、関連する複数の操作を1つの原子的な単位として扱います。
- すべての手順が成功すると変更はコミットされ、どれか1つでも失敗するとすべてロールバックされます。
- これにより、複雑な更新中のデータ不整合を防ぎます。
詳細
多くのシステムでは、操作は独立していません。たとえば、2つの口座間で送金するには、両方の残高を更新する必要があります。片方の更新が成功してもう片方が失敗すると、システムは不整合になります。
トランザクションは、操作をまとめることでこれを解決します:
BEGIN UPDATE account A UPDATE account B COMMIT
実行中にどれかの手順が失敗した場合、データベースはロールバックを行い、トランザクション内のそれまでの変更をすべて取り消します。
これにより、途中までの更新が発生しないことが保証されます。操作全体が正常に完了するか、システムが以前の状態に戻るかのどちらかです。
トランザクションは、正確性が重要なシステム、たとえば金融システム、在庫管理、予約システムなどで不可欠です。トランザクションがないと、障害や同時実行の問題によって簡単にデータが壊れてしまいます。
ACID特性
ACID特性は、データベースのトランザクションを信頼性が高く安全にする保証を定義します。
- 原子性は、トランザクション内のすべての操作が完全に成功するか、完全に失敗するかのどちらかであることを保証します。
- 一貫性は、データベースが常に有効な状態に保たれることを保証します。
- 分離性と永続性は、トランザクション同士が干渉せず、コミットされたデータが永続的であることを保証します。
詳細
ACIDは、リレーショナルデータベースがトランザクション中の正しさを保証するために適用する4つの特性の集合です。
原子性とは、トランザクションが分割不可能であることを意味します。どれか1つでも失敗すると、トランザクション全体がロールバックされ、部分的な更新は防がれます。
一貫性は、トランザクション完了後に、データベースが制約、リレーション、データの妥当性など、すべてのルールに従っていることを保証します。無効な状態は決して許可されません。
分離性は、同時に実行される複数のトランザクションが互いに干渉しないことを保証します。並行システムであっても、各トランザクションは単独で実行されているかのように動作します。
永続性は、トランザクションが一度コミットされると、直後にシステムがクラッシュしてもデータが永続的に保存されることを保証します。
これらの特性により、データベースは障害、並行処理、複雑な操作を扱ってもデータを破損させずに済みます。正しさが重要な信頼性の高いシステムを構築するための基盤となります。
クエリ最適化
データベースはクエリ最適化を使って、クエリを実行するための最も効率的な方法を判断します。
- クエリプランナーはクエリを分析し、効率的な実行戦略を選択します。
- インデックスを使うか、全表スキャンを行うかなど、データへのアクセス方法を決定します。
- 最適化により、大規模データセットでのレイテンシとリソース使用量が削減されます。
詳細
データベースにクエリを送信しても、単純に手順どおりに実行するわけではありません。代わりに、データベースはまずクエリプランナーと呼ばれるコンポーネントを使ってクエリを分析します。
プランナーは、クエリを実行するための複数の方法を評価し、最も効率的なものを選択します。これには、インデックスを使うかどうか、テーブルをどのように結合するか、処理の順序をどうするかといった判断が含まれます。
たとえば、あるクエリがメールアドレスでユーザーを検索し、その列にインデックスが存在する場合、データベースはすべての行をスキャンするのではなくインデックスを使用します。これにより、大規模データセットでは実行時間を数秒から数ミリ秒に短縮できることがあります。
この処理の結果として実行計画が生成され、クエリが内部でどのように実行されるかが正確に定義されます。
データ量が増えるにつれて、クエリ最適化は重要になります。最適化が不十分なクエリは、応答の遅延、CPU使用率の増加、システムのボトルネックを引き起こす可能性があります。一方、適切に最適化されたクエリは、システムを高速かつスケーラブルに保ちます。
レプリケーション
レプリケーションは、複数のデータベースインスタンスにデータをコピーすることで、信頼性とスケーラビリティを向上させます。
- データはプライマリデータベースから1つ以上のレプリカにコピーされます。
- 書き込みは通常プライマリに行われ、読み取りはレプリカ全体に分散できます。
- レプリケーションは可用性、耐障害性、読み取り性能を向上させます。
詳細
レプリケーションは、同じデータの複数のコピーを異なるデータベースサーバー上に保持するために使われます。最も一般的な構成は、プライマリ-レプリカモデルです。
このモデルでは、プライマリデータベースがすべての書き込み操作を処理します。その後、加えられた変更はレプリカデータベースに伝播されます。これらのレプリカはデータのコピーを保持し、読み取りリクエストを処理できます。
この構成にはいくつかの利点があります。プライマリデータベースに障害が発生した場合、レプリカが引き継ぐことができ、システムの可用性が向上します。また、1つのデータベースに負荷を集中させる代わりに、複数のレプリカにクエリを分散することで、読み取りトラフィックをスケールできます。
ただし、レプリケーションには複雑さも伴います。データがプライマリに書き込まれてからレプリカに反映されるまでに、わずかな遅延が発生することがあります(replication lag)。つまり、レプリカが常に最新のデータを持っているとは限りません。
このトレードオフはあるものの、レプリケーションはスケーラブルで耐障害性のあるバックエンドシステムを構築するための中核的な技術です。
シャーディング
シャーディングは、単一のシステムに依存するのではなく、データを複数のサーバーに分割してデータベースをスケールさせる方法です。
- データはシャードに分割され、各シャードは全データセットの一部を保存します。
- シャーディングは、より多くのデータベースサーバーを追加することで水平スケーリングを可能にします。
- マシン間で負荷を分散し、大規模なデータセットと高トラフィックに対応します。
詳細
データが増えると、単一のデータベースサーバーはいずれボトルネックになります。シャーディングは、データをより小さな断片に分割し、それぞれのシャードを別のサーバーに保存することでこれを解決します。
例えば:
Shard 1 → users 1–1M
Shard 2 → users 1M–2M
Shard 3 → users 2M–3M
リクエストは、アクセスされるデータに基づいて適切なシャードへルーティングされます。これにより、システムは水平にスケールし、はるかに大きなワークロードを処理できます。
トレードオフは複雑さが増すことです。特に、複数のシャードからデータを必要とするクエリではその傾向が強くなります。
質問セクション
1 / 5
このレッスンはプレミアムコンテンツです
プレミアムにアップグレードしてぼかしを解除し、全文を読めるようにしましょう。