【问题标题】:One to One relationship many tables一对一关系多表
【发布时间】:2018-01-29 15:38:27
【问题描述】:

我是使用数据库的新手,我正在尝试设计一个新的数据库,我认为我需要在许多表中建立一对一的关系。

为了演示我的设计,假设我正在构建一个时间表数据库作为示例。我首先为具有一对多关系的人创建一个表

CREATE TABLE person (
    person_id SERIAL NOT NULL, 
    name VARCHAR,
    PRIMARY KEY (person_id)
);

接下来,我创建一个事件表,其中包含人员关系的许多部分

CREATE TABLE events (
    event_id SERIAL NOT NULL, 
    type VARCHAR,
    name VARCHAR,
    person_id INTEGER,
    time TIMESTAMP WITHOUT TIME ZONE,
    PRIMARY KEY (event_id),
    FOREIGN KEY(person_id) REFERENCES person (person_id)
);

现在假设我有两种不同类型的事件,它们有不同的信息,比如吃饭和作业

CREATE TABLE meals (
    event_id INTEGER NOT NULL, 
    food VARCHAR,
    utensils VARCHAR,
    PRIMARY KEY (event_id),
    FOREIGN KEY(event_id) REFERENCES events (event_id)
);

CREATE TABLE homework (
    event_id INTEGER NOT NULL, 
    subject VARCHAR,
    completed BOOLEAN,
    score FLOAT,
    PRIMARY KEY (event_id),
    FOREIGN KEY(event_id) REFERENCES events (event_id)
);

现在,我尝试以这种方式设计数据库的原因是,有时您可能只想显示每个人的基本事件列表,而不管该事件是什么。例如,如果我按如下方式初始化表

INSERT INTO person (name) VALUES ('Brad');
INSERT INTO events (type, name, person_id, time) VALUES ('meal', 'lunch', 1, '12/28/2016 12:00:00')    
INSERT INTO events (type, name, person_id, time) VALUES ('meal', 'breakfast', 1, '12/28/2016 12:00:00');
INSERT INTO meals (event_id, food, utensils) VALUES (1, 'eggs', 'fork');
INSERT INTO meals (event_id, food, utensils) VALUES (2, 'turkey sandwich', 'hands');
INSERT INTO events (type, name, person_id, time) VALUES ('homework', 'final project', 1, '12/28/2016 18:00:00');
INSERT INTO homework (event_id, subject, completed, score) VALUES (3, 'Math', 'T', 0.93);

然后我可能想为 Brad 生成所有事件的列表

SELECT (events.time, events.type, events.name) FROM events 
LEFT JOIN person ON person.person_id = events.person_id 
WHERE person.name = 'Brad';

这很简单,我很困惑的是,如果我想看看 Brad 吃了什么怎么办。我想我可能可以在personeventseventsmeals 之间使用两个JOIN 语句,但是如果我只想浏览Brads 事件并获取有关每个事件的所有额外信息怎么办? (例如,如果活动是一顿饭,告诉我他吃了什么,如果是家庭作业,告诉我他得到的分数)?

总的来说,我有几个问题。

  1. 这是一个好的数据库设计还是我应该考虑的其他问题?上面的每个潜在用例,再加上几个,都是我需要使用数据库的标准。
  2. 如何轻松确定在哪个表中查找事件表中任何给定事件的更多信息?这里有几个想法——我可以将包含有关事件的更多信息的另一个表的名称存储在事件表中(即,将type 列替换为table 列)但我想我在某处读到是个坏主意。

还有一些注意事项,我正在使用 Postgresql 作为数据库。除了我在这里展示的内容之外,我正在构建的实际数据库中的每个表都有更多详细信息。我只是想说明我想要达到的目的。最后,我正在使用 sqlalchemy 的 ORM 构建/访问数据库,所以如果有一个漂亮的技巧我可以使用 relationships 来做这将有助于了解这一点。

【问题讨论】:

  • 好问题,解释清楚。据我了解,我认为您的数据库设计没有那么糟糕,不过我想看看其他人的想法。

标签: sql database postgresql sqlalchemy


【解决方案1】:

如果您想获取每个事件的所有详细信息,您将遇到问题,因为保存事件详细信息的表具有不同类型的列。而且您当然不想在代码中对各种事件详细信息表名称进行硬编码,毕竟当您想要添加或删除表或更改名称时会发生什么?你必须到处更新你的代码!

所以首先我会说你会想看这里。比如:

CREATE OR REPLACE VIEW event_details AS
    SELECT * FROM meals
    UNION ALL
    SELECT * FROM homework;

这将允许您一次选择所有事件类型的详细信息,例如

SELECT * FROM event_details WHERE event_id IN (
    SELECT event_id FROM events WHERE person_id = (
        SELECT person_id
        FROM person
        WHERE name = 'Brad'
    )
)

当然不行,因为表结构不同。因此,您需要找到某种方法以统一的方式表示数据;例如,对每条记录执行ROW_TO_JSON

CREATE OR REPLACE VIEW event_details AS
    SELECT ROW_TO_JSON(meals.*) AS details FROM meals
    UNION ALL
    SELECT ROW_TO_JSON(homework.*) AS details FROM homework;

现在这个查询:

SELECT * FROM event_details WHERE (details->>'event_id')::INTEGER IN (
    SELECT event_id FROM events WHERE person_id = (
        SELECT person_id
        FROM person
        WHERE name = 'Brad'
    )
)

