พื้นฐาน Transaction
transaction ทำให้หลาย statement เป็นหน่วยเดียวที่แบ่งแยกไม่ได้ คุณเปิดด้วย BEGIN รัน statement ที่ต้องการจัดกลุ่ม แล้วปิดด้วย COMMIT เพื่อเก็บงานไว้ หรือ ROLLBACK เพื่อทิ้ง ไม่มีอะไรใน transaction ที่ connection อื่นมองเห็นจนกว่าคุณจะ commit (docs)
ทุกอย่างด้านล่างรันบน PostgreSQL 18.6 (postgres:18.6, aarch64, Docker Desktop บน Mac, กันยายน 2026) ตัวอย่าง session เดียวคือ fence sql ที่รันจากบนลงล่างในฐานข้อมูลเปล่าหนึ่งตัว ตัวอย่างสอง session เป็นการรันจริงของสองโปรเซส psql ส่วน snippet ของ driver รันอย่างละหนึ่งครั้ง (pg 8.23.0, psycopg 3.3.6, pgx v5.11.0, sqlx 0.9.0)
ทุกตัวอย่างใช้ table accounts เล็ก ๆ ตัวเดียว CHECK constraint สำคัญ เพราะเป็นวิธีที่เราจะทำให้ statement ล้มเหลวโดยตั้งใจ
-- sql/setup.sqlCREATE TABLE accounts ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, owner text NOT NULL, balance numeric NOT NULL CHECK (balance >= 0));
INSERT INTO accounts (owner, balance) VALUES ('Ada', 500), ('Linus', 100);การโอนเงินแบบคลาสสิก
หัวข้อที่มีชื่อว่า “การโอนเงินแบบคลาสสิก”โอน 100 จาก Ada ไป Linus คือสอง update ที่ต้องเกิดพร้อมกัน ถ้าเซิร์ฟเวอร์ crash ระหว่างสองอัน atomicity รับประกันว่าไม่มีการเปลี่ยนแปลงใดรอด เงินจึงไม่ถูกสร้างหรือหายไป
-- sql/transfer.sqlBEGIN;UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus';COMMIT;const client = await pool.connect();try { await client.query('BEGIN'); await client.query('UPDATE accounts SET balance = balance - $1 WHERE owner = $2', [100, 'Ada']); await client.query('UPDATE accounts SET balance = balance + $1 WHERE owner = $2', [100, 'Linus']); await client.query('COMMIT');} catch (e) { await client.query('ROLLBACK'); throw e;} finally { client.release();}with conn.transaction(): cur.execute("UPDATE accounts SET balance = balance - %s WHERE owner = %s", (100, "Ada")) cur.execute("UPDATE accounts SET balance = balance + %s WHERE owner = %s", (100, "Linus"))# the block commits on success and rolls back on any exceptiontx, err := conn.Begin(ctx)if err != nil { return err}defer tx.Rollback(ctx) // a no-op after a successful Commitif _, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance - $1 WHERE owner = $2", 100, "Ada"); err != nil { return err}if _, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance + $1 WHERE owner = $2", 100, "Linus"); err != nil { return err}return tx.Commit(ctx)let mut tx = pool.begin().await?;sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE owner = $2") .bind(100_i64).bind("Ada").execute(&mut *tx).await?;sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE owner = $2") .bind(100_i64).bind("Linus").execute(&mut *tx).await?;tx.commit().await?;หลัง commit ยอดคงเหลือสะท้อนทั้งสอง update ไปพร้อมกัน
-- sql/after-transfer.sqlSELECT * FROM accounts ORDER BY id; id | owner | balance----+-------+--------- 1 | Ada | 400 2 | Linus | 200(2 rows)เลือกทิ้งงาน
หัวข้อที่มีชื่อว่า “เลือกทิ้งงาน”ถ้าอะไรดูผิดก่อน commit ROLLBACK ทิ้งทุกการเปลี่ยนแปลงตั้งแต่ BEGIN เหมือน transaction ไม่เคยรัน สังเกตว่าใน transaction คุณเห็นการเปลี่ยนแปลงที่ยังไม่ commit ของตัวเอง (Ada แสดง 300) หลัง rollback มันหายไป
-- sql/rollback.sqlBEGIN;UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';SELECT owner, balance FROM accounts ORDER BY id;-- on reflection, cancel everything:ROLLBACK;SELECT owner, balance FROM accounts ORDER BY id;const client = await pool.connect();await client.query('BEGIN');await client.query('UPDATE accounts SET balance = balance - $1 WHERE owner = $2', [100, 'Ada']);await client.query('ROLLBACK');client.release();with conn.transaction(): cur.execute("UPDATE accounts SET balance = balance - %s WHERE owner = %s", (100, "Ada")) raise psycopg.Rollback() # leaves the block and rolls it back; no error escapestx, err := conn.Begin(ctx)if err != nil { return err}if _, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance - $1 WHERE owner = $2", 100, "Ada"); err != nil { return err}return tx.Rollback(ctx)let mut tx = pool.begin().await?;sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE owner = $2") .bind(100_i64).bind("Ada").execute(&mut *tx).await?;tx.rollback().await?;BEGINUPDATE 1 owner | balance-------+--------- Ada | 300 Linus | 200(2 rows)
ROLLBACK owner | balance-------+--------- Ada | 400 Linus | 200(2 rows)driver ทั้งสี่ แต่ละ snippet รันหนึ่งครั้งกับฐานข้อมูล scratch ที่รีเซ็ตเป็น Ada 500 / Linus 100 ก่อนทุกครั้ง กลุ่มสุดท้ายคือตัวอย่าง savepoint จากด้านล่าง จึงพิมพ์ไว้ที่นี่ด้วย:
$ (each snippet run once; the scratch database is reset before every run)[transfer] Node.js (pg 8.23.0): Ada 400, Linus 200[transfer] Python (psycopg 3.3.6): Ada 400, Linus 200[transfer] Go (pgx v5.11.0): Ada 400, Linus 200[transfer] Rust (sqlx 0.9.0): Ada 400, Linus 200[rollback] Node.js (pg 8.23.0): Ada 500, Linus 100[rollback] Python (psycopg 3.3.6): Ada 500, Linus 100[rollback] Go (pgx v5.11.0): Ada 500, Linus 100[rollback] Rust (sqlx 0.9.0): Ada 500, Linus 100[savepoint] Node.js (pg 8.23.0): rolling back to the savepoint: 23514 | Ada 400, Linus 200[savepoint] Python (psycopg 3.3.6): rolling back to the savepoint: 23514 | Ada 400, Linus 200[savepoint] Go (pgx v5.11.0): rolling back to the savepoint: 23514 | Ada 400, Linus 200[savepoint] Rust (sqlx 0.9.0): rolling back to the savepoint: 23514 | Ada 400, Linus 200ชีวิตของ transaction
หัวข้อที่มีชื่อว่า “ชีวิตของ transaction”ทุก transaction เดินทางเส้นเดียวกัน: เริ่ม รัน statement และจบในสองทางใดทางหนึ่งเท่านั้น เมื่อคุณตัดสินใจแล้วไม่มีสถานะกึ่งกลาง
flowchart TD
A[BEGIN] --> B[Run statements]
B --> C{"Everything correct?"}
C -->|Yes| D[COMMIT: changes become permanent]
C -->|No| E[ROLLBACK: changes discarded]
D --> F[Transaction ends]
E --> F ไม่มี BEGIN ก็ไม่มีอะไร atomic
หัวข้อที่มีชื่อว่า “ไม่มี BEGIN ก็ไม่มีอะไร atomic”โดยปกติ connection อยู่ในโหมด auto-commit: แต่ละ statement เดี่ยวคือ transaction ของตัวเอง และถูก commit ทันทีที่เสร็จ ที่นี่เราย้าย 500 จาก Linus (ที่มีแค่ 200) ไป Ada ด้วยสอง statement แยกกัน การเพิ่มเงินสำเร็จและถูก commit ทันที แล้วการหักเงินล้มเหลวที่ CHECK:
-- @expect-error 23514-- move 500 from Linus (who has 200) to Ada, WITHOUT BEGIN: each statement is its own transactionUPDATE accounts SET balance = balance + 500 WHERE owner = 'Ada';UPDATE accounts SET balance = balance - 500 WHERE owner = 'Linus';UPDATE 1ERROR: new row for relation "accounts" violates check constraint "accounts_balance_check"DETAIL: Failing row contains (2, Linus, -300).-- sql/half-done.sqlSELECT owner, balance, sum(balance) OVER () AS total FROM accounts ORDER BY id; owner | balance | total-------+---------+------- Ada | 900 | 1100 Linus | 200 | 1100(2 rows)Ada ได้ 500 ที่ไม่มีใครเสีย: ยอดรวมจาก 600 กลายเป็น 1100 นี่คือสถานะทำครึ่งเดียวที่ transaction มีไว้ป้องกัน ดึง 500 กลับด้วยมือ:
-- sql/repair.sqlUPDATE accounts SET balance = balance - 500 WHERE owner = 'Ada';ทีนี้สอง statement เดิมภายใน BEGIN … COMMIT การหักเงินล้มเหลว และ PostgreSQL ใส่ transaction เข้าสถานะ aborted: ทุกคำสั่งหลังจากนั้นถูกปฏิเสธด้วย SQLSTATE 25P02 จนกว่าคุณจะจบ transaction แม้แต่ COMMIT ก็ช่วยไม่ได้ และมันตอบด้วย command tag ROLLBACK ไม่ใช่ COMMIT:
-- @expect-error 23514BEGIN;UPDATE accounts SET balance = balance + 500 WHERE owner = 'Ada';UPDATE accounts SET balance = balance - 500 WHERE owner = 'Linus';SELECT 1;COMMIT;BEGINUPDATE 1ERROR: new row for relation "accounts" violates check constraint "accounts_balance_check"DETAIL: Failing row contains (2, Linus, -300).ERROR: current transaction is aborted, commands ignored until end of transaction blockROLLBACK-- sql/atomic-check.sqlSELECT owner, balance, sum(balance) OVER () AS total FROM accounts ORDER BY id; owner | balance | total-------+---------+------- Ada | 400 | 600 Linus | 200 | 600(2 rows)ไม่มีอะไรเปลี่ยน: Ada 400, Linus 200 ยอดรวม 600 การเพิ่มเงินที่รันก่อนความล้มเหลวถูกทิ้งไปพร้อมกับทุกอย่าง
Rollback บางส่วนด้วย SAVEPOINT
หัวข้อที่มีชื่อว่า “Rollback บางส่วนด้วย SAVEPOINT”บางครั้งคุณอยากยกเลิกแค่บางส่วนของ transaction โดยไม่ทิ้งส่วนที่เหลือ SAVEPOINT คือจุดหมายที่มีชื่อภายใน transaction ROLLBACK TO ย้อนกลับไปที่จุดนั้น เก็บทุกอย่างก่อนหน้าและทิ้งทุกอย่างหลังจากนั้น มันยังเป็นวิธีเดียวที่จะกู้จาก error แล้วไปต่อได้: ROLLBACK TO พา transaction ออกจากสถานะ aborted (docs)
statement ที่ล้มเหลวต้องล้มเหลวจริง อันนี้ทำให้ยอดของ Ada ติดลบและชน CHECK (UPDATE ที่ไม่ตรงกับ row ใดเลย เช่น WHERE owner = 'Nobody' ไม่ raise error เลย มันแค่รายงาน UPDATE 0 จึงไม่มีอะไรให้ rollback ไป)
-- @expect-error 23514BEGIN;UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';SAVEPOINT after_debit;UPDATE accounts SET balance = balance - 1000 WHERE owner = 'Ada'; -- violates the CHECK-- the transaction is now aborted; undo just the failed part:ROLLBACK TO after_debit;UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus';COMMIT;const client = await pool.connect();try { await client.query('BEGIN'); await client.query("UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada'"); await client.query('SAVEPOINT after_debit'); try { await client.query("UPDATE accounts SET balance = balance - 1000 WHERE owner = 'Ada'"); } catch (e) { console.log('rolling back to the savepoint:', e.code); await client.query('ROLLBACK TO after_debit'); } await client.query("UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus'"); await client.query('COMMIT');} catch (e) { await client.query('ROLLBACK'); throw e;} finally { client.release();}with conn.transaction(): cur.execute("UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada'") try: with conn.transaction(): # a nested block is a SAVEPOINT cur.execute("UPDATE accounts SET balance = balance - 1000 WHERE owner = 'Ada'") except psycopg.errors.CheckViolation: print("rolling back to the savepoint: 23514") cur.execute("UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus'")tx, err := conn.Begin(ctx)if err != nil { return err}defer tx.Rollback(ctx)tx.Exec(ctx, "UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada'")tx.Exec(ctx, "SAVEPOINT after_debit")if _, err := tx.Exec(ctx, "UPDATE accounts SET balance = balance - 1000 WHERE owner = 'Ada'"); err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { fmt.Println("rolling back to the savepoint:", pgErr.Code) } tx.Exec(ctx, "ROLLBACK TO after_debit")}tx.Exec(ctx, "UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus'")return tx.Commit(ctx)let mut tx = pool.begin().await?;sqlx::query("UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada'") .execute(&mut *tx).await?;sqlx::query("SAVEPOINT after_debit").execute(&mut *tx).await?;if let Err(e) = sqlx::query("UPDATE accounts SET balance = balance - 1000 WHERE owner = 'Ada'") .execute(&mut *tx).await{ println!("rolling back to the savepoint: {}", e.as_database_error().unwrap().code().unwrap()); sqlx::query("ROLLBACK TO after_debit").execute(&mut *tx).await?;}sqlx::query("UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus'") .execute(&mut *tx).await?;tx.commit().await?;BEGINUPDATE 1SAVEPOINTERROR: new row for relation "accounts" violates check constraint "accounts_balance_check"DETAIL: Failing row contains (1, Ada, -700).ROLLBACKUPDATE 1COMMITการหักเงินและการเพิ่มเงินสุดท้ายถูกเก็บไว้ เฉพาะ statement ที่ล้มเหลวระหว่าง savepoint กับ ROLLBACK TO ที่ถูกทิ้ง เริ่มจาก Ada 400 / Linus 200 ผลคือ Ada 300 / Linus 300 และยอดรวมยัง 600:
-- sql/savepoint-check.sqlSELECT owner, balance, sum(balance) OVER () AS total FROM accounts ORDER BY id; owner | balance | total-------+---------+------- Ada | 300 | 600 Linus | 300 | 600(2 rows)สิ่งที่ rollback ไม่ย้อน
หัวข้อที่มีชื่อว่า “สิ่งที่ rollback ไม่ย้อน”rollback ทิ้งการเปลี่ยนแปลง row และใน PostgreSQL ทิ้งการเปลี่ยน schema ด้วย: DDL เป็น transactional แต่ไม่คืนค่า sequence ดังนั้น identity id มีช่องว่างหลัง rollback (เหตุผลเดียวกับบทเรียน upsert):
-- sql/side-effects.sqlCREATE TABLE ledger (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, note text);
BEGIN;INSERT INTO ledger (note) VALUES ('rolled back') RETURNING id;ROLLBACK;
INSERT INTO ledger (note) VALUES ('kept') RETURNING id;
BEGIN;CREATE TABLE scratch (x int);INSERT INTO scratch VALUES (1);ROLLBACK;SELECT to_regclass('scratch') AS scratch_exists;CREATE TABLEBEGIN id---- 1(1 row)
INSERT 0 1ROLLBACK id---- 2(1 row)
INSERT 0 1BEGINCREATE TABLEINSERT 0 1ROLLBACK scratch_exists----------------
(1 row)insert ที่ถูก rollback ใช้ id 1 ไปแล้ว row ถัดไปจึงได้ 2 และ CREATE TABLE ใน transaction ที่ถูก rollback ไม่ทิ้ง table ไว้ (to_regclass คืน NULL)
ทุก transaction ที่เขียนข้อมูลได้ transaction id pg_current_xact_id() คืนค่านั้น ซึ่งเท่าเดิมตลอดหนึ่ง transaction แต่ใหม่ทุกครั้งสำหรับแต่ละ statement ที่ auto-commit (ตัวเลขของคุณจะต่างไป รูปแบบจะไม่ต่าง):
-- sql/xid.sqlBEGIN;SELECT pg_current_xact_id() = pg_current_xact_id() AS same_xid_in_tx;COMMIT;SELECT pg_current_xact_id() AS first_autocommit;SELECT pg_current_xact_id() AS second_autocommit;BEGIN same_xid_in_tx---------------- t(1 row)
COMMIT first_autocommit------------------ 324811(1 row)
second_autocommit------------------- 324812(1 row)transaction ที่ลืมไว้ไม่ฟรี
หัวข้อที่มีชื่อว่า “transaction ที่ลืมไว้ไม่ฟรี”transaction ที่เปิดอยู่ถือ lock และ snapshot ของมัน session ที่รัน BEGIN แล้วเงียบไปจะปรากฏใน pg_stat_activity เป็น idle in transaction และใครที่ต้องใช้ row ที่มันแตะต้องรอ session A update Ada แล้วไม่ทำอะไรเลย 3 วินาที session B ที่วินาทีแรกดู pg_stat_activity และพยายาม update row เดียวกัน:
-- session A: scriptBEGIN;UPDATE accounts SET balance = balance WHERE owner = 'Ada';\! sleep 3ROLLBACK;-- session A: psql outputBEGINUPDATE 1ROLLBACK
-- session B: script\! sleep 1SELECT state, wait_event_type, queryFROM pg_stat_activityWHERE datname = current_database() AND pid <> pg_backend_pid() AND backend_type = 'client backend';\timing onUPDATE accounts SET balance = balance WHERE owner = 'Ada';-- session B: psql output state | wait_event_type | query---------------------+-----------------+------------------------------------------------------------ idle in transaction | Client | UPDATE accounts SET balance = balance WHERE owner = 'Ada';(1 row)
Timing is on.UPDATE 1Time: 1997.304 ms (00:01.997)B รอประมาณสองวินาที — จนกว่า A จบ — เพื่อการเปลี่ยนแปลงที่ A ไม่ได้ต้องการด้วยซ้ำ เซิร์ฟเวอร์จบ session แบบนี้ให้ได้ด้วย idle_in_transaction_session_timeout connection ถูกปิดด้วย FATAL และ client ต้องเชื่อมต่อใหม่:
-- session A: scriptSET idle_in_transaction_session_timeout = '1s';BEGIN;SELECT 1 AS still_alive;\! sleep 2SELECT 2 AS after_the_nap;-- session A: psql outputSETBEGIN still_alive------------- 1(1 row)
FATAL: terminating connection due to idle-in-transaction timeoutserver closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request.connection to server was lostReading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”ผู้ช่วย AI ถูกขอให้เขียนการโอนแบบ atomic ด้วย node-postgres และเขียนเส้นทางปกติ พร้อมคอมเมนต์ที่บอกตรงกับที่เราต้องการเป๊ะ:
🐛 หา bug ในโค้ดนี้
// AI comment: "transfer money atomically inside a transaction"async function transfer(pool, from, to, amount) {const client = await pool.connect();await client.query('BEGIN');await client.query('UPDATE accounts SET balance = balance - $1 WHERE owner = $2', [amount, from]);await client.query('UPDATE accounts SET balance = balance + $1 WHERE owner = $2', [amount, to]);await client.query('COMMIT');client.release();}เฉลย
มัน atomic เฉพาะตอนที่ไม่มีอะไรผิดพลาด เมื่อ UPDATE ที่สอง (หรือแรก ถ้า Ada เงินไม่พอ) throw การควบคุมออกจากฟังก์ชันก่อน COMMIT และก่อน client.release() ไม่มี ROLLBACK ดังนั้น connection ค้างอยู่ใน transaction ที่ aborted และไม่ถูกคืนเข้า pool รันสามครั้งด้วยจำนวนที่ Ada จ่ายไม่ไหว (pg 8.23.0, pool 5 ตัว, amount = 1000):
$ node transfer.js buggy # 3 calls of transfer(pool, "Ada", "Linus", 1000), pool of 5call failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"call failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"call failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"after 3 failed calls: [{"state":"idle in transaction (aborted)","n":3}]pool: total 3 idle 0 waiting 0
$ node transfer.js fixedcall failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"call failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"call failed: 23514 new row for relation "accounts" violates check constraint "accounts_balance_check"after 3 failed calls: []pool: total 1 idle 1 waiting 0สาม backend ค้างเป็น idle in transaction (aborted) และ pool เสียไปสามจากห้า client หลังความล้มเหลวแบบนี้ห้าครั้ง ทุก request จะรอ connection ตลอดไป คุณจะหาเจอด้วย query แบบนี้:
SELECT state, count(*) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid() AND state <> 'idle' GROUP BY state;วิธีแก้คือรูปแบบ try / catch / finally จากแท็บ transfer: ROLLBACK บนเส้นทาง error และ release() ใน finally เพื่อให้ connection ถูกคืนเสมอ การเรียกที่ล้มเหลวสามครั้งเดิมไม่ทิ้งอะไรเปิดอยู่ (การรันที่สองข้างบน):
async function transfer(pool, from, to, amount) { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('UPDATE accounts SET balance = balance - $1 WHERE owner = $2', [amount, from]); await client.query('UPDATE accounts SET balance = balance + $1 WHERE owner = $2', [amount, to]); await client.query('COMMIT'); } catch (e) { await client.query('ROLLBACK'); throw e; } finally { client.release(); }}Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”transaction คือกระดาษทดส่วนตัว ทุกสิ่งที่คุณทำลงบนกระดาษของคุณก่อน คนอื่นมองไม่เห็น COMMIT เผยแพร่ทั้งแผ่นในพริบตาเดียว ROLLBACK ฉีกทิ้ง นอก BEGIN PostgreSQL ห่อแต่ละ statement ด้วยกระดาษแผ่นเล็กของมันเอง error ในกระดาษไม่ได้ทำให้ statement เดียวล้ม — มันทำให้กระดาษเสีย และมีแต่ ROLLBACK (หรือ ROLLBACK TO savepoint) เท่านั้นที่ให้เขียนต่อได้
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- ทำ transaction ให้สั้น transaction ที่เปิดค้างถือ lock (B รอสองวินาทีข้างบน) และตามที่บทเรียน MVCC แสดง มันยับยั้งการเก็บกวาด row version เก่า
- error ใน transaction พาไปสถานะ aborted (
25P02สำหรับทุกคำสั่งหลังจากนั้น)COMMITบน transaction ที่ aborted กลายเป็น rollback เงียบ ๆ UPDATEที่ไม่ตรงกับอะไรเลยไม่ใช่ error ตรวจจำนวน row (UPDATE 0) ถ้าต้องรู้- ในโค้ดแอป จับคู่
BEGINกับROLLBACKบนเส้นทาง error เสมอ และปล่อย connection ในfinallyตัวอย่าง driver ข้างบนแสดงรูปแบบ bug ข้างบนแสดงสิ่งที่เกิดขึ้นถ้าไม่มี - ตั้ง
idle_in_transaction_session_timeoutบน production เป็นตาข่ายรองรับโค้ดที่ลืม - sequence และ
nextvalไม่ถูก rollback อย่าพึ่งพา id ที่ไม่มีช่องว่าง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
ห่อ statement ที่เกี่ยวข้องด้วย BEGIN...COMMIT | Atomicity — ทั้งหมดสำเร็จหรือไม่มีอะไรเกิดขึ้น (ยอดรวม 600 ข้างบน) | ต้องจัดการเส้นทาง error (ROLLBACK) เอง และ transaction ที่เปิดค้างถือ lock นานขึ้น |
| ปล่อยให้แต่ละ statement auto-commit | โค้ดง่ายกว่า ไม่ต้องจัดการ transaction | error กลางทางทิ้งงานครึ่งหนึ่งที่ commit ไปแล้ว (ยอดรวม 1100 ข้างบน) |
SAVEPOINT รอบ statement เสี่ยง | กู้จากความล้มเหลวหนึ่งครั้งโดยไม่เสียส่วนที่เหลือ | แต่ละ savepoint เริ่ม subtransaction ใช้ในที่ที่คาดว่าจะเกิด error ไม่ใช่รอบทุก statement |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “หลาย statement atomic โดยอัตโนมัติ” ถ้าไม่มี
BEGINแต่ละ statement commit เอง ความล้มเหลวระหว่างกลางทิ้งข้อมูลทำครึ่งเดียว (Ada +500 โดยไม่มีการหักเงินคู่กัน) - “COMMIT commit เสมอ” บน transaction ที่ aborted มัน rollback และบอกด้วย tag
ROLLBACK - “error ล้มแค่ statement นั้น” ใน transaction มันล้มส่วนที่เหลือด้วย จนกว่าจะ
ROLLBACKหรือROLLBACK TO - “ROLLBACK ย้อนทุกอย่าง” มันไม่คืนค่า sequence (id 1 ถูกเผาไปแล้ว) และไม่ใช่ตัวแทนของการปิด connection ให้ถูกต้อง
- “ลืมปิด transaction ไม่เป็นอันตราย” มันบล็อกผู้เขียนคนอื่นและยับยั้ง vacuum เป็นหนึ่งในเหตุการณ์ production ที่พบบ่อยที่สุด