การ select และการกรอง
การอ่านคือสิ่งที่พบบ่อยที่สุดที่คุณทำกับฐานข้อมูล และ SELECT คือวิธีที่ทำ SELECT อธิบายว่าคุณต้องการ column ไหน row ไหนผ่านเกณฑ์ เรียงอย่างไร และกี่แถว เซิร์ฟเวอร์ส่งชุดผลลัพธ์ — ศูนย์ row หรือมากกว่า — โดยไม่เปลี่ยนอะไรบนดิสก์
ทุก output ด้านล่างเป็นของจริงจาก PostgreSQL 18.6 (postgres:18.6, aarch64, Docker Desktop บน Mac, กันยายน 2026) รันในฐานข้อมูลเปล่าหนึ่งตัว จากบนลงล่าง เริ่มด้วยชุดข้อมูลมาตรฐานของคอร์ส row ที่คุณเห็นที่นี่จึงเป็น row เดียวกับที่ได้จากบท insert ตัวเลขเวลามาจาก Mac เครื่องนี้
-- sql/setup.sqlCREATE TABLE authors ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, born int);CREATE TABLE books ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, author_id bigint REFERENCES authors (id), published int, copies_sold int NOT NULL DEFAULT 0);CREATE TABLE sales ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, book_id bigint REFERENCES books (id), region text NOT NULL, copies int NOT NULL);INSERT INTO authors (name, born) VALUES ('Mara Linde', 1968), ('Owen Pryce', 1975), ('Sela Vance', 1990);INSERT INTO books (title, author_id, published, copies_sold) VALUES ('Quiet Harbors', 1, 2003, 450), ('The Glass Orchard', 1, 2018, 1250), ('Tidal Notes', 2, 2011, 600), ('Unlinked Draft', NULL, 2020, 0);INSERT INTO sales (book_id, region, copies) VALUES (2, 'East', 900), (3, 'West', 400), (1, 'West', 400), (3, 'East', 200), (2, 'West', 150), (1, 'East', 50);การเลือก column
หัวข้อที่มีชื่อว่า “การเลือก column”ลิสต์ column ที่ต้องการหลัง SELECT ใช้ * เฉพาะตอนสำรวจเร็ว ๆ ในโค้ดจริงให้ระบุชื่อ column เพื่อให้ผลลัพธ์คงที่แม้มีคนเพิ่ม column ทีหลัง
-- sql/select-columns.sqlSELECT 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 | Mara Linde | 1968 2 | Owen Pryce | 1975 3 | Sela Vance | 1990(3 rows)SELECT * คืน column ทั้งหมดที่ table มีในวันนี้:
-- sql/select-star.sqlSELECT * FROM books WHERE id = 1; id | title | author_id | published | copies_sold----+---------------+-----------+-----------+------------- 1 | Quiet Harbors | 1 | 2003 | 450(1 row)driver แต่ละตัวถูกรันหนึ่งครั้งกับชุดข้อมูลนี้ (ผลลัพธ์รวมไว้ท้ายบทเรียน) รายละเอียดหนึ่งที่เห็นได้ที่นั่น: node-postgres คืน id แบบ bigint เป็นสตริง (id: '1') เพราะตัวเลข JavaScript เก็บจำนวนเต็ม 64 บิตได้ไม่ครบ
การกรองด้วย WHERE
หัวข้อที่มีชื่อว่า “การกรองด้วย WHERE”WHERE เก็บเฉพาะ row ที่เงื่อนไขเป็นจริง คุณเปรียบเทียบด้วย =, <, >, <=, >= และรวมเงื่อนไขด้วย AND และ OR ได้
-- sql/select-where.sqlSELECT name, bornFROM authorsWHERE born >= 1970 AND born < 1995;const res = await pool.query( 'SELECT name, born FROM authors WHERE born >= $1 AND born < $2', [1970, 1995],);cur.execute( "SELECT name, born FROM authors WHERE born >= %s AND born < %s", (1970, 1995),)rows, err := conn.Query(ctx, "SELECT name, born FROM authors WHERE born >= $1 AND born < $2", 1970, 1995)let rows = sqlx::query("SELECT name, born FROM authors WHERE born >= $1 AND born < $2") .bind(1970_i32) .bind(1995_i32) .fetch_all(&pool) .await?; name | born------------+------ Owen Pryce | 1975 Sela Vance | 1990(2 rows)IN, BETWEEN และการจับคู่รูปแบบ
หัวข้อที่มีชื่อว่า “IN, BETWEEN และการจับคู่รูปแบบ”IN ตรวจการเป็นสมาชิกในลิสต์ BETWEEN คือช่วงแบบรวมปลายทั้งสอง (รวม 1968 และ 1975 ด้านล่าง) และ LIKE จับคู่รูปแบบข้อความ
-- sql/select-between-in.sqlSELECT name, bornFROM authorsWHERE born BETWEEN 1968 AND 1975;
SELECT title FROM books WHERE published IN (2003, 2011); name | born------------+------ Mara Linde | 1968 Owen Pryce | 1975(2 rows)
title--------------- Quiet Harbors Tidal Notes(2 rows)LIKE แยกตัวพิมพ์เล็กใหญ่ PostgreSQL มี ILIKE สำหรับการจับคู่แบบไม่สนตัวพิมพ์ ในรูปแบบ % ตรงกับตัวอักษรกี่ตัวก็ได้ และ _ ตรงกับหนึ่งตัวพอดี
-- sql/select-ilike.sqlSELECT 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------------+------ Mara Linde | 1968(1 row)กฎเรื่องตัวพิมพ์เทียบข้างกัน ('m%' เป็นตัวพิมพ์เล็ก ชื่อขึ้นต้นด้วย M):
-- sql/select-like-case.sqlSELECT name, name LIKE 'm%' AS like_lower_m, name ILIKE 'm%' AS ilike_lower_m, name LIKE '_ara%' AS underscore_matches_one_charFROM authors WHERE id = 1; name | like_lower_m | ilike_lower_m | underscore_matches_one_char------------+--------------+---------------+----------------------------- Mara Linde | f | t | t(1 row)การจัดการ NULL
หัวข้อที่มีชื่อว่า “การจัดการ NULL”NULL หมายถึง “ไม่ทราบ” จึงไม่ทำตัวเหมือนค่าปกติ คุณทดสอบด้วย = ไม่ได้ ใช้ IS NULL และ IS NOT NULL แทน Unlinked Draft ในชุดข้อมูลไม่มี author
-- sql/select-is-null.sqlSELECT title FROM books WHERE author_id IS NULL;const res = await pool.query('SELECT title FROM books WHERE author_id IS NULL');cur.execute("SELECT title FROM books WHERE author_id IS NULL")rows, err := conn.Query(ctx, "SELECT title FROM books WHERE author_id IS NULL")let rows = sqlx::query("SELECT title FROM books WHERE author_id IS NULL") .fetch_all(&pool) .await?; title---------------- Unlinked Draft(1 row)ส่วนที่เหลือของตรรกะสามค่า รันบน table เดียวกัน = NULL ไม่คืนอะไร <> 1 ทิ้ง row ที่ author เป็น NULL ด้วย เพราะ unknown <> 1 คือ unknown ไม่ใช่ true ส่วน IS DISTINCT FROM ปฏิบัติต่อ NULL เหมือนค่าปกติ และ count(column) ข้าม NULL ขณะที่ count(*) ไม่ข้าม:
-- sql/select-null.sqlSELECT title FROM books WHERE author_id = NULL;SELECT title FROM books WHERE author_id <> 1 ORDER BY id;SELECT title FROM books WHERE author_id IS DISTINCT FROM 1 ORDER BY id;SELECT count(*) AS all_rows, count(author_id) AS non_null_author_id FROM books; title-------(0 rows)
title------------- Tidal Notes(1 row)
title---------------- Tidal Notes Unlinked Draft(2 rows)
all_rows | non_null_author_id----------+-------------------- 4 | 3(1 row)NULL ยังมีตำแหน่งการเรียง: ท้ายสุดในการเรียงจากน้อยไปมากโดยค่าเริ่มต้น และหน้าสุดเมื่อใช้ NULLS FIRST:
-- sql/select-null-order.sqlSELECT title, author_id FROM books ORDER BY author_id;SELECT title, author_id FROM books ORDER BY author_id NULLS FIRST, id; title | author_id-------------------+----------- Quiet Harbors | 1 The Glass Orchard | 1 Tidal Notes | 2 Unlinked Draft |(4 rows)
title | author_id-------------------+----------- Unlinked Draft | Quiet Harbors | 1 The Glass Orchard | 1 Tidal Notes | 2(4 rows)การเรียงลำดับและการแบ่งหน้า
หัวข้อที่มีชื่อว่า “การเรียงลำดับและการแบ่งหน้า”ORDER BY เรียงผลลัพธ์ เพิ่ม DESC สำหรับเรียงจากมากไปน้อย LIMIT จำกัดจำนวน row และ OFFSET ข้าม row จากบนสุด — สองอย่างรวมกันให้คุณเปิดดู table ใหญ่ทีละหน้า
-- sql/select-order-limit.sqlSELECT 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------------+------ Owen Pryce | 1975 Mara Linde | 1968(2 rows)ใน pgAdmin: รันตัวอย่างใดก็ได้ใน Query Tool ผลลัพธ์ปรากฏใน grid Data Output ด้านล่าง ซึ่งคุณเรียง column ได้โดยคลิกหัวคอลัมน์
ลำดับการเรียงแน่นอนได้ก็ต่อเมื่อ key ที่ใช้เรียงไม่ซ้ำ sales มี tie (สอง row ที่ 400 copies) เพิ่ม tiebreaker เช่น primary key เพื่อให้ลำดับทำซ้ำได้:
-- sql/select-order-tiebreaker.sqlSELECT id, region, copies FROM sales ORDER BY copies DESC, id LIMIT 3; id | region | copies----+--------+-------- 1 | East | 900 2 | West | 400 3 | West | 400(3 rows)ข้อความเรียงตาม collation ของฐานข้อมูล ไม่ใช่ตามค่าไบต์ ใน en_US.utf8 apple กับ Apple อยู่ติดกัน ใน "C" ตัวพิมพ์ใหญ่ทุกตัวมาก่อนตัวพิมพ์เล็กทุกตัว:
-- sql/select-collation.sqlSELECT datcollate AS database_collation FROM pg_database WHERE datname = current_database();SELECT w AS default_collation FROM (VALUES ('apple'), ('Banana'), ('cherry'), ('Apple')) t(w) ORDER BY w;SELECT w AS c_collation FROM (VALUES ('apple'), ('Banana'), ('cherry'), ('Apple')) t(w) ORDER BY w COLLATE "C"; database_collation-------------------- en_US.utf8(1 row)
default_collation------------------- apple Apple Banana cherry(4 rows)
c_collation------------- Apple Banana apple cherry(4 rows)และ LIMIT ที่ไม่มี ORDER BY คืน row บางส่วน ตามลำดับที่เซิร์ฟเวอร์อ่านได้ มันดูคงที่จนกระทั่งมี row หนึ่งถูก update (PostgreSQL เขียน row เวอร์ชันใหม่ไว้คนละที่) แล้วสอง row แรกก็เปลี่ยน:
-- sql/limit-without-order.sqlSELECT id, title FROM books LIMIT 2;UPDATE books SET copies_sold = copies_sold WHERE id = 1;SELECT id, title FROM books LIMIT 2;SELECT id, title FROM books ORDER BY id LIMIT 2; id | title----+------------------- 1 | Quiet Harbors 2 | The Glass Orchard(2 rows)
UPDATE 1 id | title----+------------------- 2 | The Glass Orchard 3 | Tidal Notes(2 rows)
id | title----+------------------- 1 | Quiet Harbors 2 | The Glass Orchard(2 rows)OFFSET ลึกช้า keyset paging ไม่ช้า
หัวข้อที่มีชื่อว่า “OFFSET ลึกช้า keyset paging ไม่ช้า”OFFSET n ข้าม row โดยไม่อ่านไม่ได้: เซิร์ฟเวอร์สร้าง n + limit row แรกแล้วทิ้ง n ตัว ต้นทุนโตตามเลขหน้า keyset (“seek”) paging ขอ row หลัง key ล่าสุดที่คุณเห็นแทน บน 200,000 row หน้า 15,001 ทั้งสองวิธี (plan แสดง row ที่อ่านจริง):
-- sql/offset-vs-keyset.sqlCREATE TABLE events (id int PRIMARY KEY, payload text);INSERT INTO events SELECT g, md5(g::text) FROM generate_series(1, 200000) g;ANALYZE events;EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY ON)SELECT id FROM events ORDER BY id LIMIT 10 OFFSET 150000;EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY ON)SELECT id FROM events WHERE id > 150000 ORDER BY id LIMIT 10;CREATE TABLEINSERT 0 200000ANALYZE QUERY PLAN------------------------------------------------------------------------------------------------------ Limit (actual time=19.105..19.108 rows=10.00 loops=1) -> Index Only Scan using events_pkey on events (actual time=0.012..15.379 rows=150010.00 loops=1) Heap Fetches: 150010 Index Searches: 1 Planning Time: 0.095 ms Execution Time: 19.118 ms(6 rows)
QUERY PLAN------------------------------------------------------------------------------------------------- Limit (actual time=0.004..0.006 rows=10.00 loops=1) -> Index Only Scan using events_pkey on events (actual time=0.004..0.005 rows=10.00 loops=1) Index Cond: (id > 150000) Heap Fetches: 10 Index Searches: 1 Planning Time: 0.039 ms Execution Time: 0.010 ms(7 rows)OFFSET 150000 อ่าน 150,010 row ใช้ราว 19 ms ส่วนแบบ keyset อ่าน 10 row ใช้ไมโครวินาทีไม่กี่ตัว หน้าลึก ๆ ผู้ใช้จริงแทบไม่ต้องการ แต่ crawler และงาน export ต้องการตลอด ข้อแลกเปลี่ยน: keyset ต้องมี key เรียงที่ไม่ซ้ำและมี index (ORDER BY id หรือ (created_at, id)) และกระโดดไป “หน้า 300” ตรง ๆ ไม่ได้
การจับคู่รูปแบบและ index
หัวข้อที่มีชื่อว่า “การจับคู่รูปแบบและ index”คำแนะนำที่ว่า ILIKE '%text%' “ใช้ index ธรรมดาไม่ได้” เป็นจริง และทางแก้คือ trigram index บน 200,000 row เดิม พบ 4 row จาก 200,000 กับ ILIKE บน '%A3F9C%' (ข้อมูลเป็น hex ตัวพิมพ์เล็ก จึงทำให้ ILIKE มีความหมาย): แม้มี B-tree ธรรมดาบน column PostgreSQL ก็อ่านทั้ง table แต่เมื่อมี GIN index ของ pg_trgm มันอ่าน heap block ห้าบล็อก:
-- sql/ilike-trigram.sqlCREATE EXTENSION IF NOT EXISTS pg_trgm;CREATE INDEX events_payload_btree ON events (payload);EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY ON)SELECT id FROM events WHERE payload ILIKE '%A3F9C%';CREATE INDEX events_payload_trgm ON events USING gin (payload gin_trgm_ops);EXPLAIN (ANALYZE, COSTS OFF, BUFFERS OFF, SUMMARY ON)SELECT id FROM events WHERE payload ILIKE '%A3F9C%';CREATE EXTENSIONCREATE INDEX QUERY PLAN-------------------------------------------------------------------- Seq Scan on events (actual time=11.414..102.843 rows=4.00 loops=1) Filter: (payload ~~* '%A3F9C%'::text) Rows Removed by Filter: 199996 Planning Time: 0.296 ms Execution Time: 102.865 ms(5 rows)
CREATE INDEX QUERY PLAN--------------------------------------------------------------------------------------------- Bitmap Heap Scan on events (actual time=0.157..0.167 rows=4.00 loops=1) Recheck Cond: (payload ~~* '%A3F9C%'::text) Rows Removed by Index Recheck: 1 Heap Blocks: exact=5 -> Bitmap Index Scan on events_payload_trgm (actual time=0.147..0.147 rows=5.00 loops=1) Index Cond: (payload ~~* '%A3F9C%'::text) Index Searches: 1 Planning Time: 0.188 ms Execution Time: 0.185 ms(9 rows)แนวคิดเดียวกันกับ WHERE lower(email) = ...: index ธรรมดาบน payload ถูกเมิน แต่ expression index บนนิพจน์เดียวกันถูกใช้:
-- sql/lower-expression-index.sqlEXPLAIN (COSTS OFF) SELECT id FROM events WHERE lower(payload) = 'abc';CREATE INDEX events_payload_lower ON events (lower(payload));EXPLAIN (COSTS OFF) SELECT id FROM events WHERE lower(payload) = 'abc'; QUERY PLAN------------------------------------------------ Gather Workers Planned: 1 -> Parallel Seq Scan on events Filter: (lower(payload) = 'abc'::text)(4 rows)
CREATE INDEX QUERY PLAN---------------------------------------------------- Bitmap Heap Scan on events Recheck Cond: (lower(payload) = 'abc'::text) -> Bitmap Index Scan on events_payload_lower Index Cond: (lower(payload) = 'abc'::text)(4 rows)โมดูล indexes ลงลึกทั้งหมดนี้ ประเด็นตรงนี้คือรูปแบบของ query ตัดสินว่า index ช่วยได้หรือไม่
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”ผลลัพธ์ของ driver ที่สัญญาไว้ก่อนหน้า:
$ (each snippet run once against the course dataset in a scratch database; the program then prints the values of one column)[Choosing columns] Node.js (pg 8.23.0): [ { id: '1', name: 'Mara Linde', born: 1968 }, { id: '2', name: 'Owen Pryce', born: 1975 }, { id: '3', name: 'Sela Vance', born: 1990 } ][Choosing columns] Python (psycopg 3.3.6): (1, 'Mara Linde', 1968) (2, 'Owen Pryce', 1975) (3, 'Sela Vance', 1990)[Choosing columns] Go (pgx v5.11.0): Mara Linde, Owen Pryce, Sela Vance[Choosing columns] Rust (sqlx 0.9.0): Mara Linde, Owen Pryce, Sela Vance[WHERE] Node.js (pg 8.23.0): Owen Pryce, Sela Vance[WHERE] Python (psycopg 3.3.6): Owen Pryce, Sela Vance[WHERE] Go (pgx v5.11.0): Owen Pryce, Sela Vance[WHERE] Rust (sqlx 0.9.0): Owen Pryce, Sela Vance[BETWEEN + ILIKE] Node.js (pg 8.23.0): Mara Linde[BETWEEN + ILIKE] Python (psycopg 3.3.6): Mara Linde[BETWEEN + ILIKE] Go (pgx v5.11.0): Mara Linde[BETWEEN + ILIKE] Rust (sqlx 0.9.0): Mara Linde[IS NULL] Node.js (pg 8.23.0): Unlinked Draft[IS NULL] Python (psycopg 3.3.6): Unlinked Draft[IS NULL] Go (pgx v5.11.0): Unlinked Draft[IS NULL] Rust (sqlx 0.9.0): Unlinked Draft[ORDER BY / LIMIT / OFFSET] Node.js (pg 8.23.0): Owen Pryce, Mara Linde[ORDER BY / LIMIT / OFFSET] Python (psycopg 3.3.6): Owen Pryce, Mara Linde[ORDER BY / LIMIT / OFFSET] Go (pgx v5.11.0): Owen Pryce, Mara Linde[ORDER BY / LIMIT / OFFSET] Rust (sqlx 0.9.0): Owen Pryce, Mara Lindeผู้ช่วย AI ถูกขอ endpoint หนังสือที่แบ่งหน้า และเขียน query ที่ตรงไปตรงมา:
🐛 หา bug ในโค้ดนี้
-- GET /api/books?page=N (AI comment: "simple pagination, 2 per page")SELECT id, title FROM books LIMIT 2 OFFSET :page_offset;เฉลย
มันผ่านทุกเทสต์บน table ที่นิ่ง เมื่อไม่มี ORDER BY เซิร์ฟเวอร์คืน row ตามลำดับใดก็ได้ และลำดับเปลี่ยนจริงเมื่อข้อมูลถูกแก้ (แสดงข้างบน) ที่นี่ผู้ใช้อ่านหน้า 1 ของ feed สี่ row มีคนแก้ row 1 แล้วผู้ใช้ขอหน้า 2:
-- sql/pagination-bug.sql-- page 1 of a "newest first" list, 2 per page, but no ORDER BYCREATE TABLE feed (id int PRIMARY KEY, title text);INSERT INTO feed VALUES (1, 'one'), (2, 'two'), (3, 'three'), (4, 'four');SELECT id, title FROM feed LIMIT 2 OFFSET 0;-- someone edits row 1 while the user reads page 1 ...UPDATE feed SET title = 'one (edited)' WHERE id = 1;-- ... page 2:SELECT id, title FROM feed LIMIT 2 OFFSET 2;CREATE TABLEINSERT 0 4 id | title----+------- 1 | one 2 | two(2 rows)
UPDATE 1 id | title----+-------------- 4 | four 1 | one (edited)(2 rows)หน้า 2 แสดง row 1 อีกครั้ง (ที่แก้แล้ว) และไม่เคยแสดง row 3 ทั้งสองเงียบ: ไม่มี error ไม่มี warning วิธีแก้คือการเรียงที่แน่นอนบน key ที่ไม่ซ้ำ:
-- sql/pagination-fix.sqlSELECT id, title FROM feed ORDER BY id LIMIT 2 OFFSET 0;SELECT id, title FROM feed ORDER BY id LIMIT 2 OFFSET 2; id | title----+-------------- 1 | one (edited) 2 | two(2 rows)
id | title----+------- 3 | three 4 | four(2 rows)Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”SELECT คือ pipeline: FROM (row ไหนมีอยู่) แล้ว WHERE (ตัวไหนผ่าน) แล้ว select list แล้ว ORDER BY แล้ว LIMIT/OFFSET แต่ละขั้นเห็นเฉพาะสิ่งที่ขั้นก่อนหน้าส่งให้ นั่นคือเหตุผลที่การเปรียบเทียบกับ NULL ทิ้ง row แบบเงียบ ๆ (ขั้น WHERE เก็บเฉพาะ true) ทำไม LIMIT ที่ไม่เรียงจึงไม่แน่นอน (ไม่เคยกำหนดลำดับ) และทำไม OFFSET ลึกจึงแพง (ขั้นก่อนหน้าต้องสร้าง row เหล่านั้นก่อน OFFSET จะทิ้งได้)
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”NULLใช้ตรรกะสามค่า: การเปรียบเทียบกับNULLไม่จริงไม่เท็จแต่ไม่ทราบ row เหล่านั้นจึงถูกWHEREตัดออก ใช้IS NULL(หรือIS DISTINCT FROM) แทน= NULLเสมอLIMITที่ไม่มีORDER BYคืนส่วนย่อยตามอำเภอใจ — เซิร์ฟเวอร์คืน row ลำดับใดก็ได้ และลำดับนั้นเปลี่ยนได้ระหว่างการรัน จับคู่LIMITกับORDER BYที่แน่นอนพร้อม tiebreaker ที่ไม่ซ้ำเสมอILIKE '%x%'ใช้ B-tree ธรรมดาไม่ได้ GIN index ของpg_trgmทำให้เร็ว (ราว 100 ms เหลือ 0.2 ms ข้างบน)lower(col) = ...ต้องใช้ expression index- ควรใช้ keyset paging แทน
OFFSETลึกกับ table ใหญ่ - เลือกเฉพาะ column ที่ต้องการ ทำให้ชุดผลลัพธ์เล็กและโค้ดทนต่อการเปลี่ยน schema
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| ลิสต์ column ชัดเจน | API คงที่ ข้อมูลผ่านเครือข่ายน้อยลง ไม่รั่ว column อ่อนไหว | ต้องแก้ query เมื่อ column ที่ต้องการเปลี่ยน |
SELECT * | เขียนเร็ว สะดวกสำหรับ prototype หรือสำรวจข้อมูล | พังแบบเงียบ ๆ เมื่อมีคนเพิ่ม/ลบ/เปลี่ยนชื่อ column ทีหลัง และอาจรั่ว column ที่ไม่ควรเปิดเผย |
กรองด้วย WHERE ใน SQL | ถูกผลักลงฐานข้อมูล ใช้ index ได้ มีเฉพาะ row ที่ต้องการข้ามเครือข่าย | ต้องเข้าใจ SQL และวางแผน index ให้ตรงกับรูปแบบตัวกรอง |
| ดึงทุกอย่างแล้วกรองในโค้ดแอป | ตรรกะฝั่งแอปง่าย ไม่ต้องเขียน SQL ซับซ้อน | สิ้นเปลืองแบนด์วิดท์ พลาดโอกาสใช้ index และช้าลงเมื่อ table โต |
แบ่งหน้าด้วย LIMIT/OFFSET | เรียบง่าย กระโดดไปหน้าใดก็ได้ | ต้นทุนโตตาม offset (19 ms ที่ 150,000 row) |
| Keyset paging | ต้นทุนต่อหน้าคงที่ | ต้องมี key เรียงที่ไม่ซ้ำและมี index กระโดดหน้าแบบสุ่มไม่ได้ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
WHERE col <> 1คืนทุก row ที่ไม่ใช่ 1” ไม่รวม row ที่colเป็น NULL (พิสูจน์ข้างบน) - “
LIMIT 10ให้ 10 row แรก” ให้ 10 row ตัวไหนไม่ระบุถ้าไม่มีORDER BY - “
BETWEENไม่รวมขอบบน” รวมทั้งสองปลาย - “
ILIKEคือLIKEที่ช้ากว่า” มันยังใช้ index เดียวกับLIKEไม่ได้ด้วย trigram index ทำให้เร็วทั้งคู่ - “การเรียงเหมือนกันทุกที่” มันตาม collation:
apple, Apple, Bananaในen_US.utf8,Apple, Banana, appleใน"C"