юни 292009
 

Ще започна с кратко припомняне към изискванията към БД при 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

 Posted by at 9:34
юни 252009
 

Хем съм чел за тази мотика, но пак не съм я запомнил…

Правих (или преписвах) една процедура, която прехвърля данни за един месец от едн място на друго. И двете таблици (да кажем, че са source_tab и dest_tab) са partitioned, но с различна организация: source_tab има partition за всеки ден, а dest_tab е историческа и като такава е нацепена на месец (и компресирана). Така че exchange partition не ми върши работа. За това процедурата, съвсем схематично, изглежда така:

ALTER INDEX dest_tab_index MODIFY PARTITION part_to_be_loaded UNUSABLE;

INSERT /*+ append */ INTO dest_tab 
SELECT ... FROM source_tab 
WHERE record_date BETWEEN ... ;

ALTER INDEX dest_tab_index REBUILD PARTITION part_to_be_loaded;

do_some_checks;

if (transfer_is_succesfull) then
  for part_names in (the partitions from source_tab) loop
    ALTER TABLE source_tab TRUNCATE PARTITION ...;
    dbms_stats.gather_table_stats(ownname => owner, 
                                  tabname => source_tab,
                                  partname => daily_partition_name,
                                  estimate_percent => null, -- the partition is empty anyway
                                  cascade => true);
  end loop;

  dbms_stats.gather_table_stats(ownname => owner, 
                                tabname => dest_tab,
                                partname => monthly_partition_name,                                    
                                estimate_percent => 25,
                                cascade => true);
end if;

Ключовият момент тук е събирането на статистиките. След truncate на всеки partition от таблицата-източник, събирам статистика за него, за да знае оптимизатора, че е празен. Това става в цикъл за всички дневни партишъни от прехвърленият месец. На края събирам статистики и за новозареденият partition в таблицата dest_tab.

И така, пускам аз процедурата с някакво очакване да свърши за определено време. Обаче нещо много взе да се бави. Нищо, викам си, нали претакам като зелева чорба гигабайти даннни… И продължавам да си върша другата работа. И така час, два, три…

(Сигурен съм, че който е настъпвал тази мотика, вече знае какъв е проблема)

На края реших да поогледам какво, аджеба, прави още моята процедурка. Съвсем изненадващо открих, че тя се мотае на генерирането на статистики за празните партишъни. Още по-изненадан бях, когато видях и каква заявка изпълнява – заявка за пресмятане на статистика за цялата таблица source_tab. Която си е доволно голяма.

Разковничето е в настройките по подразбиране на процедурата dbms_stats.gather_table_stats, и по-точно на стойността по подразбиране на параметъра granularity. Този параметър, според документацията, има следните стойности:

‘ALL’ – gathers all (subpartition, partition, and global) statistics

‘AUTO’- determines the granularity based on the partitioning type. This is the default value.

‘DEFAULT’ – gathers global and partition-level statistics. This option is obsolete, and while currently supported, it is included in the documentation for legacy reasons only. You should use the ‘GLOBAL AND PARTITION’ for this functionality. Note that the default value is now ‘AUTO’.

‘GLOBAL’ – gathers global statistics

‘GLOBAL AND PARTITION’ – gathers the global and partition level statistics. No subpartition level statistics are gathered even if it is a composite partitioned object.

‘PARTITION ‘- gathers partition-level statistics

‘SUBPARTITION’ – gathers subpartition-level statistics.

И така, по подразбиране, ако подадем име на partition и не подадем стойност за granularity, процедурата събира статистика за разпределението на данните в цялата таблица И в съответетеният partition. И така 30 пъти в моя цикъл.

Оправията, след като проблема е ясен, е следната:

dbms_stats.gather_table_stats(ownname => owner, 
                                tabname => source_tab,
                                partname => daily_partition_name,
                                granularity=> 'PARTITION', -- !!!
                                estimate_percent => null, 
                                cascade => true);

