외래 키가 포함 된 행을 어떻게 삽입합니까?


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 ;

몇 번 읽어 보았지만 이제는 두 번째 해결책을 제공했습니다. 나는 그것을 좋아한다. 시스템을 처음 시작할 때 알려진 값으로 데이터베이스를 부트 스트랩하기 위해 지금 사용하십시오.
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] JOINval 이 삭제되지 않습니다foo . 대신에 NULL입력됩니다 foo_id.

  • VALUES하위 쿼리 의 표현식은 @ypercube의 CTE 와 동일 합니다. 공통 테이블 표현식 은 추가 기능을 제공하고 큰 쿼리에서 더 읽기 쉽지만 최적화 장벽으로도 사용됩니다. 따라서 하위 쿼리는 일반적으로 위의 어느 것도 필요하지 않을 때 약간 빠릅니다.

  • id열 이름은 광범위한 안티 패턴입니다. 해야 foo_id하고 bar_id또는 아무것도 설명. 많은 테이블을 조인 할 때 이름이 id... 인 여러 열이 생깁니다 .

  • 대신 text또는 일반을 고려하십시오 . 길이 제한을 실제로 적용해야하는 경우 제약 조건을 추가하십시오 .varcharvarchar(n)CHECK

  • 명시 적 유형 캐스트를 추가해야 할 수도 있습니다. 때문에 VALUES식 직접 (같은 표에 부착되지 않는다 INSERT ... VALUES ...), 유형 도출 될 수없고 기본 데이터 유형은 모든 예에서 작동하지 않을 수있는 타입 선언 명시하지 않고 사용된다. 첫 번째 행에서 충분하면 나머지는 줄을 지어 나옵니다.

INSERT에 FK 행이 동시에 누락되었습니다.

단일 SQL 문foo 에서 존재하지 않는 항목을 즉석 에서 작성하려는 경우 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 인데 아직 존재하지 않습니다 . 첫 번째 진술 의 필요성을 설명하기 위해 행 .DISTINCTINSERT

단계별 설명

  1. 첫 번째 CTE sel는 여러 행의 입력 데이터를 제공합니다. 부질 valVALUES표현은 소스로 테이블 또는 부질로 대체 될 수있다. 즉시 LEFT JOIN하는 foo추가하기 foo_id위해 기존의 type행. 다른 모든 행은 foo_id IS NULL이 방법을 사용합니다.

  2. 두 번째 CTE는 고유 한 새 유형 ( )을에 ins삽입 하고 새로 생성 된 -을 함께 삽입하여 행을 삽입합니다.foo_id IS NULLfoofoo_idtype

  3. 최종 외부 INSERT는 이제 모든 행에 대해 foo.id를 삽입 할 수 있습니다. 유형이 이미 존재하거나 2 단계에서 삽입되었습니다.

엄밀히 말하면 두 인서트는 "병렬로"발생하지만 이것이 단일 명령문이므로 기본 FOREIGN KEY제한 조건은 불평하지 않습니다. 참조 무결성은 기본적으로 명령문 끝에 적용됩니다.

Postgres를위한 SQL Fiddle 9.3. (9.1에서 동일하게 작동합니다.)

이러한 쿼리를 여러 개 동시에 실행하면 경쟁 조건 이 매우 작 습니다 . 관련 질문에 대한 자세한 내용은 여기여기참조하십시오 . 실제로 동시로드가 많은 경우에만 발생합니다. 다른 답변으로 광고 된 것과 같은 솔루션 캐싱과 비교할 때, 기회는 매우 작습니다 .

반복 사용 기능

반복 사용을 위해 레코드 배열을 매개 변수로 사용 unnest(param)하고 VALUES표현식 대신 사용하는 SQL 함수를 작성합니다 .

또는 레코드 배열의 구문이 너무 복잡하면 쉼표로 구분 된 문자열을 parameter로 사용하십시오 _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;


Postgres 9.5의 UPSERT 기능

