Как реализовать отношение «многие ко многим» в PostgreSQL?

102

Я считаю, что название говорит само за себя. Как создать структуру таблицы в PostgreSQL, чтобы установить связь «многие ко многим».

Мой пример:

Product(name, price);
Bill(name, date, Products);
Раду Георгиу
источник
2
удалите продукты из таблицы счетов, создайте новую таблицу с именем «bill_products» с двумя полями: одно указывает на продукты, другое указывает на счет. сделайте эти два поля первичным ключом этой новой таблицы.
Marc B
Итак, bill_products (счет, товары); ? И они оба ПК?
Radu Gheorghiu
1
да уж. по отдельности они будут FK, указывающим на свои таблицы, и вместе они будут PK для новой таблицы.
Marc B
Итак, bill_product (product ссылается на product.name, на Bill ссылается на bill.name, (product, bill) первичный ключ)?
Radu Gheorghiu
Они укажут, какими будут поля PK в таблицах Product и Bill.
Marc B

Ответы:

307

Операторы SQL DDL (язык определения данных) могут выглядеть следующим образом:

CREATE TABLE product (
  product_id serial PRIMARY KEY  -- implicit primary key constraint
, product    text NOT NULL
, price      numeric NOT NULL DEFAULT 0
);

CREATE TABLE bill (
  bill_id  serial PRIMARY KEY
, bill     text NOT NULL
, billdate date NOT NULL DEFAULT CURRENT_DATE
);

CREATE TABLE bill_product (
  bill_id    int REFERENCES bill (bill_id) ON UPDATE CASCADE ON DELETE CASCADE
, product_id int REFERENCES product (product_id) ON UPDATE CASCADE
, amount     numeric NOT NULL DEFAULT 1
, CONSTRAINT bill_product_pkey PRIMARY KEY (bill_id, product_id)  -- explicit pk
);

Я сделал несколько корректировок:

  • Отношение n: m обычно реализуется в отдельной таблице - bill_productв этом случае.

  • Я добавил serialстолбцы в качестве суррогатных первичных ключей . В Postgres 10 или новее вместо этого рассмотрите IDENTITYстолбец . Увидеть:

    Я очень рекомендую это, потому что название продукта вряд ли уникально (не лучший «естественный ключ»). Кроме того, обеспечение уникальности и ссылка на столбец во внешних ключах обычно дешевле для 4-байтовых integer(или даже 8-байтовых bigint), чем для строки, хранящейся как textили varchar.

  • Не используйте имена основных типов данных в dateкачестве идентификаторов . Хотя это возможно, это плохой стиль и приводит к запутанным ошибкам и сообщениям об ошибках. Используйте допустимые идентификаторы в нижнем регистре без кавычек . Никогда не используйте зарезервированные слова и по возможности избегайте двойных кавычек идентификаторов в смешанном регистре.

  • «имя» - плохое имя. Я переименовал столбец таблицы productв product( product_nameили аналогичный). Это лучшее соглашение об именах . В противном случае, когда вы присоединитесь пару таблиц в запросе - что вы делаете много в реляционной базе данных - вы в конечном итоге с несколькими столбцами под названием «имя» и должны использовать псевдонимы столбцов , чтобы разобраться в беспорядок. Это бесполезно. Другой распространенный анти-шаблон - это просто «id» в качестве имени столбца.
    Я не уверен, как billбудет называться. bill_idв этом случае, вероятно, будет достаточно.

  • priceимеет тип данных numeric для хранения дробных чисел точно в том виде, в каком они были введены (тип произвольной точности вместо типа с плавающей запятой). Если вы имеете дело исключительно с целыми числами, сделайте это integer. Например, вы можете сохранить цены в центах .

  • amount( "Products"В вашем вопросе) переходит в связующей таблице bill_productи типа , numericа также. Опять же, integerесли вы имеете дело исключительно с целыми числами.

  • Вы видите , внешние ключи в bill_product? Я создал и изменения каскадных: ON UPDATE CASCADE. Если product_idили bill_idдолжен измениться, это изменение каскадно распространяется на все зависимые записи, bill_productи ничего не прерывается. Это просто ссылки, не имеющие самостоятельного значения.
    Я также использовал ON DELETE CASCADEдля bill_id: Если счет удаляется, его детали умирают вместе с ним.
    Не так для продуктов: вы не хотите удалять продукт, который используется в счете. Postgres выдаст ошибку, если вы попытаетесь это сделать. productВместо этого вы должны добавить еще один столбец, чтобы пометить устаревшие строки («мягкое удаление»).

  • Все столбцы в этом базовом примере заканчиваются NOT NULL, поэтому NULLзначения не допускаются. (Да, все столбцы - столбцы первичного ключа определяются UNIQUE NOT NULLавтоматически.) Это потому, что NULLзначения не имеют смысла ни в одном из столбцов. Облегчает жизнь новичку. Но ты не уйдешь так легко, вы должны понимать , NULLобработка в любом случае. Дополнительные столбцы могут разрешать NULLзначения, функции и объединения могут вводить NULLзначения в запросы и т. Д.

  • Прочтите главу CREATE TABLEв руководстве .

  • Первичные ключи реализуются с уникальным индексом в ключевых столбцах, что позволяет быстро выполнять запросы с условиями в столбцах PK. Однако последовательность ключевых столбцов важна в многоколоночных ключах. Поскольку ПК на bill_productна (bill_id, product_id)в моем примере, вы можете добавить еще один индекс на раз product_idили (product_id, bill_id)если у вас есть запросы ищут данный product_idи нет bill_id. Увидеть:

  • Прочтите главу об указателях в руководстве .

Эрвин Брандштеттер
источник
Как я могу создать индекс для таблицы сопоставления bill_product? Обычно это должно выглядеть так: CREATE INDEX idx_bill_product_id ON booked_rates(bill_id, product_id). Это правильно?
codyLine 05
1
@codyLine: этот индекс создается автоматически ПК.
Эрвин Брандштеттер,
1
@ErwinBrandstetter: Не следует создавать индекс для столбца bill_product для столбца product_id?
Кристиан,
2
@ ChristianB.Almeida: Да, во многих случаях это полезно. Я добавил немного об индексировании.
Эрвин Брандштеттер,
1
@Jakov: Для каждой банкноты в таблице есть только одна строка bill. Нам нужна сумма на добавленный элемент bill_product.
Эрвин Брандштеттер