给你:

{"event_id":1,"food":"eggs","utensils":"fork"}
{"event_id":2,"food":"turkey sandwich","utensils":"hands"}
{"event_id":3,"subject":"Math","completed":true,"score":0.93}

然后您可以解析 JSON 并用它做您想做的事。当您想要添加、删除或重命名表格时,您只能在视图中执行此操作。

现在请注意,我并不是说这是一个很好的(或唯一的)方法。我不清楚每个事件类型有一个单独的表,而不是只有一个 events 表并将特定于类型的数据放在 JSONB 字段中。这将使查询变得更容易和更快,并且如果您使用 JSONB,也可以对特定于类型的数据进行索引。根据您展示的示例,我认为这将是一个更好的设计。

【讨论】:

  • 感谢您的详细回复。我想我可以看到拥有单个事件表的好处,其中所有公共列都是标准列,然后将剩余数据放入 JSONB 字段。快速获取事件列表非常容易,然后应用程序可以决定是否需要额外的数据。这种架构有什么缺点吗?
  • 我无法真正评论缺点,因为我在这方面没有太多经验。我可以告诉你,以难以查询或查询缓慢的方式存储数据块肯定很糟糕,但 JSONB 解决了这两个问题。一般来说,我总是更喜欢平面表结构而不是存储大量 JSONB 数据,并且肯定可以选择在单个事件表中包含更多“详细”字段,并且只从与类型相关的字段中读取数据。但是,如果您将查询的大部分数据作为普通字段,然后将少量数据存储为 JSONB,我会很高兴的。
  • 好的,考虑一下。我将在剩下的时间里不回答这个问题,看看是否还有其他想法/解决方案,然后明天接受答案。
【解决方案2】:

所以@eurotrash 的回答很好地回答了我提出的问题,所以我接受了他的正确答案,但根据他的回答,我想出了我想要的更好的数据库设计分享以防其他人有类似的问题。从本质上讲,我们将删除events 表,而是创建一个物化视图来表示事件信息。首先,我们需要修改mealshomework 表的设置方式,并包含以前在events 表中的信息

CREATE TABLE meals (
    meal_id SERIAL NOT NULL, 
    name VARCHAR,
    person_id INTEGER,
    time TIMESTAMP WITHOUT TIME ZONE,
    food VARCHAR,
    utensils VARCHAR,
    PRIMARY KEY (meals_id),
    FOREIGN KEY(person_id) REFERENCES person (person_id)
);

CREATE TABLE homework (
    homework_id SERIAL NOT NULL, 
    name VARCHAR,
    person_id INTEGER,
    time TIMESTAMP WITHOUT TIME ZONE,
    subject VARCHAR,
    completed BOOLEAN,
    score FLOAT,
    PRIMARY KEY (homework_id),
    FOREIGN KEY(person_id) REFERENCES person (person_id)
);

现在,我们可以使用以下方法初始化我们的数据库:

INSERT INTO person (name) VALUES ('Brad');
INSERT INTO meals (name, person_id, time, food, utensils) VALUES ('breakfast', 1, '12/28/2016 6:00:00', 'eggs', 'fork');
INSERT INTO meals (name, person_id, time, food, utensils) VALUES ('lunch', 1, '12/28/2016 12:00:00', 'turkey sandwich', 'hands');
INSERT INTO homework (name, person_id, time, subject, completed, score) VALUES ('final project', 1, '12/28/2016 18:00:00', 'Math', 'T', 0.93);

然后使用共同信息创建一个新的材质视图

CREATE MATERIALIZED VIEW events AS 
SELECT meal_id as id, 'meals' as table, name, person_id, time FROM meals
UNION ALL
SELECT homework_id as id, 'homework' as table, name, person_id, time from homework;

给了

 id |  table   |     name      | person_id |        time
----+----------+---------------+-----------+---------------------
  1 | meals    | breakfast     |         1 | 2016-12-28 06:00:00
  2 | meals    | lunch         |         1 | 2016-12-28 12:00:00
  1 | homework | final project |         1 | 2016-12-28 18:00:00

最后,为了确保events 视图始终是最新的,我们可以创建触发器以在mealshomework 根据https://stackoverflow.com/a/23963969/3431189 更改时更新视图

CREATE OR REPLACE FUNCTION refresh_events_view()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
    REFRESH MATERIALIZED VIEW events;
    RETURN null;
end $$;

CREATE TRIGGER refresh_events_view
AFTER INSERT or UPDATE or DELETE or TRUNCATE
ON meals FOR EACH STATEMENT
EXECUTE PROCEDURE refresh_events_view();

CREATE TRIGGER refresh_events_view
AFTER INSERT or UPDATE or DELETE or TRUNCATE
ON homework FOR EACH STATEMENT
EXECUTE PROCEDURE refresh_events_view();

这为我们提供了两全其美的优势(至少在我看来),因为 mealshomework 的每个特定字段仍然存在,而且我们仍然可以获得始终保持最新的事件“表”可以用来快速查询我们是否只需要每个事件的基本信息(即名称、时间等)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-27
    • 2011-03-30
    • 1970-01-01
    • 2021-07-11
    • 2018-10-03
    • 1970-01-01
    相关资源
    最近更新 更多