データベースのリレーショナルモデルが重要なのはなぜですか?


61

上司と一緒にデータベースを実装する必要があるプロジェクトに近づいています。私たちは非常に小さな新興企業なので、職場環境は非常に個人的なものです。

彼は以前に私に会社のデータベースの1つを与えてくれましたが、RDBMSの学校で教えられた(そして読んだ)ものに完全に反しました。たとえば、ここには1つのテーブルで構成されるデータベース全体があります(独立したデータベースごとに)。これらのテーブルの1つは20列以上の長さであり、コンテキストのために、1つのテーブルの列名の一部を次に示します。

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

ポイントは、エンティティデータ(名前、サイズ、購入日など)を保持する個々のテーブルが必要な場所であり、データベースごとにすべてを1つの大きなテーブルに押し込みます。

この設計を改善したいのですが、適切に正規化されセグメント化されたデータモデルが実際にこの製品を改善する理由がわかりません。私は大学のデータベース設計に精通しており、その方法を理解ていますが、これが実際にデータベースを改善する理由はわかりません。

優れたリレーショナルスキーマがデータベースを改善するのはなぜですか?


33
一言:正規化。
ロバートハーヴェイ

9
近い投票者-自分を正当化する!:
ロビーディー

6
新入社員は、確立された手順を、その理由が技術的に正しくなくても、背後にある理由を理解せずに批判するのが一般的です。まず、上司がそのように構築した理由を見つけてください。彼/彼女は、それが良いデザインではないことを非常によく知っているかもしれませんが、それをより良くするための知識(または、おそらく、時間)を持っていません。現在の設計の理由を丁寧に認めれば、提案する変更はおそらくより積極的に受け取られます。
ペドロ

5
He [the boss] had given me one of his databases before and it completely went against what I was taught (and read about) in school for RDBMS<-現実の世界へようこそ!
モーズ

5
私のお気に入りのリレーショナルデータベースの引用を思い出します:「それが痛むまで正規化し、それが機能するまで非正規化します」
ジェイク

回答:


70

通常、パフォーマンスの引数は最も直感的なものです。特に、誤って正規化されたデータベースに適切なインデックスを追加するのがどのように難しいかを指摘する必要があります(注:非正規化によって実際にパフォーマンスが向上する場合がありますが、リレーショナルデータベースに慣れていない場合、これらのケースを参照してください)。

もう1つは、ストレージサイズの引数です。多くの冗長性を持つ非正規化テーブルには、はるかに多くのストレージが必要です。これはパフォーマンスの側面にも影響します。データが多いほど、クエリが遅くなります。

また、理解するのが少し難しい議論もありますが、実際にはより多くのハードウェアを投げて解決できないため、より重要です。それがデータの一貫性の問題です。適切に正規化されたデータベースは、特定のIDを持つ製品が常に同じ名前を持っていることに注意します。しかし、非正規化されたデータベースではこのような不整合が発生する可能性があるため、不整合の回避に関して特別な注意を払う必要があります。これにより、プログラミングに時間がかかり、顧客満足度を犠牲にするバグが発生します。


19
非正規化の主なエッジケースの1つは、特にデータウェアハウジングです。大量のデータが変更されないことが保証されており、ストレージスペースを犠牲にしてより迅速かつ効率的にクエリしたい場合です。良い答えです。これは、3NF以外のものが望ましい理由がわからないSQL初心者の単なる参考です。


11
一貫性の議論が「理解しにくい」理由はわかりません。私にとってはもっと簡単に思えます。値が変更された場合、その値のすべてのコピーを更新する必要があります。単一のコピーを更新することは、同じデータの数百または数千のコピーを更新するよりもエラーが少なくなります。これはデータ間の関係にも同様に当てはまります。(リレーションシップを2つの方法で保存している場合、リレーションシップの両方のコピーを更新する必要があります。)これは、非正規化DBで非常に一般的な問題です。それはです非常に(例外はビュータイプの使用をマテリアライズされた)実際には、この破損を防止することは困難。
jpmc26

4
その最後の段落は太字で強調表示する必要があります。:-)正規化がなければ、データの整合性を保証することは不可能です。すべての正規化されていないデータベースは最終的に何らかのデータ異常を示すため、ビジネスロジックレイヤーでのみ入力を制御するのはばかげたことです。
-DanK

