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

2019-12-25

Bugzilla на Български в GitHub

Преди точно 14 години на този ден качих локализацията на Bugzilla 2.18.1 в хранилището на проекта Mozilla-BG в OpenFMI. Обмислях използването на Bugzilla като система за следене на бъгове за домашния ми сървър от лятото на 2005 и когато разглеждах проекта забелязах, че няма локализация на български, така че реших да съдействам. Превода беше направен през лятото на 2005 и през есента започнах да търся начин да го публикувам. Първоначално си мислех за SourceForge, но в крайна сметка реших да стана част от проекта Mozilla-BG след като бях поканен от Огнян Кулев.
Локализацията беше преместена в BitBucket през 2012 след като OpenFMI беше изведен от експлоатация, така че хранилището беше преобразувано от Subversion (SVN) към Mercurial (Hg). Тогава някои проблеми с SVN хранилището станаха очевидни, но не намерих време да ги оправя до сега. Е, беше време да мигрирам проекта към Git и да го публикувам в GitHub, което постигнах миналата седмица. Проекта Bugzilla-BG е вече на GitHub с цялата си история от Декември 2005. И от тогава можехте да пробвате локализацията на Sotirov-BG.Net Bugzilla.
Локализацията измина дълъг път от CVS на домашния ми сървър първоначално към SVN, после Hg и най-накрая в Git. Единствения ми проект минал по този път :-)

Забавлявайте се!
Весела Коледа!

2019-12-05

Режим картина-в-картината в Firefox 71

Firefox 71 излезе преди два дни (виж бележки към версията) и аз вече абсолютно харесвам новия режим на картина-в-картината за просвирване на видеоклипове докато сърфирате. Наистина е важно за мен, защото това е обичайния начин по който сърфирам в днешно време - да имам видео изпълняващо се на заден план и слушайки го докато сърфирам уебсайтове. Единствения недостатък беше, че трябваше да превключвам към раздела с видеото от време на време, когато исках наистина да видя какво върви. Това понякога изискваше търсенето на раздела, защото обичайно имам много раздели отворени в различни групи (организирани с  Panorama View разширението).
Сега моето онлайн изживяване е доста по-приятно, защото мога просто да пусна видео, да го извадя в отделен малък прозорец където ми харесва на екрана и да продължа със сърфирането или правенето на нещо друго. Така мога да продължа да държа под око видеото дори когато се прехвърля извън Firefox. Супер полезно е дори когато работя върху нещо, защото нормално използвам много онлайн ресурси. Новата възможност е достъпна само под Windows за сега, но  Mozilla обещава да бъде достъпна за MacOS и Linux със следващата версия очаквана след около месец.
Благодаря Mozilla, за това че постоянно подобряваш онлайн изживяването ми!

2019-10-18

Нови възможности в MySQL 8.0.18

В понеделник, Oracle пусна MySQL 8.0.18 (виж още бележките към версията) и за разлика предходните версии имаше доста информация за това какво ще бъде включено в тази. лично присъствах на няколко представяния и четох някои публикации в социалните мрежи. Както вече писах основните нови възможности в тази версия са хеш съединения и EXPLAIN ANALYZE, които целят съответно да подобрят производителността на някои заявки и дадат по-добра представа за това как плана на оптимизатора се сравнява с действителното изпълнение.

Оптимизация с хеш съединение

Преди 8.0.18 единствения тип съединение извършван от MySQL беше вложения цикъл (цикъл в цикъл, вътрешен цикъл във външен цикъл). И имаше два алгоритъма изпълняващи го:
  • Nested-Loop Join - най-простия където всеки ред от външния цикъл се подава към вътрешния цикъл за обработка. Очевидния недостатък на този е, че вътрешната таблица трябва да бъде четена много пъти.
  • Block Nested Loop (BNL) - използва буфериране на редове от външната таблица, за да намали броя на пъти в които вътрешната таблица се чете. В този случай, вместо да се подава само един ред към вътрешния цикъл множество редове от буфера могат да бъдат подадени наведнъж. Това значително намалява броя пъти в които вътрешната таблица трябва да бъде четена. Разбира се това изисква повече памет (виж join_buffer_size). Този алгоритъм се използва за range, index или ALL типове съединения.
Има също и Batched Key Access (BKA) алгоритъм, който използва буфериране както BNL, но поставя ключовете на редовете в буфера и ги подава наведнъж към машината на базата данни за намиране в индекса. Ключовете след това се ползват за извличане на редове от вътрешната таблица. Този алгоритъм може да се ползва когато има достъп чрез индекс към вътрешната таблица.

Тук идват хеш съединенията. Идеята е да се построи хеш таблица на стойностите от външната таблица, която нормално е най-малката. След това се чете вътрешната таблица и се търсят съвпадения в хеш таблицата. Така и двете таблици в най-добрия случай могат да бъдат прочетени само по веднъж. Хеш съединенията са най-подходящи за големи резултати където не могат да се ползват индекси. Поради тяхната природа те са полезни само за съединения с равенство. От 8.0.18 MySQL ще избере хеш съединение пред BNL за всяка заявка, която използва условие с равенство за съединение (напр. ref или eq_ref типове съединения) и не ползва индекси. Да пробваме.

За примера, добих нова таблица с име job_sal с диапазон на заплати според позиция към схемата dept_emp и също създадох 1 милион повече служители. Сега да кажем, че искате да намерите всички служители с заплата извън диапазона. Аз бих написал заявка като тази:

SELECT E.ename, E.sal, JS.sal_min, JS.sal_max
  FROM emp     E,

       job_sal JS
 WHERE E.job = JS.job

   AND E.sal NOT BETWEEN JS.sal_min AND JS.sal_max;


Която като се има в предвид липсата на индекси ще бъде изпълнена с BNL алгоритъм в MySQL преди 8.0.18, но в новата версия същата заявка ще се възползва от оптимизацията с хеш съединение както е показано в следващия план за изпълнение в дървовиден формат:

