Ще започна с кратко припомняне към изискванията към БД при commit
:
– I: преди да се случи commit, всички промени на данни са неокончателни и трябва да не се виждат от другите потребители. С други думи, за всички потребители освен мен, промените, които правя не съществуват, докато не ги потвърдя с commit
– A: не е допустимо в нито един момент половината промени да излязат видими и успешни, а другите – не. Или всичките (може и милиони) редове са променени, или нито един
– C: при commit
промените, които съм направил, изведнъж започват да се виждат и да важат за другите потребители на системата. И това трябва да стане in an instant, в един момент да се видят всичките промени, които не са се виждали
– D: когато кажа commit
(и БД ми потвърди, че е изпълнила командата), моите данни трябва да са наистина фиксирани на диска. Ако след една секунда спре тока, моите данни трябва вече да са окончателно записани
(Това е свободна интерпретация около изискванията за ACID транзакции, които трябва да изпълнява всяка смислена СУБД)
Една от мантрите на хората, които са запознати с начина на действие на Oracle database е, че една транзакция трябва да е толкова голяма, колкото го налага бизнес логиката. Хората, които използват някои други СУБД, се притесняват, че aко направят твърде много промени в една транзакция, че се забави фиксирането им (commit). Но това не важи в чудесният свят на Оracle.
Ще покажа с един пример от практиката (имената на главните герои са променени, за да защитя личният им живот от намесата на журналисти). Наложи ми се да претакам голям обем от данни между 2 БД, при това – през мрежата. Като цяло скоростта на прехвърляне не ми беше голям проблем, тъй процедурата се наложи еднократно. За това не съм се напъвал да оптимизирам нещата, просто ги пуснах да се случат:
SQL> set timi on
SQL> insert /*+ append */ into some_big_table
2 select * from other_big_table@other_db
3 where . . .;
284379885 rows created
Elapsed: 01:23:12.05
SQL> commit;
Commit complete.
Elapsed: 00:00:00.71
Как се случва това?
Всъщност при commit почти няма работа, която трябва да се свърши. Благодарение на супер подлият механизъм за четене на данни в Oracle, въпреки изискването I, промените се случват върху самите данни преди да се знае дали ще се последва commit или rollback. С други думи когато insert-а вмъква данните, той не работи върху тяхно копие в някоя staging area, а наистина върши работата върху реалните данни, без да се интересува от това, че транзакцията не е завършила (освен това променя данните само в паметта, без да се интересува дали са записани на диска).
Впоследствие, при изпълнение на изискване D, commit-а също не се оглежда дали блоковете с данни, променени от транзакцията, са записани на диска. Важно е redo log buffer-а да е записан. За това моя процес хич не се занимава да помни кои блокове е променил, или да ги издирва по време на commit.
Единствената работа, която трябва да се свърши при commit, е да се запише в една система таблица края на транзакцията (т.е. един update на един ред, което удовлетворята изискванията C и A), и да се flush-не незаписаната част от redo log буфера в redo log файловете (което ни гаранитра D). Последното не е голяма хамалогия, защото rego log буфера така или иначе се записва от background process-а LGWR редовно във redo log файловете:
– на всеки 3 секунди
– ако има 1 MB незаписани данни в redo log buffer
– ако redo log buffer е пълен на 1/3 от размера си
– при всеки commit
И така, въпреки че транзакцията може да помени много гигабайти данни, при commit най-много да има 1 MB за записване. И това е в най-лошият случай: ако този 1 MB се е случил в последните 3 секунди и е нямало друг commit в тези 3 секунди. Това не е много 🙂
Тук, обаче, излизат няколко подробности:
– ако използвам олигофренски код (примерно, скрит в някой java framework), който прави commit при всеки единичен statement (дори и нищо да не е променил), ще разкажа играта на LGWR. Ясно защо – защото всеки commit сритва lgwr да пише. Всъщност този проблем е частично заобиколен при по-новите версии на pl/sql, но това си е материал за отделна статия
– от описанието си личи, че Oracle е проектиран за бързо и лесно commit-ване. Не така стоят нещата при rollback. Ако трябва да направя rollback на една тлъста транзакция, ще ми отнеме много време (не и в конкретния случай, защо – по-надолу). Но предвид факта, че транзакциите се правят с идеята да завършват успешно (поне в 90+ процента от случаите), бързият commit срещу бавен rollback е доба сделка. Не така стояха нещата в една информационна система, за която се сещам (писана за една държавна структура). В тази система 87% от транзакциите завършваха с rollback. Btw, програмата беше писана на java 😉
* * *
Все пак има и ограничение за размера на транзакцията. Това ограничение е в размера на undo сегмента – той може да е произволно голям, но не и безкраен. Когато се достигне зададеното от администратора или от размера на твърдия диск ограничение за размера му (има и технологично ограничение, но то е абсурдно голямо), транзакцията гръмва и се случва автоматичен rollback. Защото БД иска винаги да е сигурна, че няма да остави данните неконсистентни.
Но… всъщност ако става дума за пренасяне на много големи по размер данни (както е в случая), има един хитър номер: подсказката /*+ append */
, която съм използвал в примера. Така нещата хем стават по-бързо, хем почти не се генерира undo. Това е благодарение на механизма с гръмкото име „Direct-Path Load“. Но за това друг път, тоя постинг и без това стана безсрамно дълъг…
Любопитен факт:
Не знам дали сте забелязали, но при Oracle commit
не е валидна SQL команда. В SQLPlus, както и във всеки друг инструмент за работа с данни в oracle, изразите commit
и rollback
се превръщат в извикване на OCI (или аналогична) функция XCTEND