ブログトップ

公開日:

更新日:

14 min read

エンジニアリング

動くだけのコードから保守可能なコードへ:データベース設計の基本

動くだけのコードから保守可能なコードへ:データベース設計の基本のイメージ

エンジニアリングの世界では、「動くコード」と「良いコード」は全く異なるものです。 今回は、実務で遭遇した典型的な「動くだけの設計」を例に、なぜそれが問題なのか、 そしてどのように改善すべきかを考えてみましょう。

とある「動くだけの設計」の例

以下は、実際のプロジェクトで遭遇したデータベース設計です。

   CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    division_id INT,      -- 法人・個人の区分
    clbirthy INT NOT NULL, -- 誕生年
    clbirthm INT NOT NULL, -- 誕生月
    clbirthd INT NOT NULL, -- 誕生日
    FOREIGN KEY (division_id) REFERENCES divisions(id)
);

一見すると動作するコードです。しかし、この設計には大きな問題が潜んでいます。

誕生日の分割保存が引き起こす問題

まず目につくのが、誕生日を年、月、日に分割して保存している点です。 このような実装は、入力ページのレイアウトが年月日それぞれをドロップダウンにしているため、 それぞれを個別のカラムに保存すれば良い」という単純な思考から生まれたものでしょう。 さらに各フィールドをNOT NULLにすることで、一見データの整合性も担保されているように見えます。

しかし、この設計には深刻な問題があります。 最も重大な問題は、データの整合性チェックが事実上不可能になることです。 例えば、2月31日のような存在しない日付が入力可能になってしまいます。 また、閏年の考慮も困難です。

日付計算も非常に複雑になります。 例えば、年齢を計算するために必要なSQLを見てみましょう。

   SELECT
    CASE
        WHEN (MONTH(CURRENT_DATE) > clbirthm) OR
             (MONTH(CURRENT_DATE) = clbirthm AND DAY(CURRENT_DATE) >= clbirthd)
        THEN YEAR(CURRENT_DATE) - clbirthy
        ELSE YEAR(CURRENT_DATE) - clbirthy - 1
    END as age
FROM customers;

このような複雑な計算は、バグの温床となりやすく、また保守性も著しく低下させます。 さらに、データベースが持つ日付機能を全く活用できないため、 インデックスの効率が低下し、範囲検索も困難になります。

法人格の区分値化における過剰な設計

次に注目すべきは、法人・個人の区分をマスターテーブルで管理している点です。 具体的には以下のような設計になっています:

   -- マスターテーブル
CREATE TABLE divisions (
    id INT PRIMARY KEY,
    code VARCHAR(20),  -- 'individual', 'corporate' が格納されている
    name VARCHAR(50)   -- '個人', '法人' が格納されている
);

-- 顧客テーブル
CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    division_id INT,   -- divisions テーブルのIDを参照
    -- 他のフィールド...
    FOREIGN KEY (division_id) REFERENCES divisions(id)
);

さらに深刻な問題として、命名の不適切さがあります。divisionという名称からは、 このテーブルが何の区分を表しているのかが全く推察できません。 一般的にdivisionという単語は「部門」や「部署」を意味することが多く、 このコードを初めて読む開発者は、部署マスターテーブルだと誤解する可能性が高いでしょう。

実際、私はマスターテーブルの内容を見るまで、カラムの意味が分かりませんでした。

特に、このシステムには設計関連のドキュメントが一切存在しません。 そのため、コード自体が唯一の情報源となります。 このような状況では、テーブルやカラムの名前は、それ自体が文書としての役割を果たさなければなりません。

この設計は、おそらく以下のような考え方から生まれたものでしょう:

  1. 「区分値はマスターテーブルで管理すべき」という一般的な設計原則がある
  2. 将来的に法人・個人以外の区分が増えるかもしれない
  3. 外部キー制約を使えば、不正な値が入るのを防げる

ここでの最大の問題は、「何の区分なのか」が名前から明確に分からないことです。 例えばcorporate_statuslegal_entity_typeといった名前であれば、このテーブルの目的は一目瞭然です。

一見すると理にかなっているように見えますが、実際にはいくつかの問題があります。

具体的な問題点

まず、単純な検索をする場合でも、必ずJOINが必要になります。

   -- 法人顧客を検索する場合
SELECT c.*
FROM customers c
JOIN divisions d ON c.division_id = d.id
WHERE d.code = 'corporate';

