Есенен семинар на БГПО ’09

 Общи  Коментарите са изключени за Есенен семинар на БГПО ’09
Авг. 102009
 

Лятото е в разгара си, и ако искам то да свърши, то е само за да дойде по-скоро есенния семинар на БГПО. А той е толкова далече…

Преглед на имената от супер-ранно-предварителния списък тук показва стабилно присъствие на хард-лайнери (за какъвто се имам и аз). Но този път едно име направо ме втрещи: Anjo Kolk, Owner, Miracle Benelux, Oracle – Google integration.

Anjo Kolk! Човекът, който измисли YAPP и показа на света как трявба да се гони performance в Oracle! Неговият YAPP метод (Yet Another Performance Profiling Method) е една от основните разлики межу Oracle и другите СУБД.

Anjo е работил 16 години в Oracle, при това доста време в Oracle Server Performance Group. Между другото, в Oracle той е работил и по интеграцията на БД с големи ISV като Amdocs (примерно) и е ходил при много телеком клиенти като NTT Docomo (примерно) 🙂 . След като напуска Oracle, Anjo работи в Precise Software, Veritas Software и Symanctec, а напоследък си е сам шеф в Miracle Benelux, където, предполагам, другарува с Mogens Nørgaard. Покрай другото Anjo (заедно Mogens Nørgaard и други добри хора) е и един от основателите на The Oak Table Network.

 Posted by at 16:59
Авг. 102009
 

Задачката е следната:

Близо до офиса има една кръчма (още се спори дали се казва „Дупката“ или „Гнусното“). В нея влизат и излизат много хора. Понеже е доста голяма, има си регистър на влизащите и излизащите. В основната таблица (покрай другото) има и колонки time_in и time_out, които показват даден човек кога е влязъл и кога е излязъл. Ако се случи да дойде втори път (което е съвсем възможно, предвид че въпреки гадната обстановка, в Дупката готвят много вкусно), просто се вкарва нов запис.

Шефа на Дупката обича да знае разни неща. И като човек, обичащ да знае разни неща, иска да знае в кой момент от денонощието има най-много хора в кръчмата, и като цяло как се движи натоварването. Но понеже заведението е наистина голяма, няма начин някой да седи и д брои – трябва да се използват записите в таблицата. Въпреки че Дупката работи денонощно, натоварването е променливо.

В таблицата има около 500-600 хиляди записа на ден. Естествено, използва се partitioning, за да бъде управляем обема.

Ще представя един вариант, както и част от пътя, по който стигнах до него. Естествено, приемам всякакви предложения. Подозирам, че все някой е решавал такава задачка преди мен и аз сега откривам топлата вода.

Първата идея е да се направи pl/sql функция, която да проверява във всеки един момент колко хора е имало. Понеже колонките за дата в таблицата са от тип DATE, ще проверим всяка секунда за дадено денонощие (такава е точността на типа DATE). С един цикъл, за всяка секунда “X”, търсим посещения по следния начин:

SELECT X, COUNT(1)
  FROM client_visits 
 WHERE X BETWEEN time_in AND time_out;

Това се записва в една таблица, от която ваденето на графики става лесно.

Всичко това е хубаво, само че за дадените обеми нещото отнема абсурдно много време на тестовата система: около 30 часа за 24 часов период. И това при наличие на индекс по (time_in, time_out) – иначе няма шанс.

Тук се сещам за един въпрос, който много хора забравят: а трябва ли ни наистина толкова детайлна информация (за всяка секунда)? (виж „Oracle Insights“, глава 3: „Waste Not, Want Not“)

Полезно: Много често се оказва, че с леко облекчаване на бизнес изискванията, задачата става много по-лесно решима.

Така е и в този случай – шефа на Дупката е доволен да знае и как се движи натоварването по минути, не му трябва чак по секунди. Така цикълът (с малко променено условие) ще има 60 пъти по-малко итерации и ще пусне само 1440 заявки, вместо 86400. И (на нашата система) отнема само малко повече от половин час.

Обаче… всеки печен DB expert знае, че в общия случай ако нещо може да стане само с SQL, то е добре да се направи само с SQL. Да стане с една, пък била тя и тежка, заявка – вместо с 1440 по-леки. Естествено, има и изключения, но са много редки.

Първоначално тръгнах по грешен път. Сетих се, че не ми трябва да проверявам всяка секунда (или минута) – стига ми да проверявам за всеки момент, в който влиза някой. Ако в даден момент не влиза никой, там няма да има покачване на натоварването.

select time_in,
       (select count(1)
           from client_visits t2
          where t2.time_in < = t1.time_in
            and t2.time_out >= t1.time_in
            )
  from client_visits t1
 where time_in >= trunc(sysdate - 1) -- гледаме данните за 1 ден
   and time_in < trunc(sysdate)

Това няма да помогне на шефа на дупката да разбере кога има най-малко натоварване, но той така или иначе не се интересува от това - той си оразмерява кръчмето за максималното натоварване, а движението в натоварването му трябва за да си нагоди грубо персонала. Освен това входящите посетители са толкова много (500-600 хиляди посещения на ден), че и без това почти на всяка секунда има поне един влизащ и няма опасност да се пропусне нещо интересно.