2
@IsmaelMiguel通常のプラクティスでは、このようなマスターデータはデータベースから完全に削除されることはありません。使用不可になったというフラグを設定することで、ソフト削除するだけです。この特定のケースでは、製品と注文の間に外部キー関係があるとよいでしょう。つまり、注文によって参照されている製品を削除しようとすると、データベースはエラーをスローします。
フィリップ

24

私は上司とデータベースを実装する必要があります...

専用のデータベース管理ソフトウェアを使用すると、かなり簡単になるかもしれません(ごめんなさい、抵抗できませんでした)。

lngStoreID | vrStoreName | lngCompanyID | vrCompanyName | lngProductID | vrProductName

このデータベースが、どの製品がどこで、いつ、誰によって販売されたかを「記録」するだけの場合、「OKデータベース」の定義を十分に拡張できる可能性あります。このデータが使用されている場合は何も他には、それは本当にかなり貧弱です。

しかし...

このデータを使用するアプリケーション/クエリの応答が不十分/遅いですか?そうでなければ、解決すべき実際の問題はありません。確かに、見た目も見た目もfeelいですが、それが機能する場合、それが「より良い」ことを示唆するための「ポイント」を取得することはありません。

不十分なデータモデリングが原因であるように見える明確な症状(つまり問題)を見つけることができる場合は、より良いソリューションのプロトタイプを作成します。これらの「データベース」のいずれかのコピーを取得し、データを正規化し、ソリューションの実行が改善されるかどうかを確認します。それがかなり良い場合(そして、このデータの更新操作が大幅に改善されることを完全に期待します)、上司に戻って改善を示します。

彼のデータの「単一テーブルビュー」を.. well ..ビューで再作成することは完全に可能です。


11
単一テーブルweltanschauungへの抵抗は、結合を理解していないSQLの経験の浅い人から生じることがよくあります。特に、欠落データ、つまり外部結合に関してです。
ロビーディー

6
@RobbieDeeより一般的には、非正規化されたデータが不整合になることで破損するのを見た人々からです。私はそのような人です。Phillが示唆する状況でのみこの種の構造を検討します:これは、データが決して更新されない、または完全に消去されて他のソースから完全に再派生されることによってのみ更新されるロギング/レポートテーブルの一種です。
jpmc26

2
アプリケーションがこのようなデータベースで十分に機能する場合でも、適切に正規化されたデータベースのように柔軟ではありません。店舗名または会社名が変更された場合、店舗または会社のテーブルだけでなく、どこでも更新する必要があります。場合によっては、それが実際に必要な場合もあります(データが主にアーカイブ目的で収集される場合など)が、特定のアプリケーションについて詳しく知る必要があります。
ザックリプトン

1
@Zach:同意しました。だからこそ、販売ログはこのケースで受け入れられる可能性があります。各販売を、販売が行われたときに「現在の店舗名」ではなく、店舗の名前に関連付けたい場合、「正規化」しようとするとかなりの複雑さが生じます(テーブル名が店舗名を記録するため店舗IDごとに1つの値だけでなく、時間とともにシリーズである必要があります)
スティーブジェソップ

おそらく、提案された正規化によって導入された唯一の複雑さが、レポートに必要なすべての列を取得するためにいくつかのクエリがそれらに結合する必要があるということであれば、その変更を行うために歩いてはいけません:- )
スティーブジェソップ

14

優れたリレーショナルスキーマがデータベースを改善するのはなぜですか?

答えは次のとおりです。データベースを常に改善するとは限りません。あなたが教えられた可能性のあるものは第三標準形と呼ばれることを知っておく必要があります

状況によっては他の形式が有効であり、これが質問に答える鍵となります。あなたの例はFirst Normal Formのように見えますが、それが現在の状態をより良く感じるのに役立ちます。

