Bagaimana cara saya memasukkan baris yang berisi kunci asing?

54

Menggunakan PostgreSQL v9.1. Saya memiliki tabel berikut:

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

Katakanlah tabel pertama foodiisi seperti ini:

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

Apakah ada cara untuk menyisipkan baris bardengan mudah dengan mereferensikan footabel? Atau haruskah saya melakukannya dalam dua langkah, pertama dengan mencari footipe yang saya inginkan, dan kemudian memasukkan baris baru ke dalamnya bar?

Berikut adalah contoh pseudo-code yang menunjukkan apa yang saya harapkan bisa dilakukan:

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
sumber

Jawaban:

67

Sintaks Anda hampir baik, membutuhkan beberapa tanda kurung di sekitar subqueries dan itu akan berfungsi:

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

Diuji di SQL-Fiddle

Cara lain, dengan sintaks yang lebih pendek jika Anda memiliki banyak nilai untuk disisipkan:

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ᵀᴹ
sumber
Membacanya beberapa kali, tapi sekarang saya mengerti solusi kedua yang Anda berikan. Saya suka itu. Menggunakannya sekarang untuk mem-bootstrap database saya dengan beberapa nilai yang diketahui ketika sistem pertama kali muncul.
Stéphane
37

MASUKKAN polos

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);
  • Penggunaan LEFT [OUTER] JOINbukan [INNER] JOINberarti bahwa baris dari val tidak dijatuhkan ketika tidak ada kecocokan ditemukan di foo. Sebagai gantinya, NULLdimasukkan untuk foo_id.

  • The VALUESekspresi dalam subquery tidak sama dengan @ ypercube ini CTE. Common Table Expressions menawarkan fitur-fitur tambahan dan lebih mudah dibaca dalam permintaan besar, tetapi mereka juga berpose sebagai hambatan optimisasi. Jadi subqueries biasanya sedikit lebih cepat ketika tidak ada yang di atas diperlukan.

  • idsebagai nama kolom adalah anti-pola yang tersebar luas. Harus foo_iddan bar_idatau apa pun deskriptif. Saat bergabung dengan banyak tabel, Anda berakhir dengan beberapa kolom yang semuanya bernama id...

  • Pertimbangkan yang polos textatau varcharbukan varchar(n). Jika Anda benar-benar perlu menerapkan batasan panjang, tambahkan CHECKbatasan:

  • Anda mungkin perlu menambahkan gips tipe eksplisit. Karena VALUESekspresi tidak secara langsung dilampirkan ke tabel (seperti dalam INSERT ... VALUES ...), tipe tidak dapat diturunkan dan tipe data default digunakan tanpa deklarasi tipe eksplisit, yang mungkin tidak berfungsi dalam semua kasus. Cukup dengan melakukannya di baris pertama, sisanya akan jatuh dalam barisan.

MASUKKAN baris FK yang hilang pada saat yang sama

Jika Anda ingin membuat entri yang tidak ada foodengan cepat, dalam satu pernyataan SQL , CTE berperan:

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

Perhatikan dua baris boneka baru untuk dimasukkan. Keduanya berwarna ungu , yang belum ada foo. Dua baris untuk menggambarkan kebutuhan DISTINCTdalam INSERTpernyataan pertama .

Penjelasan langkah demi langkah

  1. CTE 1 selmenyediakan beberapa baris input data. Subquery valdengan VALUESekspresi dapat diganti dengan tabel atau subquery sebagai sumber. Segera LEFT JOINuntuk foomenambahkan baris yang foo_idsudah ada sebelumnya type. Semua baris lainnya mendapatkan foo_id IS NULLcara ini.

  2. 2 CTE insmenyisipkan berbeda jenis baru ( foo_id IS NULL) ke dalam foo, dan kembali yang baru dihasilkan foo_id- bersama-sama dengan typeuntuk bergabung kembali untuk menyisipkan baris.

  3. Bagian luar terakhir INSERTsekarang dapat menyisipkan foo.id untuk setiap baris: baik tipe yang sudah ada sebelumnya, atau sudah dimasukkan pada langkah 2.

Sebenarnya, kedua sisipan terjadi "secara paralel", tetapi karena ini adalah pernyataan tunggal , FOREIGN KEYbatasan default tidak akan mengeluh. Integritas referensial diberlakukan di akhir pernyataan secara default.

SQL Fiddle untuk Postgres 9.3. (Bekerja sama di 9.1.)

Ada kondisi balapan kecil jika Anda menjalankan beberapa dari pertanyaan ini secara bersamaan. Baca lebih lanjut di bawah pertanyaan terkait di sini dan di sini dan di sini . Benar-benar hanya terjadi di bawah beban bersamaan yang berat, jika pernah. Dibandingkan dengan solusi caching seperti yang diiklankan dalam jawaban lain, peluangnya sangat kecil .

Fungsi untuk penggunaan berulang

Untuk penggunaan berulang saya akan membuat fungsi SQL yang mengambil array catatan sebagai parameter dan digunakan unnest(param)sebagai ganti VALUESekspresi.

Atau, jika sintaks untuk array rekaman terlalu berantakan untuk Anda, gunakan string yang dipisahkan koma sebagai parameter _param. Misalnya bentuk:

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

Kemudian gunakan ini untuk mengganti VALUESekspresi dalam pernyataan di atas:

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


