เราจึงได้พิจารณาประเด็นที่เกี่ยวข้องกับ
ชื่อเรื่อง
ดังที่เราได้กล่าวไปแล้ว แต่ละแถวสามารถมีอยู่ในฐานข้อมูลหลายเวอร์ชันพร้อมกันได้ เวอร์ชันหนึ่งจะต้องแตกต่างจากเวอร์ชันอื่น ด้วยเหตุนี้ แต่ละเวอร์ชันจึงมีเครื่องหมายสองอันที่กำหนด "เวลา" ของการดำเนินการของเวอร์ชันนี้ (xmin และ xmax) ในเครื่องหมายคำพูด - เนื่องจากไม่ใช่เวลาที่ใช้ แต่เป็นตัวนับที่เพิ่มขึ้นเป็นพิเศษ และตัวนับนี้คือหมายเลขธุรกรรม
(ตามปกติแล้ว ความเป็นจริงนั้นซับซ้อนกว่า: จำนวนธุรกรรมไม่สามารถเพิ่มขึ้นได้ตลอดเวลาเนื่องจากความจุบิตที่จำกัดของตัวนับ แต่เราจะดูรายละเอียดเหล่านี้โดยละเอียดเมื่อเราถึงจุดเยือกแข็ง)
เมื่อแถวถูกสร้างขึ้น xmin จะถูกตั้งค่าเป็นหมายเลขธุรกรรมที่ออกคำสั่ง INSERT และ xmax จะเว้นว่างไว้
เมื่อแถวถูกลบ ค่า xmax ของเวอร์ชันปัจจุบันจะถูกทำเครื่องหมายด้วยหมายเลขธุรกรรมที่ดำเนินการ DELETE
เมื่อแถวถูกแก้ไขโดยคำสั่ง UPDATE จะมีการดำเนินการสองอย่าง: DELETE และ INSERT เวอร์ชันปัจจุบันของแถวตั้งค่า xmax เท่ากับจำนวนธุรกรรมที่ดำเนินการ UPDATE เวอร์ชันใหม่ของสตริงเดียวกันจะถูกสร้างขึ้น ค่า xmin ตรงกับค่า xmax ของเวอร์ชันก่อนหน้า
ฟิลด์ xmin และ xmax จะรวมอยู่ในส่วนหัวเวอร์ชันของแถว นอกเหนือจากช่องเหล่านี้แล้ว ส่วนหัวยังมีช่องอื่นๆ เช่น:
- infomask คือชุดของบิตที่กำหนดคุณสมบัติของเวอร์ชันนี้ มีค่อนข้างมาก เราจะค่อยๆ พิจารณาประเด็นหลักๆ
- ctid คือลิงก์ไปยังเวอร์ชันถัดไปของบรรทัดเดียวกัน สำหรับสตริงเวอร์ชันใหม่ล่าสุดและเป็นปัจจุบันที่สุด ctid จะอ้างอิงถึงเวอร์ชันนี้เอง ตัวเลขมีรูปแบบ (x,y) โดยที่ x คือหมายเลขหน้า y คือหมายเลขดัชนีในอาร์เรย์
- บิตแมปว่าง - ทำเครื่องหมายคอลัมน์เหล่านั้นของเวอร์ชันที่กำหนดซึ่งมีค่าว่าง (NULL) NULL ไม่ใช่ค่าประเภทข้อมูลปกติ ดังนั้นแอตทริบิวต์จะต้องจัดเก็บแยกต่างหาก
เป็นผลให้ส่วนหัวมีขนาดค่อนข้างใหญ่ - อย่างน้อย 23 ไบต์สำหรับแต่ละเวอร์ชันของบรรทัด และมักจะมากกว่านั้นเนื่องจากบิตแมป NULL หากตาราง "แคบ" (นั่นคือ มีคอลัมน์ไม่กี่คอลัมน์) ค่าใช้จ่ายอาจกินพื้นที่มากกว่าข้อมูลที่เป็นประโยชน์
แทรก
เรามาดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการดำเนินการสตริงระดับต่ำ โดยเริ่มจากการแทรก
สำหรับการทดลอง เรามาสร้างตารางใหม่โดยมีสองคอลัมน์และมีดัชนีอยู่บนคอลัมน์ใดคอลัมน์หนึ่ง:
=> CREATE TABLE t(
id serial,
s text
);
=> CREATE INDEX ON t(s);
มาแทรกหนึ่งแถวหลังจากเริ่มธุรกรรม
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
นี่คือหมายเลขธุรกรรมปัจจุบันของเรา:
=> SELECT txid_current();
txid_current
--------------
3664
(1 row)
มาดูเนื้อหาของหน้ากันดีกว่า ฟังก์ชัน heap_page_items ของส่วนขยาย pageinspect ช่วยให้คุณได้รับข้อมูลเกี่ยวกับตัวชี้และเวอร์ชันของแถว:
=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp | 1
lp_off | 8160
lp_flags | 1
lp_len | 32
t_xmin | 3664
t_xmax | 0
t_field3 | 0
t_ctid | (0,1)
t_infomask2 | 2
t_infomask | 2050
t_hoff | 24
t_bits |
t_oid |
t_data | x0100000009464f4f
โปรดทราบว่าคำว่าฮีปใน PostgreSQL หมายถึงตาราง นี่เป็นการใช้คำที่แปลกอีกอย่างหนึ่ง - รู้จักฮีป
ฟังก์ชั่นแสดงข้อมูล “ตามสภาพ” ในรูปแบบที่เข้าใจยาก หากต้องการทราบเราจะทิ้งข้อมูลไว้เพียงบางส่วนแล้วถอดรหัส:
=> SELECT '(0,'||lp||')' AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin as xmin,
t_xmax as xmax,
(t_infomask & 256) > 0 AS xmin_commited,
(t_infomask & 512) > 0 AS xmin_aborted,
(t_infomask & 1024) > 0 AS xmax_commited,
(t_infomask & 2048) > 0 AS xmax_aborted,
t_ctid
FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid | (0,1)
state | normal
xmin | 3664
xmax | 0
xmin_commited | f
xmin_aborted | f
xmax_commited | f
xmax_aborted | t
t_ctid | (0,1)
นี่คือสิ่งที่เราทำ:
- เพิ่มศูนย์ให้กับหมายเลขดัชนีเพื่อให้ดูเหมือนกับ t_ctid: (หมายเลขหน้า หมายเลขดัชนี)
- ถอดรหัสสถานะของตัวชี้ lp_flags นี่คือ "ปกติ" - ซึ่งหมายความว่าตัวชี้อ้างอิงถึงเวอร์ชันของสตริงจริงๆ เราจะดูความหมายอื่นในภายหลัง
- จากบิตข้อมูลทั้งหมด มีเพียงสองคู่เท่านั้นที่ถูกระบุจนถึงตอนนี้ บิต xmin_comitting และ xmin_aborted ระบุว่าหมายเลขธุรกรรม xmin ถูกคอมมิต (ยกเลิก) หรือไม่ บิตที่คล้ายกันสองบิตอ้างอิงถึงหมายเลขธุรกรรม xmax
เราเห็นอะไร? เมื่อคุณแทรกแถว หมายเลขดัชนี 1 จะปรากฏในหน้าตาราง โดยชี้ไปที่เวอร์ชันแรกและเวอร์ชันเดียวของแถว
ในเวอร์ชันสตริง ฟิลด์ xmin จะถูกกรอกด้วยหมายเลขธุรกรรมปัจจุบัน ธุรกรรมยังคงใช้งานอยู่ ดังนั้นจึงไม่ได้ตั้งค่าทั้งบิต xmin_comitting และ xmin_aborted
ฟิลด์ ctid เวอร์ชันแถวอ้างอิงถึงแถวเดียวกัน ซึ่งหมายความว่าไม่มีเวอร์ชันที่ใหม่กว่า
ฟิลด์ xmax ถูกกรอกด้วยหมายเลขจำลอง 0 เนื่องจากแถวเวอร์ชันนี้ยังไม่ได้ถูกลบและเป็นเวอร์ชันปัจจุบัน ธุรกรรมจะไม่สนใจตัวเลขนี้เนื่องจากมีการตั้งค่าบิต xmax_aborted
เรามาก้าวไปอีกขั้นในการปรับปรุงความสามารถในการอ่านโดยการเพิ่มบิตข้อมูลให้กับหมายเลขธุรกรรม และมาสร้างฟังก์ชันกัน เนื่องจากเราต้องการคำขอมากกว่าหนึ่งครั้ง:
=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin || CASE
WHEN (t_infomask & 256) > 0 THEN ' (c)'
WHEN (t_infomask & 512) > 0 THEN ' (a)'
ELSE ''
END AS xmin,
t_xmax || CASE
WHEN (t_infomask & 1024) > 0 THEN ' (c)'
WHEN (t_infomask & 2048) > 0 THEN ' (a)'
ELSE ''
END AS xmax,
t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE SQL;
ในรูปแบบนี้ จะชัดเจนยิ่งขึ้นว่าเกิดอะไรขึ้นในส่วนหัวของเวอร์ชันแถว:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)
ข้อมูลที่คล้ายกัน แต่มีรายละเอียดน้อยกว่ามากสามารถรับได้จากตารางโดยใช้คอลัมน์หลอก xmin และ xmax:
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3664 | 0 | 1 | FOO
(1 row)
ความสำรวม
หากธุรกรรมเสร็จสมบูรณ์ คุณจะต้องจดจำสถานะของธุรกรรม - โปรดทราบว่าธุรกรรมดังกล่าวได้ดำเนินการแล้ว ในการดำเนินการนี้ จะใช้โครงสร้างที่เรียกว่า XACT (และก่อนเวอร์ชัน 10 จะเรียกว่า CLOG (บันทึกการกระทำ) และชื่อนี้ยังสามารถพบได้ในที่ต่างๆ)
XACT ไม่ใช่ตารางแค็ตตาล็อกระบบ นี่คือไฟล์ในไดเร็กทอรี PGDATA/pg_xact มีสองบิตสำหรับแต่ละธุรกรรม: คอมมิตและยกเลิก - เช่นเดียวกับในส่วนหัวของเวอร์ชันแถว ข้อมูลนี้แบ่งออกเป็นหลายไฟล์เพื่อความสะดวกเท่านั้น เราจะกลับมาที่ปัญหานี้อีกครั้งเมื่อเราพิจารณาระงับการใช้งาน และการทำงานกับไฟล์เหล่านี้จะดำเนินการทีละหน้าเช่นเดียวกับไฟล์อื่น ๆ ทั้งหมด
ดังนั้น เมื่อธุรกรรมถูกคอมมิตใน XACT บิตที่ถูกคอมมิตจะถูกตั้งค่าสำหรับธุรกรรมนี้ และนี่คือทั้งหมดที่เกิดขึ้นระหว่างการดำเนินการ (แม้ว่าเราจะยังไม่ได้พูดถึงบันทึกก่อนการบันทึกก็ตาม)
เมื่อมีธุรกรรมอื่นเข้าถึงหน้าตารางที่เราเพิ่งดูไป จะต้องตอบคำถามหลายข้อ
- ธุรกรรม xmin เสร็จสมบูรณ์แล้วหรือยัง? ถ้าไม่เช่นนั้น ก็ไม่ควรมองเห็นเวอร์ชันที่สร้างขึ้นของสตริง
การตรวจสอบนี้ดำเนินการโดยดูที่โครงสร้างอื่นซึ่งอยู่ในหน่วยความจำที่ใช้ร่วมกันของอินสแตนซ์และเรียกว่า ProcArray ประกอบด้วยรายการกระบวนการที่ใช้งานอยู่ทั้งหมด และสำหรับแต่ละรายการจะมีการระบุจำนวนธุรกรรมปัจจุบัน (ที่ใช้งานอยู่) - หากเสร็จสิ้นแล้วจะทำอย่างไร - โดยกระทำหรือยกเลิก? หากยกเลิก ก็ไม่ควรมองเห็นเวอร์ชันของแถวเช่นกัน
นี่คือสิ่งที่ XACT มีไว้เพื่อ แม้ว่าหน้าสุดท้ายของ XACT จะถูกจัดเก็บไว้ในบัฟเฟอร์ใน RAM แต่ก็ยังมีราคาแพงในการตรวจสอบ XACT ทุกครั้ง ดังนั้น เมื่อพิจารณาสถานะธุรกรรมแล้ว สถานะธุรกรรมจะถูกเขียนไปยังบิต xmin_commit และ xmin_aborted ของเวอร์ชันสตริง หากมีการตั้งค่าหนึ่งในบิตเหล่านี้ สถานะของธุรกรรม xmin จะถือว่าทราบ และธุรกรรมถัดไปจะไม่ต้องเข้าถึง XACT
เหตุใดบิตเหล่านี้จึงไม่ถูกตั้งค่าโดยธุรกรรมที่ทำการแทรก เมื่อมีการแทรกเกิดขึ้น ธุรกรรมยังไม่ทราบว่าจะสำเร็จหรือไม่ และในขณะที่กระทำการนั้น ก็ไม่ชัดเจนว่าบรรทัดใดที่หน้ามีการเปลี่ยนแปลง อาจมีหน้าดังกล่าวจำนวนมากและการจดจำหน้าเหล่านี้จะไม่เกิดประโยชน์ นอกจากนี้ บางเพจสามารถถูกไล่ออกจากแคชบัฟเฟอร์ไปยังดิสก์ได้ การอ่านอีกครั้งเพื่อเปลี่ยนบิตจะทำให้การคอมมิตช้าลงอย่างมาก
ข้อเสียของการประหยัดคือหลังจากการเปลี่ยนแปลง ธุรกรรมใดๆ (แม้แต่ธุรกรรมที่ทำการอ่านแบบธรรมดา - SELECT) ก็สามารถเริ่มเปลี่ยนหน้าข้อมูลในแคชบัฟเฟอร์ได้
ดังนั้นเรามาแก้ไขการเปลี่ยนแปลงกันดีกว่า
=> COMMIT;
ไม่มีการเปลี่ยนแปลงใดๆ บนหน้านี้ (แต่เรารู้ว่าสถานะธุรกรรมได้รับการบันทึกใน XACT แล้ว):
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)
ตอนนี้ธุรกรรมที่เข้าถึงเพจก่อนจะต้องกำหนดสถานะธุรกรรม xmin และเขียนลงในบิตข้อมูล:
=> SELECT * FROM t;
id | s
----+-----
1 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 row)
การถอด
เมื่อแถวถูกลบ จำนวนธุรกรรมการลบปัจจุบันจะถูกเขียนลงในฟิลด์ xmax ของเวอร์ชันปัจจุบัน และบิต xmax_aborted จะถูกเคลียร์
โปรดทราบว่าค่าที่ตั้งไว้ xmax ที่สอดคล้องกับธุรกรรมที่ใช้งานอยู่จะทำหน้าที่เป็นการล็อกแถว หากธุรกรรมอื่นต้องการอัพเดตหรือลบแถวนี้ จะถูกบังคับให้รอให้ธุรกรรม xmax เสร็จสมบูรณ์ เราจะพูดคุยเพิ่มเติมเกี่ยวกับการบล็อกในภายหลัง ในตอนนี้ เราเพิ่งทราบว่าจำนวนการล็อกแถวนั้นไม่จำกัด ไม่ใช้พื้นที่ใน RAM และประสิทธิภาพของระบบไม่ได้รับผลกระทบจากจำนวน ธุรกรรมที่ "ยาว" จริง ๆ มีข้อเสียอื่น ๆ แต่จะเพิ่มเติมในภายหลัง
มาลบบรรทัดกันเถอะ
=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
txid_current
--------------
3665
(1 row)
เราเห็นว่าหมายเลขธุรกรรมเขียนอยู่ในฟิลด์ xmax แต่ไม่ได้ตั้งค่าบิตข้อมูล:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)
การยกเลิก
การยกเลิกการเปลี่ยนแปลงทำงานคล้ายกับการคอมมิต เฉพาะใน XACT เท่านั้นที่บิตที่ถูกยกเลิกจะถูกตั้งค่าสำหรับธุรกรรม การเลิกทำนั้นเร็วเท่ากับการตกลงใจ แม้ว่าคำสั่งจะเรียกว่า ROLLBACK แต่การเปลี่ยนแปลงจะไม่ถูกย้อนกลับ ทุกสิ่งที่ธุรกรรมจัดการเพื่อเปลี่ยนแปลงในหน้าข้อมูลยังคงไม่เปลี่ยนแปลง
=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)
เมื่อเข้าถึงเพจแล้ว สถานะจะถูกตรวจสอบและบิตคำใบ้ xmax_aborted จะถูกตั้งค่าเป็นเวอร์ชันแถว หมายเลข xmax นั้นยังคงอยู่ในหน้า แต่จะไม่มีใครดู
=> SELECT * FROM t;
id | s
----+-----
1 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+----------+--------
(0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 row)
ปรับปรุง
การอัปเดตจะทำงานเหมือนกับว่าได้ลบเวอร์ชันปัจจุบันของแถวออกก่อน จากนั้นจึงแทรกเวอร์ชันใหม่
=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
txid_current
--------------
3666
(1 row)
แบบสอบถามสร้างหนึ่งบรรทัด (เวอร์ชันใหม่):
=> SELECT * FROM t;
id | s
----+-----
1 | BAR
(1 row)
แต่ในหน้านี้เราเห็นทั้งสองเวอร์ชัน:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3664 (c) | 3666 | (0,2)
(0,2) | normal | 3666 | 0 (a) | (0,2)
(2 rows)
เวอร์ชันที่ถูกลบจะถูกทำเครื่องหมายด้วยหมายเลขธุรกรรมปัจจุบันในช่อง xmax นอกจากนี้ ค่านี้จะถูกเขียนทับค่าเก่า เนื่องจากธุรกรรมก่อนหน้านี้ถูกยกเลิก และบิต xmax_aborted จะถูกเคลียร์เนื่องจากยังไม่ทราบสถานะของธุรกรรมปัจจุบัน
เวอร์ชันแรกของบรรทัดตอนนี้อ้างอิงถึงเวอร์ชันที่สอง (ฟิลด์ t_ctid) ว่าเป็นเวอร์ชันที่ใหม่กว่า
ดัชนีที่สองปรากฏในหน้าดัชนีและแถวที่สองอ้างอิงถึงเวอร์ชันที่สองในหน้าตาราง
เช่นเดียวกับการลบ ค่า xmax ในเวอร์ชันแรกของแถวเป็นการบ่งชี้ว่าแถวถูกล็อก
เอาล่ะ มาทำธุรกรรมให้เสร็จสิ้นกันเถอะ
=> COMMIT;
ดัชนี
จนถึงตอนนี้เราได้พูดคุยเกี่ยวกับหน้าตารางเท่านั้น จะเกิดอะไรขึ้นภายในดัชนี?
ข้อมูลในหน้าดัชนีจะแตกต่างกันไปอย่างมาก ขึ้นอยู่กับประเภทของดัชนีเฉพาะ และแม้แต่ดัชนีประเภทเดียวก็มีหน้าประเภทต่างๆ ตัวอย่างเช่น B-tree มีหน้าข้อมูลเมตาและหน้า "ปกติ"
อย่างไรก็ตาม หน้านั้นมักจะมีอาร์เรย์ของตัวชี้ไปยังแถวและแถวนั้นเอง (เช่นเดียวกับหน้าตาราง) นอกจากนี้ในตอนท้ายของหน้ายังมีพื้นที่สำหรับข้อมูลพิเศษอีกด้วย
แถวในดัชนีอาจมีโครงสร้างที่แตกต่างกันมาก ขึ้นอยู่กับประเภทของดัชนี ตัวอย่างเช่น สำหรับ B-tree แถวที่เกี่ยวข้องกับ leaf page มีค่าคีย์การจัดทำดัชนีและการอ้างอิง (ctid) ไปยังแถวของตารางที่เกี่ยวข้อง โดยทั่วไปแล้ว ดัชนีสามารถจัดโครงสร้างในลักษณะที่แตกต่างไปจากเดิมอย่างสิ้นเชิง
จุดที่สำคัญที่สุดคือไม่มีเวอร์ชันแถวในดัชนีทุกประเภท หรือเราสามารถสรุปได้ว่าแต่ละบรรทัดจะแสดงด้วยเวอร์ชันเดียวเท่านั้น กล่าวอีกนัยหนึ่ง ไม่มีฟิลด์ xmin และ xmax ในส่วนหัวของแถวดัชนี เราสามารถสันนิษฐานได้ว่าลิงก์จากดัชนีนำไปสู่เวอร์ชันตารางทั้งหมดของแถว ดังนั้นคุณจึงสามารถทราบได้ว่าธุรกรรมจะเห็นเวอร์ชันใดโดยดูจากตารางเท่านั้น (เช่นเคย นี่ไม่ใช่ความจริงทั้งหมด ในบางกรณี แผนที่การมองเห็นสามารถเพิ่มประสิทธิภาพกระบวนการได้ แต่เราจะดูรายละเอียดเพิ่มเติมในภายหลัง)
ในเวลาเดียวกัน ในหน้าดัชนี เราจะพบตัวชี้ไปยังทั้งสองเวอร์ชัน ทั้งเวอร์ชันปัจจุบันและเวอร์ชันเก่า:
=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
itemoffset | ctid
------------+-------
1 | (0,2)
2 | (0,1)
(2 rows)
ธุรกรรมเสมือนจริง
ในทางปฏิบัติ PostgreSQL ใช้การเพิ่มประสิทธิภาพที่อนุญาตให้ "บันทึก" หมายเลขธุรกรรมได้
ถ้าธุรกรรมอ่านข้อมูลเท่านั้น จะไม่มีผลกระทบต่อการมองเห็นเวอร์ชันของแถว ดังนั้น กระบวนการบริการออก xid เสมือนให้กับธุรกรรมก่อน หมายเลขประกอบด้วยรหัสกระบวนการและหมายเลขลำดับ
การออกหมายเลขนี้ไม่จำเป็นต้องมีการซิงโครไนซ์ระหว่างกระบวนการทั้งหมดดังนั้นจึงรวดเร็วมาก เราจะทำความคุ้นเคยกับเหตุผลอื่นในการใช้หมายเลขเสมือนเมื่อเราพูดถึงการแช่แข็ง
หมายเลขเสมือนจะไม่ถูกนำมาพิจารณาแต่อย่างใดในภาพรวมข้อมูล
ในช่วงเวลาต่างๆ กัน อาจมีการทำธุรกรรมเสมือนในระบบด้วยหมายเลขที่ถูกใช้งานไปแล้วซึ่งเป็นเรื่องปกติ แต่ตัวเลขดังกล่าวไม่สามารถเขียนลงในหน้าข้อมูลได้เพราะครั้งต่อไปที่เข้าถึงหน้านั้นอาจสูญเสียความหมายทั้งหมด
=> BEGIN;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
(1 row)
หากธุรกรรมเริ่มเปลี่ยนแปลงข้อมูล จะได้รับหมายเลขธุรกรรมจริงและไม่ซ้ำกัน
=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
3667
(1 row)
=> COMMIT;
ธุรกรรมที่ซ้อนกัน
บันทึกคะแนน
กำหนดไว้ใน SQL บันทึกคะแนน (savepoint) ซึ่งช่วยให้คุณสามารถยกเลิกส่วนหนึ่งของธุรกรรมได้โดยไม่ขัดจังหวะธุรกรรมอย่างสมบูรณ์ แต่สิ่งนี้ไม่พอดีกับแผนภาพด้านบน เนื่องจากธุรกรรมมีสถานะเดียวกันสำหรับการเปลี่ยนแปลงทั้งหมด และไม่มีการย้อนกลับข้อมูลทางกายภาพ
หากต้องการใช้ฟังก์ชันนี้ ธุรกรรมที่มีจุดบันทึกจะถูกแบ่งออกเป็นหลายรายการแยกกัน ธุรกรรมที่ซ้อนกัน (ธุรกรรมย่อย) ซึ่งสถานะสามารถจัดการแยกกันได้
ธุรกรรมที่ซ้อนกันจะมีหมายเลขของตัวเอง (สูงกว่าจำนวนธุรกรรมหลัก) สถานะของธุรกรรมที่ซ้อนกันจะถูกบันทึกในลักษณะปกติใน XACT แต่สถานะสุดท้ายจะขึ้นอยู่กับสถานะของธุรกรรมหลัก: หากถูกยกเลิก ธุรกรรมที่ซ้อนกันทั้งหมดจะถูกยกเลิกด้วย
ข้อมูลเกี่ยวกับการซ้อนธุรกรรมจะถูกจัดเก็บไว้ในไฟล์ในไดเร็กทอรี PGDATA/pg_subtrans ไฟล์ต่างๆ เข้าถึงได้ผ่านบัฟเฟอร์ในหน่วยความจำที่ใช้ร่วมกันของอินสแตนซ์ ซึ่งจัดระเบียบในลักษณะเดียวกับบัฟเฟอร์ XACT
อย่าสับสนระหว่างธุรกรรมที่ซ้อนกันกับธุรกรรมที่เป็นอิสระ ธุรกรรมที่เป็นอิสระไม่ได้ขึ้นอยู่กับกันและกัน แต่อย่างใด แต่เป็นธุรกรรมที่ซ้อนกันอยู่ ไม่มีการทำธุรกรรมแบบอัตโนมัติใน PostgreSQL ปกติและบางทีเพื่อสิ่งที่ดีที่สุด: สิ่งเหล่านี้มีความจำเป็นน้อยมากและการมีอยู่ของพวกมันใน DBMS อื่น ๆ กระตุ้นให้เกิดการละเมิดซึ่งทำให้ทุกคนต้องทนทุกข์ทรมาน
มาเคลียร์ตาราง เริ่มธุรกรรมแล้วแทรกแถว:
=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
txid_current
--------------
3669
(1 row)
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
(1 row)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)
ตอนนี้เรามาใส่จุดบันทึกแล้วแทรกอีกบรรทัดหนึ่ง
=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
txid_current
--------------
3669
(1 row)
โปรดทราบว่าฟังก์ชัน txid_current() จะส่งคืนหมายเลขธุรกรรมหลัก ไม่ใช่หมายเลขธุรกรรมที่ซ้อนกัน
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3670 | 0 | 3 | XYZ
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)
ย้อนกลับไปที่จุดบันทึกแล้วแทรกบรรทัดที่สาม
=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3671 | 0 | 4 | BAR
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 | 0 (a) | (0,3)
(3 rows)
ในหน้านั้น เรายังคงเห็นแถวที่เพิ่มโดยธุรกรรมที่ซ้อนกันที่ถูกยกเลิก
เราแก้ไขการเปลี่ยนแปลง
=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3671 | 0 | 4 | BAR
(2 rows)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 (c) | 0 (a) | (0,1)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 rows)
ตอนนี้คุณสามารถเห็นได้อย่างชัดเจนว่าแต่ละธุรกรรมที่ซ้อนกันมีสถานะของตัวเอง
โปรดทราบว่าธุรกรรมที่ซ้อนกันไม่สามารถนำมาใช้อย่างชัดเจนใน SQL กล่าวคือ คุณไม่สามารถเริ่มธุรกรรมใหม่ได้หากไม่ดำเนินการธุรกรรมปัจจุบันให้เสร็จสิ้น กลไกนี้เปิดใช้งานโดยปริยายเมื่อใช้จุดบันทึก เช่นเดียวกับเมื่อจัดการข้อยกเว้น PL/pgSQL และในกรณีอื่นๆ ที่แปลกใหม่กว่าอีกจำนวนหนึ่ง
=> BEGIN;
BEGIN
=> BEGIN;
WARNING: there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING: there is no transaction in progress
COMMIT
ข้อผิดพลาดและความอะตอมมิกของการดำเนินการ
จะเกิดอะไรขึ้นหากเกิดข้อผิดพลาดขณะดำเนินการ? ตัวอย่างเช่นเช่นนี้:
=> BEGIN;
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR: division by zero
เกิดข้อผิดพลาด. ขณะนี้ธุรกรรมถือว่าถูกยกเลิกและไม่อนุญาตให้ดำเนินการใด ๆ ในนั้น:
=> SELECT * FROM t;
ERROR: current transaction is aborted, commands ignored until end of transaction block
และแม้ว่าคุณจะพยายามยอมรับการเปลี่ยนแปลง PostgreSQL จะรายงานการยกเลิก:
=> COMMIT;
ROLLBACK
เหตุใดธุรกรรมจึงไม่สามารถดำเนินต่อไปได้หลังจากเกิดความล้มเหลว? ความจริงก็คือข้อผิดพลาดอาจเกิดขึ้นในลักษณะที่เราจะสามารถเข้าถึงการเปลี่ยนแปลงบางส่วนได้ - อะตอมมิกซิตีของธุรกรรมไม่ใช่แม้แต่ธุรกรรม แต่ตัวดำเนินการจะถูกละเมิด ดังตัวอย่างของเรา โดยที่โอเปอเรเตอร์จัดการอัปเดตหนึ่งบรรทัดก่อนเกิดข้อผิดพลาด:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normal | 3669 (c) | 3672 | (0,4)
(0,2) | normal | 3670 (a) | 0 (a) | (0,2)
(0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(0,4) | normal | 3672 | 0 (a) | (0,4)
(4 rows)
ต้องบอกว่า psql มีโหมดที่ยังคงอนุญาตให้ธุรกรรมดำเนินต่อไปหลังจากเกิดความล้มเหลว ราวกับว่าการกระทำของผู้ปฏิบัติงานที่ผิดพลาดถูกย้อนกลับ
=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR: division by zero
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> COMMIT;
เดาได้ไม่ยากว่าในโหมดนี้ psql จะวางจุดบันทึกโดยนัยไว้หน้าแต่ละคำสั่ง และในกรณีที่เกิดความล้มเหลวจะเริ่มการย้อนกลับ โหมดนี้จะไม่ได้ใช้ตามค่าเริ่มต้น เนื่องจากการตั้งค่าจุดบันทึก (แม้จะไม่ได้ย้อนกลับไปยังจุดนั้นก็ตาม) เกี่ยวข้องกับโอเวอร์เฮดที่มีนัยสำคัญ
ที่มา: will.com