Yabancı anahtar içeren bir satırı nasıl eklerim?


54

PostgreSQL v9.1'i kullanma. Aşağıdaki tablolara sahibim:

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

Diyelim ki ilk masa fooböyle doldurulur:

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

Tabloya başvurarak satırları barkolayca yerleştirmenin bir yolu var mı foo? Yoksa önce fooistediğim türe bakıp sonra yeni bir satır ekleyerek bunu iki adımda mı yapmalıyım bar?

İşte ne olacağını umduğumu gösteren sahte kod örneği:

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

Yanıtlar:


67

Sözdiziminiz neredeyse iyidir, alt sorgular etrafında parantez gerekir ve işe yarayacaktır:

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'da test edildi

Eklenecek çok fazla değeriniz varsa, daha kısa sözdizimine sahip başka bir yol:

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 ;

Birkaç kez okudum, ama şimdi anladığın 2. çözümü anladım. Bunu sevdim. Sistem ilk geldiğinde veritabanımı bir avuç bilinen değere önyüklemek için şimdi kullanmak.
Stéphane

37

Düz INSERT

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);
  • Bir kullanımı LEFT [OUTER] JOINyerine [INNER] JOINgelen satırları demektir val düştü değildir eşleşme bulunduğunda foo. Bunun yerine, NULLiçin girilir foo_id.

  • Alt VALUESsorgudaki ifade, @ ypercube's CTE ile aynıdır . Genel Tablo İfadeleri ek özellikler sunar ve büyük sorgularda okunması daha kolaydır, ancak bunlar aynı zamanda optimizasyon engelleri oluşturur. Bu nedenle, alt sorguların hiçbiri, yukarıdakilerin hiçbirine ihtiyaç duyulmadığında biraz daha hızlıdır.

  • idSütun adı geniş yayılımlı bir kalıptır. Olumlu foo_idve bar_idaçıklayıcı bir şey olmalı . Bir grup masaya katılırken, hepsinin adında birden fazla sütun bulunur id.

  • Düz textveya varcharyerine düşünün varchar(n). Bir uzunluk kısıtlaması gerçekten uygulamanız gerekiyorsa, bir CHECKsınırlama ekleyin :

  • Açık tür yayınlar eklemeniz gerekebilir. İtibaren VALUESsentezleme ile doğrudan (olduğu gibi bir tablo bağlı değildir INSERT ... VALUES ...), çeşitleri elde edilemez ve varsayılan veri türleri her durumda çalışmayabilir olan, açık tür bildirimi olmaksızın kullanılır. İlk satırda yapmak yeterli, gerisi sırayla düşecek.

INSERT eksik FK satırları aynı anda

fooAnında varolmayan girişler oluşturmak istiyorsanız , tek bir SQL ifadesinde , CTE'ler araçsaldır:

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

Takılacak iki yeni boş satıra dikkat edin. Her ikisi de henüz var olmayan mor renktedirfoo . İlk açıklamada gerekliliği göstermek için iki satır .DISTINCTINSERT

Adım adım açıklama

  1. 1. CTE selkatları girdi verisi satırları sağlar. İfadeli alt sorgu val, VALUESbir tablo veya kaynak olarak alt sorgu ile değiştirilebilir. Hemen LEFT JOINetmek fooeklenecek foo_idiçin önceden varolan typesatırları. Diğer tüm satırlar foo_id IS NULLbu şekilde olsun .

  2. 2. CTE farklı yeni tipler insekler ( ) içine yerleştirir ve yeni üretilenleri döndürür - birlikte satırları eklemek için geri bir araya gelirler .foo_id IS NULLfoofoo_idtype

  3. Son dış INSERTşimdi her satır için bir foo.id ekleyebilir: önceden var olan tür veya 2. adımda eklenmiş.

Açıkçası, her iki uç da "paralel" olur, ancak bu tek bir ifade olduğu için varsayılan FOREIGN KEYkısıtlamalar şikayet etmeyecektir. Referans bütünlüğü, varsayılan olarak ifadenin sonunda uygulanır.