매개 변수 전달을위한 사용자 정의 행 유형을 작성하십시오. 우리는 그것없이 할 수 있지만 더 간단합니다.

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: typeFK 테이블에 존재하지 않는 것이 삽입됩니다. 대부분의 유형이 이미 존재한다고 가정합니다. 경쟁 조건을 완전히 확인하고 배제하기 위해 필요한 기존 행이 잠기므로 동시 트랜잭션이 방해를받지 않습니다. 귀하의 경우에 너무 편집증 인 경우 다음을 교체 할 수 있습니다.

      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(true "UPSERT") on 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 문서.
Erwin Brandstetter

1
또한이 작업을 수행 할 수 있습니다 INSERT ... RETURNING \gset에서 psql다음 psql의로 반환 된 값을 사용 :'variables'하지만, 이것은 단지 하나의 행 삽입 작동합니다.
Craig Ringer

@ ErwinBrandstetter 이것은 훌륭하지만 모든 것을 이해하기에는 SQL에 너무 익숙하지 않습니다. 작동하는 방법을 설명하는 "동시에 누락 된 FK 행 삽입"에 의견을 추가 할 수 있습니까? 또한 SQLFiddle 작업 예제에 감사드립니다!
glallen

@ glallen : 단계별 설명을 추가했습니다. 더 자세한 설명과 함께 관련 답변 및 설명서에 대한 많은 링크가 있습니다. 당신은 필요 쿼리가 무엇을 이해하기 위해 또는 당신은 당신의 머리에서 할 수있다.
Erwin Brandstetter

4

조회. 기본적으로 foo id가 필요합니다.

포스트그레스에 한정되지 않음, btw. (그리고 당신은 그렇게 태그하지 않았습니다)-이것은 일반적으로 SQL이 작동하는 방식입니다. 바로 가기가 없습니다.

응용 프로그램은 현명하지만 메모리에 foo 항목의 캐시가있을 수 있습니다. 내 테이블에는 종종 최대 3 개의 고유 필드가 있습니다.

  • 테이블 레벨 기본 키인 ID (정수 또는 무언가).
  • 식별자 (ID)는 안정적인 ID 응용 프로그램 수준으로 현명하게 사용되며 URL 등으로 고객에게 노출 될 수있는 GUID입니다.
  • Code-존재하고 존재하는 경우 고유해야하는 문자열입니다 (SQL Server : null이 아닌 필터링 된 고유 인덱스). 이것이 고객 세트 식별자입니다.

예:

  • 거래 응용 프로그램의 계정-> ID는 외래 키에 사용되는 정수입니다. -> 식별자는 Guid이며 웹 포털 등에서 사용됩니다.-항상 허용됩니다. -> 코드가 수동으로 설정됩니다. 규칙 : 일단 설정하면 변경되지 않습니다.

분명히 무언가를 계정에 연결하고 싶을 때-기술적으로 먼저 ID를 가져와야합니다. 그러나 식별자와 코드가 일단 변경되면 메모리 캔에 긍정적 인 캐시가 있으면 대부분의 조회가 데이터베이스에 도달하지 못하게됩니다.


10
오류가 발생하기 쉬운 캐시를 피하면서 단일 SQL 문에서 RDBMS가 검색하도록 할 수 있다는 것을 알고 있습니까?
Erwin Brandstetter

변경하지 않는 요소를 찾는 것이 오류가 발생하지 않는다는 것을 알고 있습니까? 또한 일반적으로 RDBMS는 라이센스 비용으로 인해 확장 성이 없으며 게임에서 가장 비싼 요소입니다. 가능한 많은 하중을받는 것이 정확히 나쁘지는 않습니다. 또한 많은 ORM이이를 지원하는 것은 아닙니다.
TomTom

14
변하지 않는 요소? 가장 비싼 요소? 라이센스 비용 (PostgreSQL의 경우)? 제정신을 정의하는 ORM? 아니, 나는 그 모든 것을 몰랐다.
Erwin Brandstetter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.