Làm cách nào để chèn một hàng chứa khóa ngoại?


54

Sử dụng PostgreSQL v9.1. Tôi có các bảng sau:

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
);

Giả sử bảng đầu tiên foođược điền như thế này:

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

Có cách nào để chèn hàng vào bardễ dàng bằng cách tham khảo foobảng không? Hoặc tôi phải làm điều đó theo hai bước, đầu tiên bằng cách tìm kiếm fooloại tôi muốn, sau đó chèn một hàng mới vào bar?

Dưới đây là một ví dụ về mã giả cho thấy những gì tôi đã hy vọng có thể được thực hiện:

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

Câu trả lời:


67

Cú pháp của bạn gần như tốt, cần một số dấu ngoặc đơn xung quanh các truy vấn con và nó sẽ hoạt động:

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

Đã thử nghiệm tại SQL-Fiddle

Một cách khác, với cú pháp ngắn hơn nếu bạn có nhiều giá trị để chèn:

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 ;

Đã đọc nó một vài lần, nhưng bây giờ tôi hiểu rằng giải pháp thứ 2 mà bạn cung cấp. Tôi thích nó. Sử dụng nó ngay bây giờ để khởi động lại cơ sở dữ liệu của tôi với một số giá trị đã biết khi hệ thống xuất hiện lần đầu tiên.
Stéphane

37

Đồng bằng

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);
  • Việc sử dụng LEFT [OUTER] JOINthay thế [INNER] JOINcó nghĩa là các hàng từ val không bị bỏ khi không tìm thấy kết quả khớp foo. Thay vào đó, NULLđược nhập cho foo_id.

  • Các VALUESbiểu hiện trong subquery không giống như @ ypercube của CTE. Biểu thức bảng chung cung cấp các tính năng bổ sung và dễ đọc hơn trong các truy vấn lớn, nhưng chúng cũng đặt ra các rào cản tối ưu hóa. Vì vậy, các truy vấn con thường nhanh hơn một chút khi không cần điều nào ở trên.

  • idnhư tên cột là một mô hình chống lan rộng. Nên foo_idbar_idhoặc bất cứ điều gì mô tả. Khi tham gia một loạt các bảng, bạn kết thúc với nhiều cột được đặt tên id...

  • Xem xét đơn giản texthoặc varcharthay vì varchar(n). Nếu bạn thực sự cần áp đặt giới hạn độ dài, hãy thêm một CHECKràng buộc:

  • Bạn có thể cần thêm phôi loại rõ ràng. Vì VALUESbiểu thức không được gắn trực tiếp vào bảng (như trong INSERT ... VALUES ...), các loại dữ liệu không thể được dẫn xuất và các kiểu dữ liệu mặc định được sử dụng mà không cần khai báo kiểu rõ ràng, có thể không hoạt động trong mọi trường hợp. Nó là đủ để làm điều đó trong hàng đầu tiên, phần còn lại sẽ xếp hàng.

CHỌN thiếu các hàng FK cùng một lúc

Nếu bạn muốn tạo các mục không tồn tại foomột cách nhanh chóng, trong một câu lệnh SQL , CTE là công cụ:

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);

Lưu ý hai hàng giả mới để chèn. Cả hai đều có màu tím , chưa tồn tại foo. Hai hàng để minh họa sự cần thiết DISTINCTtrong INSERTcâu lệnh đầu tiên .

Giải thích từng bước

  1. CTE đầu tiên selcung cấp nhiều hàng dữ liệu đầu vào. Truy vấn con valvới VALUESbiểu thức có thể được thay thế bằng bảng hoặc truy vấn con dưới dạng nguồn. Ngay lập tức LEFT JOINđể foonối thêm foo_idcho các typehàng có sẵn . Tất cả các hàng khác có được foo_id IS NULLtheo cách này.

  2. CTE thứ 2 inschèn các loại mới ( ) khác biệtfoo_id IS NULL vào foovà trả về kiểu mới được tạo foo_id- cùng với việc typenối lại để chèn các hàng.

  3. INSERTBây giờ bên ngoài cuối cùng có thể chèn một foo.id cho mỗi hàng: loại tồn tại trước hoặc nó được chèn vào bước 2.

Nói đúng ra, cả hai phần chèn đều xảy ra "song song", nhưng vì đây là một câu lệnh duy nhất , các FOREIGN KEYràng buộc mặc định sẽ không phàn nàn. Tính toàn vẹn tham chiếu được thi hành ở cuối câu lệnh theo mặc định.

Fiddle SQL cho Postgres 9.3. (Hoạt động tương tự trong 9.1.)

Có một điều kiện cuộc đua nhỏ nếu bạn chạy đồng thời nhiều truy vấn này. Đọc thêm dưới các câu hỏi liên quan ở đâyở đâyở đây . Thực sự chỉ xảy ra dưới tải nặng đồng thời, nếu có bao giờ. So với các giải pháp bộ nhớ đệm như được quảng cáo trong một câu trả lời khác, cơ hội là siêu nhỏ .

Chức năng sử dụng nhiều lần

Để sử dụng lặp đi lặp lại, tôi sẽ tạo một hàm SQL lấy một mảng các bản ghi làm tham số và sử dụng unnest(param)thay cho VALUESbiểu thức.

Hoặc, nếu cú ​​pháp cho mảng các bản ghi quá lộn xộn đối với bạn, hãy sử dụng chuỗi được phân tách bằng dấu phẩy làm tham số _param. Ví dụ về mẫu:

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

Sau đó sử dụng điều này để thay thế VALUESbiểu thức trong tuyên bố trên:

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


Chức năng với UPSERT trong Postgres 9.5