Postgres için SQL Fiddle 9.3. (9.1'de aynı şekilde çalışır.)

Bu sorgulardan birden fazlasını aynı anda yaparsanız , küçük bir yarış durumu vardır . Burada ve burada ve burada ilgili sorular altında daha fazlasını okuyun . Gerçekten de, eğer varsa, ağır eşzamanlı yük altında olur. Başka bir cevapta bildirilen gibi önbellekleme çözümleriyle karşılaştırıldığında, şans çok küçük .

Tekrarlanan kullanım için işlev

Tekrarlanan kullanım için, bir kayıt dizisini parametre olarak unnest(param)alan ve VALUESifadenin yerine kullanılan bir SQL işlevi yaratacağım .

Veya, kayıt dizileri sözdizimi sizin için fazla karışıksa, parametre olarak virgülle ayrılmış bir dize kullanın _param. Örneğin;

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

Ardından VALUESyukarıdaki ifadedeki ifadeyi değiştirmek için bunu kullanın :

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


PostGres 9.5’te UPSERT ile işlev

Parametre geçişi için özel bir satır tipi oluşturun. Onsuz yapabiliriz, ama daha basit:

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

İşlev:

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;

Aramak:

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

Eş zamanlı işlem yapılan ortamlar için hızlı ve sağlam.

Yukarıdaki sorulara ek olarak, bu ...

  • ... uygulanır SELECTveya INSERTaçık foo: typeFK tablosunda bulunmayan, henüz eklenmiş. Çoğu türün önceden var olduğunu varsayarsak. Kesinlikle emin olmak ve yarış koşullarını elemek için, ihtiyacımız olan mevcut satırlar kilitlenir (böylece eşzamanlı işlemler karışmaz). Bu sizin durumunuz için paranoyaksa, yenisini değiştirebilirsiniz:

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

    ile

      ON     CONFLICT(type) DO NOTHING
  • ... uygulanır INSERTveya UPDATE(gerçek "UPSERT") açık bar: descriptionZaten varsa type, güncellenir:

      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

    Ancak, yalnızca typegerçekten değişirse:

  • ... değerleri iyi bilinen satır türlerinde bir VARIADICparametreyle geçirir . Varsayılan maksimum 100 parametreye dikkat edin! Karşılaştırmak:

    Birden fazla satır geçmenin birçok yolu var ...

İlgili:


Senin içinde INSERT missing FK rows at the same timeörneğin, SQL Server yarış koşullarının riskini azaltmak bir İşlemde bu koyarak ki?
element11

1
@ element11: Cevap Postgres içindir, ancak tek bir SQL komutundan bahsettiğimizden , her durumda tek bir işlemdir. Daha büyük bir işlemin içinde yürütülmesi , olası yarış koşulları için sadece zaman penceresini arttırır . SQL Server gelince: veri değiştiren CTE'ler hiç desteklenmiyor (sadece SELECTbir WITHmaddede). Kaynak: MS belgeleri.
Erwin Brandstetter,

1
Bunu da sonra INSERT ... RETURNING \gsetiçinde psqlpsql olarak verilen değerleri kullanarak yapabilirsiniz :'variables', ancak bu sadece tek satır ekleri için işe yarar .
Craig Ringer

@ErwinBrandstetter bu harika, ama hepsini anlamak için sql için yeniyim, "INSERT'in aynı anda eksik olan FK satırlarını aynı anda" nasıl çalıştığını açıklayabilir misiniz? Ayrıca, SQLFiddle çalışma örnekleri için teşekkürler!
glallen

@glallen: Adım adım bir açıklama ekledim. İlgili cevaplara ve daha fazla açıklama içeren kılavuza birçok bağlantı vardır. Sen lüzum sorgu ne yaptığını anlamak için ya da başınızın üzerinden olabilir.
Erwin Brandstetter

4

Yukarı Bak. Temel olarak foo'ların çubuğuna yerleştirmeleri gerekir.

Belirli postgres değil, btw. (ve bunu öyle etiketlemedin) - bu genellikle SQL'in işleyişidir. Burada kısayol yok.

Uygulama bilge olsa da, bellekte bir foo öğeleri önbelleği olabilir. Tablolarımda en fazla 3 benzersiz alan var:

  • Tablo düzeyinde birincil anahtar olan kimlik (tamsayı veya başka bir şey).
  • Kimlik bilgisi düzeyinde kararlı kimlik uygulama düzeyi olarak kullanılan bir GUID olan tanımlayıcı (ve URL’lerde vs. müşteriye maruz kalabilir)
  • Kod - orada olabilecek ve varsa benzersiz olması gereken bir dize (sql sunucusu: boş değil, benzersiz dizine filtre uygulanmış). Bu bir müşteri seti tanımlayıcısıdır.

Örnek:

  • Hesap (bir ticaret uygulamasında) -> Kimlik, yabancı anahtarlar için kullanılan bir int'dir. -> Tanımlayıcı bir Kılavuzdur ve web portallarında vb. Kullanılır. - her zaman kabul edilir. -> Kod manuel olarak ayarlandı. Kural: bir kez ayarlandıktan sonra değişmez.

Açıkçası, bir şeyi bir hesaba bağlamak istediğinizde - önce teknik olarak Id'yi almalısınız - ancak hem Tanımlayıcı hem de Kod verildiğinde asla değişmez, bellekteki pozitif önbellek çoğu veritabanının isabet etmesini durdurur.


10
Tek bir SQL deyiminde RDBMS'nin sizin için arama yapmasına izin verebileceğinizi ve hataya açık önbellekten kaçındığınızı biliyor musunuz?
Erwin Brandstetter

Değişmeyen elementleri aramanın hataya açık olmadığını biliyor musunuz? Ayrıca, tipik olarak, RDBMS lisanslama maliyetlerinden dolayı ölçeklendirilemez ve oyundaki en pahalı unsurdur. Ondan mümkün olduğunca fazla yük alarak tam olarak kötü değil. Ayrıca, çoğu ORM başlangıçta bunu desteklemiyor.
TomTom

14
Değişmeyen elementler? En pahalı unsur? Lisans maliyetleri (PostgreSQL için)? Aklı başında ne olduğunu tanımlayan ORM'ler? Hayır, bunların farkında değildim.
Erwin Brandstetter 17:13
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.