Между другото, както се вижда, преди зареждането на данните правя partition-а на единствения non-unique индекс на dest_tab unusable и го rebuild-вам чак след наливането на данните. Според измерванията ми в конкретния случай това ми спестява около 18% от времето за зареждане, от които после 4% отиват за rebuild (прилагам и разни хватки като parallel и даже nologging). T.е. всичко става около 14% по-бързо.

 Posted by at 8:55

Време е

 Общи  Коментарите са изключени за Време е
юни 222009
 

Тази сутрин пристигнах в офиса и беше слънчево.

Преди 20 минути изведнъж се стъмни. Сякаш някой цъкна копчето на лампата. Изведнъж притъмня.

А сега започна и да вали. Едри капки, в комбинация със силен вятър. И се усилва – и дъжда, и вятъра.

В такива дни ми се иска да си дремя вкъщи, завит с някое леко одеалце. И да си чета някое хубаво фентъзи или фантастика.

Обаче ще бачкам. Съдба… 🙂

 Posted by at 8:49
юни 102009
 

Попаднах на следната обява от родния ми „Технически университет – Габрово“

Технически университет – Габрово търси да назначи асистент по математика на постоянен трудов договор.

Кандидатите трябва да притежават образователно-квалификационната степен „магистър“. Съгласно закона за висшето образование кандидатите трябва да са не по-възрастни от 35 години. Необходими са умения в областта на линейната алгебра, аналитичната геометрия и математическият анализ.

Тъй като асистентът по математика обучава студентите по изброените вече дисциплини, умения в областта на компютърната техника както и знания по английски език не са необходими.

Катедра „Висша математика“ осигурява обучение преди започване на работа.

Документи се подават до 25 юни 2009г. За допълнителна информация…

Изрично е записано, че не ти трябват нито компютърна грамотност, нито владеене на английски език. И наистина, тези умения не са ти необходими за да преподаваш суха математика на младите студенти, дошли да се научат, примерно, на „Компютърни системи и технологии“.

В крайна сметка, кой каза, че математиката има нещо общо с компютрите?

Любопитен съм дали Nicodile може да каже нещо по въпроса 😉

 Posted by at 10:05
юни 082009
 

Покрай цялата дандания с евроизборите най-дълго ще помня въпроса на една баба, която интервюираха по някоя телевизия:

Аз взимам 120 лева пенсия и с нея се изхранвам. Колко гърла има един евродепутат, че да взима 15000 лева?

Много мисля над този въпрос. Нормално е да има разлика в доходите. Но тук става дума за 125 пъти (!) разлика. 12 500 процента, за любителите на статистиката. При това без да задълбаваме в допълнителните доходи на евродепутатите.

Наистина, по 125 гърла ли имат тези хора? Света ревна за високите заплати на провалилите се мениджъри. А то май не е само в банки и мега-корпорации.

Много може да си говорим за това, че излъчените от нас евродепутати не трябва да ходят голи и боси сред колегите си, ама… 125 пъти?

Ако приемем, че това са парите, с които трябва да се изхранва… наистина, колко гърла има един евродепутат?

Ако приемем, че това е заплащането за заслугите… дали е направил нещо повече от бабата с десетки години стаж? Дали е дал на света или на народа си 2, 3, 5 пъти повече от нея? Заслужил ли 10 пъти повече? А 125 пъти?

Аз също взимам добри пари (е, далеч от евродпутатска заплата, но все пак много добри). Но често се замислям за това как в търсенето на все повече и повече губим връзката с реалността. Замислям се как бих живял със 120 лева, както живеят 1/4 от българите. Или хайде, с 400-500-600 лева, с колкото живеят много хора които също като мен бачкат здраво, но не са имали късмет с избора на професия. Не са имали късмет с дарбите, ако щеш. Хора, които се изгърбват от работа в нещастен (не по тяхна вина) завод в северозападнала България.

Как бих живял с доходите на един музеен работник, какъвто е майка ми? Или с доходите на шивачка в някой шивашки цех, като леля ми? Или на продавач в сладкарница, като един мой близък приятел… който има същите нужди, каквито имам и аз.

Но разликата между мен и тези хора бледнее в сравнение с разликата между бабата и нейният представител в европейския парламент. Той дали се замисля за това с какво е заслужил тази разлика? 125 пъти…

 Posted by at 9:22