Як вставити рядок, який містить зовнішній ключ?


54

Використання PostgreSQL v9.1. У мене є такі таблиці:

CREATE TABLE foo
(
    id BIGSERIAL     NOT NULL UNIQUE PRIMARY KEY,
    type VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE bar
(
    id BIGSERIAL NOT NULL UNIQUE PRIMARY KEY,
    description VARCHAR(40) NOT NULL UNIQUE,
    foo_id BIGINT NOT NULL REFERENCES foo ON DELETE RESTRICT
);

Скажіть, перша таблиця fooзаповнена так:

INSERT INTO foo (type) VALUES
    ( 'red' ),
    ( 'green' ),
    ( 'blue' );

Чи є спосіб вставити рядки в barлегко, посилаючись на fooтаблицю? Або я повинен зробити це в два етапи, спочатку шукаючи потрібний fooтип, а потім вставляючи новий рядок у bar?

Ось приклад псевдо-коду, що показує, що я сподівався зробити:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     SELECT id from foo WHERE type='blue' ),
    ( 'another row', SELECT id from foo WHERE type='red'  );

Відповіді:


67

Ваш синтаксис майже хороший, потребує певних дужок навколо підзапитів, і він буде працювати:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     (SELECT id from foo WHERE type='blue') ),
    ( 'another row', (SELECT id from foo WHERE type='red' ) );

Тестовано на SQL-Fiddle

Інший спосіб, із скороченням синтаксису, якщо ви маєте багато значень для вставки:

WITH ins (description, type) AS
( VALUES
    ( 'more testing',   'blue') ,
    ( 'yet another row', 'green' )
)  
INSERT INTO bar
   (description, foo_id) 
SELECT 
    ins.description, foo.id
FROM 
  foo JOIN ins
    ON ins.type = foo.type ;

Я прочитав це кілька разів, але тепер я розумію, що 2-е рішення ви надали. Мені це подобається. Використовуючи його зараз для завантаження моєї бази даних з кількома відомими значеннями, коли система вперше з'являється.
Stéphane

37

Простий ВСТУП

INSERT INTO bar (description, foo_id)
SELECT val.description, f.id
FROM  (
   VALUES
      (text 'testing', text 'blue')  -- explicit type declaration; see below
    , ('another row', 'red' )
    , ('new row1'   , 'purple')      -- purple does not exist in foo, yet
    , ('new row2'   , 'purple')
   ) val (description, type)
LEFT   JOIN foo f USING (type);
  • Використання LEFT [OUTER] JOINнатомість [INNER] JOINозначає, що рядки з val не випадають, коли не знайдено відповідності foo. Натомість NULLвводиться для foo_id.

  • VALUESВираз підзапиту робить те ж саме , як @ ypercube в КТР. Загальні табличні вирази пропонують додаткові функції та їх легше читати у великих запитах, але вони також є оптимізаційними бар'єрами. Таким чином, підзапити, як правило, трохи швидші, коли нічого з перерахованого вище не потрібно.

  • idяк назва стовпця - це широко розповсюджена антидіаграма. Повинно бути foo_idі bar_idчи що - небудь описовий характер. Приєднавшись до маси таблиць, ви закінчуєте декілька стовпців, названих id...

  • Розгляньте просту textчи varcharзамість varchar(n). Якщо вам дійсно потрібно накласти обмеження довжини, додайте CHECKобмеження:

  • Можливо, вам потрібно буде додати явні типи ролі. Оскільки VALUESвираз не прив’язаний безпосередньо до таблиці (наприклад, у INSERT ... VALUES ...), типи неможливо отримати, а типи даних за замовчуванням використовуються без явного оголошення типу, яке може працювати не у всіх випадках. Досить зробити це в першому ряду, решта ляже в ряд.

ВСТАВИТИ одночасно відсутні рядки FK

Якщо ви хочете створити неіснуючі записи fooна ходу, в одному операторі SQL CTE є інструментальним:

WITH sel AS (
   SELECT val.description, val.type, f.id AS foo_id
   FROM  (
      VALUES
         (text 'testing', text 'blue')
       , ('another row', 'red'   )
       , ('new row1'   , 'purple')
       , ('new row2'   , 'purple')
      ) val (description, type)
   LEFT   JOIN foo f USING (type)
   )
