如何在关系数据库中表示离散年龄?

Ale*_*lex 5 postgresql database-design relational-theory

我正在使用 PostgreSQL 在 Person 上存储数据。我需要存储每个人是否有能力教 0-17 岁每个年龄段的孩子。年龄是离散值,一个人可以将 18 岁(假设为 0-17 岁)中的任意数字分配给他们的帐户。这将用于预订系统,也需要构成搜索结果的一部分。

最初我考虑创建 18 个布尔字段,但这似乎效率低下。有没有更好的方法来做到这一点?我知道 Postgres 支持 JSONB,所以这是一个选项,但我不确定其含义。

有没有更好的方法来存储这些数据?

Mon*_*r13 9

年龄是离散值

好的。

一个人可以将任意数量的 18(假设 0-17 岁)分配到他们的帐户。

所以这是一个多对多的关系?

如果是这样,您只需像往常一样将数据分解为第三范式,通过一种额外的关系来表达基数。

示例如下。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
);
Run Code Online (Sandbox Code Playgroud)

数据

-- 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 ?
Run Code Online (Sandbox Code Playgroud)

逻辑

让我们得到一份关于可以教授任何课程的人的报告

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;
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+----------------+
| name     | classes        |
+----------+----------------+
| Alice    | 0,1,2,3,7,8    |
| Bertrand | 7,8,9,13,16,17 |
+----------+----------------+
Run Code Online (Sandbox Code Playgroud)

让我们得到一份关于谁可以教谁的报告,如果有的话

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;
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+----------------+
| name     | classes        |
+----------+----------------+
| Alice    | 0,1,2,3,7,8    |
| Bertrand | 7,8,9,13,16,17 |
| Carlos   | NULL           |
+----------+----------------+
Run Code Online (Sandbox Code Playgroud)

谁教第三班?

SELECT persons.name
FROM persons
    INNER JOIN persons_pupilClasses
        ON persons.personId = persons_pupilClasses.personId 
WHERE persons_pupilClasses.age = 3;
Run Code Online (Sandbox Code Playgroud)

结果:

+-------+
| name  |
+-------+
| Alice |
+-------+
Run Code Online (Sandbox Code Playgroud)

谁可以教 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);
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+
| name     |
+----------+
| Alice    |
| Bertrand |
+----------+
Run Code Online (Sandbox Code Playgroud)

谁能教大码?

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';
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+
| name     |
+----------+
| Bertrand |
+----------+
Run Code Online (Sandbox Code Playgroud)

虽然您在描述中没有提及,但我敢打赌,在您的实际业务场景中,至少有一个更重要的关系:学生。他们可能会从一个班级转到下一个班级。表示这种关系的一种简单方法是:

-- 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);
Run Code Online (Sandbox Code Playgroud)

以及一些逻辑示例:

谁教谁?

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;
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+----------+
| teacher  | pupil    |
+----------+----------+
| Alice    | Donald   |
| Bertrand | Donald   |
| Alice    | Ernest   |
| Bertrand | Ernest   |
| Bertrand | Frank    |
| Alice    | Gertrude |
| Bertrand | Hans     |
+----------+----------+
Run Code Online (Sandbox Code Playgroud)

每个人的学生是谁?

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;
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+--------------------------+
| teacher  | pupils                   |
+----------+--------------------------+
| Alice    | Donald,Ernest,Gertrude   |
| Bertrand | Donald,Ernest,Frank,Hans |
+----------+--------------------------+
Run Code Online (Sandbox Code Playgroud)

每个学生的老师是谁?

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;
Run Code Online (Sandbox Code Playgroud)

结果:

+----------+----------------+
| pupil    | teachers       |
+----------+----------------+
| Donald   | Alice,Bertrand |
| Ernest   | Alice,Bertrand |
| Frank    | Bertrand       |
| Gertrude | Alice          |
| Hans     | Bertrand       |
+----------+----------------+
Run Code Online (Sandbox Code Playgroud)

从这里应该清楚以下几点:

  • age 可能有点用词不当,它可能与参加这些课程的学生的实际自然年龄不符。

  • 避免将其视为连续范围的诱惑可能是一个好主意。这就是为什么我引用你的说法,这些是离散值

其余的,除非我误解了,否则似乎是一个相当简单的归一化问题。

顺便一提:

最初我考虑创建 18 个布尔字段,

在哪里?在person关系中?

但这似乎效率低下。

这不是低效的。这是一条红鲱鱼。

首先标准化您的设计。一旦您获得了将要处理的数据的经验,您就可以开始考虑引入优化(可能涉及非规范化)。不过,先发制人的优化只是这些可怕的想法之一。


Dav*_*ett 1

您应该在问题中添加两个关键信息以获得更好的答案:

  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 列),如果您允许这样做,那么您可能正在建模固定年龄范围(幼儿园年龄, GCSE 水平年龄等),如果是这样,您应该在数据模型中反映该模型,将人员与教学班级联系起来,并将年龄范围与教学类型而不是直接与人相关联。

进一步深入这个兔子洞,你可能还需要对科目进行建模:一个人可能只能在 GCSE 级别(16/17/18)教授特定科目(例如数学和物理),在初中教授更广泛的科目级别(13/14/15 岁),范围更广,涵盖更年轻的群体。

当然,每个年龄段都有一个单独的布尔值,但确实感觉不“干净”,除非能够每年教学确实与能够教学任何其他年份是不同的属性。