web-dev-qa-db-ja.com

リレーショナルデータベースで個別の年齢を表すにはどうすればよいですか?

PostgreSQLを使用して、個人にデータを保存しています。各人が0〜17の各年齢の子供を教えることができるかどうかを保存する必要があります。年齢は個別の値であり、Personは任意の数の18(0〜17歳と想定)を自分のアカウントに割り当てることができます。これは予約システムで使用され、検索結果の一部を形成する必要もあります。

最初は18のブールフィールドを作成することを検討しましたが、これは非効率的です。それを行うより良い方法はありますか? PostgresがJSONBをサポートしていることを理解しているので、これはオプションですが、その意味がわかりません。

このデータを保存するより良い方法はありますか?

5
Alex

年齢は離散値です

OK。

人は自分のアカウントに任意の数の18(0-17歳を想定)を割り当てることができます。

それは多対多の関係ですか?

その場合は、通常どおりデータを第3正規形に分解し、1つの追加の関係によってカーディナリティを表現します。

以下に例を示します。現在、SQL方言は、PostgreSQLである必要はありません。現在、便利なインストールがありません。

スキーマ

-- persons relation
CREATE TABLE persons (
    personId INT NOT NULL PRIMARY KEY,
    name TEXT NOT NULL
    -- Any other attributes
);

-- classes relation
CREATE TABLE pupilClasses (
    age INT NOT NULL PRIMARY KEY,
    size ENUM ('small', 'medium', 'large') NOT NULL
);

-- "Tie" relation that expresses the many-to-many cardinality
-- between persons and classes
CREATE TABLE persons_pupilClasses (
    personId INT NOT NULL,
    age INT NOT NULL,
    PRIMARY KEY (personId, age),
    FOREIGN KEY (personId) REFERENCES persons (personId)
        ON UPDATE CASCADE ON DELETE CASCADE,
    FOREIGN KEY (age) REFERENCES pupilClasses (age)
        ON UPDATE CASCADE ON DELETE CASCADE
);

データ

-- Let us populate with some data

-- A few classes
INSERT INTO pupilClasses (age, size) VALUES (0, 'small');
INSERT INTO pupilClasses (age, size) VALUES (1, 'small');
INSERT INTO pupilClasses (age, size) VALUES (2, 'small');
INSERT INTO pupilClasses (age, size) VALUES (3, 'small');
INSERT INTO pupilClasses (age, size) VALUES (4, 'small');
INSERT INTO pupilClasses (age, size) VALUES (5, 'small');
INSERT INTO pupilClasses (age, size) VALUES (6, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (7, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (8, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (9, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (10, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (11, 'medium');
INSERT INTO pupilClasses (age, size) VALUES (12, 'large');
INSERT INTO pupilClasses (age, size) VALUES (13, 'large');
INSERT INTO pupilClasses (age, size) VALUES (14, 'large');
INSERT INTO pupilClasses (age, size) VALUES (15, 'large');
INSERT INTO pupilClasses (age, size) VALUES (16, 'large');
INSERT INTO pupilClasses (age, size) VALUES (17, 'large');


-- A few persons
INSERT INTO persons (personId, name) VALUES (666, 'Alice');
INSERT INTO persons (personId, name) VALUES (667, 'Bertrand');
INSERT INTO persons (personId, name) VALUES (668, 'Carlos');

-- Who can teach to whom

-- Alice to 0−3 and 7−8 classes
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 0);
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 1);
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 2);
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 3);
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 7);
INSERT INTO persons_pupilClasses (personId, age) VALUES (666, 8);

-- Bertrand to 7−9, 13, 16−17 classes
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 7);
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 8);
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 9);
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 13);
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 16);
INSERT INTO persons_pupilClasses (personId, age) VALUES (667, 17);

-- Carlos can't teach anyone yet ☹

論理

どんなクラスでも教えることができる人のレポートを取得しましょう

SELECT persons.name, GROUP_CONCAT(pupilClasses.age) classes
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId
    INNER JOIN pupilClasses
        ON pupilClasses.age = persons_pupilClasses.age
