Как вставить строку, содержащую внешний ключ?

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'  );
Stéphane
источник

Ответы:

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 ;
ypercubeᵀᴹ
источник
Взял его несколько раз, но теперь я понимаю, что второе решение вы предоставили. Мне это нравится. Используя его сейчас, для начальной загрузки моей базы данных с несколькими известными значениями, когда система впервые появляется.
Стефан
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. 2-й CTE 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?
element11
1
@ element11: Ответ для Postgres, но поскольку мы говорим об одной команде SQL, в любом случае это одна транзакция. Выполнение этого в более крупной транзакции только увеличит временное окно для возможных условий гонки. Что касается SQL Server: CTE, модифицирующие данные, вообще не поддерживаются (только SELECTвнутри WITHпредложения). Источник: документация MS.
Эрвин Брандштеттер
1
Вы также можете сделать это с помощью INSERT ... RETURNING \gsetin, psqlзатем использовать возвращаемые значения как psql :'variables', но это работает только для вставок в одну строку.
Крейг Рингер
@ErwinBrandstetter это здорово, но я слишком новичок в sql, чтобы понять все это. Не могли бы вы добавить несколько комментариев к «ВСТАВИТЬ пропущенные строки FK одновременно», объясняющих, как это работает? также, спасибо за рабочие примеры SQLFiddle!
выпал
@glallen: я добавил пошаговое объяснение. Есть также много ссылок на соответствующие ответы и руководство с дополнительными пояснениями. Вы должны понимать, что делает запрос, или вы можете быть над головой.
Эрвин Брандштеттер,
4

Уважать. Вам в основном нужны идентификаторы foo, чтобы вставить их в панель.

Не специфично для postgres, кстати. (и вы не пометили это так) - так обычно работает SQL. Здесь нет ярлыков.

Тем не менее, в приложениях может быть кэш объектов foo в памяти. Мои таблицы часто имеют до 3 уникальных полей:

  • Идентификатор (целое число или что-то), который является первичным ключом уровня таблицы.
  • Идентификатор, который представляет собой GUID, который используется в качестве стабильного идентификатора на уровне приложения (и может быть предоставлен клиенту в URL и т. Д.)
  • Код - строка, которая может быть там и должна быть уникальной, если она там есть (сервер sql: отфильтрованный уникальный индекс не равен нулю). Это идентификатор набора клиентов.

Пример:

  • Аккаунт (в торговом приложении) -> Id - это int, используемый для внешних ключей. -> Идентификатор является Guid и используется в веб-порталах и т. Д. - всегда принимается. -> Код устанавливается вручную. Правило: после установки оно не меняется.

Очевидно, что когда вы хотите связать что-то с учетной записью - сначала вы должны, технически, получить Id - но, учитывая, что и Идентификатор, и Код никогда не меняются, когда они есть, положительный кэш в памяти может остановить большинство поисков от попадания в базу данных.

TomTom
источник
10
Вы знаете, что можете позволить СУБД выполнить поиск за вас в одном операторе SQL, избегая подверженного ошибкам кэша?
Эрвин Брандштеттер
Вы знаете, что поиск неизменяемых элементов не подвержен ошибкам? Кроме того, как правило, СУБД не масштабируется и является самым дорогим элементом в игре из-за затрат на лицензирование. Брать как можно больше нагрузки - это совсем не плохо. Кроме того, не многие ORM поддерживают это для начала.
TomTom
14
Не меняющиеся элементы? Самый дорогой элемент? Стоимость лицензирования (для PostgreSQL)? ОРМ, определяющие, что вменяемое? Нет, я не знал обо всем этом.
Эрвин Брандштеттер