このような設計には、大きく分けて三つの問題があります。

まず第一に、コードが必要以上に複雑になってしまいます。 単純な検索をするだけでもJOINが必要となり、SQLの記述量が増えてしまいます。 そのため、開発者がコードを書く際にミスを起こしやすくなります。 本来なら「法人顧客を検索する」という単純な処理が、テーブル結合を伴う複雑な処理になってしまうのです。

次に、パフォーマンスの面でも大きな問題があります。 顧客の区分を調べるたびにテーブル結合が発生するため、処理速度が低下します。 また、インデックスの効率も下がってしまいます。 特にデータ量が増えてくると、この問題の影響は顕著になってきます。 単純な真偽値の判定のために、常にテーブル結合が必要というのは、明らかにオーバーヘッドが大きすぎます。

さらに、システムの保守性という観点からも問題があります。 マスターテーブルの管理が必要になるため、データのメンテナンス作業が増えます。 また、データを更新する際の整合性チェックも複雑になります。 バックアップやリストアの際にも、二つのテーブル間の整合性を考慮する必要があり、運用の手間が増えてしまいます。

そもそも、法人格という概念自体が「法人である」か「法人でない(個人である)」かの二値しかとりえません。 これは法律で定められた概念であり、将来的に「法人でも個人でもない何か」が増える可能性はありません。

つまり、この設計は「将来の拡張性」という名の下に、不必要な複雑さを導入してしまっているのです。 これは明らかな過剰設計であり、保守性とパフォーマンスの両方に悪影響を及ぼします。

要件定義に失敗してると言っても過言ではありません。

エンジニアリング的な改善アプローチ

では、これらの問題をどのように改善すべきでしょうか。 以下が、より適切な設計例です:

   CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    is_corporate BOOLEAN NOT NULL,  -- 法人格の有無
    birth_date DATE,               -- 誕生日
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

この設計では、適切なデータ型を使用することで、多くの問題が自然に解決されています。 DATEデータ型は無効な日付を自動的に排除し、BOOLEANは二値のみを許容します。 これにより、データの整合性が自動的に保証されます。

また、コードの意図も明確になり、保守性が大幅に向上します。 データベースの機能を適切に活用することで、バグの発生リスクも低下します。 さらに、効率的なインデックスの利用や不要なJOINの排除により、パフォーマンスも向上します。

例えば、日付に関する操作は以下のように簡潔に記述できます:

   -- 年齢計算
SELECT
    name,
    TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) as age
FROM customers
WHERE is_corporate = false;

-- 今月誕生日の顧客を検索
SELECT *
FROM customers
WHERE MONTH(birth_date) = MONTH(CURDATE())
AND is_corporate = false;

エンジニアリングの本質

エンジニアリングの本質は、単に「動くコード」を書くことではありません。 実務のシステムは長期間保守され、複数の開発者が関わり、要件は常に変化します。 また、パフォーマンスも重要な要素となります。

そのため、私たちは常にシンプルさを追求する必要があります。 複雑さは保守性の敵であり、不必要な抽象化は避けるべきです。 コードの意図は常に明確であるべきです。

また、適切な技術の選択も重要です。 データベースの機能を活用し、標準的な解決策を優先することで、効率的な開発が可能になります。 車輪の再発明は避け、既存の優れた解決策を活用すべきです。

将来への配慮も忘れてはいけません。 保守性の確保、拡張性への考慮、そして適切なドキュメント化は、長期的な開発の成功に不可欠な要素です。

まとめ:エンジニアリングの価値

良いエンジニアリングは、確かに短期的には多少の手間がかかるかもしれません。 しかし、それは長期的には必ず価値のある投資となります。 特に、適切なネーミングは非常に重要です。 システムにドキュメントがない場合、コード自体が文書としての役割を果たさなければなりません。 そのため、テーブルやカラムの名前は、その目的や役割が明確に分かるものでなければならないのです。

「動くから良い」のではなく、「保守できるから良い」という考え方こそが、プロフェッショナルなエンジニアリングの基本です。 名前一つをとっても、それが後の保守性に大きく影響することを忘れてはいけません。

データベース設計に限らず、すべてのソフトウェア開発において、この原則は変わりません。 私たちは常に、目の前の実装が「動くコード」なのか、それとも「良いコード」なのかを考える必要があるのです。 そして、その判断の重要な基準の一つが、コードの意図が名前から明確に読み取れるかどうかなのです。