การ select และการกรอง
การอ่านเป็นสิ่งที่คุณทำกับฐานข้อมูลบ่อยที่สุด และ SELECT คือเครื่องมือสำหรับงานนั้น SELECT หนึ่งคำสั่งอธิบายว่าคุณต้องการ column ไหน, row ไหนที่เข้าเกณฑ์, เรียงลำดับอย่างไร และเอาเท่าไหร่ server จะส่งคืนชุดผลลัพธ์ (result set) — ศูนย์หรือมากกว่าหนึ่ง row — โดยไม่เปลี่ยนแปลงอะไรบนดิสก์
ตัวอย่างเหล่านี้สมมติว่า table authors ถูกเติมด้วย row จำนวนหนึ่งจากบทเรียน insert
การเลือก column
หัวข้อที่มีชื่อว่า “การเลือก column”แสดงรายการ column ที่คุณต้องการหลัง SELECT ใช้ * เฉพาะตอนสำรวจอย่างรวดเร็วเท่านั้น ในโค้ดจริง ให้ระบุชื่อ column เพื่อให้ผลลัพธ์คงที่ แม้ในภายหลังจะมีคนเพิ่ม column เข้ามา
SELECT id, name, born FROM authors;const res = await pool.query('SELECT id, name, born FROM authors');console.log(res.rows);cur.execute("SELECT id, name, born FROM authors")for row in cur.fetchall(): print(row)rows, err := conn.Query(ctx, "SELECT id, name, born FROM authors")defer rows.Close()let rows = sqlx::query("SELECT id, name, born FROM authors") .fetch_all(&pool) .await?; id | name | born----+-----------------+------ 1 | Mira Castellan | 1971 2 | Devon Reyes | 1985 3 | Priya Anand | 1990 4 | Tomas Holt | 1962(4 rows)การจำกัดด้วย WHERE
หัวข้อที่มีชื่อว่า “การจำกัดด้วย WHERE”WHERE เก็บไว้เฉพาะ row ที่เงื่อนไขเป็นจริงเท่านั้น คุณสามารถเปรียบเทียบด้วย =, <, >, <=, >= และรวมเงื่อนไขด้วย AND และ OR
SELECT name, bornFROM authorsWHERE born >= 1980 AND born < 1995;const res = await pool.query( 'SELECT name, born FROM authors WHERE born >= $1 AND born < $2', [1980, 1995],);cur.execute( "SELECT name, born FROM authors WHERE born >= %s AND born < %s", (1980, 1995),)rows, err := conn.Query(ctx, "SELECT name, born FROM authors WHERE born >= $1 AND born < $2", 1980, 1995)let rows = sqlx::query("SELECT name, born FROM authors WHERE born >= $1 AND born < $2") .bind(1980_i32) .bind(1995_i32) .fetch_all(&pool) .await?; name | born-------------+------ Devon Reyes | 1985 Priya Anand | 1990(2 rows)IN, BETWEEN และการจับคู่ pattern
หัวข้อที่มีชื่อว่า “IN, BETWEEN และการจับคู่ pattern”IN ตรวจสอบการเป็นสมาชิกในรายการ, BETWEEN เป็นช่วงแบบรวมปลายทั้งสองข้าง และ LIKE จับคู่ pattern ของข้อความ LIKE สนใจตัวพิมพ์เล็กพิมพ์ใหญ่ PostgreSQL ยังมี ILIKE สำหรับการจับคู่แบบไม่สนใจตัวพิมพ์เล็กพิมพ์ใหญ่ด้วย ใน pattern นั้น % จับคู่กับอักขระต่อเนื่องชุดใดก็ได้ และ _ จับคู่กับหนึ่งอักขระพอดี
SELECT name, bornFROM authorsWHERE born BETWEEN 1960 AND 1980 AND name ILIKE '%a%';const res = await pool.query( 'SELECT name, born FROM authors WHERE born BETWEEN $1 AND $2 AND name ILIKE $3', [1960, 1980, '%a%'],);cur.execute( "SELECT name, born FROM authors WHERE born BETWEEN %s AND %s AND name ILIKE %s", (1960, 1980, "%a%"),)rows, err := conn.Query(ctx, "SELECT name, born FROM authors WHERE born BETWEEN $1 AND $2 AND name ILIKE $3", 1960, 1980, "%a%")let rows = sqlx::query( "SELECT name, born FROM authors WHERE born BETWEEN $1 AND $2 AND name ILIKE $3",).bind(1960_i32).bind(1980_i32).bind("%a%").fetch_all(&pool).await?; name | born----------------+------ Mira Castellan | 1971 Tomas Holt | 1962(2 rows)การจัดการ NULL
หัวข้อที่มีชื่อว่า “การจัดการ NULL”NULL หมายถึง “ไม่ทราบ” จึงไม่ทำตัวเหมือนค่าธรรมดาทั่วไป คุณทดสอบด้วย = ไม่ได้ ให้ใช้ IS NULL และ IS NOT NULL แทน สมมติว่า author คนหนึ่งไม่มีปีเกิดที่บันทึกไว้
SELECT name FROM authors WHERE born IS NULL;const res = await pool.query('SELECT name FROM authors WHERE born IS NULL');cur.execute("SELECT name FROM authors WHERE born IS NULL")rows, err := conn.Query(ctx, "SELECT name FROM authors WHERE born IS NULL")let rows = sqlx::query("SELECT name FROM authors WHERE born IS NULL") .fetch_all(&pool) .await?;การเรียงลำดับและแบ่งหน้า
หัวข้อที่มีชื่อว่า “การเรียงลำดับและแบ่งหน้า”ORDER BY เรียงผลลัพธ์ เติม DESC เพื่อเรียงจากมากไปน้อย LIMIT จำกัดจำนวน row และ OFFSET ข้าม row จากด้านบน — เมื่อใช้ร่วมกันจะช่วยให้คุณแบ่งหน้าผ่าน table ขนาดใหญ่ได้
SELECT name, bornFROM authorsORDER BY born DESCLIMIT 2 OFFSET 1;const res = await pool.query( 'SELECT name, born FROM authors ORDER BY born DESC LIMIT $1 OFFSET $2', [2, 1],);cur.execute( "SELECT name, born FROM authors ORDER BY born DESC LIMIT %s OFFSET %s", (2, 1),)rows, err := conn.Query(ctx, "SELECT name, born FROM authors ORDER BY born DESC LIMIT $1 OFFSET $2", 2, 1)let rows = sqlx::query("SELECT name, born FROM authors ORDER BY born DESC LIMIT $1 OFFSET $2") .bind(2_i64) .bind(1_i64) .fetch_all(&pool) .await?; name | born-------------+------ Devon Reyes | 1985 Mira Castellan | 1971(2 rows)ใน pgAdmin: รันอันใดก็ได้เหล่านี้ใน Query Tool ผลลัพธ์จะปรากฏใน table Data Output ด้านล่าง และคุณยังเรียง column ได้ด้วยการคลิกที่หัว column
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”NULLใช้ตรรกะสามค่า (three-valued logic): การเปรียบเทียบกับNULLไม่ได้เป็นจริงหรือเท็จ แต่เป็นไม่ทราบ (unknown) ดังนั้น row เหล่านั้นจึงถูกตัดออกโดยWHEREจงใช้IS NULLแทน= NULLเสมอLIMITที่ไม่มีORDER BYจะคืนชุดย่อยแบบสุ่ม — server อาจคืน row ในลำดับใดก็ได้ และลำดับนั้นอาจเปลี่ยนไประหว่างการรันแต่ละครั้ง จงจับคู่LIMITกับORDER BYที่มีผลแน่นอน (deterministic) เสมอILIKEสะดวกแต่ไม่สามารถใช้ index ธรรมดาได้อย่างมีประสิทธิภาพ สำหรับการค้นหาข้อความหนัก ๆ ให้ศึกษา trigram index หรือ full-text search ในภายหลังของคอร์ส- การ select เฉพาะ column ที่คุณต้องการช่วยให้ result set เล็กและโค้ดของคุณทนทานต่อการเปลี่ยนแปลง schema
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| ระบุ column list ชัดเจน | ผลลัพธ์คงที่ (stable API), ส่งข้อมูลผ่าน wire น้อยลง, ไม่ leak column ที่อ่อนไหว | ต้องแก้ query เมื่อ column ที่ต้องการเปลี่ยน |
SELECT * | เขียนเร็ว สะดวกตอน prototype หรือสำรวจข้อมูล | พังเงียบ ๆ เมื่อมีคนเพิ่ม/ลบ/เปลี่ยนชื่อ column ทีหลัง และอาจ leak column ที่ไม่ควรออก |
กรองด้วย WHERE ใน SQL | ถูก push ไปให้ database ทำ ใช้ index ได้ ส่งเฉพาะ row ที่ต้องการผ่าน wire | ต้องเข้าใจ SQL และวางแผน index ให้ตรงกับ pattern การกรอง |
| ดึงข้อมูลทั้งหมดมากรองใน application code | เขียน logic ฝั่งแอปได้ง่าย ไม่ต้องคิด SQL ซับซ้อน | เปลือง bandwidth, พลาดโอกาสใช้ index, ช้าลงเมื่อ table โต |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้
SELECT *ใน production code — เมื่อมีคนเพิ่ม column ใหม่ทีหลัง ผลลัพธ์ที่ได้เปลี่ยนไปแบบไม่ตั้งใจ และอาจ leak column ที่อ่อนไหวออกไปด้วย ให้ระบุ column ที่ต้องการเสมอ - ห่อ column ที่มี index ด้วย function ใน
WHEREเช่นWHERE lower(email) = ...— ทำให้ index ธรรมดาใช้งานไม่ได้ ต้องใช้ expression index หรือปรับ query - คิดว่า
NULLจับคู่กับ=ได้ —WHERE born = NULLไม่คืน row ใด ๆ เลยเพราะNULLเทียบด้วยตรรกะสามค่า ต้องใช้IS NULLแทน
💡 ตัวอย่างจากของจริง
GitHub API / GitLab API — บังคับให้ client ระบุ field ที่ต้องการอย่างชัดเจนและมี pagination เสมอ เพื่อป้องกัน
SELECT *ที่แอบเปลี่ยน contract ของ response และรักษา query plan ให้คาดเดาได้ระบบ dashboard แบบ real-time — กรองข้อมูลด้วย
WHEREและ index ที่วางแผนไว้ล่วงหน้าแทนการดึงทั้ง table มากรองในโค้ด เพื่อให้ query ตอบสนองไวแม้ table มีหลายสิบล้าน row