+----------------------------------------------------------------------------------------+
| EXPLAIN                                                                                |
+----------------------------------------------------------------------------------------+
| -> Filter: (E.sal not between JS.sal_min and JS.sal_max)  (cost=499211.71 rows=442901)
    -> Inner hash join (E.job = JS.job)  (cost=499211.71 rows=442901)
        -> Table scan on E  (cost=1962.39 rows=996514)
        -> Hash
            -> Table scan on JS  (cost=0.75 rows=5)
 |
+----------------------------------------------------------------------------------------+
1 row in set (0.0023 sec)


Visual Explain Plan
Плана разкрива, че за по-малката таблица JS ще бъде хеширана и съединението ще бъде изпълнено с използването на хеш таблица. За съжаление, оптимизацията с хеш съединение не е видима в традиционния план за изпълнение, JSON и така в MySQL Workbench. Визуалния план за изпълнение (виж вдясно) така ще бъде подвеждащ показвайки BNL алгоритъм за съединението. Това е разбираемо, защото оптимизацията с хеш съединение е възможна само с новия изпълнител на итеративни алгоритми, който всъщност създава плана за изпълнение в дървовиден (TREE) формат. Този изпълнител не може да обясни някои заявки, така че можете да видите само съобщението "not executable by iterator executor". Обаче, наистина се надявам това да бъде подобрено в следващи версии, защото плана за изпълнение трябва да бъде последователна между форматите. Докладвах заявка за функционалност като бъг 97280.

Пробвах производителността на заявката за 1 милион служители и времето за изпълнение беше 0.89 сек. Забраняването на оптимизацията с хеш съединение с NO_HASH_JOIN подсказката увеличи времето за изпълнение до 1.26 сек. Хеш съединението със сигурност ще бъде много по-благотворно когато външната таблица има повече редове.

EXPLAIN ANALYZE

Тази нова възможност също идва върху дървовидния (TREE) план за изпълнение и и представлява нещо като инструмент за профилиране, защото освен информация за това как оптимизатора планира да изпълни заявката (виж отгоре) има също така информация от действителното ѝ изпълнение. Тази информация включва:
  • времето за връщане на първия ред (в мс);
  • времето за връщане на всички редове (в мс);
  • броя на прочетените редове;
  • броя цикли.
Тя прави възможно сравнението на оценките на оптимизатора към действителното изпълнение на заявката. Да пробваме с предишната заявка. Нарочно съм скъсил изхода.

+-------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                               |
+-------------------------------------------------------------------------------------------------------+
| -> Filter: (E.sal not between JS.sal_min and JS.sal_max)
       (cost=502072.82 rows=442901) (actual time=0.372..747.742 rows=915768 loops=1)
    -> Inner hash join (E.job = JS.job)
         (cost=502072.82 rows=442901) (actual time=0.355..575.011 rows=1000014 loops=1)
        -> Table scan on E  (cost=2534.62 rows=996514) (actual time=0.185..353.877 rows=1000014 loops=1)
        -> Hash
            -> Table scan on JS  (cost=0.75 rows=5) (actual time=0.133..0.144 rows=5 loops=1)
 |
+-------------------------------------------------------------------------------------------------------+
1 row in set (0.7754 sec)


Ако ви изглежда познато проверете EXPLAIN ANALYZE на PostgreSQL. Моля, забележете, че времената са в милисекунди, не секунди! Мисля, че е лесно да се забележи, че броя редове оценени от оптимизатора за четенето на таблица E се различава, защото статистиките за телицата не са точни. Добавих 1 милион редове към таблица emp, така че трябва да увелича броя на пробните страници (виж innodb_stats_persistent_sample_pages) и пусна ANALYZE TABLE отново. След като го напраих оценката на оптимизатора и действителния брой редове съвпадат. Обаче, оптимизатора е също така в грешка за редовете за операциите от по-горно ниво - хеш съединението и филтъра, но това не може да се оправи със статистики за индекси. Според Norvald H. Ryeng (виж неговата статия MySQL EXPLAIN ANALYZE) както "оценения, така и действителния брой" редове "са осреднени върху всички цикли", но аз имам само един цикъл за всички операции в плана.

Във всеки случай EXPLAIN ANALYZE е чудесно допълнение към инструментариума на оптимизатора. За съжаление, нито TREE формат нито EXPLAIN ANALYZE са достъпни дори в последния MySQL Workbench, така че още една заявка за функционалност от мен като бъг 97282.

MySQL е само OpenSSL

С тази версия поддръжката на yaSSL и WolfSSL библиотеките са премахва, така че MySQL може да се компилира само с OpenSSL. Аз лично винаги съм изграждал MySQL с OpenSSL (т.е. използвайки ключа -DWITH_SSL=system), защото това е което идва с Slackware, но разбира се по-важното е, че "комбинацията MySQL/OpenSSL е много добре изпробвана и доказана в продукция" както Georgi Kodinov обяснява в публикацията си MySQL is OpenSSL-only now !.

Пространствен

Функцията ST_Distance вече приема SRS аргуменити от всички геометрични типове, а не само аргументи от типове Point и Point, или Point и MultiPoint както преди.
Щастливо ползване на MySQL!

Обновена 2019-11-09: Това, че времената са в милисекунди не беше указано в наръчника, така че въведох бъг 97492, защото мисля също, че ще бъде добре единицата да се печата до времената.

2019-09-29

Нови използвания на TREE план за изпълнение в MySQL 8.0.18

