ข้ามไปยังเนื้อหา

การเพิ่ม row

ปัญหาที่เจอจริง: แอปพลิเคชันของคุณไม่มีอะไรให้แสดงเลยจนกว่าจะมีข้อมูลอยู่ใน table เสียก่อน การสร้างข้อมูลหมายถึงการเพิ่ม row และคำสั่งสำหรับสิ่งนั้นคือ INSERT คุณระบุชื่อ table แสดงรายการ column ที่คุณกำลังจะให้ค่า แล้วให้ค่าเหล่านั้น PostgreSQL จะเติมทุกอย่างที่คุณเว้นไว้ — id ที่ถูกสร้างขึ้น, column ที่มี DEFAULT และ column ใด ๆ ที่อนุญาตให้เป็น NULL

บทเรียนนี้ใช้ table authors และ books จากภาพรวมของโมดูล

รูปแบบพื้นฐานที่สุดแสดงรายการ column ก่อน แล้วตามด้วยค่าที่ตรงกัน

INSERT INTO authors (name, born)
VALUES ('Mira Castellan', 1971);

สังเกตว่าแท็บ psql ใช้ค่าตรง ๆ (literal value) ส่วนทุก driver ใช้ placeholder$1, $2 ในสไตล์ native ของ PostgreSQL หรือ %s สำหรับ psycopg — แล้วส่งค่าจริงแยกต่างหาก จงให้ข้อมูลของผู้ใช้แบบนี้เสมอ อย่าเอาค่ามาต่อเข้ากับ SQL string ด้วยตัวเองเด็ดขาด การปล่อยให้ driver bind ค่าให้ จะรักษา type ให้ถูกต้องและปิดประตูไม่ให้เกิด SQL injection คือกรณีที่ input ซึ่งจงใจประดิษฐ์ขึ้นมาเปลี่ยนสิ่งที่คำสั่งของคุณทำ

ใน pgAdmin: วางเวอร์ชัน psql ลงใน Query Tool แล้วรัน แผงข้อความจะรายงานว่า INSERT 0 1 ซึ่งหมายความว่ามี row หนึ่งถูกเพิ่มเข้าไป

table books มี copies_sold int NOT NULL DEFAULT 0 ถ้าคุณเพียงแค่ละ column นั้นไว้ PostgreSQL จะใช้ค่า default คุณยังเขียน DEFAULT อย่างชัดเจนเพื่อแสดงเจตนาของคุณได้ด้วย

INSERT INTO books (title, author_id, published, copies_sold)
VALUES ('The Glass Orchard', 1, 2004, DEFAULT);

ไม่ว่าจะวิธีไหน copies_sold ก็จะลงเอยเป็น 0 การละ column ไปทั้งหมดเป็นวิธีปกติทั่วไป ส่วนการเขียน DEFAULT มีประโยชน์เมื่อคุณกำลังเติมหลาย row และต้องการให้ทุกตำแหน่งถูกระบุออกมาชัด ๆ

คุณสามารถแสดงรายการ value tuple หลายอันในคำสั่งเดียวได้ ซึ่งเร็วกว่าการรัน INSERT หนึ่งครั้งต่อหนึ่ง row อย่างมาก เพราะเป็น round trip เดียวไปที่ server

INSERT INTO authors (name, born) VALUES
('Devon Reyes', 1985),
('Priya Anand', 1990),
('Tomas Holt', 1962);

driver แต่ละตัวมีเส้นทางสำหรับ bulk ที่เป็นสำนวนเฉพาะตัว: psycopg มี executemany, pgx มี CopyFrom ที่เร็วมาก และด้วย sqlx คุณสามารถ unnest array เพื่อให้คำสั่งเดียว insert ได้หลาย row

column id เป็น GENERATED ALWAYS AS IDENTITY ดังนั้น PostgreSQL จึงเป็นผู้กำหนดค่าให้ หากต้องการทราบค่าโดยไม่ต้อง query ครั้งที่สอง ให้เติม RETURNING

INSERT INTO authors (name, born)
VALUES ('Lena Whitfield', 1978)
RETURNING id;

คำสั่งนี้ทั้ง insert row และส่ง id ใหม่กลับมาให้ในการเดินทางครั้งเดียว:

