Моят личен и професионален живот

2018-11-28

Смяна на плана за изпълнение в MySQL 8.0

Преди около 6 седмици в работата трябваше да напиша нова заявка представляваща доклад, защото страницата на едно приложение се зареждаше бавно (около 25 секунди). Оказа се, че име проблем с приложението, защото то първо изпълняваше сравнително бавна заявка (отнемаща около 6 секунди) и тогава сравнително бърза заявка за всеки ред върнат от първата заявка. Втората заявка отнемаше между 0.3 и 0.5 секунди за изпълнение, но се изпълваше около 60 пъти, така че закъснението се натрупваше и обясни забавянето на страницата.
Реших да предоставя една единствена заявка създаваща целия резултат, така че трябваше да съединя 6 таблица. Една таблица (msg_progs) имаше само 13 реда, други две имаха само 64 реда (bldenv и prjprocstat), а друга само около 100 реда (p) тези четирите нямаха тенденция да растат много повече. Имаше една таблица с няколко десетки хиляди реда (m) и друга с няколко стотин хиляди реда (mtp), които със сигурност щяха да растат всеки ден. За да подобря избирателността във втората по големина таблица (m) създадох съставен индекс по три колони използвани от заявката, но проблема беше, че все още се избираха повечето от данните в таблицата. Така приключих с заявка изпълняваща се за 6-7 секунди, което беше доста по-приемливо от първоначалните 25 секунди. Не мога да разкрия всички подробности, но направих снимка на плана за изпълнение и смятам, че няма проблем да го публикувам.
Query explain plan in MySQL 5.7.20
Забележка: MySQL Workbench показва страхотно планове за изпълнение графично (забележете линиите свърващи различните операции - колкото по-голям е броя на редовете толкова по-дебела е линията). Текстовите планове за изпълнение са вече минало, нали?

Това беше до преди 3 седмици когато надградихме до MySQL 8.0.13 (от 5.7.20). Същата заявка и в същата база започна да се изпълнява по 24 секунди. Когато проверих плана за изпълнение той се беше променил (виж отдолу).
Original explain plan in MySQL 8.0
Мисля, че можете лесно да забележите, че реда на съединяване се е променил. Преди втората по големина таблица се взимаше в предвид в първия вложен цикъл, след това най-голямата таблица и накрая по-малките таблици. Сега е обратното, защото оптимизатора първо взема в предвид по-малките таблици и само тогава по-големите. Можех да използвам подсказка, за да оправя реда на съединяване (напр. JOIN_ORDER), но забелязах, че ако пресъздам съставния индекс по само две от колоните (които всъщност дават по-добра избирателност) оптимизатора избира различен план, но заявката все още се изпълвана за около 14 секунди. Накрая реших да премахна една от таблиците в съединението (най-малката msg_progs само с 13 реда), което накара оптимизатора да избере друг план за изпълнение (тук отдолу) и заявката вече се изпълнява за около 4 секунди.
Finaly explain plan in MySQL 8.0
Не съм сигурен от къде идва тази разлика освен от сървъра, но не мога да обвинявам без доказателство (напр. препратка към наръчника, която обяснява разликата), което за съжаление нямах време да търся. Ако знаете причината за смяната на плана за изпълнение в MySQL 8.0 моля уведомете ме.
Ситуацията ми напомни за базата на Oracle където след всяка главна версия имахме заявки с променен план за изпълнение и трябваше да използваме подсказки или други номера, за да ги забързаме отново. Изглежда да няма разлика при MySQL днес.
Обновяване 2018-12-17: Направих прибързано заключение по-горе, защото промяната на плана се случи (или поне беше забелязана) след надграждането на сървъра. Обаче, както се оказа истинската причина беше броя на редовете в таблиците m и mtp заради който сървъра избираше различен план на изпълнение. Открихме това експериментирайки с различни по обем набори с данни.

2018-11-27

Игра на авторитети

В управленската теория и практика има случаи в които е необходимо да се използва авторитет (напр. за налагане на крайни срокове, за решаване на конфликти, и т.н.). Само че, докато разбирам нуждата, наистина не разбирам управители на които се налага да използват авторитета си когато са останали без всякакъв друг аргумент. Имал съм ситуации в миналото когато авторитет е бил необходим, но винаги съм подхождал с неохота и никога не съм ползвал без сериозен аргумент (което е било любезно признавано). Това което имам в предвид, е че не мога просто да наложа нещо на някого ако нямам добра причини за това и ако не съм го оправдал пред засегнатите страни.
Въпросът е, че понякога аргументите на страните в обсъждане могат да изглеждат еднакво важни и действителни, така че нуждата от арбитраж и използването на авторитет може да се неизбежни. Един начин да се реши това е да се основат следващите аргументи на предишни факти като преди това записани обсъждания, споразумения, процедури и практики относно същата или сродна материя. Ами ако другата страна просто не приеме допълнителните аргументи? Е, наистина не знам, но мисля, че това говори достатъчно за другата страна. Във фирмените общества без добро управление на документацията и постоянна смяна на авторитетите знанието бързо се губи във времето. Нови правила и процедури се създават всеки ден, защото никой не я наясно или не желае да приеме това което е било определено в миналото. Така се създават легенди.

2018-11-15

Маскиране на данни и анонимизация в изданието за обществото на MySQL

