【问题标题】:PostgreSQL - How to JOIN M:M table the right way?PostgreSQL - 如何以正确的方式加入 M:M 表?
【发布时间】:2014-05-12 22:31:37
【问题描述】:

我的数据库结构如下:

CREATE TABLE categories (
    name VARCHAR(30) PRIMARY KEY
);

CREATE TABLE additives (
    name VARCHAR(30) PRIMARY KEY
);

CREATE TABLE beverages (
    name VARCHAR(30) PRIMARY KEY,
    description VARCHAR(200),
    price NUMERIC(5, 2) NOT NULL CHECK (price >= 0),
    category VARCHAR(30) NOT NULL REFERENCES categories(name) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE b_additives_xref (
    bname VARCHAR(30) REFERENCES beverages(name) ON DELETE CASCADE ON UPDATE CASCADE,
    aname VARCHAR(30) REFERENCES additives(name) ON DELETE CASCADE ON UPDATE CASCADE, 
    PRIMARY KEY(bname, aname)
);


INSERT INTO categories VALUES
    ('Cocktails'), ('Biere'), ('Alkoholfreies');

INSERT INTO additives VALUES 
    ('Kaliumphosphat (E 340)'), ('Pektin (E 440)'), ('Citronensäure (E 330)');

INSERT INTO beverages VALUES
    ('Mojito Speciale', 'Cocktail mit Rum, Rohrzucker und Minze', 8, 'Cocktails'),
    ('Franziskaner Weißbier', 'Köstlich mildes Hefeweizen', 6, 'Biere'),
    ('Augustiner Hell', 'Frisch gekühlt vom Fass', 5, 'Biere'),
    ('Coca Cola', 'Coffeeinhaltiges Erfrischungsgetränk', 2.75, 'Alkoholfreies'),
    ('Sprite', 'Erfrischende Zitronenlimonade', 2.50, 'Alkoholfreies'),
    ('Karaffe Wasser', 'Kaltes, gashaltiges Wasser', 6.50, 'Alkoholfreies');

INSERT INTO b_additives_xref VALUES
    ('Coca Cola', 'Kaliumphosphat (E 340)'),
    ('Coca Cola', 'Pektin (E 440)'),
    ('Coca Cola', 'Citronensäure (E 330)');

SqlFiddle

我想要实现的是列出所有饮料及其属性(pricedescription 等)并从b_additives_xref 表中添加另一列additives,该列包含一个包含所有添加剂的串联字符串包含在每种饮料中。

我的查询目前看起来像这样并且几乎可以正常工作(我猜):

SELECT 
    beverages.name AS name, 
    beverages.description AS description, 
    beverages.price AS price,
    beverages.category AS category, 
    string_agg(additives.name, ', ') AS additives 
FROM beverages, additives
    LEFT JOIN b_additives_xref ON b_additives_xref.aname = additives.name 
GROUP BY beverages.name
ORDER BY beverages.category;

输出如下:

Coca Cola       | Coffeeinhaltiges Erfrischungsgetränk | 2.75 | Alkoholfreies | Kaliumphosphat (E 340), Pektin (E 440), Citronensäure (E 330)
Karaffe Wasser  | Kaltes, gashaltiges Wasser           | 6.50 | Alkoholfreies | Kaliumphosphat (E 340), Pektin (E 440), Citronensäure (E 330)
Sprite          | Erfrischende Zitronenlimonade        | 2.50 | Alkoholfreies | Kaliumphosphat (E 340), Pektin (E 440), Citronensäure (E 330)
Augustiner Hell | Frisch gekühlt vom Fass              | 5.00 | Biere         | Kaliumphosphat (E 340)[...]

这当然是错误的,因为 b_additives_xref 表中只有“可口可乐”有现有行。
除了“Coca Cola”行之外,所有其他行在“additives”列中都应该有“null”或“empty field”值。我做错了什么?

【问题讨论】:

  • 提示#1:使用数字 ID 而不是 varchar 列,因为它们用作 FK。
  • +1 表示很好的问题
  • 我首先为主键设置了数字 ID,但有人告诉我 VARCHAR 和 NUMERIC FK 之间的速度并没有真正的区别。这个说法是假的吗?如果不是,那么使用数字标识符列的优势在哪里?
  • 0) 键分离值 1) 空间 2) 固定大小。 3)时间。 ad 12:考虑(多个)FK 指向相同的附加记录 ad0:考虑在(多个)FK 指向一个条目的情况下重命名或修复错字。
  • 好的,我将它切换回 NUMERICs :-) 感谢您提供信息。

标签: sql postgresql join database-design left-join


【解决方案1】:

对你的一些建议

架构

CREATE TABLE category (
   category_id int PRIMARY KEY
  ,category    text UNIQUE NOT NULL
);

CREATE TABLE beverage (
   beverage_id serial PRIMARY KEY
  ,beverage    text UNIQUE NOT NULL  -- maybe not unique?
  ,description text
  ,price       int NOT NULL CHECK (price >= 0)  -- in Cent
  ,category_id int NOT NULL REFERENCES category ON UPDATE CASCADE
                                        -- not: ON DELETE CASCADE 
);

CREATE TABLE additive (
   additive_id serial PRIMARY KEY
  ,additive    text UNIQUE
);