GROUP BY persons.personId;

結果:

+----------+----------------+
| name     | classes        |
+----------+----------------+
| Alice    | 0,1,2,3,7,8    |
| Bertrand | 7,8,9,13,16,17 |
+----------+----------------+

だれに誰が誰に教えることができるかのレポートを取得しましょう

SELECT persons.name, GROUP_CONCAT(pupilClasses.age) classes
FROM persons
    LEFT JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId
    LEFT JOIN pupilClasses
        ON pupilClasses.age = persons_pupilClasses.age
GROUP BY persons.personId;

結果:

+----------+----------------+
| name     | classes        |
+----------+----------------+
| Alice    | 0,1,2,3,7,8    |
| Bertrand | 7,8,9,13,16,17 |
| Carlos   | NULL           |
+----------+----------------+

クラス3を教えるのは誰ですか?

SELECT persons.name
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
WHERE persons_pupilClasses.age = 3;

結果:

+-------+
| name  |
+-------+
| Alice |
+-------+

誰がクラス7または9を教えることができますか?

SELECT DISTINCT persons.name
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
WHERE persons_pupilClasses.age IN (7,9);

結果:

+----------+
| name     |
+----------+
| Alice    |
| Bertrand |
+----------+

誰が大きなサイズを教えることができますか?

SELECT DISTINCT persons.name
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
    INNER JOIN pupilClasses
        ON pupilClasses.age = persons_pupilClasses.age
WHERE pupilClasses.size = 'large';

結果:

+----------+
| name     |
+----------+
| Bertrand |
+----------+

説明では触れていませんが、実際のビジネスシナリオには少なくとも1つの重要な関係があります。それは生徒です。そして、彼らはおそらくあるクラスから次のクラスに移動します。その関係を表す簡単な方法は次のとおりです。

-- pupils relation
CREATE TABLE pupils (
    pupilId INT NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    age INT NOT NULL,
    FOREIGN KEY (age) REFERENCES pupilClasses (age)
        ON UPDATE RESTRICT ON DELETE RESTRICT
);

-- A few pupils
INSERT INTO pupils (pupilId, name, age) VALUES (1313, 'Donald', 7);
INSERT INTO pupils (pupilId, name, age) VALUES (1314, 'Ernest', 7);
INSERT INTO pupils (pupilId, name, age) VALUES (1315, 'Frank', 9);
INSERT INTO pupils (pupilId, name, age) VALUES (1316, 'Gertrude', 0);
INSERT INTO pupils (pupilId, name, age) VALUES (1321, 'Hans', 13);

そしていくつかのロジックの例:

誰が誰に教えますか?

SELECT persons.name teacher, pupils.name pupil
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
    INNER JOIN pupils
        ON pupils.age = persons_pupilClasses.age;

結果:

+----------+----------+
| teacher  | pupil    |
+----------+----------+
| Alice    | Donald   |
| Bertrand | Donald   |
| Alice    | Ernest   |
| Bertrand | Ernest   |
| Bertrand | Frank    |
| Alice    | Gertrude |
| Bertrand | Hans     |
+----------+----------+

一人一人の生徒は誰ですか?

SELECT persons.name teacher, GROUP_CONCAT(pupils.name) pupils
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
    INNER JOIN pupils
        ON pupils.age = persons_pupilClasses.age
GROUP BY persons.personId;

結果:

+----------+--------------------------+
| teacher  | pupils                   |
+----------+--------------------------+
| Alice    | Donald,Ernest,Gertrude   |
| Bertrand | Donald,Ernest,Frank,Hans |
+----------+--------------------------+

それぞれの生徒の先生は誰ですか?

SELECT pupils.name pupil, GROUP_CONCAT(persons.name) teachers
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
    INNER JOIN pupils
        ON pupils.age = persons_pupilClasses.age
GROUP BY pupils.pupilId;

結果:

