MVCC-3 เวอร์ชันสตริง

เราจึงได้พิจารณาประเด็นที่เกี่ยวข้องกับ ฉนวนกันความร้อนและได้ถอยกลับไป การจัดระเบียบข้อมูลในระดับต่ำ. และในที่สุดเราก็มาถึงส่วนที่น่าสนใจที่สุด - เวอร์ชันสตริง

ชื่อเรื่อง

ดังที่เราได้กล่าวไปแล้ว แต่ละแถวสามารถมีอยู่ในฐานข้อมูลหลายเวอร์ชันพร้อมกันได้ เวอร์ชันหนึ่งจะต้องแตกต่างจากเวอร์ชันอื่น ด้วยเหตุนี้ แต่ละเวอร์ชันจึงมีเครื่องหมายสองอันที่กำหนด "เวลา" ของการดำเนินการของเวอร์ชันนี้ (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 บิตที่ถูกคอมมิตจะถูกตั้งค่าสำหรับธุรกรรมนี้ และนี่คือทั้งหมดที่เกิดขึ้นระหว่างการดำเนินการ (แม้ว่าเราจะยังไม่ได้พูดถึงบันทึกก่อนการบันทึกก็ตาม)

เมื่อมีธุรกรรมอื่นเข้าถึงหน้าตารางที่เราเพิ่งดูไป จะต้องตอบคำถามหลายข้อ

  1. ธุรกรรม xmin เสร็จสมบูรณ์แล้วหรือยัง? ถ้าไม่เช่นนั้น ก็ไม่ควรมองเห็นเวอร์ชันที่สร้างขึ้นของสตริง
    การตรวจสอบนี้ดำเนินการโดยดูที่โครงสร้างอื่นซึ่งอยู่ในหน่วยความจำที่ใช้ร่วมกันของอินสแตนซ์และเรียกว่า ProcArray ประกอบด้วยรายการกระบวนการที่ใช้งานอยู่ทั้งหมด และสำหรับแต่ละรายการจะมีการระบุจำนวนธุรกรรมปัจจุบัน (ที่ใช้งานอยู่)
  2. หากเสร็จสิ้นแล้วจะทำอย่างไร - โดยกระทำหรือยกเลิก? หากยกเลิก ก็ไม่ควรมองเห็นเวอร์ชันของแถวเช่นกัน
    นี่คือสิ่งที่ 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

เพิ่มความคิดเห็น