Както писах през Юни (виж Формат на план за изпълнение TREE в MySQL 8.0.16) нов формат на план за изпълнение беше въведен - TREE. Това беше представяне на операциите предприети от оптимизатора за задоволяването на заявка в форма с отстъпи като дърво. Новия формат беше определен като експериментален, затова чаках да видя как ще се развие в следващите версии. Е, миналия Петък докато разглеждах наръчника забелязах, че е обновен за две нови възможности идващи в MySQL 8.0.18:
  • HASH съединения ще бъдат видими само в TREE формат. Намирам това за странно, защото бихте очаквали да видите тази операция без значение от формата на плана, така че се надявам да бъде разширено в бъдеще.
  • EXPLAIN ANALYZE всъщност ще изпълнява заявката и ще дава информация за времето на изпълнение. Ще включва също и допълнителна информация на база итератор относно това как оценката на оптимизатора се сравнява с действителното изпълнение.
Вече съм нетърпелив да пробвам новата версия, която предполагам ще бъде пусната някъде към средата на Октомври. Останете на линия, тъй като ще пиша за тези нови възможности когато пристигнат и имам възможност да си поиграя с тях.

2019-07-31

Нови възможности в MySQL 8.0.17

Миналия Вторник (27-ми Юли), Oracle пуснаха MySQL 8.0.7 следвайки тримесечния цикъл на нови версии въведен миналата година с 8 серията. Това е следващата версия по "поддръжката" въвеждаща някои нови възможности, както и обезценявайки някои нестандартни такива, така че ето това, което забелязах започвайки с тези, които смятам важни за разработчици.

Индекси върху множество стойности

С помощта на създавани колони (въведени с MySQL 5.7.6) и функционални индекси (въведени с MySQL 8.0.13 за което писах преди) стана възможно да се индексират данни в сложни стойности на колони като JSON. Но в JSON можете да имате скалари както и масиви, така че търсенето в масиви не беше възможно с помощта на индекс. Индексите върху множество стойности идват, за да решат това позволявайки множество записи в индекса да сочат към един и същ запис с данни. Такива индекси се създават като всеки друго функционален индекс и се използват автоматично от оптимизатора когато е възможно. Да видим един пример - регистър на преводачи с говоримите от тях езици.

CREATE TABLE translators (
  id INT AUTO_INCREMENT,
  jdata JSON,

  PRIMARY KEY(id)
);

  • Случай 1 - масив от низове

Да създадем малко данни:

INSERT INTO translators (jdata)
VALUES ('{"name": "T1", "langs": ["English", "French", "Spanish"]}'),
       ('{"name": "T2", "langs": ["English", "Spanish"]}'),
       ('{"name": "T3", "langs": ["French", "Spanish"]}');


След това, да потърсим преводачи, които говорят Английски използвайки новия MEMBER OF оператор:

SELECT id, jdata->>'$.name', jdata->'$.langs'
  FROM translators
 WHERE 'English' MEMBER OF (jdata->'$.langs');


Или новата функция JSON_OVERLAPS ето така:

SELECT id, jdata->>'$.name', jdata->'$.langs'
  FROM translators
 WHERE JSON_OVERLAPS(jdata->'$.langs', '["English"]');


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

+----+------------------+----------------------------------+
| id | jdata->>'$.name' | jdata->'$.langs'                 |
+----+------------------+----------------------------------+
|  1 | T1               | ["English", "French", "Spanish"] |
|  2 | T2               | ["English", "Spanish"]           |
+----+------------------+----------------------------------+
2 rows in set (0.00 sec)


Като се имат в предвид данните очаквано заявките връщат T1 и T2, но не T3. Обаче, тези заявки правят пълно сканиране на таблицата, така че производителността им ще деградира с натрупването на данни в таблицата.

Execution plan without index

За щастие, вече е възможно да се добави индекс върху множество стойности ето така:

ALTER TABLE translators
  ADD INDEX idx_langs_arr ((CAST(jdata->'$.langs' AS CHAR(8) ARRAY)));

Това е функционален индекс, в който е необходимо да се използва функцията CAST в новата ключова дума ARRAY. С индекса плана за изпълнение на SELECT заявките отгоре става съответно:
Execution plan of MEMBER OF with indexExecution plan of JSON OVERLAPS with index

  • Случай 2 - масив от обекти
Малко по-различно е за масив от обекти, но само за JSONPath израза. Нека създадем малко данни (след като почистим Случай 1):

INSERT INTO translators (jdata)
VALUES ('{"name": "T1", "langs": [{"lang": "English"}, {"lang": "French"}, {"lang": "Spanish"}]}'),
       ('{"name": "T2", "langs": [{"lang": "English"}, {"lang": "Spanish"}]}'),
       ('{"name": "T3", "langs": [{"lang": "French"}, {"lang": "Spanish"}]}');


След това, нека да потърсим преводачи, които говорят Английски по същите два начина:

SELECT id, jdata->>'$.name', jdata->'$.langs[*].lang'
  FROM translators
 WHERE 'English' MEMBER OF (jdata->'$.langs[*].lang');


SELECT id, jdata->>'$.name', jdata->'$.langs[*].lang'
  FROM translators
 WHERE JSON_OVERLAPS(jdata->'$.langs[*].lang', '["English"]');


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

ALTER TABLE translators
  ADD INDEX idx_langs_obj ((CAST(jdata->'$.langs[*].lang' AS CHAR(8) ARRAY)));


Забележете леко различния синтаксис на JSONPath израза. За да работи индекса разбира се е необходимо да се използва същия израз в WHERE клаузата както в определението на индкеса. Разработчиците, които предпочитат да пазят данни направо в JSON колони трябва да бъдат щастливи от тази нова възможност, тък като тя прави възможно индексирането не само на скаларни стойности, но също така масиви.

JSON

Освен вече споменатите нов стандартен оператор MEMBER OF за търсене на стойности в JSON масиви има три нови функции JSON_OVERLAPS, JSON_SCHEMA_VALID и JSON_SCHEMA_VALIDATION_REPORT.
  • Функцията JSON_OVERLAPS сравнява два JSON документа и връща истина "ако двата документа имат някоя обща ключ-стойност двойка или елементи на масив". Както MEMBER OF и JSON_CONTAINS функцията JSON_OVERLAPS може да се облагодетелства от индекси върху множество стойности.
  • Функциите JSON_SCHEMA_VALID и JSON_SCHEMA_VALIDATION_REPORT са добавени във връзка с поддръжката на JSON Schema. Първата проверява JSON документ спрямо JSON схема и връща истина ако е верен иначе лъжа, така че да може да се ползва като CHECK ограничение. Втората ще предостави подробности по грешките от проверката под формата на JSON документ.