Тук, обаче, нещата на вървяха много добре. Честно казано, не изчаках да видя колко време е необходимо, защото всичко над 15 минути ми се струваше твърде много. Тук извадих един друг коз на добрия dataabse expert:

Полезно: Често има неясни зависимости в данните, които помагат да се свърши по-малко работа. Връзки, които са естествени и ясни за нас, но не и за оптимизатора в БД.

Една такава връзка е, че никой не стои в дупката повече от един ден. Или даже и да има такива, те са твърде малко и не променят статистиката. За това направих следната малка оптимизация:

select time_in,
       (select count(1)
           from client_visits t2
          where t2.time_in < = t1.time_in
            and t2.time_out >= t1.time_in
            and t2.time_in > trunc(sysdate) - 2 -- малко оптимизация
            )
  from client_visits t1
 where time_in >= trunc(sysdate - 1) -- гледаме данните за 1 ден
   and time_in < trunc(sysdate)

Това вероятно е подобрило малко нещата, но аз отново нямах търпението да изчакам за да видя колко. Като цяло проблема на този подход е, че се правят твърде много проверки - всъщност, около 500-600 хиляди за един ден (колкото са посещенията).

Следващата идея беше да се изгонят поне повтарящите се проверки:

SELECT time_in,
       (SELECT COUNT(1)
         FROM   client_visits t2
         WHERE  t1.time_in BETWEEN t2.time_in AND t2.time_out
         AND    t2.time_in > trunc(SYSDATE - 2))
FROM   (SELECT DISTINCT time_in -- избягваме повторните проверки
         FROM   client_visits
         WHERE  time_in >= trunc(SYSDATE - 1)
         AND    time_in < trunc(SYSDATE)) t1;

Естествено, и това се оказа твърде бавно. По този начин правим близо 86400 проверки, а вече се разбрахме, че може да направим само 1440. За това се изкуших да направя така:

SELECT time_in,
       (SELECT COUNT(1)
         FROM   client_visits t2
         WHERE  t1.time_in BETWEEN t2.time_in AND t2.time_out
         AND    t2.time_in > trunc(SYSDATE - 2))
FROM   (SELECT DISTINCT trunc(time_in, 'MI') -- намиране само по един запис за всяка минута
         FROM   client_visits
         WHERE  time_in >= trunc(SYSDATE - 1)
         AND    time_in < trunc(SYSDATE)) t1;

Описах този грешен (в случая) път, защото при по-малко данни (примерно ако имахме само 500-600 посещения) би бил полезен. Иначе аз доста бързо се сетих за алтернативата. Само да кажа за последните 2 заявки:

Полезно: Операцията DISTINCT е много тежка при голям обем от данни, защото изисква сортиране на цялото множество.

Освен това с посочените заявки аз преравям 2 пъти данните, които хич не са малко. За това аз се нуждаех от начин да си генерирам всички минути в едно денонощие, без да се ровя в таблицата. Да си измисля данни, с които да направя join. За целта използвах следния трик

Полезно: Генератор за 100 реда с числата от 1 до 100:
select rownum from dual connect by level < = 100

Така стигнах до следния вариант:

SELECT time_in,
       (SELECT COUNT(1)
           FROM client_visits t2
          WHERE t1.time_in BETWEEN t2.time_in AND t2.time_out
            AND t2.time_in > trunc(SYSDATE) - 2) cnt
  FROM (select trunc(SYSDATE - 1) + rownum / 1440 time_in
           from dual
         connect by level < 1440) t1

Това нещо вече се справя за под 7 минути. За съжаление не показва в коя минута има най-много посетители в кръчмата, а в коя нулева секунда от минута. За да сравня целите минути ми трябват и посетителите, които са влезли преди началото на следващата минута. Ако гледам с разредност "секунда", този проблем не стои. Едно друго решение за заобикаляне на този проблем е да правя всички сравнения не с точната стойност на time_in и time_out, а с trunc(time_in, 'MI') и trunc(time_out, 'MI'). Но в този случай губя индекса (което пък се решава с функционален индекс). Но в настоящият вариант мога лесно да променя на колко периода да разделя денонощието - примерно на по 5 минути, или на 10 секунди, за да получа данни с различна точност.

И така, настоящият вариант е следния:

SELECT period_start,
       (SELECT COUNT(1)
           FROM client_visits t2
          WHERE ((t2.time_in < = t1.period_start and t2.time_out > t1.period_start) -- започнали преди периода
                 or -- или
                (t2.time_in > period_start and t2.time_in < period_end)) -- започнали в периода
            AND t2.time_in > SYSDATE - 2) cnt
  FROM (select trunc(SYSDATE - 1) + rownum / 1440 period_start, -- началото на периода
               trunc(SYSDATE - 1) + (rownum + 1) / 1440 period_end -- началото на следващият период
           from dual
         connect by level < 1440) t1

Не ми е трудно да се досетя, че може да има и съвсем различно, по-елегантно и ефективно решение. Ще се радвам ако някой сподели такова 🙂

 Posted by at 8:18