MVCC-3. גרסאות מחרוזות

אז שקלנו בעיות הקשורות בִּדוּד, ועשה נסיגה בערך ארגון נתונים ברמה נמוכה. ולבסוף הגענו לחלק המעניין ביותר - גרסאות המחרוזות.

כותרת

כפי שכבר אמרנו, כל שורה יכולה להתקיים בו זמנית במספר גרסאות במסד הנתונים. יש להבדיל איכשהו בין גרסה אחת לגרסה אחרת, לשם כך, לכל גרסה יש שני סימנים הקובעים את "זמן" הפעולה של גרסה זו (xmin ו-xmax). במרכאות - כי לא הזמן ככזה משמש, אלא מונה מתגבר מיוחד. והמונה הזה הוא מספר העסקה.

(כרגיל, המציאות מורכבת יותר: מספר העסקה לא יכול לעלות כל הזמן בגלל קיבולת הסיביות המוגבלת של המונה. אבל נבחן את הפרטים הללו בפירוט כשנגיע להקפאה).

כאשר שורה נוצרת, xmin מוגדר למספר העסקה שהנפיק את הפקודה INSERT, ו-xmax נותר ריק.

כאשר שורה נמחקת, ערך ה-xmax של הגרסה הנוכחית מסומן במספר העסקה שביצעה את ה-DELETE.

כאשר שורה משתנה על ידי פקודת UPDATE, מתבצעות למעשה שתי פעולות: DELETE ו-INSERT. הגרסה הנוכחית של השורה מגדירה את xmax שווה למספר העסקה שביצעה את העדכון. לאחר מכן נוצרת גרסה חדשה של אותה מחרוזת; ערך ה-xmin שלו עולה בקנה אחד עם ערך ה-xmax של הגרסה הקודמת.

השדות xmin ו-xmax כלולים בכותרת גרסת השורה. בנוסף לשדות אלה, הכותרת מכילה אחרים, למשל:

  • infomask היא סדרה של ביטים המגדירים את המאפיינים של גרסה זו. יש די הרבה כאלה; נשקול בהדרגה את העיקריים שבהם.
  • ctid הוא קישור לגרסה הבאה והחדשה יותר של אותה שורה. עבור הגרסה החדשה והעדכנית ביותר של קו, ה-ctid מתייחס לגרסה זו עצמה. למספר יש את הצורה (x,y), כאשר x הוא מספר העמוד, y הוא מספר האינדקס במערך.
  • null bitmap - מסמן את העמודות של גרסה נתונה המכילות ערך null (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

שימו לב שהמילה heap ב-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_committed ו-xmin_aborted מציינים אם מספר העסקה xmin מחויב (מבוטל). שני ביטים דומים מתייחסים למספר העסקה xmax.

מה אנחנו רואים? כאשר אתה מכניס שורה, יופיע אינדקס מספר 1 בדף הטבלה, המצביע על הגרסה הראשונה והיחידה של השורה.

בגרסת המחרוזת, שדה ה-xmin מלא במספר העסקה הנוכחי. העסקה עדיין פעילה, כך שגם הסיביות xmin_committed וגם 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 (commit log) ועדיין ניתן למצוא את השם הזה במקומות שונים).

XACT אינו טבלת קטלוג מערכת; אלו הם הקבצים בספריית PGDATA/pg_xact. יש להם שני ביטים שהוקצו עבור כל עסקה: committed ו-aborted - בדיוק כמו בכותרת גרסת השורה. מידע זה מחולק למספר קבצים אך ורק מטעמי נוחות; נחזור לנושא זה כאשר נשקול הקפאה. והעבודה עם הקבצים האלה מתבצעת עמוד אחר עמוד, כמו עם כל האחרים.

לכן, כאשר עסקה מתבצעת ב-XACT, ה-committed bit מוגדר עבור העסקה הזו. וזה כל מה שקורה במהלך ה-commit (אם כי אנחנו עדיין לא מדברים על יומן ההקלטה מראש).