Berfungsi dengan UPSERT di Postgres 9.5

Buat jenis baris khusus untuk melewati parameter. Kita bisa melakukannya tanpa itu, tetapi lebih sederhana:

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

Fungsi:

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;

Panggilan:

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

Cepat dan solid untuk lingkungan dengan transaksi bersamaan.

Selain pertanyaan di atas, ini ...

  • ... berlaku SELECTatau INSERTaktif foo: Apa pun typeyang tidak ada dalam tabel FK, belum dimasukkan. Dengan asumsi sebagian besar tipe sudah ada. Untuk benar-benar yakin dan mengesampingkan kondisi balapan, baris yang kita butuhkan dikunci (sehingga transaksi bersamaan tidak dapat mengganggu). Jika itu terlalu paranoid untuk kasus Anda, Anda dapat mengganti:

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

    dengan

      ON     CONFLICT(type) DO NOTHING
  • ... berlaku INSERTatau UPDATE(benar "UPSERT") pada bar: Jika yang descriptionsudah ada typediperbarui:

      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
    

    Tetapi hanya jika typebenar-benar berubah:

  • ... melewati nilai juga tipe baris yang dikenal dengan VARIADICparameter. Perhatikan maksimum default 100 parameter! Membandingkan:

    Ada banyak cara lain untuk melewati banyak baris ...

Terkait:

Erwin Brandstetter
sumber
Dalam INSERT missing FK rows at the same timecontoh Anda , akan menempatkan ini dalam Transaksi mengurangi risiko kondisi balapan di SQL Server?
element11
1
@ element11: Jawabannya adalah untuk Postgres, tetapi karena kita berbicara tentang satu perintah SQL, itu adalah satu transaksi dalam hal apa pun. Menjalankannya di dalam transaksi yang lebih besar hanya akan menambah waktu untuk kemungkinan kondisi lomba. Adapun SQL Server: CTE pemodifikasi data tidak didukung sama sekali (hanya SELECTdi dalam WITHklausa). Sumber: Dokumentasi MS.
Erwin Brandstetter
1
Anda juga dapat melakukan ini dengan INSERT ... RETURNING \gsetdi psqlkemudian menggunakan nilai-nilai dikembalikan sebagai psql :'variables', tapi ini hanya bekerja untuk menyisipkan baris tunggal.
Craig Ringer
@ErwinBrandstetter ini hebat, tapi saya terlalu baru untuk sql untuk memahami semuanya, dapatkah Anda menambahkan beberapa komentar ke "MASUKKAN baris FK yang hilang sekaligus" menjelaskan cara kerjanya? juga, terima kasih atas contoh kerja SQLFiddle!
Glallen
@glallen: Saya menambahkan penjelasan langkah demi langkah. Ada juga banyak tautan ke jawaban terkait dan manual dengan penjelasan lebih lanjut. Anda perlu memahami apa yang dilakukan oleh kueri atau Anda berada di atas kepala Anda.
Erwin Brandstetter
4

Lihatlah. Anda pada dasarnya memerlukan foo id untuk memasukkannya ke dalam bar.

Tidak spesifik postgres, btw. (dan Anda tidak menandainya seperti itu) - ini umumnya cara kerja SQL. Tidak ada jalan pintas di sini.

Dari sisi aplikasi, Anda mungkin memiliki cache item foo di memori. Tabel saya sering memiliki hingga 3 bidang unik:

  • Id (integer atau sesuatu) yang merupakan kunci utama level tabel.
  • Identifier, yang merupakan GUID yang digunakan sebagai kebijakan tingkat aplikasi ID stabil (dan dapat terpapar ke pelanggan di URL, dll.)
  • Kode - string yang mungkin ada dan harus unik jika ada (sql server: indeks unik yang difilter bukan nol). Itu adalah pengidentifikasi set pelanggan.

Contoh:

  • Akun (dalam aplikasi perdagangan) -> Id adalah int yang digunakan untuk kunci asing. -> Identifier adalah Guid dan digunakan di portal web dll - selalu diterima. -> Kode diatur secara manual. Aturan: sekali diset tidak berubah.

Jelas ketika Anda ingin menautkan sesuatu ke akun - Anda pertama-tama harus, secara teknis, mendapatkan ID - tetapi mengingat baik Pengenal dan Kode tidak pernah berubah begitu mereka ada di sana, cache positif dalam memori tidak dapat menghentikan sebagian besar pencarian agar tidak memukul database.

TomTom
sumber
10
Anda sadar bahwa Anda dapat membiarkan RDBMS melakukan pencarian untuk Anda, dalam satu pernyataan SQL, menghindari cache yang rentan kesalahan?
Erwin Brandstetter
Anda sadar bahwa mencari elemen yang tidak berubah bukanlah kesalahan? Juga, biasanya, RDBMS tidak dapat diskalakan dan merupakan elemen paling mahal dalam permainan, karena biaya lisensi. Mengambil beban sebanyak mungkin dari itu tidak sepenuhnya buruk. Juga, tidak banyak ORM yang mendukungnya sejak awal.
TomTom
14
Elemen yang tidak berubah? Elemen paling mahal? Biaya lisensi (untuk PostgreSQL)? ORM mendefinisikan apa yang waras? Tidak, saya tidak menyadari semua itu.
Erwin Brandstetter