3NFルールは、データベースを「改善する」データ間の関係を確立します。

  1. 無効なデータがシステムに入るのを防ぎます(関係が1対1の場合、その上にコードが記述されていても強制的にエラーが発生します)。データベース内のデータに一貫性がある場合、データベース外で不整合が発生する可能性は低くなります。

  2. コードを検証する方法を提供します(たとえば、多対1の関係は、オブジェクトのプロパティ/動作を制限する信号です)。データベースを使用するコードを書くとき、プログラマーはコードがどのように機能するかの指標としてデータ構造に気付くことがあります。または、データベースがコードと一致しない場合、有用なフィードバックを提供できます。(残念ながら、これは希望的観測に似ています。)

  3. データベースの構築時の間違いを減らすのに非常に役立つルールを提供します。これにより、データベースの存続期間中に随時発生する可能性のある任意の要件に基づいて構築することはありません。代わりに、特定の目標を達成するために情報を体系的に評価しています。

  4. 適切なデータベース構造は、データを最小化する方法でデータを接続することでパフォーマンスを向上させ、データを取得するためのストレージ呼び出しを最小化し、メモリ内リソースを最大化し、および/または、特定のデータセットのデータの並べ替え/操作を最小化しますそれに対して実行します。しかし、「適切な」構造は、データの量、データの性質、クエリの種類、システムリソースなどに依存します。正規化すると、パフォーマンスが低下する場合があります(つまり、すべてのデータを1つのテーブルとしてロードすると、結合が遅くなる可能性があります)クエリ)。トランザクション処理(OLTP)とビジネスインテリジェンス(データウェアハウス)は大きく異なります。

データセットが小さい小規模な会社では、現在の状態に問題はないことがわかります。ただし、テーブルが大きくなると、それを使用するシステムが遅くなる可能性があるため、成長する場合、後で「修正」するのは苦痛になります。

通常、会社が成長するにつれて高速トランザクションを強調したいと思うでしょう。ただし、会社がより緊急に必要とする可能性のある他の事柄ではなく、今このプロジェクトに時間を費やしている場合、会社は決して成長しないので、その問題を抱えることはないでしょう。それが「事前最適化の課題」です。今、あなたの貴重な時間を過ごす場所です。

幸運を!


4
言及されていませんが、プログラマーにとって重要な点は、1つの「もの」を編集するには、データベース全体をループしてその単一のものを見つけて置き換えるのではなく、1行だけを編集する必要があることです。
スリーブマン

@slebetman正規化されているかどうかに関係なく、単一のテーブル内の複数の行を更新するコードサイドループを使用しないでください。WHERE句を使用します。もちろん、これらはまだ間違っている可能性がありますが、主キーを介して1行だけを一致させる必要があるため、正規化された状況ではあまり起こりません。
jpmc26

@ jpmc26:データベースをループするということは、影響を受けるすべての行を更新するクエリを作成することを意味します。単一のWHEREで十分な場合もあります。しかし、変更すべきではない行に影響を与えずに、影響を受けるすべての行を取得するために、同じテーブルへの副選択を必要とする不浄な構造を見てきました。単一のクエリではジョブを実行できない構造を見たことがあります(変更が必要なエンティティは行に応じて異なる列に存在します)
-slebetman

この質問に対する多くの優れた回答があり、これも例外ではありませんでした。
マイクチェンバレン

11

1つの大きな「ゴッドテーブル」の使用が悪い理由は複数あります。作成したサンプルデータベースの問題を説明します。スポーツイベントをモデル化しようとしていると仮定しましょう。ゲームとそれらのゲームでプレーしているチームをモデル化すると言います。複数のテーブルを持つデザインは次のようになります(これは意図的に非常に単純化されているため、より多くの正規化を適用できる場所に追いつかないでください)。

Teams
Id | Name | HomeCity

Games
Id | StartsAt | HomeTeamId | AwayTeamId | Location

単一のテーブルデータベースは次のようになります

TeamsAndGames
Id | TeamName | TeamHomeCity | GameStartsAt | GameHomeTeamId | GameAwayTeamId | Location

