ฉันจะแทรกแถวซึ่งมีรหัสต่างประเทศได้อย่างไร


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 ;

เอาไปอ่านมันสองสามครั้ง แต่ตอนนี้ฉันเข้าใจแล้วว่าทางออกที่สองที่คุณให้ไว้ ฉันชอบมัน. ใช้ตอนนี้เพื่อ bootstrap ฐานข้อมูลของฉันด้วยค่าที่ทราบจำนวนหนึ่งเมื่อระบบเริ่มต้นขึ้น
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] JOINหมายความว่าแถวจากval ยังไม่ได้ปรับตัวลดลงfooเมื่อไม่ตรงกับที่พบใน แต่NULLถูกป้อนfoo_idแทน

  • การVALUESแสดงออกในแบบสอบถามย่อยทำเช่นเดียวกับCTE ของ @ ypercube Common Table Expressionsมีคุณสมบัติเพิ่มเติมและอ่านได้ง่ายขึ้นในคิวรีขนาดใหญ่ แต่ก็เป็นอุปสรรคในการเพิ่มประสิทธิภาพเช่นกัน ดังนั้นโดยทั่วไปเคียวรีย่อยจะเร็วขึ้นเล็กน้อยเมื่อไม่จำเป็น

  • idเป็นชื่อคอลัมน์เป็นรูปแบบการป้องกันการแพร่กระจายกว้าง ควรจะเป็นfoo_idและbar_idหรือสิ่งที่สื่อความหมาย เมื่อเข้าร่วมกลุ่มของตารางคุณจะมีหลายคอลัมน์ที่มีชื่อid...

  • พิจารณาธรรมดาtextหรือแทนvarchar varchar(n)หากคุณต้องการจำกัดความยาวอย่างแท้จริงให้เพิ่มCHECKข้อ จำกัด :

  • คุณอาจต้องเพิ่มการส่งประเภทที่ชัดเจน เนื่องจากVALUESนิพจน์ไม่ได้แนบกับตารางโดยตรง (เช่นในINSERT ... VALUES ...) จึงไม่สามารถรับชนิดและใช้ชนิดข้อมูลเริ่มต้นได้หากไม่มีการประกาศประเภทที่ชัดเจนซึ่งอาจไม่สามารถใช้งานได้ในทุกกรณี ก็พอที่จะทำในแถวแรกส่วนที่เหลือจะตกหลุม

INSERT หายไปแถว FK ในเวลาเดียวกัน

หากคุณต้องการสร้างรายการที่ไม่มีอยู่ในfooทันทีในคำสั่ง SQL เดียว CTEs เป็นเครื่องมือ:

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. CTE ที่ 1 selจัดเตรียมข้อมูลอินพุตจำนวนหลายแถว แบบสอบถามย่อยที่valมีการVALUESแสดงออกสามารถถูกแทนที่ด้วยตารางหรือแบบสอบถามย่อยเป็นแหล่งที่มา ทันทีLEFT JOINเพื่อfooต่อท้ายแถวfoo_idที่มีอยู่ล่วงหน้า typeแถวอื่นทั้งหมดได้มาfoo_id IS NULLทางนี้

  2. CTE ที่ 2 insแทรกประเภทใหม่ที่แตกต่าง ( foo_id IS NULL) ลงในfooและส่งกลับที่สร้างขึ้นใหม่foo_id- พร้อมกับtypeเพื่อเข้าร่วมกลับไปที่แทรกแถว

  3. ด้านนอกขั้นสุดท้ายในINSERTขณะนี้สามารถแทรก foo.id สำหรับทุกแถว: ประเภทที่มีอยู่แล้วหรือมันถูกแทรกในขั้นตอนที่ 2

พูดอย่างเคร่งครัดแทรกทั้งสองเกิดขึ้น "ขนาน" แต่เนื่องจากนี่เป็นคำสั่งเดียวFOREIGN KEYข้อ จำกัดเริ่มต้นจะไม่บ่น Referential integrity ถูกบังคับใช้เมื่อสิ้นสุดคำสั่งโดยค่าเริ่มต้น

ซอ 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
Erwin Brandstetter

1
นอกจากนี้คุณยังสามารถทำเช่นนี้กับINSERT ... RETURNING \gsetในpsqlแล้วใช้ค่ากลับมาเป็น psql :'variables'แต่ตอนนี้ทำงานเฉพาะสำหรับแทรกแถวเดียว
Craig Ringer