כאשר עסקה אחרת תיגש לדף הטבלה שהסתכלנו עליו זה עתה, היא תצטרך לענות על מספר שאלות.

  1. האם עסקת xmin הושלמה? אם לא, אז הגרסה שנוצרה של המחרוזת לא אמורה להיות גלויה.
    בדיקה זו מתבצעת על ידי התבוננות במבנה אחר, שנמצא בזיכרון המשותף של המופע ונקרא ProcArray. הוא מכיל רשימה של כל התהליכים הפעילים, ולכל אחד מצוין מספר העסקה הנוכחית (הפעילה).
  2. אם הושלם, אז איך - על ידי התחייבות או ביטול? אם בוטלה, גם גרסת השורה לא אמורה להיות גלויה.
    בדיוק בשביל זה נועד XACT. אבל, למרות שהעמודים האחרונים של XACT מאוחסנים במאגרים ב-RAM, עדיין יקר לבדוק את XACT בכל פעם. לכן, ברגע שסטטוס העסקה נקבע, הוא נכתב לסיביות xmin_committed ו-xmin_aborted של גרסת המחרוזת. אם אחד מהסיביות הללו מוגדר, אזי המצב של העסקה xmin נחשב ידוע והטרנזקציה הבאה לא תצטרך לגשת ל-XACT.

מדוע הביטים האלה לא נקבעים על ידי העסקה עצמה מבצעת את ההוספה? כאשר מתרחשת הוספה, העסקה עדיין לא יודעת אם היא תצליח. וברגע ההתחייבות כבר לא ברור באילו שורות שונו דפים. יכול להיות שיש הרבה דפים כאלה, ושינונם לא משתלם. בנוסף, ניתן להוציא חלק מהדפים ממטמון המאגר לדיסק; קריאה בהם שוב כדי לשנות את הביטים תאט את ה-commit באופן משמעותי.

החיסרון של החיסכון הוא שלאחר שינויים, כל עסקה (אפילו כזו שמבצעת קריאה פשוטה - SELECT) יכולה להתחיל לשנות דפי נתונים ב-buffer cache.

אז בואו נתקן את השינוי.

=> 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)

לבטל

ביטול שינויים פועל בדומה ל-committing, רק ב-XACT מוגדר ה-Bit שהופסק עבור העסקה. ביטול מהיר כמו התחייבות. למרות שהפקודה נקראת 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 יש דף מטא נתונים ודפים "רגילים".

עם זאת, לדף יש בדרך כלל מערך של מצביעים לשורות ולשורות עצמן (בדיוק כמו דף טבלה). בנוסף, בסוף העמוד יש מקום לנתונים מיוחדים.

גם לשורות באינדקסים יכולים להיות מבנים שונים מאוד בהתאם לסוג האינדקס. לדוגמה, עבור עץ B, השורות הקשורות לדפי עלים מכילות את ערך מפתח האינדקס והפניה (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 לשמור נקודות (נקודת שמירה), המאפשרת לבטל חלק מעסקה מבלי להפריע לה לחלוטין. אבל זה לא מתאים לתרשים שלמעלה, מכיוון שלטרנזקציה יש את אותו סטטוס לכל השינויים שלה, ומבחינה פיזית שום נתונים לא מוחזרים.

כדי ליישם פונקציונליות זו, עסקה עם נקודת שמירה מפוצלת לכמה נפרדות עסקאות מקוננות (תת עסקה), את הסטטוס שלה ניתן לנהל בנפרד.

לעסקאות מקוננות יש מספר משלהן (גבוה ממספר העסקה הראשית). סטטוס העסקאות המקוננות נרשם בדרך הרגילה ב-XACT, אך הסטטוס הסופי תלוי בסטטוס העסקה הראשית: אם היא מבוטלת, אז גם כל העסקאות המקוננות מבוטלות.

מידע על קינון עסקאות מאוחסן בקבצים בספריית PGDATA/pg_subtrans. הגישה לקבצים מתבצעת דרך מאגרים בזיכרון המשותף של המופע, המאורגנים באותו אופן כמו מאגרי XACT.

אל תבלבל עסקאות מקוננות עם עסקאות אוטונומיות. עסקאות אוטונומיות אינן תלויות זו בזו בשום צורה, אבל עסקאות מקוננות כן. אין עסקאות אוטונומיות ב-PostgreSQL רגיל, ואולי, לטובה: יש צורך בהן לעתים רחוקות מאוד, ונוכחותן ב-DBMSs אחרות מעוררת התעללות, ממנה כולם סובלים.

בואו ננקה את הטבלה, נתחיל עסקה ונכניס את השורה:

=> 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 למעשה שם נקודת שמירה מרומזת לפני כל פקודה, ובמקרה של כשל יוזם חזרה אליה. מצב זה אינו בשימוש כברירת מחדל, מכיוון שהגדרת נקודות שמירה (גם מבלי לחזור אליהן) כרוכה בתקורה משמעותית.

הֶמְשֵׁך.

מקור: www.habr.com

הוספת תגובה