まず、これらのテーブルにインデックスを作成してみましょう。チームのホームシティのインデックスが必要な場合は、TeamsテーブルまたはTeamsAndGamesテーブルにかなり簡単に追加できます。インデックスを作成するときは常に、ディスクのどこかに保存し、テーブルに行が追加されるたびに更新する必要があることに注意してください。Teamsテーブルの場合、これは非常に簡単です。私は新しいチームを置き、データベースがインデックスを更新します。しかし、何のためにTeamsAndGames?まあ、同じことが適用されますTeams例。チームを追加すると、インデックスが更新されます。しかし、ゲームを追加するときにも起こります!ゲームの場合、そのフィールドはnullになりますが、とにかくインデックスを更新し、そのゲームのディスクに保存する必要があります。あるインデックスでは、これはそれほど悪くないようです。ただし、このテーブルに詰め込まれた複数のエンティティに多くのインデックスが必要な場合、インデックスを保存するための多くのスペースと、適用されないもののためにインデックスを更新するプロセッサ時間を浪費します。

第二に、データの一貫性。2つの別々のテーブルを使用する場合、Gamesテーブルからテーブルへの外部キーを使用Teamsして、ゲームでプレイするチームを定義できます。そしてHomeTeamIdAwayTeamId列をnull不可にすると、データベースは、私が投入したすべてのゲームに2つのチームがあり、それらのチームがデータベースに存在することを保証します。しかし、単一テーブルのシナリオはどうでしょうか?このテーブルには複数のエンティティがあるため、これらの列はNULL値を許可する必要があります(NULL値を許可せずにガベージデータを格納することもできますが、これはひどい考えです)。これらの列がNULL可能であれば、データベースは、ゲームを挿入したときに2つのチームがあることを保証できなくなります。

しかし、とにかくそれだけに行くことにした場合はどうなりますか?これらのフィールドが同じテーブル内の別のエンティティを指すように外部キーを設定します。しかし、データベースは、それらのエンティティが正しいタイプであることではなく、テーブルに存在することを確認するだけです。GameHomeTeamId別のゲームのIDを非常に簡単に設定でき、データベースはまったく文句を言いません。複数テーブルのシナリオでそれを試みた場合、データベースは適合します。

「まあ、私たちはコードでそれを絶対にしないようにするだけです」と言って、これらの問題を軽減しようとすることができます。初めてバグのないコードを書く能力と、ユーザーが試行する可能性のある奇妙な組み合わせをすべて考慮に入れる能力に自信がある場合は、先に進んでください。私は個人的にこれらのいずれかを行う能力に自信がないので、データベースに追加のセーフティネットを提供します。

