การเพิ่ม row
ปัญหาที่เจอจริง: แอปพลิเคชันของคุณไม่มีอะไรให้แสดงเลยจนกว่าจะมีข้อมูลอยู่ใน table เสียก่อน การสร้างข้อมูลหมายถึงการเพิ่ม row และคำสั่งสำหรับสิ่งนั้นคือ INSERT คุณระบุชื่อ table แสดงรายการ column ที่คุณกำลังจะให้ค่า แล้วให้ค่าเหล่านั้น PostgreSQL จะเติมทุกอย่างที่คุณเว้นไว้ — id ที่ถูกสร้างขึ้น, column ที่มี DEFAULT และ column ใด ๆ ที่อนุญาตให้เป็น NULL
บทเรียนนี้ใช้ table authors และ books จากภาพรวมของโมดูล
row เดียว
หัวข้อที่มีชื่อว่า “row เดียว”รูปแบบพื้นฐานที่สุดแสดงรายการ column ก่อน แล้วตามด้วยค่าที่ตรงกัน
INSERT INTO authors (name, born)VALUES ('Mira Castellan', 1971);await pool.query( 'INSERT INTO authors (name, born) VALUES ($1, $2)', ['Mira Castellan', 1971],);cur.execute( "INSERT INTO authors (name, born) VALUES (%s, %s)", ("Mira Castellan", 1971),)_, err := conn.Exec(ctx, "INSERT INTO authors (name, born) VALUES ($1, $2)", "Mira Castellan", 1971)sqlx::query("INSERT INTO authors (name, born) VALUES ($1, $2)") .bind("Mira Castellan") .bind(1971_i32) .execute(&pool) .await?;สังเกตว่าแท็บ 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 หนึ่งถูกเพิ่มเข้าไป
ปล่อยให้ column รับค่า default
หัวข้อที่มีชื่อว่า “ปล่อยให้ column รับค่า default”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);await pool.query( 'INSERT INTO books (title, author_id, published) VALUES ($1, $2, $3)', ['The Glass Orchard', 1, 2004],);cur.execute( "INSERT INTO books (title, author_id, published) VALUES (%s, %s, %s)", ("The Glass Orchard", 1, 2004),)_, err := conn.Exec(ctx, "INSERT INTO books (title, author_id, published) VALUES ($1, $2, $3)", "The Glass Orchard", 1, 2004)sqlx::query("INSERT INTO books (title, author_id, published) VALUES ($1, $2, $3)") .bind("The Glass Orchard") .bind(1_i64) .bind(2004_i32) .execute(&pool) .await?;ไม่ว่าจะวิธีไหน copies_sold ก็จะลงเอยเป็น 0 การละ column ไปทั้งหมดเป็นวิธีปกติทั่วไป ส่วนการเขียน DEFAULT มีประโยชน์เมื่อคุณกำลังเติมหลาย row และต้องการให้ทุกตำแหน่งถูกระบุออกมาชัด ๆ
หลาย row พร้อมกัน
หัวข้อที่มีชื่อว่า “หลาย row พร้อมกัน”คุณสามารถแสดงรายการ value tuple หลายอันในคำสั่งเดียวได้ ซึ่งเร็วกว่าการรัน INSERT หนึ่งครั้งต่อหนึ่ง row อย่างมาก เพราะเป็น round trip เดียวไปที่ server
INSERT INTO authors (name, born) VALUES ('Devon Reyes', 1985), ('Priya Anand', 1990), ('Tomas Holt', 1962);await pool.query( 'INSERT INTO authors (name, born) VALUES ($1, $2), ($3, $4), ($5, $6)', ['Devon Reyes', 1985, 'Priya Anand', 1990, 'Tomas Holt', 1962],);rows = [("Devon Reyes", 1985), ("Priya Anand", 1990), ("Tomas Holt", 1962)]cur.executemany("INSERT INTO authors (name, born) VALUES (%s, %s)", rows)rows := [][]any{ {"Devon Reyes", 1985}, {"Priya Anand", 1990}, {"Tomas Holt", 1962},}_, err := conn.CopyFrom(ctx, pgx.Identifier{"authors"}, []string{"name", "born"}, pgx.CopyFromRows(rows))let names = ["Devon Reyes", "Priya Anand", "Tomas Holt"];let years = [1985_i32, 1990, 1962];sqlx::query( "INSERT INTO authors (name, born) SELECT * FROM UNNEST($1::text[], $2::int[])",).bind(&names[..]).bind(&years[..]).execute(&pool).await?;driver แต่ละตัวมีเส้นทางสำหรับ bulk ที่เป็นสำนวนเฉพาะตัว: psycopg มี executemany, pgx มี CopyFrom ที่เร็วมาก และด้วย sqlx คุณสามารถ unnest array เพื่อให้คำสั่งเดียว insert ได้หลาย row
การดึง id ที่ถูกสร้างขึ้นกลับมา
หัวข้อที่มีชื่อว่า “การดึง id ที่ถูกสร้างขึ้นกลับมา”column id เป็น GENERATED ALWAYS AS IDENTITY ดังนั้น PostgreSQL จึงเป็นผู้กำหนดค่าให้ หากต้องการทราบค่าโดยไม่ต้อง query ครั้งที่สอง ให้เติม RETURNING
INSERT INTO authors (name, born)VALUES ('Lena Whitfield', 1978)RETURNING id;const res = await pool.query( 'INSERT INTO authors (name, born) VALUES ($1, $2) RETURNING id', ['Lena Whitfield', 1978],);console.log(res.rows[0].id);cur.execute( "INSERT INTO authors (name, born) VALUES (%s, %s) RETURNING id", ("Lena Whitfield", 1978),)new_id = cur.fetchone()[0]var id int64err := conn.QueryRow(ctx, "INSERT INTO authors (name, born) VALUES ($1, $2) RETURNING id", "Lena Whitfield", 1978).Scan(&id)let id: i64 = sqlx::query_scalar( "INSERT INTO authors (name, born) VALUES ($1, $2) RETURNING id",).bind("Lena Whitfield").bind(1978_i32).fetch_one(&pool).await?;คำสั่งนี้ทั้ง 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
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
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-rowINSERT,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-rowINSERTแทนการวน insert ทีละ row เพื่อให้ import ไฟล์หลักแสน row เสร็จในไม่กี่วินาทีแทนที่จะเป็นหลักนาที