CREATE TABLE bev_add (
    beverage_id int REFERENCES beverage ON DELETE CASCADE ON UPDATE CASCADE
   ,additive_id int REFERENCES additive ON DELETE CASCADE ON UPDATE CASCADE 
   ,PRIMARY KEY(beverage_id, additive_id)
);
  • 切勿使用“名称”作为名称。这是一个糟糕的、不具描述性的名字。
  • 使用小的代理主键,对于大表最好使用serial 列,对于小表最好使用简单的integer。很有可能,饮料和添加剂的名称并不是严格唯一的,您需要不时更改它们,这使得它们成为主键的不良候选者。 integer 列也更小,处理速度更快。
  • 如果您只有少数类别而没有其他属性,请考虑使用 enum
  • 当外键和主键的值相同时,最好使用相同的(描述性)名称。
  • 我从不使用复数形式作为表名,除非单行包含多个实例。更短,只是一个有意义的,将复数形式留给实际的复数行。
  • Just use text instead of character varying (n).
  • 在使用 ON DELETE CASCADE 为查找表定义 fk 约束之前请三思而后行
    通常,如果您(错误地)删除了一个类别,您希望自动删除所有饮料。
  • 考虑一个普通的integer 列而不是NUMERIC(5, 2)(用美分的数字代替€/$)。更小、更快、更简单。 需要时在输出上格式化。

此密切相关的答案中的更多建议和链接:
How to implement a many-to-many relationship in PostgreSQL?

查询

适应新架构和一些一般建议。

SELECT b.*, string_agg(a.additive, ', ' ORDER BY a.additive) AS additives
                                     -- order by optional for sorted list
FROM   beverage      b
JOIN   category      c USING (category_id)
LEFT   JOIN bev_add ba USING (beverage_id)  -- simpler now
LEFT   JOIN additive a USING (additive_id)
GROUP  BY b.beverage_id, c.category_id
ORDER  BY c.category;
  • 如果列名与别名相同,则不需要列别名。
  • 使用建议的命名约定,您可以方便地使用USING in joins
  • 您还需要加入categoryGROUP BY category_idcategory(建议架构的缺点)。
  • 对于大表,查询仍然会更快,因为表更小,索引更小更快,需要读取的页面更少。

【讨论】:

  • 感谢您的友好建议。就像 wildplasser 在上面的评论中建议的那样,我添加了 SERIAL 标识符列。当我注意到你的帖子时,我正要在Code Review 上发帖。我将尝试使用您建议的约定来重构布局,并在我完成后让您知道 [Code Review] 链接。
  • 当我逐点查看您的建议时,我注意到PostgreSQL DOCS 描述SERIAL 等同于col integer DEFAULT nextval('tablename_colname_seq') NOT NULL。您建议将x_id int PRIMARY KEY 用于x_id SERIAL PRIMARY KEY 以上的小桌子。真的有区别吗?当然,在我的布局的示例版本中,category 表具有category_id int PRIMARY KEY,因此没有自动增量。我可能想选择SERIAL,因为可能会添加category 条目并且
  • 经常通过网络界面删除。
  • @phew: int / serial: 数据上没有区别,只是在处理上。对于不定期输入值的小型查找表,您可能不需要serial 的功能。
  • 好的。因此,类别条目可能会发生很大变化,我将坚持使用SERIAL。还有一个问题;使用TEMP 有什么好处? PostgreSQL DOCS 仅声明 TEMP 表存在于特殊模式中。 Google 建议 TEMP 显着提高性能。尝试执行您提供的 CREATEs 我收到一条错误消息,指出 TEMP 表的约束只能引用 TEMP 表(我认为这是因为表 category 不是 TEMP 而表饮料是)。何时使用TEMP
【解决方案2】:

我相信你正在寻找这个

SELECT 
    B.name AS name, 
    B.description AS description, 
    B.price AS price,
    B.category AS category, 
    string_agg(A.name, ', ') AS additives 
FROM Beverages B
    LEFT JOIN b_additives_xref xref ON xref.bname = B.name 
    Left join additives A on A.name = xref.aname
GROUP BY B.name
ORDER BY B.category;

输出

NAME    DESCRIPTION                                 PRICE   CATEGORY        ADDITIVES
Coca Cola   Coffeeinhaltiges Erfrischungsgetränk    2.75    Alkoholfreies   Kaliumphosphat (E 340), Pektin (E 440), Citronensäure (E 330)

问题是您的 beveragesadditives 表之间存在笛卡尔积

FROM beverages, additives

每条记录都与其他记录一样。它们都需要显式连接到外部参照表。

【讨论】:

  • 这不是我想要的,但接近它(我假设)。我希望可口可乐行与所有其他饮料一样显示在您的结果中,每行一个,仅对于那些没有b_additives_xref 表条目的人,列“添加剂”字段应为 NULL。
  • 您的权利。我需要更换饮料和添加剂。我会在可以编辑的时候进行编辑
  • 谢谢你的好先生,我自己用你的查询想通了。只是需要休息一下来回顾一下你在那里做了什么,在处理了很长一段时间之后,我的头都在冒烟。
【解决方案3】:

我正在寻找的查询看起来像:

SELECT 
    B.name AS name, 
    B.description AS description, 
    B.price AS price,
    B.category AS category, 
    string_agg(A.name, ', ') AS additives 
FROM beverages B
    LEFT JOIN b_additives_xref xref ON xref.bname = B.name 
    LEFT JOIN additives A on A.name = xref.aname
GROUP BY B.name
ORDER BY B.category;

感谢 Brad 在他的回答和 cmets 中为我提供了解决方案。

【讨论】:

  • 好吧,我会这样做,但到他编辑帖子之前,他发布的查询并不是 100% 正确的。所以我自己想出来并发布了答案,在我这样做之后,他更正了他的答案并添加了可行的解决方案。
猜你喜欢
  • 2021-06-29
  • 2022-06-12
  • 1970-01-01
  • 2019-03-14
  • 2017-10-07
  • 1970-01-01
  • 2019-01-20
  • 2019-08-16
  • 1970-01-01
相关资源
最近更新 更多