(これは、外部キーを使用するのではなく、行間ですべての関連データをコピーする設計の場合、さらに悪化します。スペルやその他のデータの不一致は解決が困難です。 "Jon"が "John 「それが意図的なものだった場合(2人の別々の人だから))

第三に、ほとんどすべての列はヌル可能にする必要があるか、コピーされたデータまたはガベージデータで満たされている必要があります。ゲームは必要ありませんTeamNameかをTeamHomeCity。そのため、すべてのゲームには何らかのプレースホルダーが必要か、null可能にする必要があります。そして、もしそれがnull可能であれば、データベースは喜んでnoでゲームを取りますTeamName。また、ビジネスロジックが決して起こらないと言っていても、名前のないチームが必要になります。

別のテーブルが必要な理由は他にもいくつかあります(開発者の健全性を保持するなど)。より大きなテーブルが優れている理由はいくつかあります(非正規化によりパフォーマンスが向上する場合があります)。これらのシナリオはほとんどありません(通常、パフォーマンスメトリックがあり、それが実際に問題であり、インデックスの欠落などではないことを示す場合に最適に処理されます)。

最後に、保守しやすいものを開発します。それが「機能する」からといって、それが大丈夫というわけではありません。神のテーブル(神のクラスのような)を維持しようとするのは悪夢です。後で痛みのために自分を設定しているだけです。


1
「チーム:ID |名前| HomeCity」。スーパーボウルXXXIVがLAラムズに勝ったとアプリケーションが誤って主張しないように、データスキーマを確認してください。一方、SB XXXIV 、現在LAラムズとして知られているチームが獲得したすべてのチャンピオンシップのクエリに表示されます。良くも悪くも「神のテーブル」があり、あなたは確かに悪いものを提示しました。より良い方法は、「ゲームID |ホームチーム名|ホームチームシティ|アウェイチーム名|アウェイチームシティ|ゲームの開始場所|など...」です。これは、「New Orleans Saints @ Chicago Bears 1p Eastern」などの情報をモデル化する最初の試みとして生まれました。
スティーブジェソップ

6

今日の引用:「理論と実践は同じであるべきだ...理論上

非正規化テーブル

独自のホールド・イット・オール・テーブルには冗長データが含まれるという1つの利点があります。結合の必要がないため、その行のレポートがコーディングが非常に簡単で実行が高速になります。しかし、これは高コストで:

  • 関係の冗長なコピーを保持します(例IngCompanyIDおよびvrCompanyName)。マスターデータを更新するには、正規化されたスキーマよりも多くの行を更新する必要がある場合があります。
  • それはすべてを混ぜます。データベースレベルでの簡単なアクセス制御を保証することはできません。たとえば、ユーザーAが会社情報のみ、ユーザーBが製品情報のみを更新できるようにします。
  • データベースレベルで整合性ルールを保証することはできません(たとえば、会社IDに対して会社名が1つだけであることを強制する主キー)。
  • 正規化されたテーブルのサイズといくつかのインデックスの統計を利用して、複雑なクエリの最適なアクセス戦略を特定できるDBオプティマイザーの恩恵を完全には受けません。これにより、結合を回避するという限定的な利点がすぐに相殺される可能性があります。

正規化されたテーブル

上記の欠点は、正規化されたスキーマの利点です。もちろん、クエリは書くのがもう少し複雑かもしれません。

つまり、正規化されたスキーマ、データ間の構造と関係をより良く表現します。私は挑発的で、注文されたオフィスの引き出しのセットを使用するのに必要な規律とごみ箱の使いやすさの違いと同じ種類の違いだと言います。


5

質問には少なくとも2つの部分があると思います。

1.異なるタイプのエンティティを同じテーブルに保存しないのはなぜですか?

ここで最も重要な答えは、コードの読みやすさと速度です。A SELECT name FROM companies WHERE id = ?はaよりもはるかに読みSELECT companyName FROM masterTable WHERE companyId = ?やすく、誤ってナンセンスを照会する可能性が低くなります(たとえばSELECT companyName FROM masterTable WHERE employeeId = ?、会社と従業員が異なるテーブルに格納されている場合は不可能です)。速度については、データベーステーブルからデータを取得するには、テーブル全体を順番に読み取るか、インデックスから読み取ります。テーブル/インデックスに含まれるデータが少ない場合は両方とも高速であり、データが異なるテーブルに格納されている場合はそうです(テーブル/インデックスのいずれかを読み取るだけで済みます)。

2.単一のタイプのエンティティを、異なるテーブルに格納されるサブエンティティに分割する必要があるのはなぜですか?

ここで、主な理由はデータの不整合を防ぐためです。単一テーブルのアプローチでは、注文管理システムの場合、顧客が注文した製品の顧客名、顧客住所、製品IDを単一のエンティティとして保存できます。顧客が複数の製品を注文した場合、データベースには顧客の名前と住所のインスタンスが複数あります。最良のケースでは、データベース内に重複したデータを取得しただけなので、少し遅くなる可能性があります。しかし、最悪のケースは、データが入力されたときに誰か(または何らかのコード)がミスを犯したために、企業がデータベース内の異なるアドレスになってしまうことです。これだけでも十分です。しかし、名前に基づいて会社の住所を照会する場合(例:SELECT companyAddress FROM orders WHERE companyName = ? LIMIT 1)2つのアドレスのうちの1つが勝手に返されるだけで、不整合があることすら気付かないでしょう。ただし、クエリを実行するたびに、DBMSによってクエリが内部的に解決される方法に応じて、実際には異なるアドレスを取得する場合があります。これにより、アプリケーションがどこか別の場所で破損する可能性があり、その破損の根本原因を見つけるのは非常に困難です。

マルチテーブルアプローチでは、会社名から会社の住所への機能的な依存関係があることに気づきます(会社が1つの住所しか持てない場合)、(companyName、companyAddress)タプルを1つのテーブルに格納します(例:company)、および(productId、companyName)別のテーブルのタプル(例order)。その後UNIQUEcompanyテーブルの制約により、各会社がデータベース内に1つのアドレスのみを持ち、会社のアドレスに不整合が発生しないように強制できます。

注:実際には、パフォーマンス上の理由から、おそらく各会社に対して一意のcompanyIdを生成し、companyNameを直接使用する代わりに外部キーとして使用します。しかし、一般的なアプローチは同じままです。


3

TL; DR-学校にいたときに教えられた方法基づいてデータベースを設計しています。

10年前にこの質問を書いたかもしれません。私の前任者がなぜ彼らのデータベースを彼らのやり方で設計したのかを理解するのに時間がかかりました。次のいずれかで作業している:

  1. Excelをデータベースとして使用して、ほとんどのデータベース設計スキルを獲得したか、
  2. 彼らは学校を卒業したときからのベストプラクティスを使用しています。

テーブルに実際にID番号があるため、#1であるとは思わないので、#2と仮定します。

学校を卒業した後、AS / 400(別名IBM i)を使用する店で働いていました。私は彼らがデータベースを設計する方法に奇妙なことを発見し、データベースの設計方法を教えられた方法に従うために変更を行うことを主張し始めました。(当時はバカだった)

忍耐強い年上のプログラマーに、なぜ物事がそのように行われたのかを説明するのに時間がかかりました。私よりも古いプログラムが壊れる可能性があるため、彼らはスキーマを変更していませんでした。文字通り、あるプログラムのソースコードには、私が生まれる前の年の作成日がありました。作業中のシステムでは、データベースのクエリプランナーが処理するすべてのロジックと操作をプログラムで実装する必要がありました。(クエリの1つでEXPLAINを実行すると確認できます)

彼は私が実装しようとしていた技術については最新でしたが、「教えられたことに反して」変更を加えるよりもシステムを実行し続けることの方が重要でした。私たちのどちらかが始めたすべての新しいプロジェクトは、私たちができるリレーショナルモデルを最大限に活用しました。残念ながら、当時の他のプログラマ/コンサルタントは、そのシステムの以前の制約で作業しているかのようにデータベースを設計していました。


リレーショナルモデルに適合しなかった、私が遭遇したいくつかの例:

  • 日付はユリウス日として保存され、実際の日付を取得するには日付テーブルへの結合が必要でした。
  • 同じタイプの連続した列を持つ非正規化テーブル(例code1,code2, ..., code20
  • 長さMのN個の文字列の配列を表すNxM長のCHAR列。

これらの設計決定のために私に与えられた理由はすべて、データベースが最初に設計されたときのシステムの制約に基づいていました。

日付 - 日付を処理するために日付関数(月、日、曜日)を使用すると、すべての情報を含む可能なすべての日付のテーブルを作成するよりも処理時間がかかると言われました。

同じタイプの連続列 -プログラミング環境では、プログラムが行の一部に配列変数を作成できました。また、読み取り操作の数を減らす簡単な方法でした。

NxMの長さのCHAR列 -ファイルの読み取り操作を減らすために、構成値を1つの列に押し込む方が簡単でした。

彼らが持っていたプログラミング環境を反映するために、Cで十分に考えられていない例:

#define COURSE_LENGTH 4
#define NUM_COURSES 4
#define PERIOD_LENGTH 2

struct mytable {
    int id;
    char periodNames[NUM_COURSES * PERIOD_LENGTH];  // NxM CHAR Column
    char course1[COURSE_LENGTH];
    char course2[COURSE_LENGTH];
    char course3[COURSE_LENGTH];
    char course4[COURSE_LENGTH];
};

...

// Example row
struct mytable row = {.id= 1, .periodNames="HRP1P2P8", .course1="MATH", .course2="ENGL", .course3 = "SCI ", .course4 = "READ"};

char *courses; // Pointer used to access the sequential columns
courses = (char *)&row.course1;


for(int i = 0; i < NUM_COURSES; i++) {

    printf("%d: %.*s -> %.*s\n",i+1, PERIOD_LENGTH, &row.periodNames[PERIOD_LENGTH * i], COURSE_LENGTH,&courses[COURSE_LENGTH*i]);
}

出力

1:HR->数学
2:P1-> ENGL
3:P2- > SCI
4:P8->読み取り

私が言われたように、これのいくつかは当時ベストプラクティスと考えられていました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.