+----------+----------------+
| pupil    | teachers       |
+----------+----------------+
| Donald   | Alice,Bertrand |
| Ernest   | Alice,Bertrand |
| Frank    | Bertrand       |
| Gertrude | Alice          |
| Hans     | Bertrand       |
+----------+----------------+

これから、いくつかのことが明確になるはずです。

  • ageは、これらのクラスに参加している生徒の実際の自然な年齢に対応していない可能性があるため、少々誤解しているかもしれません。

  • それを連続的な範囲として扱う誘惑を回避することはおそらく良い考えです。これが、これらが離散値であるというあなたの陳述を引用した理由です。

残りについては、私が誤解しない限り、かなり簡単な正規化の問題のようです。

ところで:

最初は18個のブールフィールドを作成することを検討していました。

どこ? person関係で?

しかし、これは非効率的です。

非効率的ではありません。赤いニシンです。

最初にデザインを正規化します。処理するデータについての経験を積んだら、最適化(非正規化を伴う場合があります)の導入について考えることができます。プリエンプティブな最適化は、これらの恐ろしいアイデアの1つにすぎません。

8
Monitor13

最初は18のブールフィールドを作成することを検討しましたが、これは非効率的です。

「効率的」とは、ストレージサイズを意味する場合、18個のboolean列は、可能な最も効率的なソリューションの1つです。 boolean列は1バイトを占有し、アライメントパディングは必要ありません。該当しない年齢がNULLである可能性がある場合、通常はそれより少なくなります。

ストレージの「効率的な」方法は少しだけありますが、管理は簡単ではありません。特に、ビットストリングbit(18)の場合、またはプレーンなinteger(最大32ビットの情報に適しています)。

これらのオプションの詳細な説明:

ビット文字列を処理する方法:

それは本当にあなたのユースケースの詳細に依存します。重要な質問:0〜17以外の関連する値が存在する可能性はありますか?

2

より適切な回答を得るために質問に追加する必要がある重要な情報が2つあります。

  1. データのクエリはどのように行われると予想されますか?アプリケーションでデータをどのように使用するかによって、データの保存方法に大きな違いが生まれます。
  2. 人が教えることができる年齢の範囲を支配するビジネスルールは何ですか?誰かが「2、3、5、15、16、17歳など、完全に任意の年齢を教えることができるが、他の年齢は教えることができない」という可能性はありますか、それとも常に単一の範囲(0-17、2-33、 15-17など)?

上記のポイント2の後者のオプションを想定すると、個人行の値として個人が教えることができる最小年齢と最大年齢を保存することは、おそらく最良のクロスプラットフォームオプションです。この場合、8歳の子供を教えることができる人を見つけるには、WHERE lowestCanTeach<=8 AND highestCanTeach>=8。クロスプラットフォームの互換性について心配していない場合、postgresは範囲を特定のデータ型としてサポートします( 公式ドキュメント を参照)。これを正しく使用すると、モデルの実装が容易になり、効率が向上します。

複数の年齢範囲が必要な場合は、個別の年齢範囲を指定できるテーブル(列PersonID/lowestCanTeach/highestCanTeachを含む)、butが必要な場合そのため、おそらく固定の年齢範囲(保育園の年齢、GCSEレベルの年齢など)をモデル化していることになります。そうであれば、人々を教育のクラスにリンクし、年齢範囲を関連付けることにより、そのモデルをデータモデルに反映する必要があります。直接人の代わりに教育の種類で。

このうさぎの穴をさらに下っていくと、被験者もモデル化する必要があるかもしれません。ある人は、GCSEレベル(16/17/18)で特定の被験者(たとえば、数学と物理学)だけを教えることができるかもしれません。レベル(13/14/15歳)、そしてより若いグループまでのより広い範囲。

もちろん、年齢ごとに個別のブール値は機能しますが、reallyは、各年を本当に区別できるプロパティでない限り、「クリーン」に感じません。他の年のいずれかを教えることができるから。

1
David Spillett

これは分析上の問題であるため、ここでは次元設計を使用する必要があります。データを正規化して保存しますが、ビューまたはマテリアライズドビューを使用して、必要な列を提供します

0
Neil McGuigan