Oracle наскоро въведе функционалност за маскиране на данни в MySQL, но тя е достъпна само в изданието за предприятия (виж MySQL Enterprise Data Masking and De-Identification в наръчника). Не съм клиент, но съм запознат с идеята както и със съхранени процедури, така че реших да се опитам да емулирам това което MySQL са изработили за тяхното издание за предприятия на базата.

Започнах с четене на наръчника, после прегледах форматите за номера на кредитна карта и социална осигурвока, за да изляза накрая с просто изпълнение изработено тази вечер. Изработих само функциите за маскиране с общо и специално предназначение и функциите за създаване на случайни данни (напр. е-поща, номер на карта за разплащания, номер на социална осигуровка и телефонен номер в САЩ). Имам допълнителна функция gen_rnd_string за създаване на низ от случайни символи с дадена дължина (само знаци от английската азбука по подразбиране). Направих няколко теста и резултатите изглеждат обещаващо, но трябва да пробвам повече и вероятно да създавам по-добри данни, които евентуално да минават проверка, защото приложение използващо случайно създадени данни може лесно да се счупи ако не може да ги провери. Остава ми да изработя функциите за създаване на случайни данни с използване на речници за които смятам, че мога да ползвам LOAD DATA INFILE и CREATE TEMPORARY TABLE за зараждане на речник.

Това е просто моята идея за това как може да бъде изработено маскиране на данни и анонимизация дори в изданието за обществото на MySQL и като съхранени процедури, защото реализацията на MySQL се основава на потребителски дефинирани функции (UDF). Чудя се защо са избрали този подход и отдолу опита ми да отгатна възможните отговори:
  • Видимост: Потребителски дефинираната функция има глобална видимост, така че може да бъде извикана без името на схемата отпред като всяка друга вградена функция. Моята реализация трябва да бъде заредена в схема и така всяка функция ще трябва да се вика с име на схемата отпред.
  • Подразбрани аргументи: MySQL все още не поддържа подразбрани аргументи за съхранени процедури, но това е възможно за потребителски дефинирани функции тъй като те могат да се написани само "на C или C++ (или друг език, който може да използва C конвенция на извикванията)". Функциите за маскиране с общо предназначение mask_inner и mask_outer приемат незадължителен аргумент за символа с който да се маскира. В реализация със съхранени процедури това може да стане само с потребителски дефинирана променлива. Ще изработя това по-късно.
  • Скриване на подробностите по изработката: Потребителски дефинираните функции скриват изработката на MySQL в двоичния файл на споделената библиотека. Изходния код на съхранените процедури може лесно да бъде разгледан от потребител с достатъчно права (напр. администратор). До колкото ми е известно MySQL не предлага инструмент за разбъркване на кода както Oracle (виж Obfuscating PL/SQL Source Code). Отделно от това наръчника не предоставя достатъчно подробности за възможните стойности от функциите за създаване на данни.
  • Сигурност: Това може да се отнася до начина по който сървъра управлява паметта за потребителски дефинираните функции и съхранените процедури, но това е твърде навътре в нещата.
  • Производителност: Потребителски дефинираните функции по предположение трябва да имат по-добра производителност в сравнение със съхранените процедури, защото първите са извиквания на функции в компилирана за съответната система съхранена библиотека, а вторите се интерпретират от сървъра при всяко извикване.
Нетърпелив съм да обсъдя това утре на Есенната конференция на БГПО 2018 където ще има представяне на точно същата тема. Ще обновя публикацията или ще напиша нова след събитието.

Обновяване 2018-11-16 19:01 EET: BGOUG конференцията потвърди представите ми за паричните, поради които маскирането на данни е изработено като UDF функции. Една от цитираните причини беше точно "производителност".

Обновяване 2018-11-18 19:51 EET: След като си поиграх още малко и опитах да направя речниковите функции днес, се сблъсках с две ограничения, за които не бях помислил преди това. Първото ограничение е невъзможността за използване на подготвени изрази във функция (т.е. от главата Prepared SQL Statement Syntax в наръчника "SQL syntax for prepared statements can be used within stored procedures, but not in stored functions or triggers"). Второто ограничение е невъзможността за използване на LOAD DATA INFILE като подготвен израз (виж секция SQL Syntax Allowed in Prepared Statements в наръчника). Първото ограничение означава, че речниковите функции могат да бъдат изработени само като процедури. Второто ограничение означава, че е невъзможно да се зареди речника от файл, така че изработката на функция gen_dictionary_load не може да бъде завършено - тя може да създаде временната таблица за речника, но не може да го зареди. Все пак направих процедурите и успях да създам случайни градове в САЩ. Също така проверих производителността с gen_rnd_pan функцията, тъй като тя прави както създаване на случаен низ от цифри, така и изчисление на числото на Лун. На моя MySQL 5.7 сървър работещ върху Intel Pentium G3420 @ 3.2 GHz с 8 GB DDR3 RAM пуснах SELECT BENCHMARK(count, gen_rnd_pan()) и резултата за различен брой повторения беше следния:
  • 1000 пъти - 0.28 секунди;
  • 10000 пъти - 3.13 секунди;
  • 100000 пъти - 31.41 секунди;
  • 1000000 пъти - 278.19 секунди.
Освен ако MySQL не премахне ограниченията за мен темата е приключена. Мога да се опитам да направя функциите за маскиране на данни като UDF както MySQL ако намеря време и ако имам истинска нужда.