На мен и самата идея ми звучи добре, но защо му е на тазманийския дявол да рецитира Гео Милев? Решава три проблема: бизнес, технологичен и естетичен:
- Бизнес проблем – Имате таблица с данни и искате да дадете на някой представа за качеството на информацията. Искате той да придобие добра идея за качеството на данните, но не искате да ги узнава и получава в тяхната цялост. Искате да му дадете извадка, като за да няма уклон в избора най-честно е тя да е случайна.
- Технически проблем – как се вади случайна извадка от таблица по най-ефективен начин? Допускаме, че са ни нужни около 3-5%, ако редовете са достатъчно много.
- Естетически проблем – как правим задачата малко по-забавна и интересна?
За да имам стимул да мисля по техническото решение, започнах с естетическата постановка. Взех две поеми на Гео Милев (Септември и Ден на гнева), които се отличават с това, че всеки ред е силно експресивен и динамичен. Разделих ги на отделни редове и вкарах всеки ред в таблица, така че един ред от поемата да е един запис в таблицата. Повторих зареждането доста пъти, така че да получа таблица с няколко милиона реда По този начин, когато вадя случайна извадка, на практика може да се получава ново стихотворение. (Асоциацията между случайното джуркане на редове с mersenne twister и светлия образ на Таз считам за очевидна) Ако е любопитно, ето как изглежда едно напълно случайно стихотворение на Гео Милев, изрецитирано от Тазманийския дявол.
Имайки постановката, отидох към частта с оптимизирането на заявката. Таблицата с данните е в MySQL база. Най-бруталния начин да вземем извадка би бил:
SELECT * FROM poem ORDER BY RAND() LIMIT n; n е примерно 0.05 * резултата от COUNT(*)
Това ще изкара всички (милиони) редове в паметта, ще ги сортира по случаен ред и ще даде първите N записа. Това никак не изглежда оптимално. Затова прибягвам до мислене и след като то не ми се отдава – търся. Jan Kneschke дава много подробен анализ как е най-оптимално да се избере случаен запис от таблица. На мен ми беше много интересно да прочета как стига до решението, че най-оптималното е да вкарам в съхранена процедура следната заявка и да я изпълня нужния ми брой пъти:
INSERT IGNORE INTO sample_table SELECT verse FROM poem JOIN (SELECT CEIL(RAND() * (SELECT MAX(id) FROM poem)) AS id) AS line USING (id);
На мен ми трябваха около 15 минути да разбера всичките му аргументи и пак ми изглеждаше сложно. А и в моя случай тази заявка трябваше да се изпълни няколко стотици хиляди пъти. Замислих се дали не може да стане някак по-простичко. Не след дълго реших аз да си генерирам случайни id-та на записите, извън MySQL и да си ги изтегля с индивидуални заявки използвайки нещо от рода на:
INSERT IGNORE INTO sample_table SELECT * FROM poem WHERE id in (1453138, 3854421, 256246, 3225894, 2123544);
Това налага разбиването на отделните insert завки на порции от около 50 бр всяка, за да не припадне сървъра.
Имах няколко решения на техническия проблем с оптимизирането на заявката, но тъй като основната ми цел беше да реша човешкия (т.нар. „бизнес“) проблем се замислих кое все пак е най-оптималното решение. На моя личен компютър, произведен преди 4 години, заявката използваща order by rand() отнема около 25 секунди (почти) независимо от броя на редовете, които искам да извлека (таблицата съдържа 4796668 реда). Другите „оптимизирани“ заявки отнемат поне 3-4 пъти повече време на компютъра, а на мен отнеха около два часа да разбера и реализирам.
Поне в този случай най-доброто решение се оказа най-първосигналното и тъпо (в положителна светлина, ще го наречем най-просто). Поуката също се натрапва и може да медитирате върху нея четейки тези куплети на Таз Милев.