Преобразуване към FLOAT, DOUBLE и REAL

Функциите CAST и CONVERT вече могат да преобразуват към типове данни с плаваща запетая като FLOAT, DOUBLE и REAL. Нека да пробваме с неправилна стойност:

SELECT CAST('1.23.34' AS FLOAT) cast_res, CONVERT('1.23.34', FLOAT) conv_res;

+--------------------+--------------------+
| cast_res           | conv_res           |
+--------------------+--------------------+
| 1.2300000190734863 | 1.2300000190734863 |
+--------------------+--------------------+
1 row in set, 2 warnings (0.00 sec)

Има две предупреждения, така че нека ги видим с show warnings:

+---------+------+---------------------------------------------+
| Level   | Code | Message                                     |
+---------+------+---------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: '1.23.34' |
| Warning | 1292 | Truncated incorrect DOUBLE value: '1.23.34' |
+---------+------+---------------------------------------------+
2 rows in set (0.00 sec)

Преобразуването към DOUBLE и REAL произвежда различен резултат и същите предупреждения:

SELECT CAST('1.23.34' AS DOUBLE) cast_res, CONVERT('1.23.34', DOUBLE) conv_res;
SELECT CAST('1.23.34' AS REAL) cast_res, CONVERT('1.23.34', REAL) conv_res;


+----------+----------+
| cast_res | conv_res |
+----------+----------+
|     1.23 |     1.23 |
+----------+----------+
1 row in set, 2 warnings (0.00 sec)

CLONE команда

В продължение на много години MySQL администратори трябваше да разтоварват основни сървъри, прехвърлят архива по мрежата и го зареждат в реплики, за да инициализират състоянието им (виж Copy a Database from one Server to Another). Аз нямам много отношение към администрация на бази данни, но това беше тромава и досадна процедура особенно в случаите на невъзобновими грешки при репликация когато трябваше да инициализирам отново състоянието на репликите, така че го смятах за тежест. Защо не мога просто да "клонирам" основния сървър след почистване на данните в репликата? Е, в новата версия  е възможно лесно да се създават нови инстанции или инициализират наново същестуващи с разработената естесвена поддръжка на провизиране в сървъра с командата CLONE. Това става възможно с новия MySQL Clone Plugin. Можете да откриете повече за това като прочетете следните статии:
Това е фокуса на тази версии и съм сигурен, че ще промени значително работата на организации използващи обширано MySQL репликация.

Обезценяавния

Това е списък на възможносите, които биват обезценени с тази версия и ще бъдат премахнатеи в бъдещи версии:
  • Функцията FOUND_ROWS и модификатора на заявки SQL_CALC_FOUND_ROWS. Документацията предлага използването на COUNT(*) за да се намери броя редове.
  • Атрибути на числови типове данни:
    • Дължина за показване на целочислени типове данни. Вече се показва предупрежение "1681 Integer display width is deprecated and will be removed in a future release." ако се опитате да създадете таблици с INT(11) например. Аз имам доста такива определения както са били предоставени от mysqldump и MySQL Workbench, така че ще трябва да ги махна всички преди изразите да бъдат отхвърлени с грешка в бъдеще.
    • Атрибута ZEROFILL. Така или иначе никога не съм го ползвал.
    • Атрибута UNSIGNED за FLOAT, DOUBLE, и DECIMAL типове данни. Също не съм го ползвал никога.
    • AUTO_INCREMENT поддръжката за FLOAT и DOUBLE типове данни. Някой?
  • Синтаксиса  FLOAT(M,D) и DOUBLE(M,D) за указването на броя цифри за типове с плаваща запетая.
  • Логически оператори && (двоен амперсанд), който е синоним на AND, || (двоен пайп), който е синоним на OR и ! (удивителна), която е синоним на NOT. Не съм сигурен, че някога съм ги ползвал дори в ежедневни заявки, защото намирам ползването на ANDOR и NOT за доста по-изразително.
  • Ключовата дума BINARY за указване на _bin колации. Никога не съм я ползвал също така.
Разработчиците трябва да вземат в предвид отърваването от тези нестандартни възможности, за да предотвратят неприятни изненади в бъдеще. Аз ще прегледам и поправя проектите си възможно най-скоро.

Оправени бъгове