id
----
6
(1 row)

คุณสามารถ return column ใดก็ได้ที่คุณต้องการ — RETURNING id, name หรือแม้แต่ RETURNING * เพื่อรับ row ใหม่ทั้ง row รวมถึง default ที่ server เติมให้ด้วย

  • ส่งค่าผ่าน placeholder ของ driver ($1, %s) เสมอ การสร้าง SQL ด้วยการต่อ string เปิดทางให้ SQL injection และพังเมื่อเจออัญประกาศหรืออักขระพิเศษ
  • เมื่อใช้ primary key แบบ IDENTITY (หรือ serial) อย่าให้ค่า id เอง — ปล่อยให้ PostgreSQL สร้างให้ แล้วอ่านกลับมาด้วย RETURNING
  • การ insert หลาย row เป็นคำสั่งเดียวและ round trip เดียว ดังนั้นจึงเร็วกว่าการวน loop insert ทีละ row อย่างมาก
  • column ที่ไม่มีค่าและไม่มี default ต้องอนุญาตให้เป็น NULL ได้ มิฉะนั้น insert จะล้มเหลวด้วย not-null violation
ตัวเลือกBenefitCost
RETURNING หลัง INSERTได้ค่าที่ถูกสร้างขึ้น (เช่น id) กลับมาใน round trip เดียว ไม่มี race conditionต้องจำ syntax เพิ่มเติม และบาง driver ต้องอ่านผลลัพธ์แบบ query แทน exec ธรรมดา
แยก SELECT มาทีหลังเขียนง่าย เข้าใจง่ายสำหรับมือใหม่เดินทางไป-กลับเพิ่มอีกครั้ง และมี race condition ถ้ามี insert อื่นแทรกระหว่างนั้น
INSERT หลาย row ในคำสั่งเดียวเร็ว เพราะเป็น round trip เดียวคำสั่งยาวขึ้น และ error ของ row เดียวอาจทำให้ทั้งคำสั่งล้มเหลว
วน loop insert ทีละ rowโค้ดอ่านง่าย ทีละขั้นตอนช้ามาก เพราะเดินทางไป-กลับ N ครั้งสำหรับ N row
  • วน loop รัน INSERT ทีละ row แทนที่จะรวมเป็นคำสั่งเดียว — ทำให้เสีย network round-trip ต่อ row ควรใช้ multi-row INSERT, executemany, หรือ bulk copy (COPY/CopyFrom) แทน
  • เอา input ของผู้ใช้มาต่อ (concatenate) เข้ากับ SQL string ตรง ๆ — เปิดช่องให้เกิด SQL injection ให้ส่งค่าผ่าน placeholder ($1, %s) เสมอ
  • ลืมว่า column เป็น NOT NULL และไม่มี DEFAULT — insert จะล้มเหลวด้วย not-null violation ตรวจ schema ก่อนเขียน insert หรือใส่ค่าให้ครบทุก column ที่จำเป็น

💡 ตัวอย่างจากของจริง

GitHub / GitLab — งาน insert ทั่วไปในแอปส่วนใหญ่ผ่าน ORM (เช่น ActiveRecord) เพื่อความสะดวกและ maintainability แต่ในจุดที่ throughput สูง เช่น event ingestion หรือ audit log พวกเขาจะลง SQL มือพร้อม RETURNING และ multi-row insert เพื่อลด round-trip

ระบบ bulk import ไฟล์ CSV ของผู้ใช้ — ใช้ COPY หรือ multi-row INSERT แทนการวน insert ทีละ row เพื่อให้ import ไฟล์หลักแสน row เสร็จในไม่กี่วินาทีแทนที่จะเป็นหลักนาที

จะอ่านค่า id ที่ server สร้างให้ทันทีหลัง insert ได้อย่างไร?
column หนึ่งมี DEFAULT 0 และเป็น NOT NULL จะเกิดอะไรขึ้นถ้าไม่ใส่ column นั้นใน column list?
ทำไมถึงควรส่งค่าผ่าน placeholder ($1, %s) แทนการ concatenate เข้าไปใน SQL string ตรง ๆ?