, ins AS ( 
   INSERT INTO foo (type)
   SELECT DISTINCT type FROM sel WHERE foo_id IS NULL
   RETURNING id AS foo_id, type
   )
INSERT INTO bar (description, foo_id)
SELECT sel.description, COALESCE(sel.foo_id, ins.foo_id)
FROM   sel
LEFT   JOIN ins USING (type);

Зверніть увагу на два нові рядки манекена, які потрібно вставити. Обидва - фіолетові , яких ще немає в Росії foo. Два ряди, щоб проілюструвати необхідність DISTINCTу першому INSERTтвердженні.

Покрокове пояснення

  1. 1-й CTE selнадає кілька рядків вхідних даних. Підзапит valз VALUESвиразом можна замінити таблицею або підзапитом як джерелом. Негайно, LEFT JOINщоб fooдодати foo_idпопередньо існуючі typeрядки. Усі інші ряди виходять foo_id IS NULLтаким чином.

  2. Другий КТР insвставляє різні нові типи ( foo_id IS NULL) в foo, і повертає знову генеруватися foo_id- разом з typeприєднатися назад вставити рядки.

  3. Останній зовнішній INSERTвигляд тепер може вставити foo.id для кожного рядка: або тип, який існував раніше, або він був вставлений на етапі 2.

Строго кажучи, обидві вставки відбуваються "паралельно", але оскільки це одне твердження, FOREIGN KEYобмеження за замовчуванням не будуть скаржитися. Референтна цілісність виконується в кінці заяви за замовчуванням.

SQL Fiddle для Postgres 9.3. (Те саме працює в 9.1.)

Існує крихітна умова перегонів, якщо ви одночасно запускаєте кілька цих запитів. Детальніше читайте під пов’язаними питаннями тут і тут і тут . Дійсно відбувається лише під великим одночасним навантаженням, якщо і коли-небудь. У порівнянні з рішеннями кешування, як рекламоване в іншій відповіді, шанс надзвичайно малий .

Функція для багаторазового використання

Для повторного використання я створив би функцію SQL, яка приймає масив записів як параметр і використовує unnest(param)замість VALUESвиразу.

Або якщо синтаксис масивів записів занадто безладний для вас, використовуйте рядок, розділений комами, як параметр _param. Наприклад, форма:

'description1,type1;description2,type2;description3,type3'

Потім використовуйте це, щоб замінити VALUESвираз у наведеному вище твердженні:

SELECT split_part(x, ',', 1) AS description
       split_part(x, ',', 2) AS type
FROM unnest(string_to_array(_param, ';')) x;


Функція з UPSERT в Postgres 9.5

Створіть спеціальний тип рядка для проходження параметра. Ми могли б обійтися і без цього, але це простіше:

CREATE TYPE foobar AS (description text, type text);

Функція:

CREATE OR REPLACE FUNCTION f_insert_foobar(VARIADIC _val foobar[])
  RETURNS void AS
$func$
   WITH val AS (SELECT * FROM unnest(_val))    -- well-known row type
   ,    ins AS ( 
      INSERT INTO foo AS f (type)
      SELECT DISTINCT v.type                   -- DISTINCT!
      FROM   val v
      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
      RETURNING f.type, f.id
      )
   INSERT INTO bar AS b (description, foo_id)
   SELECT v.description, COALESCE(f.id, i.id)  -- assuming most types pre-exist
   FROM        val v
   LEFT   JOIN foo f USING (type)              -- already existed
   LEFT   JOIN ins i USING (type)              -- newly inserted
   ON     CONFLICT (description) DO UPDATE     -- description already exists
   SET    foo_id = excluded.foo_id             -- real UPSERT this time
   WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
$func$  LANGUAGE sql;

Виклик:

SELECT f_insert_foobar(
     '(testing,blue)'
   , '(another row,red)'
   , '(new row1,purple)'
   , '(new row2,purple)'
   , '("with,comma",green)'  -- added to demonstrate row syntax
   );

Швидкий та надійний для середовищ із одночасними транзакціями.