Във връзка с CHECK ограничения открих два бъга в предходата версия и един от тях беше оправен (виж бъг #95189 CHECK constraint comparing columns is not always enforced with UPDATE queries). За пълен пример моля, виж bug_95189_test_case.sql скрипта, така че нека просто проверим:

SET binlog_format = 'STATEMENT';
SET binlog_row_image = 'minimal';


UPDATE tst SET end_date = '2019-04-20' WHERE id = 1;
/* Error Code: 3819. Check constraint 'chk_dat' is violated. */

Значи е оправен, защото очаквано UPDATE заявката пропада дори със специфичние настройки за двоичен журнал. Другият (виж bug #95192 CHECK constraint comparing column with default value is not enforced) ще трябва да почака.

Това завършва моя преглед на новите възможности в MySQL 8.0.17 версия по поддръжка. Надявам се новите версии да донесат повече възможноти за разработчици.

2019-06-05

Формат на план за изпълнение TREE в MySQL 8.0.16

Пропуснал съм една интересна нова функционалност в последната MySQL версия 8.0.16, която покрих миналия месец. Спомената е в бележките към версията (виж Functionality Added or Changed и превъртете надолу или търсете за TREE) и също в документацията (виж Obtaining Execution Plan Information). Както се оказва нов формат на план за изпълнение TREE е бил добавен.

Досега имаше две възможности за формата на плана за изпълнение създаван с EXPLAIN израза:
  • TRADITIONAL - стария табличен план за изпълнение подходящ за конзола;
  • JSON - план за изпълнение в JSON използван от MySQL Workbench за показване на хубави графики в Visual Explain, които използвам в презентации.
Който е запознат с традиционния табличен изход знае, че той не е подравнен и понякога е трудно да се разбере реда на операциите. Сега има възможност за използване на TREE формат, който печата "създаденото итеративно дърво" с операциите подравнени на дясно подобно на плановете за изпълнение в Oracle. Това трябва да помогне на потребителите да разберат по-добре как изпълнение на заявката всъщност се подготвя. Тази функционалност все още е експериментална, така че както синтаксиса така и изхода могат да се променят в бъдеще.

Да вземем следните две заявки, които произвеждат един и същи резултат - минимална, средна и максимална заплата за отделите в организацията въз основа на DEPT и EMP схема (виж моята адаптация за MySQL и EMP and DEPT скрипта). Втората заявка е възможна само в MySQL 8.0.14 и по-нов (виж New development features in MySQL 8.0.14).

Query 1: С използване на LEFT OUTER JOIN и производна таблица

SELECT D.dname, DT.min_sal, DT.avg_sal, DT.max_sal
  FROM dept D
       LEFT OUTER JOIN
       (SELECT E.deptno,
               MIN(E.sal) min_sal, AVG(E.sal) avg_sal, MAX(E.sal) max_sal
          FROM emp E
         GROUP BY E.deptno
       ) AS DT
       ON DT.deptno = D.deptno;

Query 2: С използване на LATERAL производна таблица

SELECT D.dname, LDT.min_sal, LDT.avg_sal, LDT.max_sal
  FROM dept D,
       LATERAL
       (SELECT MIN(E.sal) min_sal, AVG(E.sal) avg_sal, MAX(E.sal) max_sal
          FROM emp E
         WHERE E.deptno = D.deptno
       ) AS LDT;


Табличните планове за изпълнение на двете заявки (с премахнати колоните partitions, possible_keys и filtered за скъсяване на изхода) и разширения изход са както следва:

Query 1

+----+-------------+------------++-------++-------------+---------+-------------------+------++-------+
| id | select_type | table      || type  || key         | key_len | ref               | rows || Extra |
+----+-------------+------------++-------++-------------+---------+-------------------+------++-------+
|  1 | PRIMARY     | D          || ALL   || NULL        | NULL    | NULL              |    4 || NULL  |
|  1 | PRIMARY     | <derived2> || ref   || <auto_key0> | 5       | dept_emp.D.deptno |    2 || NULL  |
|  2 | DERIVED     | E          || index || fk_deptno   | 5       | NULL              |   14 || NULL  |
+----+-------------+------------++-------++-------------+---------+-------------------+------++-------+
3 rows in set, 1 warning (0.01 sec)


+-------+------+-----------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                             |
+-------+------+-----------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `dept_emp`.`D`.`dname` AS `dname`... left join (
                 /* select#2 */ select `dept_emp`.`E`.`deptno` AS `deptno`,min(`dept_emp`.`E`.`sal`) AS `min_sal`... |
+-------+------+-----------------------------------------------------------------------------------------------------+

1 row in set (0.00 sec)

Query 2

+----+-------------------+------------++------++-----------+---------+-------------------+------++----------------------------+
| id | select_type       | table      || type || key       | key_len | ref               | rows || Extra                      |
+----+-------------------+------------++------++-----------+---------+-------------------+------++----------------------------+
|  1 | PRIMARY           | D          || ALL  || NULL      | NULL    | NULL              |    4 || Rematerialize (<derived2>) |
|  1 | PRIMARY           | <derived2> || ALL  || NULL      | NULL    | NULL              |    2 || NULL                       |
|  2 | DEPENDENT DERIVED | E          || ref  || fk_deptno | 5       | dept_emp.D.deptno |    1 || NULL                       |
+----+-------------------+------------++------++-----------+---------+-------------------+------++----------------------------+
3 rows in set, 2 warnings (0.0006 sec)

+-------+------+-----------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                             |
+-------+------+-----------------------------------------------------------------------------------------------------+
| Note  | 1276 | Field or reference 'dept_emp.D.deptno' of SELECT #2 was resolved in SELECT #1                       |
| Note  | 1003 | /* select#1 */ select `dept_emp`.`D`.`dname` AS `dname`, ... join lateral (
                 /* select#2 */ select min(`dept_emp`.`E`.`sal`) AS `min_sal`...                                     |
+-------+------+-----------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)


Това не е красиво, нали? Допълнителната информация печатана с SHOW WARNINGS само отбелязва участващите заявки, за да можете по-лесно да ги намерите в плана (съответстващи на първата колона id). За втората заявка има също и бележка за това как обръщението от втората заявка е разрешено в първата заявка.

Сега, ето как изглеждат изходите от новия формат на планове за изпълнение:

Query 1

+-----------------------------------------------------------------+
| EXPLAIN                                                         |
+-----------------------------------------------------------------+
| -> Nested loop left join
    -> Table scan on D
    -> Index lookup on DT using <auto_key0> (deptno=d.deptno)
        -> Materialize
            -> Group aggregate: min(e.sal), avg(e.sal), max(e.sal)
                -> Index scan on E using fk_deptno                |
+-----------------------------------------------------------------+
1 row in set (0.0025 sec)

Query 2

+----------------------------------------------------------------------+
| EXPLAIN                                                              |
+----------------------------------------------------------------------+
| -> Nested loop inner join
    -> Invalidate materialized tables (row from D)
        -> Table scan on D
    -> Table scan on LDT
        -> Materialize (invalidate on row from D)
            -> Aggregate: min(e.sal), avg(e.sal), max(e.sal)
                -> Index lookup on E using fk_deptno (deptno=d.deptno) |
+----------------------------------------------------------------------+
1 row in set, 1 warning (0.0007 sec)
Note (code 1276): Field or reference 'dept_emp.D.deptno' of SELECT #2 was resolved in SELECT #1


Красиво, нали? Виждате всички операции, които ще бъдат изпълнени и подравняването помага да се разбере как те са вложени или с други думи коя ще бъде изпълнена преди другите. Ще трябва да следя как новия формат ще се развие, но дори сега мисля, че ще бъде доста полезен за сложни заявки, така че е много добре, че са го добавили.

Благодаря ви разработчици на MySQL!

2019-05-24

Преводи на Bugzilla 5.0.5 и 5.0.6

Преди няколко дни случайно забелязах, че има нови версии на Bugzilla, а именно 5.0.5 (от 2019-01-30) и 5.0.6 (от 2019-02-09), които явно са се появили тихомълком в началото на годината. Тихомълком, защото поне аз не съм получавал никакво уведомление и явно отдавна не бях влизал през web интерфейса на инсталацията ми, за да видя уведомлението на началната страница. Ще трябва да проверя с разработчиците за уведомленията по електронна поща, защото може да са измислили нещо ново. Последната версия можете да свалите от официалната страница, но горещо ви препоръчвам да надграждате с Git - много по-лесно и бързо е, особено ако имате допълнителни разширения, кожи и други промени (виж Надграждане отдолу).

За превода

Така или иначе днес стигнах да обновя превода. Промените не бяха много, защото това са чисти версии по поддръжката без съществени нови функционалности, които да идват с нови шаблони. Всичко вече е подадено към хранилището, което отдавна искам да реорганизирам и евентуално мигрирам към Git, но все не остава време. Като цяло проекта май не се развива много добре през последните 2-3 години, така че не знам дали си заслужава. Оставям си го като задача евентуално за лятото.

Надграждане

Тази сутрин, след като подготвих преводите, успях да надградя инсталацията ми, което беше абсолютно безпроблемно следвайки инструкциите за директно надграждане от Git. С 5.0.5 таблицата bugs_fulltext беше преобразувана от MyISAM в InnoDB, а с 5.0.6 типа на полетата type_id в таблиците flaginclusions, flagexclusions и flags беше сменен от smallint на mediumint (виж Integer types), което трябва да позволи много повече флагове. Забавих се малко само защото трябваше да поизчистя някои локални промени и скрия (с .gitignore) някои файлове (журнали, кръпки, архиви с преводи, и др.). Това ме подсети и да пусна pull request за една от локалните ми промени - добавяне на viewport meta таг, което е важно в днешно време за поддръжката на мобилни устройства.

Изтегляне на преводите

Преводите можете да свалите от обичайните места по FTP или HTTPS или направо да свалите версия 5.0.6 и да надградите превода като просто разархивирате в основаната папка.

Приятно ползване! И разбира се честит Ден на българската просвета и култура и на славянската писменост!

2019-04-30

Нови възможности в MySQL 8.0.16 екосистемата

Миналия Четвъртък (2019-04-25), точно преди почивните дни за Православния Великден Оракъл пусна версия 8.0.16 на продуктите в MySQL екосистемата (включваща сървър, рутер, шел, ксъединители и workbench ГПИ). Новите версии на сървъра продължиха традицията установена с MySQL 8 на въвеждане на нови възможности, въпреки че все още са обозначавани като "Издание по поддръжката" ("Maintenance Release"). На последния Pre-FOSDEM MySQL Day Giuseppe Maxia предложи да се ползват семантични версии и аз заставам зад това, защото разбирам и подкрепям семантичните версии.

Това ще бъде наистина полезно, защото версия 8.0.15 на сървъра беше истинско издание по поддръжката оправящо само важен проблем с групова репликация. Следвайки новата схема на номериране на версиите (виж MySQL 8.0: It Goes to 11!) всички други продукти от екосистемата бяха бутнати до 8.0.15 дори без каквито и да било промени. Наистина не съм сигурен, че това е нужно, защото докато се ползва същата голяма (и малка) версия не би трябвало да има проблем със съвместимостта. А и същите номера на версии не осигуряват наистина пълна съвместимост между продуктите както моите бъгове за MySQL Workbench (напр. 90620, 92900, 92908 и 94012) показват.

MySQL Server

E, какво е новото в новата версия на сървъра. Ето това което забелязах.

CHECK ограничения

Лично аз чакам тази възможност от MySQL 4 когато започнаха да добавят големи нови възможности в сървъра и той започна да става повече като по-напредналите бази с данни. Бъг #3465 беше отворен през Април 2004 (и беше обещано да бъде оправен в MySQL 5.1), така че тази SQL възможност най-накрая е направена след повече от 15 години.

До сега (както може би сте забелязали) ключовата дума CHECK в CREATE TABLE беше приемана, но безшумно пренебрегвана. В миналото ползвахме заобиколни решения. Например можеше да се осигурят само неотрицателни стойности в колона с числов тип (цели числа и типове с фиксирана/плаваща запетая) с използването на нестандартния атрибут UNSIGNED (виж Numeric Type Attributes). По-сложни ограничения можеха да бъдат направени с тригери и вдигане на състояния (виж SIGNAL syntax) както показах в представянето ми MySQL 8 for developers на Пролетния семинар на БГПО 2018 г. и подобни все още ще са нужни.

MySQL 8.0.16 вече поддържа и двете ограничения на ниво таблица и колона (виж CHECK constraints) за всички машини за съхранение. Ограничения на ниво таблица се поставят навсякъде в CREATE TABLE извън определенията на колони и могат да се обръщат към една или повече колони дори с предварително споменаване (т.е. на колони определени по-късно в израза). Ограниченията на ниво колона се поставят в определението на колоната и могат да се обръщат само към нея. За съжаление, израза който определя условието за проверка може да ползва само литерали, детерминистични вградени функции и оператори (т.е. вградени процедури, ОПФ/и, променливи и подзаявки не са разрешение). Не могат също да се ползват CHECK ограничения на колони с референтни действия от външни ключове (т.е. ON UPDATE, ON DELETE).

Нещото което трябва да се има в предвид с CHECK ограниченията е, че грешка има само ако условието се изчисли като FALSE. Ако са изчисли UNKNOWN поради NULL стойности грешка няма да има. Това е покрито много добре от Markus Winand в статията му The Three-Valued Logic of SQL, така че горещо ви препоръчвам да я прочетете цялата.

CHECK ограниченията имат име до 64 символа и ако не бъде указано сървъра ще създаде име като [име на таблица]_chk_[пореден номер], защото имената трябва да са уникални за схемата. Полезна възможност е да се създаде, но да не се налага ограничението (т.е. с NOT ENFORCED клауза), което е като разрешаване/забраняване за тригери което е възможност която искам да видя направена в бъдещи версии.

За основна употреба и прости примери за CHECK ограничения в MySQL, моля вижте наръчника и статията MySQL 8.0.16 Introducing CHECK constraint. По-интересен пример за CHECK ограничение върху JSON данни има в статията MySQL 8.0.16: how to validate JSON values in NoSQL with check constraint.

Ето първия ми пример. Мисля, че доста често има по две дати в таблица (напр. начална и крайна дата), които трябва да представляват началото и края на нещо, така че трябва да се в хронологичен ред. Нека да го направим с таблицата emp от примерната DEPT и EMP схема на Оракъл, която съм пригодил за MySQL.

ALTER TABLE emp
  ADD COLUMN retdate DATE AFTER hiredate,
  ADD CONSTRAINT ret_after_hire CHECK (
retdate > hiredate);

Обаче, когато се опитах да обновя ред на служител, който вече има стойност за дата на наемане със следната заявка (датата на пенсиониране предполагаемо идва от грешен потребителски вход и/или лошо приложение):

UPDATE emp
   SET retdate = STR_TO_DATE('1019-04-25', '%Y-%m-%d')
 WHERE empno = 7369;

1 row(s) affected Rows matched: 1  Changed: 1  Warnings: 0

заявката неочаквано успя и нямаше грешка за "нарушено ограничение". След обсъждане с Frédéric Deschamps в Slack подадох бъг #95189. Изглежда е свързано с репликация, защото на моя самостоятелен MySQL 8.0.16 сървър под Windows проблема не се възпроизведе и заявката работи както се очаква, но пробата която направих беше на MySQL 8.0.16 сървъра ми работещ под Slackware64 -current, който репликира от моя основен MySQL 5.7.26 сървър.

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

ALTER TABLE emp
  ADD CONSTRAINT emp_chks CHECK (hiredate >= CURDATE() AND sal > 0);

Такова ограничение може да изглежда разумно (ако вземате в предвид само въвеждане на данни), но разбира се резултата е:

Error Code: 3814. An expression of a check constraint 'emp_chks' contains disallowed function: curdate.

защото CURDATE е недетерминистична функция. Такова ограничение е възможно в PostgreSQL (която също поддържа съхранени процедури и ОПФ/и), но не и в Oracle (която поддържа съхранени процедури) и MariaDB (която има горе-долу същите ограничения като MySQL, въпреки че не са ясно описани). Проблемите с това са как ще валидирате ограничението за съществуващите редове и как ще обновявате редове, защото стойността на CURDATE се сменя всеки ден. Решението е да се създаде допълнителна колона запазваща текущата дата при създаването на реда и да се ползва тя за проверка на ограничението.

Ако се опитате директно да създадете колоната и наложите ограничението (т.е. с ALTER TABLE) разбира се ще получите:

Error Code: 3819. Check constraint 'emp_chks' is violated.

защото CHECK ограничението, както другите ограничения (напр. първични и външни ключове, уникални индекси, NOT NULL) се валидират за всички редове при създаване и трябва винаги да остават валидни. Така, че нека пробвам другояче.

Първо, зареждам новата колона от съществуващите данни (напр. въз основа на hiredate колоната тъй като изрази са възможни като стойности по подразбиране от 8.0.13):

ALTER TABLE emp
  ADD COLUMN created DATE DEFAULT (hiredate);

След това, промяна на колоната и добавяне на CHECK ограничение:

ALTER TABLE emp
  MODIFY COLUMN created DATE NOT NULL DEFAULT (CURDATE()),
  ADD CONSTRAINT emp_chks CHECK (hiredate >= created AND sal > 0);

И сега да пробваме заявки:

UPDATE emp SET hiredate = '1979-04-25' WHERE empno = 7369;

INSERT INTO emp
  (empno, ename, job, mgr, hiredate, sal)
VALUES
  (9999, 'MULDER', 'INVESTIG.', 7839, '2019-04-25', 4242);

Очаквано UPDATE заявката предизвиква грешка:

Error Code: 3819. Check constraint 'emp_chks' is violated.

но INSERT заявката минава, което е неочаквано. Явно, стойността на колона created не е заредена със стойността по подразбиране когато CHECK ограничението се валидира. Пробвах същото (само, че с малко по-различен синтаксис) на Oracle XE 18 и работи както се очаква - и UPDATE и INSERT заявките нарушаваха CHECK ограничението.

Има нова таблица CHECK_CONSTRAINTS в INFORMATION_SCHEMA, която предоставя информация за създадените CHECK ограничения във всички схеми. Допълнителна информация за името на таблицата и дали CHECK ограничението е наложено или не може да се получи от таблицата TABLE_CONSTRAINTS чрез прецеждане по новата стойност CHECK на колоната CONSTRAINT_TYPE.

Пространствени данни

След ST_Distance от 8.0.14, сега ST_Length функцията също поддържа незадължителен втори параметър unit, така че е възможно да се изчисляват дължини в различните поддържани единици както са определени в INFORMATION_SCHEMA.ST_UNITS_OF_MEASURE таблицата.

Сървъра прави цялостно надграждане

Сървъра вече може да надгражда mysql схемата, речника на данните и системните таблици както и PERFORMANCE_SCHEMA, INFORMATION_SCHEMA, sys и потребителски схеми (виж What the MySQL Upgrade Process Upgrades), така че mysql_upgrade командата се пенсионира. Това е важна административна възможност, защото тя ще направи надгражданията по-лесни и по-удобни. В миналото постоянно забравях да пусна командата, което водеше до странни проблеми по-късно.

Във връзка с надграждането имах странен проблем изразяващ се в това, че сървъра не можеше да стартира и печаташе съобщение [ERROR] [MY-013384] [Server] Could not create server upgrade info file at '/var/lib/mysql/data/' в лога въпреки, че правата бяха наред. Успях да намеря този gist mysql will not start after 8.0.15 to 8.0.16 update on Ubuntu 16.04 LTS с Google и след като създадох файла mysql_upgrade_info в /var/lib/mysql/data/ и след като прехвърлих собствеността му на mysql:mysql сървъра успя да запали успешно. Надграждах от 8.0.14 всъщност, но мисля, че може да е бъг. Вероятно сървъра очаква файла да е бил създаден.

Друга интересна нова възможност е избора --validate-config за проверка на конфигурацията на сървъра както администраторите са свикнали с други сървъри (напр. Apache). Това е наистина важно особено за среди в продукция, където неочаквания престой може да е крайно неприятен. Прочетете повече в Server Configuration Validation.

Системни потребители

MySQL сметки вече се категоризират и така разграничават като системни (които притежават SYSTEM_USER привилегия) и обикновени потребители (които не притежават). Това допринася за по-добро разделение на ролите, тъй като само системни потребители могат да извършват определени административни операции върху системни сметки. Преди това всеки потребител с подходящите привилегии можеше например да изтрие всяка сметка или убие всяка връзка принадлежаща на всеки потребител. Възможно е също така да се отнемат глобални привилегии частично за определени схеми (т.е. като изключения) чрез новата системна променлива partial_revokes, което преди това извикваше задаването на права поотделно за всички съществуващи схеми и добавяне на пава за всяка нова схема.

Сигурност

Новите неща са поддръжка за TLS 1.3, възможност за обновяване на SSL сертификатите без рестартиране на сървъра (виж ALTER INSTANCE RELOAD TLS) и информация за сертификатите в таблицата keyring_keys на PERFORMANCE_SCHEMA.

MySQL Router

Рутера вече има HTTP съставка, която му позволява да излага просто web-интерфейс и REST ППИта с цел да осигури по-добра наблюдателност чрез интеграция с външни инструменти за наблюдение и управление. Други значими промени са динамичната смяна между режими с един и много господари и подобрен журнал. Сега има WITH_ROUTER CMake избор за изграждане на рутер заедно с MySQL сървъра, която по подразбиране е ON и която реших да сменя на OFF, защото планирам да продължа да изграждам рутер като отделен пакет.

MySQL Shell

Шела идва с новия Shell Reporting Framework (виж Reporting with MySQL Shell), който позволява записването, показването и наблюдението на потребителски доклади. Нетърпелив съм да я пробвам и ще напиша отделна публикация по-късно. Вижте статията MySQL Shell 8.0.16: User Defined Reports от Jesper Wisborg Krogh. Също така сега вече е възможно да се изпълнява SQL без смяна на режима, тъй като \sql командата вече не сменя режима ако ѝ е подаден SQL израз, което мисля е доста полезно.

MySQL Workbench

MySQL Workbench вече поддържа прозоречни функции в SELECT (виж бъг #90620), изрази в DEFAULT (виж бъг #92900) и като части на ключове (виж бъг #92908) както и ключовата дума LATERAL (виж бъг #94012) - всички те достъпни от предходните 8.0.x версии на сървъра. За съжаление, не виждам каквато и да е поддръжка за CHECK ограничения (виж бъг #95143, който вече беше потвърден), така че отново еднаквите номера на версии не значат нищо.

2019-03-30

Приключвам с пощата на Dir.bg

От началото на миналата година имах проблеми с пощата на Dir.bg, която ползвам от 20-тина години. Услугата беше недостъпна по IMAP и губех съобщения. След като не успях да се свържа с поддръжката писах във форумите на Линукс за българи с надеждата някой да знае нещо по въпроса. Като оставим настрана излишните и помпозни коментари, един от потребителите откликна и проблема ми беше решен (предполагам е бил някой администратор). От тогава насам имаше няколко кратковременни прекъсвания на услугата, но последната седмица забелязах, че пощата отново не е достъпна през повечето време. От вчера за съжаление отново нямам никакъв достъп нито през IMAP, нито през web интерфейса.
Това вече ме кара сериозно да се замисля да мигрирам изцяло към Gmail. Това включва:
  • копиране на около 1 ГБ писма, което не би трябвало да е проблем като се има в предвид капацитета на днешните връзки стига разбира се услугата да се появи отново на което силно се надявам, защото имам някои важни писма.
  • пренасочване на N на брой услуги към пощенския ми адрес в Gmail. Това ще е трудна задача, защото дори не съм сигурен, че ще мога да се сетя за всички въпреки, че пазя писмата за регистрация, но затова за сега смятам да задържа и адреса в Dir.bg докато не прехвърля всичко. Вече започнах, защото не получавах писмата от БГПО.
  • обновяване на страници в Интернет, изходен код и други.
Наистина не знам защо се получи така и съжалявам, защото услугата ми беше полезна и удобна. Ползвах я без проблеми доста дълго (още от времето когато нямаше Gmail и имаше Yahoo). Явно обаче напоследък услугата не се поддържа добре и мисля, че имат проблеми с нежелана поща и други атаки.

Това е. Може би ще пусна нещо кратко, когато приключа с миграцията.