Tạo một loại hàng tùy chỉnh để truyền tham số. Chúng ta có thể làm mà không cần nó, nhưng nó đơn giản hơn:

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

Chức năng:

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;

Gọi:

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

Nhanh chóng và vững chắc cho môi trường với các giao dịch đồng thời.

Ngoài các truy vấn trên, ...

  • ... áp dụng SELECThoặc INSERTbật foo: Bất kỳ typecái nào không tồn tại trong bảng FK, được chèn vào. Giả sử hầu hết các loại tồn tại trước. Để hoàn toàn chắc chắn và loại trừ các điều kiện cuộc đua, các hàng hiện tại chúng tôi cần đã bị khóa (để các giao dịch đồng thời không thể can thiệp). Nếu đó là quá hoang tưởng cho trường hợp của bạn, bạn có thể thay thế:

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

    với

      ON     CONFLICT(type) DO NOTHING
  • ... áp dụng INSERThoặc UPDATE(đúng "UPSERT") trên bar: Nếu descriptionđã tồn tại thì nó typeđược cập nhật:

      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

    Nhưng chỉ khi typethực sự thay đổi:

  • ... chuyển các giá trị như các loại hàng nổi tiếng với một VARIADICtham số. Lưu ý tối đa mặc định là 100 tham số! So sánh:

    Có nhiều cách khác để vượt qua nhiều hàng ...

Liên quan:


Trong INSERT missing FK rows at the same timeví dụ của bạn , việc đưa điều này vào Giao dịch có làm giảm rủi ro về điều kiện chủng tộc trong SQL Server không?
tử11

1
@ Element11: Câu trả lời là dành cho Postgres, nhưng vì chúng ta đang nói về một lệnh SQL duy nhất , đó là một giao dịch trong mọi trường hợp. Thực hiện nó trong một giao dịch lớn hơn sẽ chỉ làm tăng cửa sổ thời gian cho các điều kiện cuộc đua có thể. Đối với SQL Server: CTE sửa đổi dữ liệu hoàn toàn không được hỗ trợ (chỉ SELECTbên trong một WITHmệnh đề). Nguồn: Tài liệu MS.
Erwin Brandstetter

1
Bạn cũng có thể làm điều này với INSERT ... RETURNING \gsettrong psqlsau đó sử dụng các giá trị trả như psql :'variables', nhưng điều này chỉ hoạt động cho chèn hàng duy nhất.
Craig Ringer

@ErwinBrandstetter điều này thật tuyệt, nhưng tôi còn quá mới để hiểu tất cả, bạn có thể thêm một số nhận xét vào "CHỌN thiếu các hàng FK cùng một lúc" giải thích cách hoạt động của nó không? Ngoài ra, cảm ơn các ví dụ làm việc SQLFiddle!
glallen

@glallen: Tôi đã thêm một lời giải thích từng bước. Ngoài ra còn có nhiều liên kết đến các câu trả lời liên quan và hướng dẫn sử dụng với nhiều lời giải thích hơn. Bạn cần hiểu những gì truy vấn làm hoặc bạn có thể ở trên đầu của bạn.
Erwin Brandstetter

4

Tra cứu. Về cơ bản bạn cần id của foo để chèn chúng vào thanh.

Không postgres cụ thể, btw. (và bạn đã không gắn thẻ nó như thế) - đây thường là cách SQL hoạt động. Không có phím tắt ở đây.

Tuy nhiên, ứng dụng khôn ngoan, bạn có thể có bộ nhớ cache của các mục foo trong bộ nhớ. Các bảng của tôi thường có tối đa 3 trường duy nhất:

  • Id (số nguyên hoặc một cái gì đó) là khóa chính cấp bảng.
  • Mã định danh, là GUID được sử dụng làm cấp độ ứng dụng ID ổn định (và có thể được hiển thị cho khách hàng trong URL, v.v.)
  • Mã - một chuỗi có thể ở đó và phải là duy nhất nếu nó ở đó (máy chủ sql: chỉ mục duy nhất được lọc không phải là null). Đó là một bộ định danh khách hàng.

Thí dụ:

  • Tài khoản (trong ứng dụng giao dịch) -> Id là số nguyên được sử dụng cho khóa ngoại. -> Mã định danh là một Hướng dẫn và được sử dụng trong các cổng web, v.v. - luôn được chấp nhận. -> Mã được đặt thủ công. Quy tắc: một khi thiết lập nó không thay đổi.

Rõ ràng khi bạn muốn liên kết một cái gì đó với một tài khoản - trước tiên, về mặt kỹ thuật, bạn phải lấy Id - nhưng được cung cấp cả Mã định danh và Mã không bao giờ thay đổi một khi chúng ở đó, bộ đệm tích cực trong bộ nhớ sẽ ngăn hầu hết các tra cứu truy cập cơ sở dữ liệu.


10
Bạn có biết rằng bạn có thể để RDBMS thực hiện tìm kiếm cho bạn, trong một câu lệnh SQL duy nhất, tránh bộ đệm dễ bị lỗi không?
Erwin Brandstetter

Bạn có biết rằng việc tìm kiếm các yếu tố không thay đổi không dễ bị lỗi? Ngoài ra, thông thường, RDBMS không thể mở rộng và là yếu tố đắt nhất trong trò chơi, do chi phí cấp phép. Lấy càng nhiều tải từ nó càng tốt không hẳn là xấu. Ngoài ra, không có nhiều ORM hỗ trợ để bắt đầu.
TomTom

14
Yếu tố không thay đổi? Yếu tố đắt nhất? Chi phí cấp phép (đối với PostgreSQL)? ORM xác định những gì lành mạnh? Không, tôi không nhận thức được tất cả về điều đó.
Erwin Brandstetter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.