Окрім запитів вище, це ...

  • ... застосовується SELECTабо INSERTвмикається foo: все, typeщо не існує у таблиці FK, все-таки вставлено. Припускаючи, що більшість типів існують. Щоб бути абсолютно впевненим і виключати умови перегонів, наявні нам потрібні рядки заблоковані (щоб одночасні транзакції не могли перешкоджати). Якщо це занадто параноїчно для вашого випадку, ви можете замінити:

      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows

    з

      ON     CONFLICT(type) DO NOTHING
  • ... застосовується INSERTабо UPDATE(справжнє "UPSERT") на bar: Якщо descriptionвже існує type, оновиться:

      ON     CONFLICT (description) DO UPDATE     -- description already exists
      SET    foo_id = excluded.foo_id             -- real UPSERT this time
      WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed

    Але лише якщо typeнасправді зміниться:

  • ... передає значення як добре відомі типи рядків з VARIADICпараметром. Зверніть увагу на максимум 100 параметрів! Порівняйте:

    Існує багато інших способів проходження декількох рядків ...

Пов'язані:


У вашому INSERT missing FK rows at the same timeприкладі, чи введення цього в транзакцію зменшить ризик перегонів на SQL Server?
елемент11

1
@ element11: Відповідь стосується Postgres, але оскільки ми говоримо про одну команду SQL, це будь-яка транзакція в будь-якому випадку. Виконання його всередині більшої транзакції лише збільшить часовий вікн для можливих умов гонки. Що стосується SQL Server: CTE, що змінюють дані, взагалі не підтримуються (лише SELECTвсередині WITHпункту). Джерело: Документація МС.
Ервін Брандстеттер

1
Ви також можете зробити це з INSERT ... RETURNING \gsetв psqlтой використовувати повернені значення , як PSQL :'variables', але це працює тільки для окремих вставок рядків.
Крейг Рінгер

@ErwinBrandstetter це чудово, але я занадто новий, щоб sql, щоб усе це зрозуміти, чи можете ви додати деякі коментарі до "ВСТАВКИ відсутніх рядків FK одночасно", пояснюючи, як це працює? також дякую за робочі приклади SQLFiddle!
glallen

@glallen: я додав покрокове пояснення. Також є багато посилань на відповідні відповіді та посібник із додатковими поясненнями. Вам потрібно зрозуміти, що робить запит або у вас може бути над головою.
Ервін Брандстеттер

4

Пошук. В основному вам потрібні foo id, щоб вставити їх у бар.

Не специфічні постгреси, btw. (і ви не позначали це так) - це, як правило, працює SQL. Тут немає ярликів.

Однак, мудре застосування, можливо, у пам'яті може бути кеш елементів Foo. Мої таблиці часто містять до 3 унікальних полів:

  • Ідентифікатор (ціле число чи щось таке), що є первинним ключем рівня таблиці.
  • Ідентифікатор - це GUID, який використовується як стабільний рівень програми для ідентифікації ID (і може піддаватися дії клієнта в URL-адресі тощо)
  • Код - рядок, який може бути там і повинен бути унікальним, якщо він є (сервер sql: відфільтрований унікальний індекс не нуль). Це ідентифікатор набору клієнтів.

Приклад:

  • Рахунок (у додатку для торгівлі) -> Id - це int, що використовується для зовнішніх ключів. -> Ідентифікатор - це керівництво та використовується на веб-порталах тощо - завжди приймається. -> Код встановлюється вручну. Правило: після встановлення це не змінюється.

Очевидно, що коли ви хочете щось пов’язати з обліковим записом - спочатку ви повинні технічно отримати Id - але, якщо ідентифікатор, так і код ніколи не змінюються, коли вони є, позитивний кеш у пам'яті може зупинити більшість пошукових запитів від ударів по базі даних.


10
Ви знаєте, що можете дозволити RDBMS робити пошук за вами в одному операторі SQL, уникаючи кешованих помилок кеш?
Ервін Брандстеттер

Вам відомо, що пошук елементів, що не змінюються, не схильний до помилок? Також, як правило, RDBMS не є масштабованим та найдорожчим елементом у грі через витрати на ліцензування. Взяти з нього якомога більше навантаження - це не зовсім погано. Крім того, не багато ORM підтримують, що для початку.
TomTom

14
Незмінні елементи? Найдорожчий елемент? Витрати на ліцензування (для PostgreSQL)? ОРМ, що визначають, що є здоровим? Ні, я не знав про все це.
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.