@ErwinBrandstetter นี่ยอดเยี่ยม แต่ฉันใหม่เกินกว่าที่ sql จะเข้าใจได้ทั้งหมดคุณสามารถเพิ่มความคิดเห็นใน "INSERT INSERT หายไปแถว FK ในเวลาเดียวกัน" อธิบายว่ามันทำงานอย่างไร ขอบคุณสำหรับตัวอย่างการทำงานของ SQLFiddle!
glallen

@glallen: ฉันเพิ่มคำอธิบายทีละขั้นตอน นอกจากนี้ยังมีลิงค์เชื่อมโยงไปยังคำตอบที่เกี่ยวข้องและคู่มือพร้อมคำอธิบายเพิ่มเติม คุณต้องเข้าใจว่าการสืบค้นทำอะไรหรือคุณอาจเป็นหัวหน้าของคุณ
Erwin Brandstetter

4

ค้นหา โดยทั่วไปคุณต้องการ foo id เพื่อแทรกลงในแถบ

ไม่ใช่ postgres เฉพาะ btw (และคุณไม่ได้ติดแท็กแบบนี้) - นี่เป็นวิธีการทำงานของ SQL โดยทั่วไป ไม่มีทางลัดที่นี่

แอปพลิเคชันที่ชาญฉลาด แต่คุณอาจมีแคชของรายการ foo ในหน่วยความจำ ตารางของฉันมักมีเขตข้อมูลที่ไม่ซ้ำกันสูงสุด 3 รายการ:

  • รหัส (จำนวนเต็มหรือบางอย่าง) ที่เป็นคีย์หลักของระดับตาราง
  • ตัวบ่งชี้ซึ่งเป็น GUID ที่ใช้เป็นระดับแอปพลิเคชัน ID ที่เสถียร (และอาจถูกเปิดเผยต่อลูกค้าใน URL ฯลฯ )
  • รหัส - สตริงที่อาจจะมีและจะต้องไม่ซ้ำกันถ้ามันมี (เซิร์ฟเวอร์ sql: กรองดัชนีที่ไม่ซ้ำกันในไม่เป็นโมฆะ) นั่นคือตัวระบุชุดลูกค้า

ตัวอย่าง:

  • บัญชี (ในแอพพลิเคชั่นการซื้อขาย) -> Id เป็น int ที่ใช้สำหรับกุญแจต่างประเทศ -> ตัวระบุเป็น Guid และใช้ในเว็บพอร์ทัล ฯลฯ - ยอมรับเสมอ -> ตั้งรหัสด้วยตนเอง กฎ: เมื่อตั้งค่าแล้วจะไม่เปลี่ยน

เห็นได้ชัดว่าเมื่อคุณต้องการเชื่อมโยงบางสิ่งบางอย่างกับบัญชี - ก่อนอื่นคุณต้องได้รับรหัสทางเทคนิค - แต่ทั้ง Identifier และ Code จะไม่เปลี่ยนแปลงเมื่อพวกเขาอยู่ที่นั่นแคชเชิงบวกในหน่วยความจำสามารถหยุดการค้นหาจากการกดฐานข้อมูล


10
คุณทราบหรือไม่ว่าคุณสามารถปล่อยให้ RDBMS ทำการค้นหาให้คุณในคำสั่ง SQL คำเดียวเพื่อหลีกเลี่ยงการแคชผิดพลาด?
Erwin Brandstetter

คุณทราบหรือไม่ว่าการค้นหาองค์ประกอบที่ไม่เปลี่ยนแปลงนั้นไม่ได้เกิดข้อผิดพลาดได้ง่าย? นอกจากนี้โดยทั่วไป RDBMS นั้นไม่สามารถปรับขนาดได้และเป็นองค์ประกอบที่แพงที่สุดในเกมเนื่องจากค่าลิขสิทธิ์ การรับภาระมากที่สุดเท่าที่จะทำได้ก็ไม่เลว นอกจากนี้ยังมี ORM ไม่มากที่สนับสนุนการเริ่มต้นด้วย
TomTom

14
องค์ประกอบที่ไม่เปลี่ยนแปลง? องค์ประกอบที่แพงที่สุด? ค่าลิขสิทธิ์ (สำหรับ PostgreSQL)? ORMs กำหนดสติอะไร ไม่ฉันไม่รู้ทั้งหมด
Erwin Brandstetter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.