<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>C, PHP, VB, .NET</title>
	<atom:link href="http://www.cphpvb.net/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.cphpvb.net</link>
	<description>Дневникът на Филип Петров</description>
	<lastBuildDate>Thu, 17 May 2012 21:05:24 +0000</lastBuildDate>
	<language>bg-BG</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.4-beta4</generator>
		<item>
		<title>Извеждане на няколко произволни реда</title>
		<link>http://www.cphpvb.net/db/8125-multiple_random_rows/</link>
		<comments>http://www.cphpvb.net/db/8125-multiple_random_rows/#comments</comments>
		<pubDate>Thu, 17 May 2012 21:01:03 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8125</guid>
		<description><![CDATA[Вече разгледахме методи за извеждане на един произволен ред от таблица. Понякога обаче ние се нуждаем от повече. Например &#8220;последни 10 статии&#8221;, &#8220;последни 100 влизания в системата&#8221; и т.н. Затова в настоящата статия ще направя сравнение между методи за извеждане на множество от произволни редове. Ще се фокусираме върху сравнение между оптимизирания JOIN метод (който [...]]]></description>
			<content:encoded><![CDATA[<p>Вече разгледахме методи за извеждане на един произволен ред от таблица. Понякога обаче ние се нуждаем от повече. Например &#8220;последни 10 статии&#8221;, &#8220;последни 100 влизания в системата&#8221; и т.н. Затова в настоящата статия ще направя сравнение между методи за извеждане на множество от произволни редове. Ще се фокусираме върху сравнение между оптимизирания JOIN метод (който даде най-добри резултати при извеждането на един ред от таблица) и стандартния ORDER BY RAND().<span id="more-8125"></span></p>
<p><strong>Тестова постановка</strong></p>
<p>Ще използваме примерната база от данни World и по-специално нейната таблица city, която има 4079 реда. Създаваме общо 8 тестови съхранени процедури:</p>
<blockquote>
<pre>DELIMITER //
CREATE PROCEDURE test1_1(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name FROM city 
      ORDER BY RAND() LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test1_10(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name FROM city 
      ORDER BY RAND() LIMIT 10;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test1_100(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name FROM city 
      ORDER BY RAND() LIMIT 100;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test1_1000(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name FROM city 
      ORDER BY RAND() LIMIT 1000;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test6_1(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name
     FROM city JOIN
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, 
                  @num:=@num+1 
           FROM (SELECT @num:=0) AS a, city LIMIT 1) AS tmp 
          ON tmp.num = city.id;
     ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test6_10(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name
     FROM city JOIN
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, 
                  @num:=@num+1 
           FROM (SELECT @num:=0) AS a, city LIMIT 10) AS tmp 
          ON tmp.num = city.id;
     ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test6_100(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name
     FROM city JOIN
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, 
                  @num:=@num+1 
           FROM (SELECT @num:=0) AS a, city LIMIT 100) AS tmp 
          ON tmp.num = city.id;
     ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test6_1000(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name
     FROM city JOIN
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, 
                  @num:=@num+1 
           FROM (SELECT @num:=0) AS a, city LIMIT 1000) AS tmp 
          ON tmp.num = city.id;
     ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;</pre>
</blockquote>
<p><strong>Резултати</strong></p>
<p>Резултатите от тестовете са показателни:</p>
<blockquote>
<pre>CALL test1_1(1000);
Query OK, 0 rows affected (4.31 sec)

CALL test6_1(1000);
Query OK, 0 rows affected (0.69 sec)

CALL test1_10(1000);
Query OK, 0 rows affected (5.92 sec)

CALL test6_10(1000);
Query OK, 0 rows affected (4.01 sec)

CALL test1_100(1000);
Query OK, 0 rows affected (50.69 sec)

CALL test6_100(1000);
Query OK, 0 rows affected (50.97 sec)

CALL test1_1000(1000);
Query OK, 0 rows affected (10 min 2.53 sec)

CALL test6_1000(1000);
Query OK, 0 rows affected (9 min 54.92 sec)</pre>
</blockquote>
<p>Съвсем ясно се вижда, че първоначалната значителна преднина на test6 (модифицирания JOIN метод) се губи много бързо. Докато при извеждането на 10 произволни реда все още има някаква (но вече не голяма) преднина, то при 100 произволни реда вече тестовете са с изравнена бързина. Това се потвърждава и от теста с 1000 реда, при който разликите наистина са в рамките на нормалното отклонение.</p>
<p><strong>Разлики между методите</strong></p>
<p>Трябва сериозно да се отбележи една много важна разлика между двата метода &#8211; ORDER BY RAND() LIMIT X връща точно X <span style="text-decoration: underline;">различни реда</span> (т.е. не са напълно произволни, а са зависими един от друг), докато оптимизирания JOIN метод може да върне и повтарящи се редове (имаме независимост между произволните редове). Ето пример за това:</p>
<blockquote>
<pre>SELECT DISTINCT name FROM city 
ORDER BY RAND() LIMIT 1000;
...
1000 rows in set (0.02 sec)

SELECT DISTINCT city.name
FROM city JOIN
     (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, @num:=@num+1 
      FROM (SELECT @num:=0) AS a, city LIMIT 1000) AS tmp 
     ON tmp.num = city.id;
...
861 rows in set (0.00 sec)</pre>
</blockquote>
<p>Виждате, че при оптимизирания JOIN метод се получиха 139 повторения, докато при ORDER BY RAND() нито едно.</p>
<p>Другата, наистина много важна разлика е, че JOIN метода има съществена нужда от поредни идентификационни номера без дупки! При ORDER BY RAND() това не е така. Ако ние имаме нужда от изтриване на редове от таблицата, то поддържането на поредни идентификационни номера не е никак приятна, нито пък препоръчителна операция (особено ако числата са по първичен ключ).</p>
<p><strong>Заключение</strong></p>
<p>Когато очакваме извеждане на резултат, то обикновено ние не се интересуваме от повтарящи се редове, а напротив &#8211; не ги искаме. Поради тази причина при извикване на повече от 10 произволни реда ще се окаже, че ORDER BY RAND() е по-добрия метод.</p>
<p>Когато очакваме наистина произволни редове, а не различни (зависими от предишните) такива, то може да използвате JOIN метода, като преди това трябва да сте сигурни, че в реда няма дупки между идентификационните номера. Ако има такива, то ще се наложи да ги пренаредите с UPDATE заявка, която при всички положения ще е много тежка операция (допълнително ще трябва да се погрижите за AUTO_INCREMENT стойността, която ще трябва да се намали).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8125-multiple_random_rows/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Full-Text търсене с InnoDB в MySQL</title>
		<link>http://www.cphpvb.net/db/8114-full-text-search-in-mysql/</link>
		<comments>http://www.cphpvb.net/db/8114-full-text-search-in-mysql/#comments</comments>
		<pubDate>Wed, 16 May 2012 10:34:50 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8114</guid>
		<description><![CDATA[С наближаването на официалното излизане на стабилна версия 5.6 на MySQL идва време да поговорим и едно дългоочаквано нововъведение &#8211; full text search в InnoDB. Досега беше налично само за MyISAM таблици, но от предварителната версия за разработчици 5.6.4 вече е може да се използва и от по-силния си събрат. Това беше и едно от [...]]]></description>
			<content:encoded><![CDATA[<p>С наближаването на официалното излизане на стабилна версия 5.6 на MySQL идва време да поговорим и едно дългоочаквано нововъведение &#8211; full text search в InnoDB. Досега беше налично само за MyISAM таблици, но от предварителната версия за разработчици 5.6.4 вече е може да се използва и от по-силния си събрат. Това беше и едно от малкото липсващи неща в InnoDB, което нерядко караше разработчиците да използват MyISAM.<span id="more-8114"></span></p>
<p>Нека преди да започнем с примери да създадем тестова база от данни. Тя ще се състои от две таблици &#8211; потребители и публикации. Всеки потребител има уникален номер и име. В публикациите пазим уникален номер, номер на автор, заглавие, съдържание, дата на създаване и номер на статия, на която се отговаря (ако се публикува отговор на друго съобщение). Ще има два вида статии &#8211; едните ще са &#8220;главни&#8221; (те ще имат попълнено заглавие, но няма да имат попълнена стойност за &#8220;отговор на&#8221;), а другите ще са &#8220;отговори&#8221; (те ще имат попълнен &#8220;отговор на&#8221;, но няма да имат заглавие). Ето и въпросните две таблици:</p>
<blockquote>
<pre>CREATE DATABASE fts
  DEFAULT CHARACTER SET utf8
  DEFAULT COLLATE utf8_general_ci;

USE fts;

CREATE TABLE users(
  id SMALLINT UNSIGNED PRIMARY KEY,
  username VARCHAR(30) NOT NULL UNIQUE
)ENGINE=InnoDB;

CREATE TABLE posts(
  id INT UNSIGNED PRIMARY KEY,
  author SMALLINT UNSIGNED,
  FOREIGN KEY(author) REFERENCES users(id),
  title VARCHAR(255) NULL DEFAULT NULL,
  data TEXT NOT NULL,
  created DATETIME NOT NULL,
  reply_to INT UNSIGNED NULL DEFAULT NULL,
  FOREIGN KEY(reply_to) REFERENCES posts(id)
)ENGINE=InnoDB;</pre>
</blockquote>
<p>Нека създадем и един тригер, който ще се грижи за интегритета на данните:</p>
<blockquote>
<pre>DELIMITER //
CREATE TRIGGER post_integrity_check
BEFORE INSERT ON posts FOR EACH ROW
BEGIN
  DECLARE errormsg VARCHAR(255);
  DECLARE main_post_title VARCHAR(255);
  IF NEW.title IS NULL
  THEN
    IF NEW.reply_to IS NULL
    THEN
      SET errormsg = "Основните съобщения трябва да имат заглавие";
      SIGNAL SQLSTATE '45000' SET message_text = errormsg;
    ELSE
      SELECT title INTO main_post_title
      FROM posts
      WHERE id = NEW.reply_to;
      IF main_post_title IS NULL
      THEN
        SET errormsg = "Отговарите могат да са само към главно съобщение";
	SIGNAL SQLSTATE '45000' SET message_text = errormsg;
      END IF;
    END IF;
  ELSE
    IF NEW.reply_to IS NOT NULL
    THEN
      SET errormsg = "Отговорите не могат да предефинират заглавието";
      SIGNAL SQLSTATE '45000' SET message_text = errormsg;
    END IF;
  END IF;
END//
DELIMITER ;</pre>
</blockquote>
<p>Нека вмъкнем примерни данни. В случая взехме няколко произволни съобщения от форума <a title="Оффроуд България" href="http://www.OFFRoad-Bulgaria.com" target="_blank">OFFRoad-Bulgaria.com</a>. Изтеглете файла със заявките <a href="http://www.cphpvb.net/wp-content/uploads/2012/05/insert.txt">от тук</a> (ако не виждате текста &#8211; кодировката на файла е UTF8).</p>
<p>Ето една най-проста примерна заявка &#8211; изкарайте информация за тема 10 и всички нейни отговори, като сортирате по дата на въвеждане в базата от данни:</p>
<blockquote>
<pre>SELECT IF(posts.title IS NOT NULL, posts.title, 'REPLY') AS title, 
       users.username AS author, posts.created AS created, posts.data AS contents
FROM posts JOIN users ON posts.author = users.id
WHERE posts.id = 10 OR posts.reply_to = 10
ORDER BY posts.created\G</pre>
</blockquote>
<p>Сега да пристъпим към същината на темата &#8211; fulltext търсене. Досега знаем как можем да търсим шаблони в текстови полета чрез използване на оператор LIKE. Например ако искаме да намерим идентификационните номера на всички съобщения, които съдържат думата &#8220;Лада&#8221; някъде в името на заглавието (ако е основно съобщение, защото отговорите нямат заглавия) или някъде в съдържанието си, то можем да направим следното:</p>
<blockquote>
<pre>SELECT id FROM posts
WHERE title LIKE '%Лада%' OR data LIKE '%Лада%';
+----+
| id |
+----+
|  1 |
|  5 |
+----+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Това е много лошо решение, защото:</p>
<ul>
<li>Търсенето по този начин е изключително бавно. Дори да съществуват индекси те никога няма да се използват. Проверете &#8211; направете индекс по колона &#8220;title&#8221; и направете EXPLAIN на заявката &#8211; ще видите, че този индекс няма да присъства дори в &#8220;possible keys&#8221;. Причината е, че нашия шаблон започва с &#8220;%&#8221;;</li>
<li>Няма никакви критерии за сравнение &#8220;кой от резултатите е по-точен&#8221;. Ако например намерените резултати са два идентификационни номера &#8220;X&#8221; и &#8220;Y&#8221;, то не можем да кажем, кое от двете попадения е по-подходящо за нас;</li>
<li>Следствие от недостатъка на предишната точка е, че при нарастването на базата от данни ще имаме все повече и повече резултати при стандартни търсения по обикновена дума и резултатните ни таблици ще стават все по-големи. При липсата на очевидна подредба потребителят ще се &#8220;загуби&#8221; в резултатите. Няма никаква гаранция, че това, което излиза първо ще е това, което е &#8220;най-точно&#8221; попадение.</li>
</ul>
<p>С други думи на този етап ние искаме по някакъв начин да разширим стандартното търсене в текста с нещо допълнително &#8211; критерий за сравнение между резултатите (за да можем да ги подреждаме преди да ги изпратим към потребителя) и наличието на индекси (с което търсенето да бъде бързо). Решението e:</p>
<p><strong>Създаване на Fulltext индекс</strong></p>
<p>За да създадете въпросния индекс трябва да изпълните следната команда:</p>
<blockquote>
<pre>ALTER TABLE posts ADD FULLTEXT(title, data);</pre>
</blockquote>
<p>Като колони може да подавате само CHAR, VARCHAR или *TEXT типове данни. В последствие вие може да търсите чрез командата &#8220;MATCH (&#8230;) AGAINST (&#8230;)&#8221; по колоните описани в индекса. Например ако искаме да направим нещо подобно на предложеното по-горе търсене, то ще напишем:</p>
<blockquote>
<pre>SELECT id FROM posts
WHERE MATCH(title, data) AGAINST ('Лада');
+----+
| id |
+----+
|  1 |
|  5 |
+----+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Тук обаче нещата са се получили по доста различен начин. Въпросният индекс всъщност се състои от думи, тяхната &#8220;тежест&#8221; и референции към редовете, в които се съдържат. Тежестта на всяка дума зависи от това &#8220;в колко от редовете е налична&#8221; (по-рядка дума ще има по-голяма тежест и обратно). Наличието на думи от търсената фраза в текста ще добавят различни &#8220;тегла&#8221;. Именно общото тегло можем да използваме, за да сортираме нашите заявки. Ето как можем да видим въпросните тегла:</p>
<blockquote>
<pre>SELECT id, MATCH(title, data) AGAINST('Лада') AS score
FROM posts
WHERE MATCH(title, data) AGAINST ('Лада')
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
|  1 | 1.0845428705215454 |
|  5 | 0.9728003144264221 |
+----+--------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Имайте в предвид, че в реално приложение няма нужда да слагате MATCH&#8230;AGAINST във връщаните колони и ORDER BY score в заявката. Ако те липсват, то MySQL автоматично ще сортира резултатната таблица по същия начин, както показания по-горе.</p>
<p>Колкото по-голяма е общата тежест, толкова по-добро е попадението. Наличието на MATCH&#8230;AGAINST&#8230; два пъти (един път в колоните на SELECT и един път в WHERE) НЕ добавя допълнително натоварване &#8211; MySQL ще направи търсенето само веднъж и ще го преизползва.</p>
<p>В MySQL 5.6 има три вида търсене. Ще разгледаме всеки един от тях:</p>
<p><strong>Търсене с естествен език (IN NATURAL LANGUAGE MODE)<br />
</strong></p>
<p>Това е търсенето по подразбиране. Ако напишете MATCH(колони) AGAINST (&#8220;text&#8221;), то все едно сте написали MATCH(колони) AGAINST (&#8220;text&#8221; IN NATURAL LANGUAGE MODE). Този метод за търсене е възможно най-елементарен &#8211; просто се добавя тежестта на всяко едно попадение към общата тежест за всеки ред. Отсъствието на дума НЕ намалява общата тежест. Нека го демонстрираме с още три примера свързани с предишния:</p>
<blockquote>
<pre>SELECT id, MATCH(title, data) AGAINST('мост') AS score
FROM posts
WHERE MATCH(title, data) AGAINST ('мост')
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
| 11 | 1.0986145734786987 |
| 10 | 0.9155634045600891 |
+----+--------------------+
2 rows in set (0.00 sec)

SELECT id, MATCH(title, data) AGAINST('мост Лада') AS score
FROM posts
WHERE MATCH(title, data) AGAINST ('мост Лада')
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
| 11 | 1.0986145734786987 |
|  1 | 1.0845428705215454 |
|  5 | 0.9728003144264221 |
| 10 | 0.9155634045600891 |
+----+--------------------+
4 rows in set (0.00 sec)

SELECT id, MATCH(title, data) AGAINST('мост Лада Нива') AS score
FROM posts
WHERE MATCH(title, data) AGAINST ('мост Лада Нива')
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
|  5 |   2.61989426612854 |
|  1 |  2.169085741043091 |
| 11 | 1.0986145734786987 |
| 10 | 0.9155634045600891 |
+----+--------------------+
4 rows in set (0.00 sec)</pre>
</blockquote>
<p>Какво се случи:</p>
<ul>
<li>Думата &#8220;Лада&#8221; е налична само в постове 1 и 5 (проверете);</li>
<li>Думата &#8220;мост&#8221; е налична само в постове 10 и 11 (проверете);</li>
<li>При търсене на &#8220;мост Лада&#8221; се взеха всички редове, които съдържат думите &#8220;мост&#8221; и &#8220;Лада&#8221; и всеки ред получи своята тежест спрямо наличието на думите в него;</li>
<li>При търсене на &#8220;мост Лада Нива&#8221; ние добавихме още една ключова дума &#8211; &#8220;Нива&#8221;. Тя очевидно НЕ е налична в постове 10 и 11, но е налична в 1 и 5. Тя съответно добави по-голяма тежест при тях.</li>
</ul>
<p>Все пак можем да забележим нещо важно &#8211; думите &#8220;Лада&#8221; и &#8220;Нива&#8221; са едновременно налични и в двете съобщения 1 и 5, но те получиха съвсем различни оценки. Това е така, понеже освен &#8220;тежестта на думата&#8221; и нейното &#8220;наличие или отсъствие&#8221; се пресмятат още допълнителни неща &#8211; колко пъти се срещат попаденията в реда и колко уникални думи има в реда.</p>
<p>Важно е да се отбележи, че не се търси по абсолютно всичко. Търсената фраза може да съдържа всякакви символи, но за &#8220;дума&#8221; ще се разчете само наличието на букви, цифри, долна черта или една(!) единична кавичка. Думите се разделят чрез разделители. Стандартните разделители са празен интервал, запетайка и точка. Наличието на &#8220;специален символ&#8221; (нито един от посочените дотук) също ще бъде счетен за разделител на думи. Тоест &#8220;мост$Лада&#8221; ще е същото като &#8220;мост Лада&#8221;.</p>
<p>Допълнително важат и следните правила:</p>
<ul>
<li>Всяка &#8220;прекалено кратка&#8221; дума ще бъде пропусната (не оказва влияние на резултата). При InnoDB по подразбиране това са двубуквените думи. Възможно е да променяте тази минимална дължина на дума чрез променливата innodb_ft_min_token_size. Това ограничение е важно, за да се премахнат попадения от предлози, съюзи и частици като &#8220;на&#8221;, &#8220;ще&#8221;, &#8220;до&#8221;, &#8220;и&#8221;, &#8220;по&#8221; и т.н. Имайте го в предвид, понеже може да доведе до неочаквани резултати. Ако например сте направили минималната дължина на дума от четири букви (както например е по подразбиране в MyISAM), то търсенето на думата &#8220;УАЗ&#8221; ще е невъзможно и няма да върне никакъв резултат (<em>бел. ред. сега разбирате и причината, поради която в много сайтове когато търсите нещо се изисква &#8220;да въведете поне X символа&#8221;</em>).;</li>
<li>Думи от т.нар. &#8220;стоп списък&#8221; (stopwords) ще бъдат пропускани. Ще разгледаме този списък по-надолу.</li>
<li>Стандартно (освен ако не сте избрали специфичен collation) не се прави разлика между малки и големи букви;</li>
<li>Добре е да отбележим и една важна разлика между Full Text Search в InnoDB и MyISAM. При MyISAM ако една дума присъства в повече от 50% от редовете, то тя ще бъде пропусната (този процент може да се коригира). При InnoDB на този етап не е така.</li>
</ul>
<p><strong>Разширено търсене с естествен език (WITH QUERY EXPANSION)</strong></p>
<p>Понякога търсените фрази съдържат рядко използвани ключови думи, които имат значение на синоними на други, които са с по-широк спектър. Нека например предположим, че един човек търси наличието на думата &#8220;21213&#8243; и по стандартния метод тя връща само едно единствено попадение. Да, но реално за човека думата &#8220;21213&#8243; всъщност означава &#8220;модел 21213 на Лада Нива&#8221;, т.е. той би очаквал да получи и статии свързани и с думите &#8220;Лада&#8221; и &#8220;Нива&#8221;, а не само с търсената от него &#8220;21213&#8243;. Това е реален проблем, защото СУБД няма как да има информация за тази допълнителна връзка между думите.</p>
<p>Именно тук се намесва &#8220;разширеното търсене&#8221;. При него практически търсенето се извършва на две стъпки:</p>
<ol>
<li>На първата стъпка се намират редовете с търсената дума. От тях се подбират други ключови думи, които MySQL определя като &#8220;значими&#8221; (с достатъчно голяма тежест, но не уникални, а общи за въпросните редове). Тези нови ключови думи се предполага, че са значими за редовете;</li>
<li>Прави се повторно търсене, но този път се добавят и подбраните нови ключови думи.</li>
</ol>
<p>Ето един пример:</p>
<blockquote>
<pre>SELECT id, MATCH(title, data) AGAINST('Уазка') AS score
FROM posts
WHERE MATCH(title, data) AGAINST ('Уазка')
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
| 10 | 1.4016318321228027 |
+----+--------------------+
1 row in set (0.00 sec)

SELECT id, MATCH(title, data) 
           AGAINST('Уазка' WITH QUERY EXPANSION) AS score
FROM posts
WHERE MATCH(title, data) 
      AGAINST ('Уазка' WITH QUERY EXPANSION)
ORDER BY score DESC;
+----+--------------------+
| id | score              |
+----+--------------------+
| 10 |   72.7892074584961 |
| 11 |  5.612302303314209 |
|  8 | 3.1514804363250732 |
|  3 |  2.191800594329834 |
|  6 |  1.733217716217041 |
|  2 | 0.8657563924789429 |
|  7 | 0.7457319498062134 |
+----+--------------------+
7 rows in set (0.02 sec)</pre>
</blockquote>
<p>Както се вижда ясно думата &#8220;Уазка&#8221; е налична само в една статия, но при разширеното търсене бяха отсети още няколко такива. Излезли са допълнителни статии, при това никак не лошо подредени по значимост. По този начин СУБД успя да свърже &#8220;Уазка&#8221; с &#8220;УАЗ&#8221; и някои други ключови думи.</p>
<p>От друга страна трябва да знаете, че разширеното търсене почти винаги вмъква много &#8220;шум&#8221; в резултатите, т.е. излизат резултати, които наистина нямат нищо общо с търсеното. По-често тяхната обща тежест е ниска (можете ги отрежете с LIMIT или с HAVING(score&gt;&#8230;)), но понякога може да получат и незаслужено висока оценка. Принципното правило за зависимостта между &#8220;шума&#8221; и &#8220;правилните резултати&#8221; е следното: колкото по-кратка е фразата, толкова по-малък шум се очаква и обратно &#8211; при дълги фрази очаквайте по-голям шум.</p>
<p><strong>Булево търсене (IN BOOLEAN MODE)</strong></p>
<p>Булевото търсене вече ни дава част от възможностите на истинска &#8220;търсачка&#8221; така, както я познаваме от Google, Yahoo, Altavista и подобни. При него важат същите правила, както при нормалното търсене с естествен език, но има допълнителни оператори, които ни помагат значително да прецизираме. Те са следните:</p>
<ul>
<li>Без оператор: Думата може да присъства (ще придаде тежест), но може и да не присъства. Абсолютно аналогично е като резултат при нормалното търсене &#8211; между търсените думи във фразата се слага логическо &#8220;или&#8221;;</li>
<li>Оператор &#8220;+&#8221;: Поставяйки &#8220;+&#8221; пред дума в търсената фраза ние указваме, че думата е задължителна. Например ако търсим за &#8220;Лада Нива&#8221;, то стандартно бихме могли да получим попадения съдържащи само &#8220;Лада&#8221; и съдържащи само &#8220;Нива&#8221;. Ако търсим за &#8220;+Лада +Нива&#8221;, то ние указваме, че е напълно задължително и двете думи да присъстват в резултатните редове;</li>
<li>Оператор &#8220;-&#8221;: Действието му е точно обратното на &#8220;+&#8221; &#8211; този оператор указва, че указаната дума е задължително да НЕ присъства в резултатните редове. Например &#8220;+Лада -Калина&#8221; ще върне всички редове с дума &#8220;Лада&#8221;, но от тях ще пропусне тези, в които присъства дума &#8220;Калина&#8221;;</li>
<li>Оператор &#8220;~&#8221;: С него се прави &#8220;обратна тежест&#8221;, т.е. думата след него ще намали резултатите, вместо да ги увеличи. Един вид това е по-слабо действие спрямо оператор &#8220;-&#8221; &#8211; не желаем напълно за изключваме резултатите, но все пак ги отчитаме като по-лоши;</li>
<li>Оператор &#8220;@&lt;разстояние&gt;&#8221;: Указва минимално разстояние (в байтове), което трябва да е между думите. Например ако търсим за &#8216; &#8220;Лада Нива&#8221; @20&#8242;, то системата ще даде тежест на думите &#8220;Лада&#8221; и &#8220;Нива&#8221;, но само тогава, когато те са на разстояние повече от 20 байта една от друга. Задължително е да ограждате ключовите думи (може да са повече от две) с двойни кавички и да слагате оператора след тях. Този оператор е удобен тогава, когато вие желаете да взимате резултати по търсените думи, но искате да избегнете конкретна често срещана комбинация от тях. Този оператор е наличен само за InnoDB;</li>
<li>Ограждащи скоби &#8220;(&#8221; и &#8220;)&#8221;: Групират думи в &#8220;подизрази&#8221;. Използва се в комбинация с друг оператор, за да се даде общо значение за две фрази. Например &#8220;+(Лада Нива)&#8221; ще изиска и двете думи да са налични (все едно сме сложили + и пред двете);</li>
<li>Оператори &#8220;&gt;&#8221; и &#8220;&lt;&#8221;: Увеличава или намалява тежестта на думата. Например търсенето на &#8220;+Двигател +(&gt;бензинов &lt;дизелов)&#8221; ще намери резултати едновременно за бензиновите и за дизеловите двигатели, но тези, които са бензинови ще получат по-голяма тежест;</li>
<li>Оператор &#8220;*&#8221;: Има действието на &#8220;%&#8221; при търсене с LIKE, с тази разлика, че може да се използва само в края на думата. Тоест този оператор указва, че търсената дума &#8220;започва с&#8230;&#8221;. Например ако търсите &#8220;УАЗ*&#8221;, то ще получите и резултати &#8220;УАЗ&#8221;, &#8220;УАЗа&#8221; и &#8220;УАЗка&#8221; (и др. думи започващи с &#8220;уаз&#8221;, ако има такива). Тук &#8220;прекалено късите&#8221; думи не се пропускат!;</li>
<li>Точна фраза: Когато оградите фраза с двойни кавички, то ще търсите &#8220;точно съвпадение&#8221;. Изключение се прави само ако е използван оператора за разстояние (описан по-горе).</li>
</ul>
<p><span style="text-decoration: underline;"><em>ВАЖНО: При използване на режим &#8220;IN BOOLEAN MODE&#8221; НЕ се извършва автоматично сортиране!!!</em></span></p>
<p>Примери:</p>
<p>1. Търсим статиите с точна фраза &#8220;Лада Нива&#8221;, но без тези, които включват думата &#8220;метан&#8221;:</p>
<blockquote>
<pre>SELECT id, MATCH(title, data)
           AGAINST('"Лада Нива" -метан' IN BOOLEAN MODE) AS score
FROM posts
WHERE MATCH(title, data)
      AGAINST('"Лада Нива" -метан'  IN BOOLEAN MODE)
ORDER BY score DESC;</pre>
</blockquote>
<p>2. Търсим статиите, в които се коментира едновременно УАЗ и ГАЗ (и техните вариации като УАЗ469, ГАЗ66 и т.н.):</p>
<blockquote>
<pre>SELECT id, MATCH(title, data)
           AGAINST('+(УАЗ* ГАЗ*)' IN BOOLEAN MODE) AS score
FROM posts
WHERE MATCH(title, data)
      AGAINST('+(УАЗ* ГАЗ*)' IN BOOLEAN MODE)
ORDER BY score DESC;</pre>
</blockquote>
<p><strong>Стоп думи (STOPWORDS)</strong></p>
<p>Както беше споменато в началото, &#8220;стоп думите&#8221; са такива, които се срещат често в езика и би следвало да се пропускат като незначителни. MySQL има малък набор от такива думи на английски език. Те се намират в база от данни &#8220;information_schema&#8221; в таблица &#8220;innodb_ft_default_stopword&#8221;. Ще видите, че таблицата има единствена колона с име &#8220;value&#8221; и е от тип &#8220;varchar(30)&#8221;.</p>
<p>Ако желаете вие сами да си съставите списък със &#8220;стоп думи&#8221;, то е необходимо да създадете нова таблица със същата структура, както innodb_ft_default_stopword, т.е.:</p>
<blockquote>
<pre>CREATE TABLE yourdb.stopwords(
  value VARCHAR(30) PRIMARY KEY
)ENGINE = InnoDB;</pre>
</blockquote>
<p>След което е нужно да пренасочите глобалната променлива innodb_ft_server_stopword_table да сочи към нея:</p>
<blockquote>
<pre>SET innodb_ft_server_stopword_table = "yourdb/stopwords";</pre>
</blockquote>
<p>Разбира се може да укажете да се зарежда винаги при стартиране на сървъра чрез my.cnf.</p>
<p><strong>Ограничения</strong></p>
<p>На този етап (към версия 5.6.5) има редица ограничения към използването на Fulltext търсене. Ето трите най-съществени от тях:</p>
<ul>
<li>Не се поддъжа при partitioning;</li>
<li>В InnoDB може да правите не повече от един Full-Text индекс на таблица;</li>
<li>Ако правите индекс по повече от една колона (както е в примера по-горе), то всички колони трябва да са от един и същи тип и collation;</li>
<li>В Against може да се подава само константен низ (т.е. не може да се подават стойности от променливи).</li>
</ul>
<p><strong>Зад сцената</strong></p>
<p>За разлика от MyISAM, където индекса се поддържа като специално B+ дърво, в InnoDB се използват помощни релационни таблици. Това разбира се има своите предимства и недостатъци. Предимствата са по-голяма скалируемост и евентуална паралелизация, от което и лесно поддържане на конкурентни транзакции (при MyISAM транзакции няма, с което и B+ дървото излиза като по-добрия избор). В случая обаче е важно да разберем по какъв начин се обновява индекса при различните видове операции за промяна на данните:</p>
<ul>
<li>Промяна на индекса само при достигане COMMIT: Индекса се променя винаги само и единствено при <span style="text-decoration: underline;">завършване</span> на транзакция. Междинните промени вътре в транзакция не се отчитат. Ако имате транзакция, в която вмъквате/променяте/изтривате данни, то вие няма да видите промени във Full-Text индекса дори вътре в самата транкация. Ще видите промените чак след като я завършите с команда COMMIT;</li>
<li>INSERT заявки: При завършване на insert заявка промените във Full-Text индекса се записват в специален кеш, който по подразбиране е 32MB и се контролира от променливата innodb_ft_cache_size. Този кеш се прехвърля в &#8220;индексните таблици&#8221; на дисковото устройство тогава, когато се запълни (или когато сървъра получи команда за изключване). Ако сървъра бъде спрян по неестествен начин, то при следващото включване се извършва синхронизация на кеша с физически записаните данни;</li>
<li>UPDATE заявки: Обновяването на индексните таблици се извършва директно върху индексните таблици при достигането на COMMIT на транзакцията. Това е &#8220;скъпа операция&#8221;, защото често налага обновяване на множество редове от тях;</li>
<li>DELЕTE заявки: Операциите за изтриване са доста бързи, понеже реално не се изтриват редове от индексните таблици. Вместо това се записва уникалния номер (FTS_DOC_ID &#8211; виж следващия раздел) в специална помощна таблица за &#8220;изтрити редове&#8221;. В последствие, когато се прави търсене, MySQL ще се обърне към тази таблица с изтрити редове, за да ги филтрира от резултатната таблица. Така DELЕTE заявките стават прости и бързи, понеже въобще не променят индекса, но от друга страна при продължително изпълнение на такива се получава постоянно намаляване на бързодействието на MATCH&#8230; AGAINST заявките, както и заемане на все по-голямо и по-голямо количество информация (индексите продължително растат). Решението на този проблем е да изтриете индекса и да го създадете наново. Това може да отнеме значително време при големи таблици. Алтернативно може да дадете стойност &#8220;1&#8243; на глобалната променлива innodb_optimize_fulltext_only, след което да извършите команда OPTIMIZE TABLE&#8230; на въпросната таблица. Така ще се изпълнят до &#8220;innodb_ft_num_word_optimize&#8221; на брой (по подразбиране 2000) промени по индекса.</li>
</ul>
<p><strong>Допълнителни уточнения<br />
</strong></p>
<p>Поддържането на Full-Text индекс е &#8220;скъпа&#8221; операция, която отнема доста ресурси. Забележете, че вмъкването или промяната на информация в таблица води до мащабно преструктуриране и обновяване на целия индекс. Поради тази причина, особено при големи обеми от данни, може да се окаже така, че при добавянето или променянето на няколко реда в таблица ще е по-добре да премахнете индекса, да добавите данните и след това да създадете индекса отново. Това често се използва от големите системи &#8211; при тях програмните продукти правят промените в таблиците на т.нар. &#8220;batches&#8221;, при които Full-Text индексите се пресъздават на всеки &#8220;batch&#8221;.</p>
<p>Full-Text индексите използват колона в таблицата с име &#8220;FTS_DOC_ID&#8221; (винаги с главни букви) от тип BIGINT UNSIGNED с AUTO_INCREMENT (<a title="innodb-fts-performance" href="http://blogs.innodb.com/wp/2011/07/innodb-fts-performance/" target="_blank">източник</a>). В бъдеще се предвижда да може да указвате и друга колона с произволно име, която да замести FTS_DOC_ID. Ако такава колона няма, то ще бъде създадена и по нея ще бъде създаден индекс (но създадената автоматично колона ще бъде &#8220;скрита&#8221; за вас, т.е. няма да я виждате). В редица случаи може да сметнете за подходящо вие сами да си създадете такава колона, особено тогава, когато вашата таблица има нужда от първичен ключ с &#8220;id&#8221;-та &#8211; тогава ще можете да споделите тази колона както за ваш първичен ключ, така и за Full-Text индекса. Освен това се използва и уникален индекс с име FTS_DOC_ID_INDEX, които също може да създадете. Или казано по друг начин &#8211; ако използвате Full-Text индекси, то е добре вашата таблица да има следната структура:</p>
<blockquote>
<pre>CREATE TABLE tbl(
FTS_DOC_ID BIGINT unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (FTS_DOC_ID),
...
) ENGINE=InnoDB;

CREATE UNIQUE INDEX FTS_DOC_ID_INDEX ON tbl(FTS_DOC_ID);</pre>
</blockquote>
<p>Не е задължително FTS_DOC_ID да е PRIMARY KEY &#8211; може спокойно да използвате друг ключ. В този случай FTS_DOC_ID трябва допълнително да го ограничите с NOT NULL и UNIQUE (за да предотвратите вмъкването на невалидни данни в последствие).</p>
<p>Друга оптимизация може да се получи при използване на InnoDB с Full-Text индекс на многопроцесорни (многоядрени) компютри (забележете, че това е нововъведение и MyISAM не се възползва от многопроцесорните системи при FT индекси). Броят на нишките, които работят паралелно с Full-Text индекса се контролира от променливата InnoDB_ft_sort_pll_degree (по подразбиране със стойност 2, т.е. 2 нишки). Възможните стойности могат да са степени на двойката и е препоръчително да изберете по-голям или равен на броя на процесорите (ядрата).</p>
<p>Използването на оператор &#8220;@&lt;разстояние&gt;&#8221; в булевото търсене на този етап прави заявките за търсене значително по-бавни. При оператор &#8220;*&#8221; също се наблюдава забавяне спрямо другите оператори, но определено е на приемливо ниво.</p>
<p>Естествено може да създавате Full-Text индекс и при самото създаване на таблицата (т.е. без да използвате ALTER TABLE, както беше в примера по-горе). Това става чрез:</p>
<blockquote>
<pre>CREATE TABLE tbl(
...
FULLTEXT(&lt;колона/и&gt;),
...
) ENGINE=InnoDB;</pre>
</blockquote>
<p>Или алтернативно на ALTER TABLE може да добавите индекса чрез специална заявка:</p>
<blockquote>
<pre>CREATE FULLTEXT INDEX idx 
ON &lt;таблица&gt;(&lt;колона/и&gt;);</pre>
</blockquote>
<p><strong>Задача</strong>: В примерната база от данни направете така, че отговорите на статии да приемат автоматично заглавието на главната статия, на която те отговарят.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8114-full-text-search-in-mysql/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Късметче от кафе</title>
		<link>http://www.cphpvb.net/lada/8121-fortune-slip/</link>
		<comments>http://www.cphpvb.net/lada/8121-fortune-slip/#comments</comments>
		<pubDate>Wed, 16 May 2012 09:26:41 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Лада Нива]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8121</guid>
		<description><![CDATA[Когато си поръчате кафе в заведение обикновено ви дават разни късметчета. Аз не пия (кафе!), но все пак в една сладкарница успях да се сдобия с първото си листче с късмет. Сега кажете, че не е тематично: Точно като за шофьор на Лада Нива!]]></description>
			<content:encoded><![CDATA[<p>Когато си поръчате кафе в заведение обикновено ви дават разни късметчета. Аз не пия (кафе!), но все пак в една сладкарница успях да се сдобия с първото си листче с късмет. Сега кажете, че не е тематично:<span id="more-8121"></span></p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/kasmetche.jpg"><img class="aligncenter size-medium wp-image-8122" title="Късметче от кафе" src="http://www.cphpvb.net/wp-content/uploads/2012/05/kasmetche-300x225.jpg" alt="Късметче от кафе" width="300" height="225" /></a></p>
<p>Точно като за шофьор на Лада Нива!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/lada/8121-fortune-slip/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Пред блока&#8230;</title>
		<link>http://www.cphpvb.net/other/8117-pred-bloka/</link>
		<comments>http://www.cphpvb.net/other/8117-pred-bloka/#comments</comments>
		<pubDate>Wed, 16 May 2012 09:21:24 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Общи работи]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8117</guid>
		<description><![CDATA[Докато ремонтираме джиповете&#8230; &#8230; си правим удобства&#8230; &#8230; но все още ни липсва електрически ток, за да гледаме телевизия&#8230; Жалко, че от общината минаха и ни прибраха удобствата с камион за боклук&#8230;]]></description>
			<content:encoded><![CDATA[<p>Докато ремонтираме джиповете&#8230;<br />
<span id="more-8117"></span><br />
<a href="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka.jpg"><img class="aligncenter size-medium wp-image-8118" title="Гиги пред УАЗ-а на Стефан" src="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka-300x225.jpg" alt="Гиги пред УАЗ-а на Стефан" width="300" height="225" /></a></p>
<p>&#8230; си правим удобства&#8230;</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka-1.jpg"><img class="aligncenter size-medium wp-image-8119" title="Гиги на дивана" src="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka-1-300x225.jpg" alt="Гиги на дивана" width="300" height="225" /></a></p>
<p>&#8230; но все още ни липсва електрически ток, за да гледаме телевизия&#8230;</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka-2.jpg"><img class="aligncenter size-medium wp-image-8120" title="Гиги и Митко на дивана пред блока" src="http://www.cphpvb.net/wp-content/uploads/2012/05/pred-bloka-2-300x225.jpg" alt="Гиги и Митко на дивана пред блока" width="300" height="225" /></a></p>
<p>Жалко, че от общината минаха и ни прибраха удобствата с камион за боклук&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/other/8117-pred-bloka/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Бушонно табло на Лада Нива</title>
		<link>http://www.cphpvb.net/lada/8109-fuses-vaz-21213-lada-niva/</link>
		<comments>http://www.cphpvb.net/lada/8109-fuses-vaz-21213-lada-niva/#comments</comments>
		<pubDate>Tue, 08 May 2012 15:16:20 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Лада Нива]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8109</guid>
		<description><![CDATA[Предлагам схема на бушонното табло за Лада Нива (ВАЗ 21213). При по-новия модел 21214 има още четири допълнителни бушона в отделен блок. Стандартните би трябвало да имат пълно съответствие, но все пак ако сте с инжекционна Нива първо проверете в официалното ръководство. В Лада Нива има два основни блока с предпазители (бушони) &#8211; голям и [...]]]></description>
			<content:encoded><![CDATA[<p>Предлагам схема на бушонното табло за Лада Нива (ВАЗ 21213). При по-новия модел 21214 има още четири допълнителни бушона в отделен блок. Стандартните би трябвало да имат пълно съответствие, но все пак ако сте с инжекционна Нива първо проверете в официалното ръководство. <span id="more-8109"></span></p>
<p>В Лада Нива има два основни блока с предпазители (бушони) &#8211; голям и малък. И двата се намират отпред в ляво спрямо шофьора, точно зад лоста за отваряне на предния капак. Бушонните табла са с пластмасов предпазен капак, който се сваля целия и на който са обозначени номерата на бушоните. Големият блок с предпазители има 10 бушона, а малкия 6 (т.е. общо са 16). Използват се &#8220;пръчковидни&#8221; бушони, а не ножични. При ВАЗ 21214 има още един блок с четири бушона вляво при компютъра. Ето какво предпазват съответните бушони според официалното ръководство на <strong>ВАЗ 21213</strong>:</p>
<ol>
<li>16A
<ul>
<li>Електродвигател на вентилатора за отоплението;</li>
<li>Реле на моторчето за миене на предните фарове;</li>
<li>Електродвигател на моторчето за миене на предните фарове (само в процес на миене, но не при стартиране и извеждане в изходна позиция след спиране!);</li>
<li>Ключ (реле) за включване на отоплението на задното стъкло;</li>
<li>Електродвигател на моторчето за задната чистачка;</li>
<li>Електродвигател на казанчето за пръскалката на задното стъкло;</li>
<li>Електродвигател на казанчето за пръскалките на предното стъкло.</li>
</ul>
</li>
<li>8A
<ul>
<li>Моторче за предните чистачки (реле и електродвигател);</li>
<li>Мигачи, бутон за аварийни светлини и лампи по таблото свързани с мигачите;</li>
<li>Фарове за заден ход;</li>
<li>Генератор;</li>
<li>Контролна лампа за зареждане на акумулатора;</li>
<li>Контролна лампа за блокиран междуосев диференциал;</li>
<li>Контролна лампа за ръчна спирачка;</li>
<li>Контролна лампа за ниво на спирачната течност;</li>
<li>Контролна лампа за маслото;</li>
<li>Указател за температурата на охлаждащата течност;</li>
<li>Указател за нивото на горивото в резервоара;</li>
<li>Тахометър (оборотомер).</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден ляв фар на дълги светлини;</li>
<li>Контролна лампа за включени дълги светлини (включва се само ако е включена контролната лампа за габаритни светлини).</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден десен фар на дълги светлини.</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден ляв фар на къси светлини.</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден десен фар на къси светлини.</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден ляв фар за габаритни светлини;</li>
<li>Заден десен фар за габаритни светлини;</li>
<li>Крушки за осветление на задния номер;</li>
<li>Контролна лампа за включени габаритни светлини.</li>
</ul>
</li>
<li>8A
<ul>
<li>Преден десен фар за габаритни светлини;</li>
<li>Заден ляв фар за габаритни светлини;</li>
<li>Лампи за осветление на таблото;</li>
<li>Лампа на ключа за управление на вентилатора на парното;</li>
<li>Лампа за осветяване на запалката.</li>
</ul>
</li>
<li>16A
<ul>
<li>Нагревател за задното стъкло;</li>
</ul>
</li>
<li>16A
<ul>
<li>Клаксон;</li>
<li>Цокъл за лампа под предния капак;</li>
<li>Плафони за осветление на купето;</li>
<li>Задни стопове;</li>
<li>Фарове за мъгла.</li>
</ul>
</li>
<li>Не се използва.</li>
<li>Не се използва.</li>
<li>8A
<ul>
<li>Електродвигател на моторчето за миене на предните фарове (в момент на включване и в процес на прибиране на чистачките в начална позиция при спиране);</li>
<li>Ключ (реле) за чистачките на предното стъкло;</li>
<li>Електродвигател за пръскалките на фаровете &#8211; доколкото знам двигателя е един общ на казанчето за чистачки, но пък аз нямам чистачки на фаровете тъй или иначе така, че може да бъркам. Възможно е да има различни по вид казанчета и при някои да има два електромотора.</li>
</ul>
</li>
<li>Не се използва.</li>
<li>16A
<ul>
<li>Запалка.</li>
</ul>
</li>
<li>Не се използва.</li>
</ol>
<p>При някои по-стари модификации (зависи от годината на производство) запалката може да се намира на 10 бушон. При други (още по-стари) е възможно запалката да е на 13 бушон, като при тях 13-ти бушон е 16А, а 10-ти 8А (информация от <a title="niva-faq.msk.ru" href="http://www.niva-faq.msk.ru" target="_blank">niva-faq.msk.ru</a>).</p>
<p>При <strong>ВАЗ 21214</strong>, както вече споменах по-горе, има четири допълнителни бушона. Първият и третия (по 15А) контролират компютъра и инжекциона, а четвъртия (отново 15А) контролира релетата на вентилаторите за охлаждане, датчиците обслужващи инжекциона и датчиците по охладителната система. Вторият е 30А и нямам представа какво обслужва, най-вероятно може би не се използва. Доколкото знам електродвигателите на вентилаторите за охлаждане си имат самостоятелни бушони някъде в моторния кош. Може и да бъркам.</p>
<p>Разбира се ако имате газова уредба, аларма, централно заключване и подобни екстри, то &#8220;народното творчество&#8221; на електричарите/газаджиите най-вероятно е влязло в действие. Те рядко имат практика да ви оставят листче със списък на направените промени. Така например е и при мен, като освен газова уредба и аларма имам и допълнителни перки за охлаждане (които не са вързани към допълнителното бушонно табло на 21214, защото при мен такова няма). Съвсем стандартно можете да намерите и разни бушонни гнезда из моторния кош. В такива случаи няма какво друго да направите освен да се ориентирате на място&#8230;</p>
<p><strong>Възможни проблеми</strong>: Противно на очакванията най-честия проблем не е свързан с изгарянето на бушон, ами с липсата на контакт с бушона в таблото. Досега това ми се е случвало само с предните фарове, като по-точно с късите светлини. Наивна грешка на всеки начинаещ шофьор е, че ако единия фар спре да работи, то трябва да се смени крушката. Крушките на фаровете на Лада Нива почти никога не горят. До тях по принцип достига по-слаб ток от максималния допустим, което пък ги кара да светят малко по-слабо отколкото на другите коли, но за сметка на това, както споменах, много рядко изгарят.</p>
<p>Решението на споменатия проблем е тривиално &#8211; вадите бушона и го слагате обратно в гнездото му. Ако не сте взели необходимите предпазни мерки (виж &#8220;ВАЖНО&#8221; по-долу), то в този момент бушона може да изгори (с искри, които може да ви уплашат).</p>
<p>Гнездото на бушона впрочем е по-дълбоко, отколкото изглежда, че е. То си има гнезда, където бушона да &#8220;хлътне&#8221; (&#8220;щракне&#8221; или както искате го наречете). Иначе бушона може спокойно да бъде сложен (при това да дава контакт) и без да е вмъкнат правилно в гнездото. Тогава обаче ще има доста по-малка контактна повърхност и това е един от честите случаи, при които в последствие спира да дава контакт. Проверявайте си бушоните всеки път, след като някой &#8220;майстор&#8221; е поправял колата ви в сервиз. Съвсем възможно е да не са сложени както трябва.</p>
<p><strong>ВАЖНО</strong>: Преди да сменяте бушони или да правите каквито и да е промени по бушонното табло, винаги откачайте клемите на акумулатора (или използвайте ключ-маса ако имате такава)! Използването на монета от пет стотинки вместо бушон е възможно за аварийни ситуации (ако нямате резервен бушон, но тогава защо нямате?), но е силно НЕпрепоръчително, защото може да доведе до сериозен пожар! Затова НЕ използвайте монети вместо бушони &#8211; по-добре вземете бушон от други прибори (или от неизползваните гнезда). И да &#8211; по-добре винаги носете комплект резервни бушони.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/lada/8109-fuses-vaz-21213-lada-niva/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Уязвимост в PHP CGI версии </title>
		<link>http://www.cphpvb.net/network-security/8101-vulnerability-in-php-cgi-verions-5-4-2-and-earlier/</link>
		<comments>http://www.cphpvb.net/network-security/8101-vulnerability-in-php-cgi-verions-5-4-2-and-earlier/#comments</comments>
		<pubDate>Fri, 04 May 2012 18:48:10 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[ПТСК]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8101</guid>
		<description><![CDATA[На 3 май 2012г изтече информация за ужасяваща уязвимост, която е налична във всички версии на PHP от последните 8 години. Важи за всички версии на PHP, включително най-новата към момента 5.4.2, за която разработчиците твърдят, че оправя проблема (а не го). В тази статия ще се опитам да опиша кратка хронология и актуална информация. [...]]]></description>
			<content:encoded><![CDATA[<p>На 3 май 2012г изтече информация за ужасяваща уязвимост, която е налична във всички версии на PHP от последните 8 години. Важи за всички версии на PHP, включително най-новата към момента 5.4.2, за която разработчиците твърдят, че оправя проблема (а не го). В тази статия ще се опитам да опиша кратка хронология и актуална информация.<span id="more-8101"></span></p>
<p>Първо да успокоим (почти) всички потребители, че уязвимостта засяга само тези инсталации, които използват PHP като чист CGI или mod_cgid модул. Ако използвате PHP като Apache модул (най-често срещаното), FastCGI или suPHP, то уязвимостта не ви засяга.</p>
<p>В какво се състои проблемът? Както пише в <a title="PHP vulnerability" href="http://www.php.net/archive/2012.php#id2012-05-03-1" target="_blank">новината на сайта php.net</a>, при &#8220;някои CGI инсталации на PHP&#8221; може да подавате командни параметри директно към php приложението ако в query string не присъства знакът &#8220;=&#8221;. Това всъщност е описано в <a title="CGI RFC" href="http://www.ietf.org/rfc/rfc3875" target="_blank">RFC на CGI</a>, но очевидно през 2004г. не е отчетено от разработчиците на PHP (докато например при Apache са го отбелязали много коректно) и така нещата продължават и до днес.</p>
<p>Един пример за възползване от уязвимостта е чрез отваряне на URL подобен на &#8220;http://www.yourwebsite.dom/phppage.php?-s&#8221; би се извикала команда &#8220;/&lt;път до php&gt;/php-cgi -s /&lt;път до файла&gt;/phppage.php&#8221;, което от своя страна <span style="text-decoration: underline;">ще отпечата сорс кода на файла</span> директно в браузъра! Разбира се това е най-грубия възможен пример и почти най-опасния. За да видите с какви параметри можете да си &#8220;играете&#8221; пуснете &#8220;php -h&#8221; &#8211; повечето от тях може да се използват. Тази уязвимост най-общо позволява правенето на следните &#8220;поразии&#8221;:</p>
<ul>
<li>Чрез директно отпечатване на сорс кода на PHP файловете атакуващия може да открие парола за базата от данни, пътища до важни файлове, SALT кодове, да търси други налични пропуски в сигурността и т.н.;</li>
<li>Ако атакуващият има възможност да качи php файл на сървъра, то може да го изпълни, а от там да получи достъп до много пикантни възможности &#8211; запис на файлове с phishing сайтове, качване на вирус в оригиналния сайт, подмяна на файлове за download, &#8220;хакване&#8221; на оригиналния сайт, изпращане на спам и т.н.;</li>
<li>Да се направи напълно ефективна self-DoS атака (сървъра атакува самия себе си).</li>
</ul>
<p>Предполагам, че има и по-засукани възможности, но тези са първите, които ми дойдоха на ум. Честно казано аз лично не съм виждал по-тежка и едновременно с това лесна за използване уязвимост, като изключим ерата на &#8220;ранния IIS&#8221;, където имаше няколко подобни истории.</p>
<p>Сега малко хронология:</p>
<p>На 13.01.2012г. хора от холандския сайт <a title="eindbazen.net" href="http://eindbazen.net/2012/05/php-cgi-advisory-cve-2012-1823/" target="_blank">eindbazen.net</a> (последвайте връзката, за да прочетете статията от първоизточника) откриват уязвимостта по време на конференция свързана с информационната сигурност.</p>
<p>На 17.01.2012г. написват e-mail до security@php.net с пълна информация относно проблема. По всяка вероятност никой не обръща внимание.</p>
<p>На 01.02.2012г. хората отново питат екипа на PHP по e-mail с въпрос &#8220;какво става&#8221;. Тогава получават отговор, че техния доклад е препратен &#8220;до когото трябва&#8221;. Вероятно точно тогава наистина са му обърнали (някакво) внимание.</p>
<p>На 23.02.2012г. разработчитите се свързват обратно и потвърждават наличието на уязвимостта.</p>
<p>&#8230; zzz &#8230;</p>
<p>На 05.04.2012г. хората от блога отново питат екипа на PHP &#8220;какво става бе хора?&#8221;. Получават отговор, че &#8220;ние от PHP работим по въпроса&#8221;.</p>
<p>На 20.04.2012г. хората от блога пак настояват за нещо повече по въпроса, като този път са &#8220;малко по-настоятелни&#8221;. Шест дни по-късно получават информация, че е подготвен &#8220;draft advisory&#8221;.</p>
<p>На 02.05.2012г. екипа на PHP им пише, че &#8220;в момента изпробват кръпка (patch) и искат малко повече време, преди да я пуснат в публичното пространство&#8221;.</p>
<p>На 03.05.2012г. някой в екипа на PHP прави фатална грешка -<strong> без да иска маркира въпросния bug report като &#8220;публичен&#8221;</strong>, т.е. видим от всички, при това без да е дадено дори временно решение. В китайски, руски и всякакви други форуми моментално се реагира и информацията изтича в публичното пространство. Веднага &#8220;плъзват&#8221; ботове, които правят опити за намиране на уязвими сайтове. Колко от намерените такива са хакнати остава отворен въпрос.</p>
<p>Още същия ден екипа на PHP реагира и изважда версии 5.3.12 и 5.4.2, които включват въпросния &#8220;patch, който все още се тества&#8221;, но промотиран като &#8220;работещ&#8221;. Оказва се, че тази кръпка никак НЕ работи, т.е. не работи както се очаква. Сайтовете работещи на PHP-CGI и mod_cgid все още продължават да са уязвими при определени условия (кръпката не отчита възможността за вмъкване на whitespace преди query string). Към момента (05.05.2012г.) единственото привидно правилно работещо решение е налично на <a title="eindbazen.net" href="http://eindbazen.net/2012/05/php-cgi-advisory-cve-2012-1823/" target="_blank">eindbazen.net</a>.</p>
<p>Използването на PHP като CGI модул (всеки скрипт се изпълнява като отделен процес) беше много популярно в миналото. Смяташе се, че това е по-сигурния начин да се изпълняват скриптовете (всъщност и до сега си има много адекватни аргументи, че това наистина е така въпреки, че с казаното по-горе нещата изглеждат трагично различни). С течение на времето все повече хостинг провайдъри започнаха да пускат инсталациите си като Apache модул, но все пак достатъчно много стари сървъри останаха на CGI режим. И достатъчно много нови. За щастие рядко във варианти като изказаните два &#8211; по-често се използва suPHP или FastCGI.</p>
<p>Ако все пак имате сайт на споделен хостинг сървър, който използва подобна уязвима инсталация, то едва ли имате достъп да слагате подобни &#8220;third party&#8221; кръпки. Разбира се, че администраторите биха решили проблема (ако са достатъчно отговорни) временно, докато излезе истински работещ официален patch. Ако обаче администраторите са безотговорни (а такива има много &#8211; университетски сървъри, които са зарязани някъде незнайно в коя зала; сървър в министерство, зарязан също незнайно къде и все такива подобни примери), то се намирате пред реално изпитание. Най-бързото работещо решение на проблема е следното &#8211; създайте .htaccess файл, който филтрира заявките с mod_rewrite по следния начин (взех го от коментарите в блога на eindbazen):</p>
<blockquote>
<pre>RewriteEngine on
RewriteCond %{QUERY_STRING} ^[^=]*$
RewriteCond %{QUERY_STRING} %2d|\- [NC]
RewriteRule .? - [F,L]</pre>
</blockquote>
<p>Обобщение: Е не очаквах точно от екипа на PHP подобни безобразия. Не толкова, че се е получил подобен &#8220;бъг&#8221;, а по-скоро за отношението към него.</p>
<p><em><strong>Редакция 08.05.2011г.</strong> &#8211; от екипа на PHP извадиха нова версия 5.4.3, в която уязвимостта е премахната коректно.</em></p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/network-security/8101-vulnerability-in-php-cgi-verions-5-4-2-and-earlier/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Произволен ред от таблица</title>
		<link>http://www.cphpvb.net/db/8080-random-row-from-a-table/</link>
		<comments>http://www.cphpvb.net/db/8080-random-row-from-a-table/#comments</comments>
		<pubDate>Fri, 04 May 2012 13:53:21 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8080</guid>
		<description><![CDATA[В тази статия ще разгледам няколко различни алгоритми за прочитане на псевдопроизволен ред от таблица в MySQL, които намерих в интернет. Фокусирах се само и единствено върху чисто SQL методи. Съществуват и други, които комбинират генериране на произволно число в приложението и изпращане на готови стойности към базата от данни. База от данни За тестова [...]]]></description>
			<content:encoded><![CDATA[<p>В тази статия ще разгледам няколко различни алгоритми за прочитане на псевдопроизволен ред от таблица в MySQL, които намерих в интернет. Фокусирах се само и единствено върху чисто SQL методи. Съществуват и други, които комбинират генериране на произволно число в приложението и изпращане на готови стойности към базата от данни.<span id="more-8080"></span></p>
<p><strong>База от данни<br />
</strong></p>
<p>За тестова таблица ще използваме &#8220;city&#8221; от базата от данни &#8220;world&#8221; (примерна тестова база от данни в сайта на MySQL). В нея първичен ключ е колона &#8220;id&#8221;, в който стойностите са поредни (AUTO_INCREMENT) и <span style="text-decoration: underline;">БЕЗ ДУПКИ</span> (няма изтрити стойности). Последното е важно, защото повечето от методите разчитат именно на това свойство, за да се възползват от допълнителна бързина. Целта ни е <span style="text-decoration: underline;">с една заявка</span> да извлечем <span style="text-decoration: underline;">едно</span> произволно име (колона name) от базата от данни. Някои от методите позволяват и повече.</p>
<p><strong>Test 1: Класически метод</strong></p>
<p>Този метод се споменава в <a title="MySQL документация" href="http://dev.mysql.com/doc/refman/5.5/en/mathematical-functions.html#function_rand" target="_blank">документацията на MySQL</a>. В него се прави следното:</p>
<blockquote>
<pre>SELECT name FROM city 
ORDER BY RAND() LIMIT 1;</pre>
</blockquote>
<p>Той създава по едно произволно число за всеки ред от таблицата и след това сортира редовете спрямо тези произволни числа. Правейки LIMIT 1 ние взимаме само първия ред от резултатната таблица. Ако искате повече, то просто увеличете числото.</p>
<p>Класическият метод работи без да има значение какви са стойностите в колоната id. Използва се индекс по колоната &#8220;name&#8221; и има &#8220;Using temporary; Using filesort&#8221;.</p>
<p><strong><strong>Test 2: </strong>Произволно id в WHERE</strong></p>
<p>Методът е взаимстван <a title="mysql_random_row" href="http://akinas.com/pages/en/blog/mysql_random_row/" target="_blank">от блога на Akinas</a>. В тази заявка е много важно id да са поредни и &#8220;без дупки&#8221;. Първо се взима броя на редовете в таблицата (който се приема и за най-голямата стойност на id), след това се взима първото id, което е след тази стойност.</p>
<blockquote>
<pre>SELECT RAND()*COUNT(*) 
INTO @randid FROM city;

SELECT id, name FROM city 
WHERE id &gt;= @randid
LIMIT 1;</pre>
</blockquote>
<p>С този метод НЕ можете да вземете повече от един произволен ред наведнъж. Също така е много важно броя на редовете в таблицата да отговаря на най-голямото id в нея, а също така разчита на равномерното разпределение на id-та (т.е. може да има &#8220;дупки&#8221;, но те също трябва да са равномерно разпределени).</p>
<p><strong><strong>Test 3: </strong>Произволно id в WHERE с непоредни числа</strong></p>
<p>Методът отново е взаимстван <a title="mysql_random_row" href="http://akinas.com/pages/en/blog/mysql_random_row/" target="_blank">от блога на Akinas</a>. Този метод е абсолютно същия както предишния, но с тази разлика, че няма нужда по колоната &#8220;id&#8221; да имаме поредни стойности от 1 до N, а може да са в произволен интервал от A до B:</p>
<blockquote>
<pre>SELECT FLOOR(MIN(id)+ RAND()*(MAX(id)-MIN(id)+1)) 
INTO @randid FROM city;

SELECT name
FROM city
WHERE id &gt;= @randid
LIMIT 1;</pre>
</blockquote>
<p>Както и в предишния метод &#8211; тук силно се разчита на добро равномерно разпределение на id-тата. Липсата на &#8220;дупки&#8221; между тях ще дава правилни резултати. Отново не можем да взимаме повече от един произволен ред наведнъж.</p>
<p><strong><strong>Test 4: </strong>ORDER BY RAND() с отрязване на част от таблицата</strong></p>
<p>Този метод е предложен <a title="Josh Hartman" href="www.warpconduit.net/2011/03/23/selecting-a-random-record-using-mysql-benchmark-results/" target="_blank">от Josh Hartman</a> и се основава на намаляване на броя на редовете, които трябва да се сортират чрез ORDER BY RAND().</p>
<blockquote>
<pre>SELECT name FROM city 
WHERE RAND()&lt;(SELECT ((1/COUNT(*))*10) FROM city) 
ORDER BY RAND() 
LIMIT 1;</pre>
</blockquote>
<p>Както се вижда от кода &#8211; в WHERE клаузата се взима произволно число, което се проверява дали е по-малко от дадена референтна стойност. Числото 1/count(*) е умножено по 10, за да не стане референтната стойност прекалено малка, което да създаде по-голям риск от &#8220;пропускане&#8221; (вижте забележката след параграфа). Ако искате да се върнат N реда, то трябва да направите &#8220;&#8230;(N/COUNT(*))*10&#8230;&#8221; и &#8220;&#8230;LIMIT N&#8221;. Накрая се извършва &#8220;ORDER BY RAND()&#8221;, за да не се дава предимство на по-малките id-та спрямо по-големите.</p>
<p><span style="text-decoration: underline;">ВАЖНО</span>: Този метод НЕ гарантира връщане на резултат! Винаги съществува възможност, макар и в случая минимална, RAND() да връща винаги стойности над референтната, с което накрая да се окаже, че не е върнат никакъв ред.</p>
<p><strong><strong>Test 5: </strong>JOIN метод</strong></p>
<p>Това е класически начин за изваждане на произволен ред от таблица. Взима се оригиналната таблица и към нея по &#8220;id&#8221; се присъединява едно единствено произволно число, което е от 1 до максималното в таблицата city:</p>
<blockquote>
<pre>SELECT city.name
FROM city JOIN
     (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS id) AS tmp
     ON city.id &gt;= tmp.id
LIMIT 1;</pre>
</blockquote>
<p>Задължително в този метод е id-тата да са поредни започващи от 1 нататък и да няма &#8220;дупки&#8221; или поне да са равномерно разпределени. С този метод НЕ можете да вземете повече от един произволен ред накуп &#8211; увеличаването на стойността в LIMIT ще върне поредни редове, а не произволни!</p>
<p><strong>Test 6: JOIN метод чрез използване на локална променлива<br />
</strong></p>
<p>Предложен в <a title="Random row from MySQL table at MySQL Diary" href="http://www.mysqldiary.com/how-to-produce-random-rows-from-a-table/" target="_blank">MySQLDiary.com</a>,  това е подобрение на предишния метод, тъй като позволява да взимаме повече от един произволен ред (увеличавайки стойността след LIMIT във вътрешната таблица):</p>
<blockquote>
<pre>SELECT city.name
FROM city JOIN
     (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, @num:=@num+1 
      FROM (SELECT @num:=0) AS a, city LIMIT 1) AS tmp 
     ON tmp.num = city.id;</pre>
</blockquote>
<p>При така показания метод е задължително да НЯМА дупки.</p>
<p><strong>Тест 7: OFFSET метод</strong></p>
<p>Предложен от <a title="msyql-alternative-order-by-rand" href="http://www.electrictoolbox.com/msyql-alternative-order-by-rand/" target="_blank">electrictoolbox.com</a>. Методът се състои в следната последователност:</p>
<ol>
<li>Намираме общия брой на редовете в таблицата и го записваме в променлива;</li>
<li>Генерираме произволно число и го умножаваме по общия брой редове от таблицата;</li>
<li>Извличаме ред от таблицата чрез LIMIT 1 и OFFSET намереното в т.2 число.</li>
</ol>
<p>Първият проблем идва в това, че не можем да подаваме променливи в LIMIT. Затова се налага да използваме подготвени заявки:</p>
<blockquote>
<pre>SELECT FLOOR(RAND()*COUNT(*)) INTO @count 
FROM city;

PREPARE stmt 
FROM 'SELECT name INTO @temp FROM city LIMIT 1 OFFSET ?';

EXECUTE stmt USING @count;</pre>
</blockquote>
<p>При този метод имаме едно голямо предимство &#8211; за разлика от всички алтернативни на ORDER BY RAND() методи тук въобще НЕ зависим от колоната id, т.е. няма проблем да има &#8220;дупки&#8221; в нея. Недостатък е, че не може да извлича повече от един произволен ред в една заявка &#8211; налага се за n реда да изпълните n заявки (при ORDER BY RAND() не е така).</p>
<p><strong>Тестова постановка</strong></p>
<p>Създаваме 7 съхранени процедури, всяка от която съдържа по един от методите. Входния параметър на тази заявка е число, което има смисъла на &#8220;брой повторения&#8221;, които ще направим на метода:</p>
<blockquote>
<pre>DELIMITER //
CREATE PROCEDURE test1(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name INTO @temp FROM city 
      ORDER BY RAND() LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test2(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT RAND()*COUNT(*) INTO @randid FROM city;
	  SELECT name INTO @temp FROM city 
      WHERE id &gt;= @randid
      LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test3(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT FLOOR(MIN(id)+ RAND()*(MAX(id)-MIN(id)+1)) INTO @randid 
      FROM city;
      SELECT name INTO @temp FROM city
      WHERE id &gt;= @randid
      LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test4(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT name INTO @temp FROM city 
      WHERE RAND()&lt;(SELECT ((1/COUNT(*))*10) FROM city) 
      ORDER BY RAND() 
      LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test5(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name INTO @temp
     FROM city JOIN 
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS id) AS tmp
	      ON city.id &gt;= tmp.id
      LIMIT 1;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test6(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
     SELECT city.name INTO @temp
     FROM city JOIN
          (SELECT CEIL(RAND() * (SELECT MAX(id) FROM city)) AS num, 
                  @num:=@num+1 
           FROM (SELECT @num:=0) AS a, city LIMIT 1) AS tmp 
          ON tmp.num = city.id;
     ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;

DELIMITER //
CREATE PROCEDURE test7(tests INT)
BEGIN
  label1: LOOP
    SET tests = tests - 1;
    IF tests &gt; 0 THEN
      SELECT FLOOR(RAND()*COUNT(*)) INTO @count FROM city;
      PREPARE stmt 
      FROM 'SELECT name INTO @temp FROM city LIMIT 1 OFFSET ?';
      EXECUTE stmt USING @count;
      ITERATE label1;
    END IF;
    LEAVE label1;
  END LOOP label1;
END//
DELIMITER ;</pre>
</blockquote>
<p>Забележете, че при извикването на всяка заявка се прави &#8220;SELECT name INTO @temp&#8221;, за да записваме върнатия произволен ред в променлива, вместо да го връщаме към приложението като резултатна таблица. Освен това обърнете специално внимание на test7 &#8211; при него беше много по-подходящо да изместим &#8220;PREPARE stmt&#8230;&#8221; заявката извън тялото на цикъла. Нарочно това не е направено, за да не се дава предимство на метода спрямо останалите. Впрочем разликата не е значителна (в порядъка 2-3 секунди), но все пак така е правилно.</p>
<p>След това изпълняваме всяка една процедура два пъти, като между пусканията на всеки две различни процедури спираме MySQL сървъра (за да сме сигурни, че няма вътрешни оптимизации, които да променят резултата):</p>
<blockquote>
<pre>mysql&gt; CALL test1(10000);
Query OK, 1 row affected (39.56 sec)

mysql&gt; CALL test1(10000);
Query OK, 1 row affected (39.52 sec)

mysql&gt; CALL test2(10000);
Query OK, 1 row affected (18.33 sec)

mysql&gt; CALL test2(10000);
Query OK, 1 row affected (18.13 sec)

mysql&gt; CALL test3(10000);
Query OK, 1 row affected (19.11 sec)

mysql&gt; CALL test3(10000);
Query OK, 1 row affected (18.97 sec)

mysql&gt; CALL test4(10000);
Query OK, 1 row affected (38.77 sec)

mysql&gt; CALL test4(10000);
Query OK, 1 row affected (38.64 sec)

mysql&gt; CALL test5(10000);
Query OK, 1 row affected (1.91 sec)

mysql&gt; CALL test5(10000);
Query OK, 1 row affected (1.84 sec)

mysql&gt; CALL test6(10000);
Query OK, 1 row affected (1.42 sec)

mysql&gt; CALL test6(10000);
Query OK, 1 row affected (1.41 sec)

mysql&gt; CALL test7(10000);
Query OK, 1 row affected (27.44 sec)

mysql&gt; CALL test7(10000);
Query OK, 1 row affected (27.66 sec)</pre>
</blockquote>
<p>Ясно се вижда, че класическия метод (test1) и предложената модификация (test4) дават възможно най-лоши резултати. При това test4 не гарантира въобще, че ще се върне какъвто и да е резултат &#8211; затова директно можете да го отхвърлите като възможност.</p>
<p>Тестовете с произволно число в where (test2 и test3) стоят непосредствено след това, като са двойно по-бързи. Те от своя страна помежду си дават несъществена разлика в бързодействието. Това ни насочва към това да препоръчаме &#8220;по-сигурния&#8221; от тях &#8211; test3.</p>
<p>Абсолютните фаворити са JOIN методите (test5 и test6). При това втория, който дава по-добри възможности, дава и по-добри резултати.</p>
<p>Колкото до test7 &#8211; той определено се справя много по-добре от test1, при това няма недостатъците на останалите тестове. За жалост при него не е възможно да се извлече повече от един ред накуп в една заявка.</p>
<p><strong>Извод</strong></p>
<p>При напълно разнородни данни, т.е. такива без никаква подредба по идентификатор, използвайте класическия метод (test1) когато искате да изкарате множество редове накуп или методът с offset (test7) когато искате да изкарате един единствен произволен ред.</p>
<p>При напълно подредени стойности по колоната id &#8211; ако например тя е primary key с auto_increment и няма изтрити редове, то JOIN методите са в пъти по-бързи! При тях втория (test6) е за предпочитане, тъй като позволява връщане на повече от един произволен ред в една заявка.</p>
<p>Ако все пак има &#8220;дупки&#8221; в колоната &#8220;id&#8221;, но преценявате, че те са сравнително равномерно разпределени, то може да се възползвате и от методите с произволно число в where (test2 и test3). При тях по-добър, пък макар и минимално по-бавен, е test3. Отново напомням, та макар и с риск да стана досаден, че при едно извикване на заявката можете да взимате само един произволен ред, а не множество такива (това е сериозен недостатък ако ви трябват няколко произволни реда и в такъв случай обезсмисля метода).</p>
<p>Не използвайте test4, тъй като той е сбъркан по дизайн и неефективен по бързодействие.</p>
<p>Нека синтезираме казаното и да определим подходящите методи за взимане на :</p>
<ul>
<li>Подредни числа, без дупки: test6</li>
<li>Равномерно разпределени числа, но с дупки и при нужда само от един произволен ред: test 5</li>
<li>Без каквато и да е подредба или неравномерно разпределени числа и при нужда само от един произволен ред: test7</li>
<li>Без каквато и да е подредба или неравномерно разпределени числа и при нужда от повече от един произволен ред: test1</li>
</ul>
<p>П.П. Всички тестове са проведени при InnoDB таблица. Ако използвате MyISAM таблица, то всички методи, които използват COUNT(*) &#8211; тестове 2, 4 и 7 &#8211; ще работят по-бързо, понеже в MyISAM броят на редовете в таблиците се кешира.</p>
<p><strong>Задача</strong>: Прехвърлете данните от таблица city в MyISAM хранилище и проведете същите тестове.</p>
<p>П.П.2. Последна редакция на 09.05.2012г.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8080-random-row-from-a-table/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>Броня за Лада Нива</title>
		<link>http://www.cphpvb.net/lada/8093-lada-niva-front-bumper/</link>
		<comments>http://www.cphpvb.net/lada/8093-lada-niva-front-bumper/#comments</comments>
		<pubDate>Thu, 03 May 2012 17:55:35 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Лада Нива]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8093</guid>
		<description><![CDATA[От няколко дни моята Лада Нива се издокара с нова предна броня. Причината никак не е приятна &#8211; катастрофа. Старата броня тъй или иначе си беше &#8220;кашкавалена&#8221; още като наследство от предишния собственик. Не, че не подлежеше на ново поредно &#8220;изчукване&#8221;, но реших да се насоча към нова, което е и правилното решение за тези [...]]]></description>
			<content:encoded><![CDATA[<p>От няколко дни моята Лада Нива се издокара с нова предна броня. Причината никак не е приятна &#8211; катастрофа. Старата броня тъй или иначе си беше &#8220;кашкавалена&#8221; още като наследство от предишния собственик. Не, че не подлежеше на ново поредно &#8220;изчукване&#8221;, но реших да се насоча към нова, което е и правилното решение за тези консумативи.<span id="more-8093"></span></p>
<p>Оригиналната нова предна броня за Лада Нива не е кой знае колко скъпа. В общи линии е на цената на &#8220;направи си сам&#8221;. Оригиналната броня е алуминиева и честно казано доста добра. Аз обаче реших да пристъпя към &#8220;тунинг&#8221;. Най-вече стана брагодарение на колегата <a title="Владимир Раденков (Vladyas)" href="http://www.offroad-bulgaria.com/member.php?u=1825" target="_blank">Владимир Раденков (vladyas)</a>, който ме убеди, да пристъпя към тази стъпка.  Ето и резултата &#8211; Ладата вече се усмихва:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2391.jpg"><img class="aligncenter size-medium wp-image-8094" title="Предна броня Лада Нива" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2391-300x225.jpg" alt="Предна броня Лада Нива" width="300" height="225" /></a></p>
<p>Захващането се прави на оригиналните буфери. Ъглите по краищата налагат леко рязане на решетката. Ето как изглежда отстрани:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2390.jpg"><img class="aligncenter size-medium wp-image-8095" title="Предна броня Лада Нива погледната отстрани" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2390-300x225.jpg" alt="Предна броня Лада Нива погледната отстрани" width="300" height="225" /></a></p>
<p>Всъщност едно от слабите места на оригиналната броня е именно захващането &#8211; сравнително слабо е. Затова са сложени две допълнителни подпори. Важно е да не са много здрави, т.е. да могат при удар да се чупят първо те. Все пак не са хванати за рама защото Нивата е самоносеща конструкция:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2392.jpg"><img class="aligncenter size-medium wp-image-8096" title="Подпори за предна броня на Лада Нива" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2392-300x225.jpg" alt="Подпори за предна броня на Лада Нива" width="300" height="225" /></a></p>
<p>Както се вижда от снимките по-горе &#8211; калниците са отрязани. Това поне помага да не се дерат в гумите при попадане в дупка &#8211; припомням, че съм с <a title="lassa ok 144" href="http://www.cphpvb.net/lada/7784-lassa-ok-144/" target="_blank">Ласа ОК 144</a>, които са значително по-големи от оригиналните. Бронята следва тяхната линия. Така предницата придобива съвсем нов вид &#8211; много от &#8220;карантията&#8221; става видима:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2393.jpg"><img class="aligncenter size-medium wp-image-8097" title="Лада Нива - поглед отпред и отдолу" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2393-300x225.jpg" alt="Лада Нива - поглед отпред и отдолу" width="300" height="225" /></a></p>
<p>Ето изглед на бронята отдолу:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2396.jpg"><img class="aligncenter size-medium wp-image-8098" title="Броня погледната отдолу" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2396-300x225.jpg" alt="Броня погледната отдолу" width="300" height="225" /></a></p>
<p>&#8230; и отгоре:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2397.jpg"><img class="aligncenter size-medium wp-image-8099" title="Броня погледната отгоре" src="http://www.cphpvb.net/wp-content/uploads/2012/05/IMG_2397-300x197.jpg" alt="Броня погледната отгоре" width="300" height="197" /></a></p>
<p>Към това към ремонта се прибави още изправяне на геометрията, смяна и боядисване на маска (напълно изгнила + унищожена), изправяне и рязане на калник, изкърпване на дупки (изгнило) по задния калник&#8230; и т.н. Дали по принуда или не &#8211; такива моменти си идват рано или късно.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/lada/8093-lada-niva-front-bumper/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Автоматичен meta description за WordPress</title>
		<link>http://www.cphpvb.net/other/8090-automatic-meta-description-wordpress/</link>
		<comments>http://www.cphpvb.net/other/8090-automatic-meta-description-wordpress/#comments</comments>
		<pubDate>Sat, 28 Apr 2012 01:37:10 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Общи работи]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8090</guid>
		<description><![CDATA[По подразбиране WordPress не включва никакви meta description тагове в HTML кода. Представям ви лесен начин да генерирате автоматично такива за вашите страници и категории. За описание на страниците се взима първото изречение от текста в текущата статия, а за категориите тяхното описание зададено в WordPress. Кодът е малко &#8220;мръсен&#8221; и може да се изпипа, [...]]]></description>
			<content:encoded><![CDATA[<p>По подразбиране WordPress не включва никакви meta description тагове в HTML кода. Представям ви лесен начин да генерирате автоматично такива за вашите страници и категории. За описание на страниците се взима първото изречение от текста в текущата статия, а за категориите тяхното описание зададено в WordPress. Кодът е малко &#8220;мръсен&#8221; и може да се изпипа, но ще опиша в коментари кое-как и защо е направено. Методът е следния &#8211; отворете header.php файла на вашата тема и добавете кода в &lt;head&gt; часта:<span id="more-8090"></span></p>
<blockquote>
<pre>&lt;?php
// В началото добавяме подзаглавието на сайта
$meta = get_bloginfo('description').': ';
// Проверява дали е страница или пост
if (is_singular()){
	// Намираме къде свършва първото изречение
	$pos = mb_strpos($post-&gt;post_content, ". ", 0, 'UTF-8');
	// Ако е намерено първо изречение, то го добавяме
	if($pos !==false)
	   $meta .= mb_substr($post-&gt;post_content, 0, $pos, 'UTF-8');
	// В противен случай взимаме целия пост
	else $meta .= $post-&gt;post_content;
}
elseif (is_category()){ // ако сме в категория с много статии
	// Добавяме описанието на категорията
	$meta .= category_description();
}
// Задължително премахваме всякакви HTML тагове
// и прекодираме кавичките и специалните символи
$meta = htmlentities(strip_tags($meta), ENT_QUOTES, 'UTF-8');
// Премахваме нови редове, табулации и празни интервали
// Поредици от празни интервали се получават често, когато
// имате много къс първи параграф в който няма точка.
$meta = str_replace(array("\n","\r","\t",'   ','  '),' ', $meta);
// Ако meta description е по-дълъг от 255, то го скъсяваме
if(mb_strlen($meta)&gt;=255) $meta = mb_substr($meta, 0, 254);
// Отпечатваме мета тага
echo "&lt;meta name='description' content='{$meta}' /&gt;\n";
?&gt;</pre>
</blockquote>
<p>Предполагам, че може да се поправи и да стане доста по-прилично. Направих редица тестове и изглежда, че се справя добре с UTF-8 кодировка. За 404 и страници генерирани от търсене не се слага meta description. Ако някой забележи грешки или намери начин за оптимизация, нека сподели.</p>
<p>Някои по-нататъшни насоки:</p>
<ul>
<li>Винаги започвайте статиите си с описателно първо изречение, като по възможност включвайте важната ключова дума;</li>
<li>Избягвайте да започвате с параграф с едно единствено изречение;</li>
<li>Стремете се първото ви изречение да е в рамките на 150 символа.</li>
</ul>
<p>Колкото до мета тага за ключови думи (meta keywords) &#8211; там нещата са доста по-сложни. Автоматизираното разпознаване на добри ключови думи не е толкова проста работа. По-нататък евентуално ще завърша скрипт и за това.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/other/8090-automatic-meta-description-wordpress/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MathJax 2.0 &#8211; нова ера в писането на формули!</title>
		<link>http://www.cphpvb.net/fun/8086-mathjax-2-0/</link>
		<comments>http://www.cphpvb.net/fun/8086-mathjax-2-0/#comments</comments>
		<pubDate>Tue, 17 Apr 2012 19:39:14 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Математика]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8086</guid>
		<description><![CDATA[На 26 февруари 2012г е излязла новата версия 2.0 на MathJax. Малко ме е срам, но разбрах за това чак сега. А миналата година написах публикация на тема &#8220;Технологии за въвеждане и изобразяване на математически формули в Уеб 2.0 приложения&#8221;, в която отбелязах именно MathJax като един от лидерите тогава. Какво ново във версия 2.0? [...]]]></description>
			<content:encoded><![CDATA[<p>На 26 февруари 2012г е излязла новата версия 2.0 на <a title="MathJax" href="http://www.mathjax.org" target="_blank">MathJax</a>. Малко ме е срам, но разбрах за това чак сега. А миналата година <a title="Публикации на Филип Петров" href="http://www.cphpvb.net/publications/" target="_blank">написах публикация</a> на тема &#8220;Технологии за въвеждане и изобразяване на математически формули в Уеб 2.0 приложения&#8221;, в която отбелязах именно MathJax като един от лидерите тогава.<span id="more-8086"></span></p>
<p>Какво ново във версия 2.0? На първо място вече се поддържа не само LateX и MathML, но и AsciiMath! Това вече наистина го направи незаменим лидер сред софтуера за писане на математически формули в уеб сайтове, защото практически вече поддържа всички възможни стандарти. При това резултата се изобразява в браузъра в текстови вид, а не като картинка, което спомага да имаме индексиране от търсачките и поддръжка на copy/paste с други локални текстови редактори. Накрая, но не последно, формулите се изобразяват значително по-бързо спрямо предишната версия (всъщност MathJax беше най-бавната технология от тези, които разгледах).</p>
<p>Ето един пример за формула написана с MathJax 2.0 с режим displayMath:</p>
<p>[math]\sin{x} \cos{y} = \frac{1}{2}\left[ \sin{(x-y)}+\sin{(x+y)} \right][/math]</p>
<p>&#8230; и с режим inlineMath: [mathi]\sin{x} \cos{y} = \frac{1}{2}\left[ \sin{(x-y)}+\sin{(x+y)} \right][/mathi]</p>
<p>Ако виждате формулата от резултата, натиснете с десен бутон, за да видите допълнителни &#8220;екстри&#8221;. Може да правите &#8220;zoom&#8221; (при това доста удобен) и също така можете да копирате формулата в TeX или MathML формат. Също така ви се дава възможност да изобразявате с html/css, mathml или svg.</p>
<p>Освен това MathJax се инсталира буквално елементарно. Свалете последната версия от уебсайта и я разархивирайте в поддиректория &#8220;mathjax&#8221; в root директорията на вашия домейн (внимание &#8211; файловете са доста). След това вмъкнете следния код в &lt;head&gt; частта на вашите страници, в които искате да имате визуализиране на формули:</p>
<blockquote><p><code>&lt;script type='text/x-mathjax-config'&gt; MathJax.Hub.Config({tex2jax: {inlineMath: [['[mathi]','[/mathi]']], displayMath: [['[math]','[/math]']]}})&lt;/script&gt;<br />
&lt;script type='text/javascript' src='http://www.cphpvb.net/mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML'&gt;&lt;/script&gt;</code></p></blockquote>
<p>Разбира се вместо &#8220;cphpvb.net&#8221; сложете вашия собствен домейн. От тук нататък всеки текст, който е ограден с <code>[mathi]</code> и <code>[/mathi]</code> или <code>[math]</code> и <code>[/math]</code> ще се визуализира чрез MathJax скрипта (освен ако тези &#8220;BB кодове&#8221; не са вътре в тагове &lt;pre&gt; или &lt;code&gt;).</p>
<p>Конкретно за WordPress &#8211; ако искате да зареждате скрипта само когато е използван, а не на всяка страница, то може да използвате следния код:</p>
<blockquote>
<pre>$math_shortcode_found = false;
foreach ($posts as $post) {
   if (strpos($post-&gt;post_content, '[math')!==false){
      $math_shortcode_found = true;
      break;
   }
}
if ($math_shortcode_found) {
   echo ... // тук отпечатвате script таговете
}</pre>
</blockquote>
<p>Това ще облекчи изпращането на ненужни данни към потребителя (към момента 14690 байта) когато не се използват формули в дадена статия, но за сметка на това ще натовари сървъра (при всеки преглед на всяка статия ще се прави проверка за наличието на тага). По-добро решение е да слагате паразитен таг <code>[mathi][/mathi]</code> в началото на всяка статия, в която ще използвате математически формули и в PHP кода да сравнявате само началото на статията:</p>
<blockquote>
<pre>$math_shortcode_found = false;
foreach ($posts as $post) {
   if (strncmp($post-&gt;post_content, '[mathi][/mathi]', 15) == 0){
      $math_shortcode_found = true;
      break;
   }
}
if ($math_shortcode_found) {
   echo ... // тук отпечатвате script таговете
}</pre>
</blockquote>
<p>Празният таг е inline и няма да направи нов ред, а съдържанието му също е празно, затова mathjax няма да изобрази нищо. Неудобството е, че трябва да пишете този празен таг във всяка статия, в която ви трябва математическа формула. По-лошото обаче е, че този паразитен празен таг ще се показва във вашия RSS feed, както и евентуално може да повлияе на търсачките. Затова най-добре ще е да го добавяме в края на текста на статиите ни:</p>
<blockquote>
<pre>$math_shortcode_found = false;
foreach ($posts as $post) {
   if(mb_strlen($post-&gt;post_content)&gt;15 &amp;&amp;
      substr_compare($post-&gt;post_content, '[mathi][/mathi]', -15, 15) === 0
     ){
      $math_shortcode_found = true;
      break;
   }
}
if ($math_shortcode_found) {
   echo ... // тук отпечатвате script таговете
}</pre>
</blockquote>
<p>MathJax 2.0 наистина е най-добрия софтуер за математически формули в уеб, който е излизал до този момент. Моят дневник вече официално е:</p>
<p><a href="http://www.mathjax.org/" target="_blank"><img class="aligncenter" title="Powered by MathJax" src="http://www.mathjax.org/badge.gif" alt="Powered by MathJax" border="0" /><br />
</a>[mathi][/mathi]</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/fun/8086-mathjax-2-0/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Христос Воскресе</title>
		<link>http://www.cphpvb.net/family/8075-%d1%85%d1%80%d0%b8%d1%81%d1%82%d0%be%d1%81-%d0%b2%d0%be%d1%81%d0%ba%d1%80%d0%b5%d1%81%d0%b5/</link>
		<comments>http://www.cphpvb.net/family/8075-%d1%85%d1%80%d0%b8%d1%81%d1%82%d0%be%d1%81-%d0%b2%d0%be%d1%81%d0%ba%d1%80%d0%b5%d1%81%d0%b5/#comments</comments>
		<pubDate>Sat, 14 Apr 2012 23:04:42 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Семейни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8075</guid>
		<description><![CDATA[След цял ден подготовка за боядисване на яйца и месене на козунак, разбира се с активна помощ на нашата съседка Гиги: &#8230; тази вечер отидохме на черква и вече можем да си кажем официално &#8211; ХРИСТОС ВОСКРЕСЕ! Свещичките от снимката си ги донесохме от там и си ги запазихме по пътя до вкъщи запалени. А [...]]]></description>
			<content:encoded><![CDATA[<p>След цял ден подготовка за боядисване на яйца и месене на козунак, разбира се с активна помощ на нашата съседка Гиги:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/04/Gigi-i-Milena.jpg"><img class="aligncenter size-medium wp-image-8076" title="Гиги и Милена" src="http://www.cphpvb.net/wp-content/uploads/2012/04/Gigi-i-Milena-300x225.jpg" alt="Гиги и Милена" width="300" height="225" /></a></p>
<p>&#8230; тази вечер отидохме на черква и вече можем да си кажем официално &#8211; ХРИСТОС ВОСКРЕСЕ!<span id="more-8075"></span></p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/04/Hristos-Voskrese.jpg"><img class="aligncenter size-medium wp-image-8077" title="Христос Воскресе" src="http://www.cphpvb.net/wp-content/uploads/2012/04/Hristos-Voskrese-300x225.jpg" alt="Христос Воскресе" width="300" height="225" /></a></p>
<p>Свещичките от снимката си ги донесохме от там и си ги запазихме по пътя до вкъщи запалени. А червеното яйце от миналата година се оказа здраво. Едно суеверие казва, че това било на щастие:</p>
<p><a href="http://www.cphpvb.net/wp-content/uploads/2012/04/qice.jpg"><img class="aligncenter size-medium wp-image-8079" title="Миналогодишно яйце от Великден" src="http://www.cphpvb.net/wp-content/uploads/2012/04/qice-300x225.jpg" alt="Миналогодишно яйце от Великден" width="300" height="225" /></a></p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/family/8075-%d1%85%d1%80%d0%b8%d1%81%d1%82%d0%be%d1%81-%d0%b2%d0%be%d1%81%d0%ba%d1%80%d0%b5%d1%81%d0%b5/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MySQL Query Cache</title>
		<link>http://www.cphpvb.net/db/8071-mysql-query-cache/</link>
		<comments>http://www.cphpvb.net/db/8071-mysql-query-cache/#comments</comments>
		<pubDate>Sat, 14 Apr 2012 17:12:58 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8071</guid>
		<description><![CDATA[Използването на &#8220;кеш за заявки&#8221; е една добра възможност за повишаване на бързодействието на СУБД. В MySQL функционалността се нарича &#8220;query cache&#8221;. Идеята е да се записват готови резултати от вече изпълнени SELECT заявки в паметта и така те да се използват наготово. За целта се използва хеш таблица. Правилната настройка на кеша за заявки [...]]]></description>
			<content:encoded><![CDATA[<p>Използването на &#8220;кеш за заявки&#8221; е една добра възможност за повишаване на бързодействието на СУБД. В MySQL функционалността се нарича &#8220;query cache&#8221;. Идеята е да се записват готови резултати от вече изпълнени SELECT заявки в паметта и така те да се използват наготово. За целта се използва хеш таблица. Правилната настройка на кеша за заявки може да доведе до значително ускорение в бързодействието на системата. За да направим това обаче трябва да разберем как той функционира и съответно какво можем да спечелим и какво можем да загубим от използването му.<span id="more-8071"></span></p>
<p><strong>Общи настройки, променливи и формули<br />
</strong></p>
<p>Четирите най-основни настройки за кеша на заявки са:</p>
<ul>
<li>Тип на кеша (query_cache_type): приема стойности 0 (изключен), 1 (постоянно включен) и 2 (работи &#8220;до поискване&#8221;);</li>
<li>Размер (query_cache_size): колко памет да бъде отделена за кеша;</li>
<li>Максимален размер на резултат от заявка (query_cache_limit): какво количество информация да отделяме максимално за всяка една заявка;</li>
<li>Минимален размер на резултат от заявка (query_cache_min_res_unit): кеширането на прекалено малки резултати от заявки може да покаже по-лоши резултати, отколкото липсата на кеширане въобще.</li>
</ul>
<p>И четирите променливи се задават чрез конфигурационния файл my.cnf. Може да видите текущите настройки от самата среда чрез следната команда:</p>
<blockquote>
<pre>mysql&gt; SHOW VARIABLES LIKE 'query%';
+------------------------------+----------+
| Variable_name                | Value    |
+------------------------------+----------+
| query_alloc_block_size       | 8192     |
| query_cache_limit            | 1048576  |
| query_cache_min_res_unit     | 4096     |
| query_cache_size             | 67108864 |
| query_cache_type             | ON       |
| query_cache_wlock_invalidate | OFF      |
| query_prealloc_size          | 8192     |
+------------------------------+----------+
7 rows in set (0.00 sec)</pre>
</blockquote>
<p>Преди да започнем с оптимизацията на кеша е много важно да отбележим изключително важна променлива &#8211; query_cache_wlock_invalidate. Ако тя е изключена (както е в примера, а това е и стойността по подразбиране) и използвате MyISAM, то SELECT заявки подадени към кеша ще се изпълнят успешно дори ако таблицата е заключена! Разбира се това в общия случай не е опасно, защото ако вие искате строга защита на данните, то тъй или иначе ще използвате InnoDB и ще се възползвате от транзакции.</p>
<p>Относно оптимизацията на кеша &#8211; за нас важна информация (след като системата е работила известно време в работен режим) е как точно се използва той:</p>
<blockquote>
<pre>mysql&gt; SHOW STATUS LIKE 'Qcache%';
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Qcache_free_blocks      | 5333     |
| Qcache_free_memory      | 21925712 |
| Qcache_hits             | 1334956  |
| Qcache_inserts          | 390420   |
| Qcache_lowmem_prunes    | 227914   |
| Qcache_not_cached       | 64768    |
| Qcache_queries_in_cache | 19173    |
| Qcache_total_blocks     | 44897    |
+-------------------------+----------+
8 rows in set (0.00 sec)</pre>
</blockquote>
<p>Основните неща, които трябва регулярно да изчисляваме  са три:</p>
<p>1. За <span style="text-decoration: underline;">текущо запълване на кеша</span> използвайте следната формула (заета памет разделено на заделена памет):</p>
<p style="text-align: center;">((query_cache_size &#8211; Qcache_free_memory) / query_cache_size )*100</p>
<p style="text-align: left;">От нашия пример имаме запълване от 67%. Разбира се, че колкото по-плътно е запълването, толкова по-добре използваме заделената памет.</p>
<p style="text-align: left;">2. <span style="text-decoration: underline;">Ефективност на кеша</span> се измерва като &#8220;процент от заявките, които се възползват от кеша&#8221;. Колкото повече заявки се възползват от кеша, толкова по-добре. Формулата за това идва от отношението между съвпаденията в кеша и общите заявки за търсене в кеша:</p>
<p style="text-align: center;">(Qcache_hits / (Qcache_hits+Qcache_inserts+Qcache_not_cached) )*100</p>
<p>От показания пример стойността е близо до 75%.</p>
<p>3. <span style="text-decoration: underline;">Честотата на използване на заявки</span> от кеша се получава като отношението между  общите заявки към кеша и броят на вмъкнатите нови резултати от заявки в него, или:</p>
<p style="text-align: center;">Qcache_hits / Qcache_inserts</p>
<p>В примера честотата е приблизително 3,42. Колкото по-голямо е това число, толкова по-добре.</p>
<p><strong>Какво се кешира?</strong></p>
<p>Първото нещо, което трябва да знаете е, че се кешират само цели заявки. Вложени заявки, части от обединения и изгледи НЕ се кешират. Освен това резултатите в кеша се влияят от малки и главни букви и всякакви минимални разлики в кода на заявката! Например заявките &#8220;select * from users&#8221; и &#8220;Select * from users&#8221; за query cache се третират като различни. Същото важи за интервали, нови редове, коментари и всякакви други козметични разлики. Ето ви една основателна причина да спазвате стриктна конвенция при писането на SQL заявки.</p>
<p>Друго важно нещо е, че кеша работи с блокове памет. С други думи той чете директно входящия трафик на подадените заявки и започва да търси за съвпадение още с първите получени байтове и не изчаква заявката да бъде получена цяла. Това означава, че кеша работи на ниво още преди заявката да е компилирана, което впрочем обяснява и казаното горе за разликите между малки и главни букви на иначе идентични заявки.</p>
<p>Кешират се само и единствено SELECT заявки (динамични) и подготвени/параметризирани SELECT заявки (след MySQL 5.1). Извикването на съхранени процедури НЕ се кешира. Не се кешират и заявки, които използват локални или глобални променливи. Също така кешът НЯМА да работи ако са използвани функции като RAND(), NOW(), CURDATE(), CURTIME(), FOUND_ROWS() и т.н., т.е. функции които поради външни фактори могат да предизвикат различен резултат при едно и също извикване. Няма да се кешират заявки, които използват временни таблици (temporarily tables) и заявки, които генерират &#8220;предупреждения&#8221; (warnings).</p>
<p><span style="text-decoration: underline;">При промяна на таблица</span> (изпълнение на insert, update или delete заявка върху нея) <span style="text-decoration: underline;">се премахват от кеша всички заявки, които зависят от нея</span>! Това означава, че е много неподходящо да използвате кеш ако таблиците, които използвате, много често се обновяват.</p>
<p><strong>Фрагментация</strong></p>
<p>Данните в кеша за заявки се записват в т.нар. &#8220;блокове&#8221;, които са с променлива дължина. В началото (при празен кеш) има един голям свободен блок. Когато се запишат данни в кеша блока се разделя на две, данните се записват в първия и след като се запишат (кеша за заявки не знае размера на резултатната таблица по време на записа) се освобождава незаетото пространство от текущия блок (т.е. прави се &#8220;trim&#8221;). Същото се повтаря нататък за останалия свободен блок. Така първоначално кеша ще се запълва равномерно.</p>
<p>С течение на времето обаче някои заявки ще се премахват от кеша, т.е. техните блокове ще бъдат освобождавани. По този начин остават &#8220;дупки&#8221; от празни блокове. Това се нарича &#8220;фрагментация&#8221; на блоковото пространство на кеша. Постъпващите нови резултати започват да търсят свободно място именно сред тези свободни блокове и започват да ги запълват поред. Когато резултатната таблица от нова заявка е по-голяма като размер от блока, в който иска да се запише, то MySQL ще потърси нов по-голям блок и ще премести текущо записаните данни в него. Това разбира се води до проблеми с производителността. В най-лошия случай блоковото пространство ще е силно фрагментирано и ще се отнемат много ресурси за преместване на информация от един блок в друг.</p>
<p>Един от начините да забележите, че вашето блоково пространство е фрагментирано е да наблюдавате променливата Qcache_lowmem_prunes. Тази променлива показва колко заявки са били &#8220;изхвърлени&#8221; от кеша, за да освободят блоково пространство за нови. MySQL премахва заявки от кеша на принципа &#8220;най-малко ползвана&#8221;. Ако стойността на тази променлива постоянно се увеличава, а в същото време имате много налична памет, то може с основание може да подозирате фрагментация.</p>
<p>Друг важен показател е отношението между Qcache_free_blocks и Qcache_total_blocks. Колкото по-близо е до 1/2 (което е най-лошия възможен случай), толкова по-зле е положението с разпределението на данните в кеша и обратно &#8211; колкото по-малко е числото, толкова по-малка фрагментация може да очаквате.</p>
<p>Фрагментацията на блоковото пространство може да се намали чрез оптимизиране на големината на минималната дължина на блок (променливата query_alloc_block_size). Увеличавайки стойността на тази променлива ще намали фрагментацията, но за сметка на това заявки с по-малки резултатни таблици ще изхабяват повече памет, отколкото им е била необходима. Един добър метод за груба настройка е да изчислите средната големина на резултат записан в кеша по следната формула:</p>
<p style="text-align: center;">(query_cache_size – Qcache_free_memory) / Qcache_queries_in_cache</p>
<p>Ако вашата система е обаче е много разнородна, т.е. често има както големи, така и малки резултатни таблици, то оптимизацията на кеша срещу фрагментация ще е много трудна задача и намирането на оптимални стойности за блокова големина няма да бъдат толкова тривиални за намиране. В такива случай прочетете по-долу в графата &#8220;кеш до поискване&#8221;.</p>
<p><strong>Дефрагментиране на кеша</strong></p>
<p>Командата FLUSH QUERY CACHE дефрагментира блоковото пространство, като подрежда заетите блокове памет последователно от началото към края на заетата памет. По принцип операцията е бърза, но трябва да знаете, че по този начин целия кеш се &#8220;заключва&#8221;, а това практически &#8220;блокира&#8221; целия сървър &#8211; всички входящи връзки влизат в &#8220;спящ режим&#8221; докато кеша не се преподреди. Затова в никакъв случай не трябва да се прекалява с употребата на тази команда.</p>
<p>Ако искаме да автоматизираме процеса на дефрагментация, то можем да създадем събитие в системната таблица:</p>
<blockquote>
<pre>USE mysql

CREATE EVENT flush_query_cache
ON SCHEDULE EVERY 60 MINUTE
STARTS '2012-04-14 07:15:00'
DO FLUSH QUERY CACHE;</pre>
</blockquote>
<p>Разбира се трябва да сте включили изпълнението на събитията. Може да помислите и за по-сложна система за дефрагментация, като следите натоварването на сървъра и извършвате дефрагментацията на кеша и други оптимизационни дейности когато то е достатъчно ниско.</p>
<p><strong>Оптимизиране на кеша спрямо начина за употреба</strong></p>
<p>Пределно ясно за всички е, че колкото по-добре запълнен е един кеш, колкото колкото по-ефективен е и колкото по-голяма е честотата на заявките, толкова по-добре ще бъде употребен. Обратно &#8211; при ниска ефективност и малка честота на използване ние губим от наличието на кеш. Причината е, че този кеш също изисква ресурси за поддръжка. Затова е много важно да знаем как да го оптимизираме.</p>
<p>В началото трябва да отбележим, че много хора погрешно увеличават размера на кеша до много големи размери. Категорично трябва да отбележим, че в общия случай <span style="text-decoration: underline;">по-големия кеш НЕ означава по-добре работеща система</span>. Напротив &#8211; ще забележите, че увеличаването на кеша от един момент нататък не помага на бързодействието на системата, а в някои случаи води дори до забавяне.</p>
<p>Преди да направите първите настройки трябва да прецените какъв тип приложения ще работят със системата. Ако например преобладават &#8220;малки заявки&#8221; (връщащи малки като обем резултатни таблици), а &#8220;големите&#8221; са малко на брой и не се използват често, то може да настроите ниски стойности на query_cache_min_res_unit и query_cache_limit &#8211; така ще кеширате презимно &#8220;малките&#8221; заявки, а ще отхвърляте &#8220;големите&#8221;. Обратно &#8211; ако приложенията ви ще правят чести заявки с големи резултатни таблици (разбира се повтарящи се такива!), то може да се лишите от кеша за &#8220;малките заявки&#8221; като увеличите query_cache_min_res_unit и сложите достатъчно голям query_cache_limit. При &#8220;разнородни&#8221; заявки към СУБД, т.е. честия случай, в който една система се използва за много различни приложения и различни бази от данни, то по-скоро направете диапазона широк (по-ниска стойност за query_cache_min_res_unit и по-висока за query_cache_limit) въпреки очаквано по-голямата фрагментация. Накрая ако приложението ви рядко прави еднотипни заявки и повечето заявки към базата от данни връщат различни резултати (напр. многопотребителска система, в която всеки потребител изисква свое собствено съдържание и то с редки потворения), то вероятно ще бъде по-добре ако напълно се лишите от кеша за заявки и го изключите.</p>
<p>Друго нещо, за което може да помислите е да организирате приложението си да прави &#8220;отложени промени&#8221;. Това означава да изпълнявате delete, update и/или insert заявки на &#8220;накуп&#8221; през зададени интервали от време, а не непосредствено след подаването им. По този начин вие съществено ще съкратите изхвърлянето на заявки от кеша и ще подобрите бързодействието им. От друга страна обаче това означава, че е възможно да позволите на потребителите си да прочетат &#8220;стара информация&#8221; от кеша. Затова използвайте този метод само при обновявания на таблиците, които не са задължителни &#8220;на момента&#8221; за правилната работа на системата. Също така имайте в предвид, че по този начин се носи и съответен риск при срив на уеб сървъра да се изгуби информация, която иначе е потвърдена на потребителя, че е записана.</p>
<p><strong>Използване на кеш до поискване</strong></p>
<p>Когато не споделяме СУБД с други потребители, а го използваме и настройваме самостоятелно, то със сигурност можем да спечелим изключително много от използването на &#8220;режим на кеш до поискване&#8221;. В този режим една заявка ще бъде записана в кеша само тогава, когато ние специално укажем, че това трябва да стане. За целта се добавя специален атрибут &#8220;SQL_CACHE&#8221; непосредствено след команда SELECT. Например ако ще се изпълнява заявка за изкарване на най-новите 10 статии на първа страница на даден сайт, то очакваме много потребители да четат тези данни и то множество пъти. Ако кеша е в режим &#8220;до поискване&#8221;, то можем да укажем, че искаме заявката да попадне в него по следния начин:</p>
<blockquote>
<pre>SELECT SQL_CACHE id, title, author
FROM articles 
ORDER BY date DESC
LIMIT 10;</pre>
</blockquote>
<p>Обратно &#8211; ако не желаем дадена заявка да попада в кеша, то можем да направим следната заявка с параметър SQL_NO_CACHE:</p>
<blockquote>
<pre>SELECT SQL_NO_CACHE last_login_time
FROM users
WHERE user = "ivan";</pre>
</blockquote>
<p>За улеснение може да приемете, че при query_cache_type = 1 всяка заявка ще бъде с атрибут &#8220;SQL_CACHE&#8221; по подразбиране, а при query_cache_type = 2 ще бъде с атрибут &#8220;SQL_NO_CACHE&#8221; по подразбиране. Виждате, че можете съвсем спокойно да се възползвате от тази тактика независимо в какъв режим се работи.</p>
<p>Използването на тази техникани позвовява да направим изключително ефективна система, която се възползва от кеша за заявки максимално добре. При правилен подбор на това &#8220;кои заявки да се кешират и кои не&#8221; ние можем да минимизираме фрагментацията, да увеличим ефективността и да се възползваме максимално добре от заделената памет. Създаването на една такава максимално ефективна система включва в себе си много прецизно планиране на всяко едно действие в нея.</p>
<p><strong>Заключение</strong></p>
<p>Като финал ще направим следното предупреждение, което може да се използва като основно правило за администраторите: <span style="text-decoration: underline;">лошо работещия кеш за заявки носи много повече вреда за производителността отколкото напълно изключения кеш за заявки</span>. Ако винаги имате това наум и взимате необходимите мерки, то би трябвало вашия сървър да работи добре.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8071-mysql-query-cache/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Обобщение на резултатите от реформата</title>
		<link>http://www.cphpvb.net/politics/8069-education-system-restructure-summary/</link>
		<comments>http://www.cphpvb.net/politics/8069-education-system-restructure-summary/#comments</comments>
		<pubDate>Fri, 13 Apr 2012 10:49:22 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Политика]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8069</guid>
		<description><![CDATA[Настоящата статия представя мое лично мнение и по никакъв начин НЕ цели да отразява позиция на учебното заведение, в което работя, нито на каквито и да е групи, в които членувам или хора с които имам работни взаимоотношения. Преди почти две години написах една статия със заглавие &#8220;Привет на чалгата в науката&#8220;. В нея изказах [...]]]></description>
			<content:encoded><![CDATA[<p><em>Настоящата статия представя мое лично мнение и по никакъв начин НЕ цели да отразява позиция на учебното заведение, в което работя, нито на каквито и да е групи, в които членувам или хора с които имам работни взаимоотношения.<br />
</em></p>
<p>Преди почти две години написах една статия със заглавие &#8220;<a title="Привет на чалгата в науката" href="http://www.cphpvb.net/politics/5751-hi-chalga-in-science/" target="_blank">Привет на чалгата в науката</a>&#8220;. В нея изказах някои от моите лични опасения относно подготвяната реформа в науката и образованието. Първата част от тази реформа отдавна вече е факт &#8211; &#8220;Закон за развитие на академичния състав&#8221; функционира пълноценно от месец април 2011г., т.е. вече измина една година откакто беше приет. Някои неща се случиха така, както си мислих, че ще станат, други не (за някои точки определено съм грешал). Време е за равносметка&#8230; Относно друга част на реформата, а именно &#8220;рейтинг на висшите учебни заведения&#8221; засега няма да си давам мнението.<span id="more-8069"></span></p>
<p>Резултат №1:<strong> масова хабилитация на професори</strong></p>
<p>Създаде се лавина от конкурси за хабилитиране в длъжност &#8220;професор&#8221;. Това беше ясно за всички в университетските и научните среди, но много малко хора имаха изгода да го повдигнат на дневен ред като проблем. Поради тази причина и въпросния проблем си назряваше тихомълком. Днес дори медиите се &#8220;самосезираха&#8221;: за последната година <a title="Хабилитационен бум" href="http://www.segabg.com/article.php?id=597060" target="_blank">в България има нови 309 професори</a>. Или казано по друг начин &#8211; удвоихме хабилитациите на професори в България. Причината е ясна за всички &#8211; отпадането на изискването професорите да са с научна степен &#8220;доктор на науките&#8221;, което явно преди е било голяма пречка.</p>
<p>Резултат №2:<strong> има реална опасност да се &#8220;раздават&#8221; докторски степени на асистенти</strong></p>
<p>Това, което тепърва може да &#8220;избухне&#8221; като проблем е вързано с нуждата от спешен преход на много асистенти към длъжност &#8220;главен асистент&#8221;. Според ЗРАС и правилниците на университетите, длъжност &#8220;асистент&#8221; може да се заема максимум за четири години. През това време младият преподавател трябва едновременно да изпълнява норматива си за водени часове и да пише дисертационен труд, за да добие научна степен &#8220;доктор&#8221;, с която да може да кандидатства за &#8220;главен асистент&#8221;. За никой в момента не е тайна, че асистентската професия въобще не е популярна в България (средната възраст на преподавателите в университетите говори добре за това). За сметка на това университетите се стремят да приемат колкото се може повече студенти (за да получат по-голямо финансиране). Така, че асистентите в университетите освен притиснати от срока, в който трябва да станат доктори, се оказва, че в момента взимат и повече часове от нормалното (става въпрос по-скоро за популярните специалности). Това ще се промени в близко бъдеще, тъй като предстои поколение от период на демографски срив, но към текущия момент проблема е голям, особено за популярните специалности.</p>
<p>От казаното в предишния параграф не можем да кажем, че &#8220;привлекателността&#8221; на професията се е повишила, напротив &#8211; заплащането е същото както преди, а задълженията са се увеличили. Това разбира би трябвало да се отчете от ръковоствата в университетите и има опасност те да направят всичко възможно да задържат персонала си, та дори това да означава да се даде научна степен на човек, който не я заслужава. <span style="text-decoration: underline;">Това все пак е редно да се отбележи, че не е грешка на закона</span>. Той сам по себе си е добър в частта си да изисква по-мотивирана работа от младите учени. Потенциалният проблем е следствие на наследената липса на престиж в длъжността &#8220;асистент&#8221; и силно занижения качествен контрол на научните трудове (за което ще говоря по-долу). Тоест реално може да се получи така, че в желанието си да се повишат изискванията за една длъжност ще се стигне до негативния ефект некачествени хора &#8220;да израстнат в кариерата през задната врата&#8221; и в крайна сметка да получим точно обратния ефект на заложения като философия в закона. Ако това се случи, то престижът на асистентската длъжност ще бъде уронен още повече и това по никакъв начин няма да спомогне за преодоляването на проблема &#8220;набиране на млади кадри в науката&#8221; в бъдеще.</p>
<p>Резултат №3:<strong> университетите (временно?) спряха да се интересуват от приемането на външни хора за научна степен &#8220;доктор&#8221;</strong></p>
<p>Горе казах, че броят на професорите се е увеличил, но това се оказва, че не е съвсем вярно за научна степен &#8220;доктор&#8221;. Там даже се наблюдава обратното &#8211; силно понижение на броя на защитилите доктори. В предишните си статии по темата аз очаквах да има &#8220;лавина от докторски дисертации&#8221;. Оказа се, че дълбоко съм сгрешил в преценката си. Предполагам това се дължи на грешната ми лична оценка относно престижа на научно-образователната степен. Аз очаквах, че либерализирането на режима за придобиване на научни степени ще отприщи интерес от страна на завършващи студенти, а от друга страна в университетите ще се заформят корупционни практики за лесно придобиване на такива. Това не се случи. Напротив &#8211; университетите по-скоро се фокусираха върху собствения си персонал от асистенти и започнаха да пренебрегват външните. Може би няма достатъчно финансова изгода от развитие на докторантските програми, а може би проблемът е само моментен, защото персонала на университетите се съсредоточи върху своето собствено кариерно израстване и липсват време и ресурси за допълнително обслужване на докторанти. Признавам си &#8211; не знам&#8230;</p>
<p>Освен това по тази точка трябва да се отбележи, че вече има налични големи съмнения върху обективността на критериите при сравнение на дипломите на &#8220;вътрешни&#8221; и &#8220;външни&#8221; докторанти.</p>
<p>Резултат №4:<strong> деградиране на престижа на академичните длъжности</strong></p>
<p>Пълната децентрализация на процеса на хабилитиране и процеса на оценка на дисертационните трудове от една страна се счита като голям позитив. От друга страна обаче <span style="text-decoration: underline;">пълната липса</span> на какъвто и да е централен коректив довежда до силни противоречия в научните среди. От една страна научната степен &#8220;доктор&#8221; е валидна за цялата страна, а от друга всеки университет има свои собствени критерии за това как да се присъжда и за какво да се присъжда.</p>
<p>Всъщност проблемът при научна степен &#8220;доктор&#8221; е незначително по-малък в сравнение с тази при хабилитираните преподаватели, особено тези на длъжност &#8220;професор&#8221;. Традицията в българското общество е отредила едно престижно място на хората, които са достигнали това ниво в кариерата си. От друга страна този престиж вече е поднесен под съмнение. Наблюдават се явни задкулисни &#8220;борби&#8221; между &#8220;професорите от преди&#8221; и &#8220;новите професори&#8221;. Дори започва движение за създаване на неформални организации като &#8220;клуб на пълните професори&#8221;, т.е. &#8220;професори, които са с научна степен доктор на науките&#8221;. Подобно развитие очевидно ще доведе до явна деградация на духа в академичните среди и може да доведе до спадане на престижа на преподавателската професия, особено що се отнася до длъжността &#8220;професор&#8221;.</p>
<p>Резултат №5:<strong> напълно безконтролно оценяване на научни трудове<br />
</strong></p>
<p>От една страна държавата гарантира дипломите за бакалавър, магистър, доктор и доктор на науките, но от друга има пълна липса на минимални държавни критерии за качество. Липсва какъвто и да е качествен контрол на национално ниво. Изискванията почти навсякъде са предимно количествени, но по никакъв начин не и качествени. Практически на много места се оказва така, че е възможно да се отчитат бройки на публикации без да се взима под внимание тяхното съдържание.</p>
<p>Освен това няма какъвто и да е ред за приемане на жалби от хора, които по един или друг начин са били в конфликт с учебните заведения, в които работят &#8211; получава се така, че <span style="text-decoration: underline;">ощетените могат да се оплачат само пред хората, които са ги ощетили</span>. Не наричаше ли някой тази практика &#8220;феодални старци&#8221;? Не стана ли положението с обективността на оценяването на научните трудове още по-тежко спрямо предишното (а философията на закона трябваше да търси точно обратното)?</p>
<p>Резултат №6:<strong> предстоящ хаос поради неотчитане на демографските процеси</strong></p>
<p>Това, че университетите отпушиха хабилитационните си програми и разрешиха ТЕКУЩИЯ си проблем с незаетите бройки от една страна е добре. За съжаление го правят на принципа &#8220;а след нас потоп&#8221;. Само след две години започва да добива пълнолетие поколението от 1996 година, а именно тогава започва и една сравнително дълга поредица от години, в които броят на студентите стремително ще намалее. Именно в този период от време очаквайте да станем свидетели на големи &#8220;борби за часове&#8221; в доста от по-непопулярните специалности в университетите, където преподавателите няма да могат да си изпълняват нормативите. Те всъщност и сега ги има, но положението все пак е &#8220;закърпено&#8221; на този етап.</p>
<p>Затова Либерализацията на заемането на длъжностите от една страна е нещо много добро, но пълната абдикация на централния орган от друга е нещо много лошо. Отново няма баланс между интересите на университетите и интересите на държавата. Не беше ли по-мъдро да се инвестира в млади кадри, вместо да се наливат безумно големи пари в развитието на стари (повечето от които скоро ще са в процес на пенсиониране)? Казвам това не за да кажа нещо лошо за по-възрастните ми колеги &#8211; за тях смятам, че в по-голямата си част заслужават много повече, отколкото получават. Казвам го, защото реалността наистина е такава &#8211; бедни сме и трябва да се разпростираме според чергата си.</p>
<p>Впрочем за случващото се у нас имаме много пресен пример от Румъния. Нашата съседка преди нас либерализира и децентрализара присъждането на научни степени и звания. Малко след като ние приехме нашия закон (който е направен по пълно подобие на техния), в Румъния беше приет нов закон, който прекрати либерализацията на научните степени и звания и спешно спря лавината от присъдждани звания и степени. При това там създадоха не един, а два нови контролни органа &#8211; &#8220;Национална комисия за атестация на кадрите и на дипломите&#8221; (изготвящ критериите за оценка на дипломи) и &#8220;Национален съвет за научните изследвания&#8221; (общо казано &#8220;изпитна комисия&#8221;).</p>
<p>Дали реформата в Румъния е била погрешна, а ние сме я направили по-добре от тях не се наемам да кажа. От всичко, което виждам в момента обаче мога да твърдя, че румънците вече се връщат от пътя, по който ние в момента вървим. И не са доволни от него&#8230;</p>
<p>Впрочем към момента в България вече Софийски Университет и Шуменски университет са въвели частични мораториуми върху хабилитациите на преподаватели в тях. При тях проблемът обаче не е, че отчитат някакви демографски процеси, а е чисто финансов &#8211; броят на хабилитациите при тях е надскочил бюджетите на учебните заведения. При всички случаи обаче процесът на румънския опит започва да се проявява и при нас.</p>
<p>Резултат №7:<strong> неясни критерии за назначаване на научните журита</strong></p>
<p>В закона е записано ясно, че научните журита се състоят от две части &#8211; хабилитирани преподаватели от самия университет и външни преподаватели от други университети. Това разбира се са едни много добри намерения. От тук нататък обаче всичко е оставено &#8220;на произвола на съдбата&#8221;:</p>
<ul>
<li>Няма никаква прозрачност при назначаването на хора в научни журита;</li>
<li>Формират се групи със съответни &#8220;сфери на влияние&#8221;.</li>
</ul>
<p>Или казано по-простичко едни университети помагат на кадрите на други, в замяна другите правят същото обратно за първите. Освен това по този начин ръководствата на университетите добиват огромен контрол над научното развитие на персонала си (което напомням по философията на закона трябва да е отделно от длъжностното). Реално начинът, по който се назначава едно научно жури може да определи дали оценявания човек ще получи или няма да получи положителна оценка. При това, както споменах по-горе, няма адекватна възможност за протестиране. Това разбира се води само до едно единствено нещо: формиране на групи от т.нар. &#8220;феодални старци&#8221;, но този път на още по-лошото &#8211; местно ниво.</p>
<p>Резултат №8:<strong> законът НЕ доведе до увеличаване на младите учени</strong></p>
<p>А това уж всъщност беше основната му цел &#8211; да насърчи младите хора. Всъщност се получи точно обратното. Няма какво да се лъжем &#8211; младите хора досега ставаха асистенти главно заради лекия режим на работа, който им позволяваше да работят паралелно по странични проекти, при това рядко научни. Те по никакъв начин не бяха и все още не са мотивирани финансово. В момента &#8220;лекия режим&#8221; вече никак не е толкова лек (притиснати са от сроковете за защита на дисертация) и интереса естествено още повече намалява. В момента положението се крепи главно заради последиците от икономическата криза и нарастналата безработица. Ако икономиката се стабилизира, то не се съмнявайте, че кризата за намиране на млади научни кадри ще стане още по-тежка.</p>
<p>Аз смятам, че отсега нататък в университетите ще се засилва познатата ни тенденцията за наемане на &#8220;оборотни преподаватели&#8221;, т.е. млади хора, които стават асистенти за няколко години, но после напускат. Целите на такива хора също отдавна са ясни за всички &#8211; набор на персонал за частни фирми и/или натрупване на престиж в личната автобиография, който да им помогне при реализация в чужбина. Научната дейност при такива хора обикновено е сведена до минимум. Впрочем никой не може да гарантира, че наличните млади хора, работещи в сферата на науката и образованието в момента, няма да напуснат системата при евентуално стабилизиране на бизнес климата в страната. Даже на този етап аз силно ще бъда очуден ако това не се случи и текущите кадри се задържат при сегашните условия на труд&#8230;</p>
<p><strong>Обобщен резултат: преподавателите и учените изместиха фокуса си от научната работа към бизнес интереси<br />
</strong></p>
<p>Липсата на ясни минимални критерии за оценка на научни трудове в комбинация с огромното влияние на ръководствата на университетите над персонала, както и явната обезценка на научните степени и звания, водят до това, че преподавателите и учените все по-малко се интересуват от наука. За сметка на това за тях става все по-важен кариеризма, т.е. вписването в организацията на финансовите потоци. Казано по-простичко &#8211; ако направите голямо научно откритие, то не е гарантирано по никакъв начин, че въобще ще се издигнете, но ако успеете да се впишете в бизнес модела и да спомогнете за облагодетелстването на съответния &#8220;кръг на интереси&#8221;, то издигането ви е вързано в кърпа. Колкото до научните приноси на избралите втория начин &#8211; за израстването на един преподавател/учен в момента е важно &#8220;да има бройки публикации&#8221; и няма абсолютно никакво значение какво е тяхното качество. Преди поне то се съблюдаваше от ВАК, въпреки постоянните протести и масово неодобрение.</p>
<p><strong>Не всичко е толкова лошо</strong></p>
<p>Да се сатанизира всичко в настоящия закон и да се отхвърля цялата му философия ще бъде точно толкова погрешно, колкото отхвърлянето на всички стари наложени практики с ВАК, както направи самия той. Не е добре да се повтаря грешката на написания закон при работата по очевидната нужда от написване на нов такъв. Затова смятам за важно да се отбележат и положителните страни на проведената реформа:</p>
<ul>
<li>Първо трябва да кажа сериозно, че <span style="text-decoration: underline;">децентрализацията не е лошо нещо</span>. Просто трябва да се направи &#8220;крачка назад&#8221; и да се намери баланс между държавния контрол и автономията. В никакъв случай не мога да бъда привърженик на обратно пълно централизиране на контрола (въпреки, че според мен ще води до по-добри резултати от сега постигнатите).</li>
<li>Второ <span style="text-decoration: underline;">наложените по-строги изисквания за заемане на академични длъжности на ниските нива (асистент и главен асистент) са нещо много добро</span>. Със сигурност трябва да бъде запазено, но и последвано от още допълнителни мерки. На първо време трябва спешно да се помисли за въвеждането на стимули за трайно задържане на млади хора в сферата на науката и образованието (със сигурност на първо място финансови). Освен това трябва да се промени философията на кариерното развитие &#8211; младите хора трябва да се фокусират върху иновациите и научните открития, а не към други обслужващи финансовите потоци дейности.</li>
<li><span style="text-decoration: underline;">Обособяването на научни журита е добра практика</span>. Просто трябва да бъде изгладен начина на тяхното формиране и да бъде контролирано тяхното функциониране.</li>
<li><span style="text-decoration: underline;">Въведеното уеднаквяване на академичните длъжности между научните организации и университетите е силно позитивно за системата</span>. Посоката, в която трябва да се продължи е да станат и еднакви по качество, а не само на хартия.</li>
</ul>
<p>Надявам се, че следващото правителство ще прояви желание да поправи белите и да осъществи тези неща. За текущото вече няма време, а няма и признаци за желание. За съжаление отново загубихме четири ценни години като управляващите отричаха, чупиха и продаваха вече съграденото, а не успяваха да изградят ново, което да е по-хубаво.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/politics/8069-education-system-restructure-summary/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Подготвени и параметризирани заявки</title>
		<link>http://www.cphpvb.net/db/8068-prepared-sql-statements/</link>
		<comments>http://www.cphpvb.net/db/8068-prepared-sql-statements/#comments</comments>
		<pubDate>Wed, 11 Apr 2012 16:08:12 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8068</guid>
		<description><![CDATA[Стандартно една SQL заявка преминава през два етапа &#8211; подготовка (което включва лексикален и синтактичен анализ) и изпълнение. Подготовката е нещо подобно на компилацията при езиците за програмиране &#8211; текстът въведен в заявката се подготвя до изпълним за средата код. След това този код се изпълнява и се връща резултат към софтуерния продукт. Подготвените заявки [...]]]></description>
			<content:encoded><![CDATA[<p>Стандартно една SQL заявка преминава през два етапа &#8211; подготовка (което включва лексикален и синтактичен анализ) и изпълнение. Подготовката е нещо подобно на компилацията при езиците за програмиране &#8211; текстът въведен в заявката се подготвя до изпълним за средата код. След това този код се изпълнява и се връща резултат към софтуерния продукт. Подготвените заявки ни дават възможност да разделим тези две стъпки и да ги изпълняваме отделно една от друга. Това означава, че имаме възможност първо да &#8220;компилираме&#8221; заявката с една команда, а да я изпълним в последствие чрез втора.</p>
<p>Подготвените заявки могат да бъдат с и без параметри. Ето един пример за такава заявка без параметри:<span id="more-8068"></span></p>
<blockquote>
<pre>mysql&gt; USE world;
Database changed
mysql&gt; PREPARE citycount FROM
    -&gt; 'SELECT COUNT(*) FROM city';
Query OK, 0 rows affected (0.00 sec)
Statement prepared</pre>
</blockquote>
<p>В горния пример ние подготвихме заявката &#8216;SELECT COUNT(*) FROM city&#8217; за изпълнение. В последствие можем да я изпълним по следния начин:</p>
<blockquote>
<pre>mysql&gt; EXECUTE citycount;
+----------+
| COUNT(*) |
+----------+
|     4079 |
+----------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>&#8220;Параметризирани&#8221; са тези подготвени заявки, които не са просто статично зададени, а на които им подаваме различни параметри, според които можем да имаме различен резултат. Например ако искаме не просто броя на всичките градове в системата, а градовете от определена държава, то можем да подготвим заявката по следния начин:</p>
<blockquote>
<pre>mysql&gt; PREPARE citycount_by_country_code FROM
    -&gt; 'SELECT COUNT(*) FROM city WHERE CountryCode = ?';
Query OK, 0 rows affected (0.30 sec)
Statement prepared</pre>
</blockquote>
<p>Обърнете внимание на въпросителния знак &#8211; той указва мястото, където ще бъде &#8220;вмъкнат&#8221; параметъра в последствие. Ето как правим това:</p>
<blockquote>
<pre>mysql&gt; SET @ccode = 'BGR';
Query OK, 0 rows affected (0.00 sec)

mysql&gt; EXECUTE citycount_by_country_code USING @ccode;
+----------+
| COUNT(*) |
+----------+
|       10 |
+----------+
1 row in set (0.00 sec)

mysql&gt; SET @ccode = 'GBR';
Query OK, 0 rows affected (0.00 sec)

mysql&gt; EXECUTE citycount_by_country_code USING @ccode;
+----------+
| COUNT(*) |
+----------+
|       81 |
+----------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Разбира се параметрите може да са повече от един, като единственото което трябва да съблюдаваме е техния ред:</p>
<blockquote>
<pre>mysql&gt; PREPARE citycount_by_code_and_district FROM
    -&gt; 'SELECT COUNT(*) FROM city
    '&gt;  WHERE CountryCode = ? AND District = ?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

mysql&gt; SET @ccode = 'BGR';
Query OK, 0 rows affected (0.00 sec)

mysql&gt; SET @district = "Plovdiv";
Query OK, 0 rows affected (0.00 sec)

mysql&gt; EXECUTE citycount_by_code_and_district
    -&gt; USING @ccode, @district;
+----------+
| COUNT(*) |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>След като спрем да ги използваме е нужно да &#8220;почистим&#8221;, т.е. да освободим паметта от подготвените/параметризираните заявки, които сме направили. Това се прави чрез командата DEALLOCATE:</p>
<blockquote>
<pre>mysql&gt; mysql&gt; DEALLOCATE PREPARE citycount;
Query OK, 0 rows affected (0.00 sec)

mysql&gt; DEALLOCATE PREPARE citycount_by_code
Query OK, 0 rows affected (0.00 sec)

mysql&gt; DEALLOCATE PREPARE citycount_by_code_and_district;
Query OK, 0 rows affected (0.00 sec)</pre>
</blockquote>
<p>Освен това е редно да споменем, че параметризирани заявки могат да се правят и върху INSERT/UPDATE/DELETE, както и върху множество други оператори, а не само върху SELECT.</p>
<p>Какво печелим от подготвените заявки без параметри? Идеята за тях се свежда до това да направим подготовката на заявката само веднъж, а след това да я изпълняваме множество пъти. Ако трябва да изпълняваме множество от едни и същи динамични заявки, то за всяка една от тях ще имаме процес на подготвяне и изпълнение, т.е. при N заявки ще имаме N подготовки, а иначе ще имаме само една. Все пак имайте и в предвид това, че <span style="text-decoration: underline;">за нуждите на еднократно изпълнение на заявка подготвените заявки са по-бавни от динамичните</span>.  Причината е, че се заделят допълнителни ресурси за поддържане на &#8220;дървото на изпълнение&#8221; на компилираната заявка. Тоест ние бихме спечелили от подготвени заявки без параметри само тогава, когато изпълняваме заявките много пъти в една сесия.</p>
<p>В общия случай обаче от подготвените заявки без параметри не се печели почти нищо, защото както подготвените, така и обикновените динамични заявки ще се възползват от query_cache, а кешът  тъй или иначе е най-бързия възможен начин за връщане на резултат от заявките. Тоест <span style="text-decoration: underline;">от подготвените заявки без параметър печелим тогава, когато се налага да изпълняваме заявките много поредни пъти и НЕ се използва query_cache</span>. Като частична полза може да се каже това, че при повторни изпълнения на заявката към сървъра се подава само низ &#8220;EXECUTE &lt;име-на-заявка&gt;&#8221;, а не целия текст на SQL заявката (което може да ви спести малко количество мрежови трафик за всяка заявка).</p>
<p>Що се отнася до параметризираните заявки &#8211; там вече предимството на подготвената заявка се проявава в повече практически ситуации. Предимствата основно са три:</p>
<ul>
<li>Ако трябва да изпълните една и съща заявка много пъти и <span style="text-decoration: underline;">с различни параметри</span>, то параметризираните заявки ще имат значително по-голямо бързодействие спрямо динамичните;</li>
<li>При многократно изпълнение се намалява мрежовия трафик (вече го споменахме в предишния параграф), макар и незначително;</li>
<li>Предотвратява възможностите за атаки от тип &#8220;SQL Injection&#8221;, понеже логиката на заявките вече е предварително зададена и няма начин да бъде променена по време на изпълнение. Все пак внимавайте &#8211; това, че сме защитени от такива атаки не означава, че &#8220;сме си свършили работата&#8221;. Много по-добре е въобще да НЕ се изпълнява SQL заявка при въведени невалидни данни, отколкото да се изпълнява такава, което очевидно ще изхаби ресурси, а резултатът от нея (би трябвало да) е предварително известен (тоест безсмислено търсен). Валидирането на коректността на данните като правило е добре на първо място да е задача на приложението (при това извършено непосредствено след получаването им) и в никакъв случай НЕ трябва отговорността за това да се прехвърля напълно върху системата за съхранението им.</li>
</ul>
<p>Ако параметризирана заявка се изпълнява еднократно или множество пъти, но с едни и същи параметри, то предимството от първата точка в общия случай се изгубва по същия начин, както в параметризираните заявки без параметри &#8211; в тези случаи с предимство ще се стремим да се възползваме от кеша на заявките (query cache).</p>
<p>Сега остава да дискутираме и начините за практическа употреба на подготвени заявки в реално действащи приложения. Горният метод за изпълнение на подготвени заявки се нарича &#8220;SQL интерфейс&#8221;. С него софтуерните продукти работещи с MySQL база от данни подават заявките по същия начин, както го правят с динамичните заявки, а подготовката и изпълнението се правят изцяло от страната на СУБД.</p>
<p>Съществуват и други, значително по-добри начини за работа с подготвени заявки, например MySQL C API client library (за програми написани са езика за програмиране C), MySQL Connector/J (за приложения на Java) и MySQL Connector/Net (за ADO.Net). При традиционния протокол заявките се изпращат към MySQL като текстови низ, след което самата СУБД конвертира стойностите на параметрите до съответния тип данни. Споменатите интерфейси позволяват програмата да комуникира директно със СУБД като предава данните като бинарна поредица, като до голяма степен се запазват типовете на данните. По-важното в случаят е, че <span style="text-decoration: underline;">интерфейсите минимизират мрежовия трафик между програмата и СУБД</span> и то този път значително &#8211; практически се предават само стойностите на параметрите, заедно с число-идентификатор на заявката, а не целия й текст (дори в SQL интерфейса трябваше да се подава думата &#8220;EXECUTE&#8221; и името на заявката като текстови низове, което е доста по-голямо количество информация спрямо едно обикновено число-идентификатор). <span style="text-decoration: underline;">Затова е препоръчително да се възползвате от средите за програмиране, които предлагат възможност за работа с такива интерфейси</span>.</p>
<p>Например в PHP разширението mysqli.so (забележете, че в края на името има &#8220;i&#8221;, защото старото разширение mysql.so не се възползва от споменатия интерфейс) работи аналогично на C API client library (има почти пълно съответствие между двете). Ето примерен код за работа с параметризирани заявки в PHP:</p>
<blockquote>
<pre>// Свързваме се с базата от данни
$link = mysqli_connect('хост','име','парола','база от данни');
if(mysqli_connect_errno()){
	echo 'Проблем при връзката с базата от данни';
	exit;
}
// Заявката, която ще подготвим
$query = "SELECT COUNT(*) AS citiesN FROM city WHERE CountryCode = ?";
// Параметъра, който ще подадем
$ccode_param = "BGR";
// Инициализиране на инстанция за заявката
$statement = mysqli_stmt_init($link);
// Подготвяме заявката за изпълнение
if (mysqli_stmt_prepare($statement, $query)){
    // Подаваме параметъра към вече подготвената заявка
    // "s" означава "string", т.е. параметъра е текстови низ
    mysqli_stmt_bind_param($statement, "s", $ccode_param);
   // Изпълняваме заявката
   mysqli_stmt_execute($statement);
   // Ще записваме резултата от заявката в променлива $result
   mysqli_stmt_bind_result($statement, $result);
   // Взимаме резултата от изпълнението на заявката
   // Той ще бъде записан в $result
   mysqli_stmt_fetch($statement);
   // Отпечатваме резултата
   if(isset($result)) echo "Броят на градовете в $ccode_param e $result";
   else "Броят на градовете в $ccode_param e 0";
   // Затваряме интерфейса за параметризираната заявка
   mysqli_stmt_close($statement);
}
// Затваряме връзката с базата от данни
mysqli_close($link);</pre>
</blockquote>
<p>В горния пример разбира се ние изпълнихме заявката само веднъж, т.е. не се възползвахме от предимствата на подготвените заявки, а даже напротив &#8211; разходвахме (макар и минимално) допълнителни ресурси. Тук обаче е момента да отбележим нещо много важно: <span style="text-decoration: underline;">винаги затваряйте интерфейса за параметризираната заявка от момента, от който няма да я използвате повече</span>  &#8211; от примера това става с извикването на функцията mysqli_stmt_close. Често срещана практика при програмистите е да забравят да затварят връзките си към базите от данни, което не е прекалено голям проблем, понеже стандартно те се затварят с края на изпълнението на PHP скрипта. При изпълнение на подготвени заявки обаче се поддържа отделна нишка с връзка към сървъра за всяка една параметризирана заявка, а ние казахме, че използваме подготвените заявки предимно тогава, когато ще ги изпълняваме многократно, т.е. очаква се скрипта да работи с големи обеми от информация и да отнема дълго време за изпълнение. Умножете броя на връзките за параметризирани заявки по броя на потребителите, които използват системата в един момент от време и ще видите, че ненужно поддържаните връзки към базата от данни могат да отнемат значително количество ресурси на СУБД, без да ги употребяват. В практиката често се среща претоварване на SQL сървър поради неправилно менажиране на подготвени заявки от страна на приложението, което работят с него.</p>
<p>Някои от интерфейсите, като например Connector/J и Connector/Net поддържат и подготовка на заявките от клиентската страна, т.е. ЧАСТ от &#8220;компилирането&#8221; се извършва от приложението, а СУБД го приема като (полу)готов за изпълнение код. В PHP &#8220;подготовка на заявките от страна на клиента&#8221; (client-side prepared statements) се поддържа от разширението PDO. На този етап това предлага доста по-бързо изпълнение на подготвени заявки изпълнени еднократно, но е значително по-бавно при многократно изпълнение на една и съща заявка спрямо &#8220;подготовката от страна на сървъра&#8221; (server-side prepared statements, което разгледахме като практика по-горе).</p>
<p><strong>Обобщение</strong>: Използвайте подготвени и параметризирани заявки тогава, когато се налага да изпълнявате една и съща заявка множество пъти. Когато е възможно винаги се възползвайте от интерфейсите за подаване на подготвени заявки, а не SQL интерфейса, но в същото време не забравяйте стриктно да затваряте неактивните връзки към базата данни. Подготвените заявки НЕ трябва да се използват като основно средство за сигурност.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8068-prepared-sql-statements/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Кога The Exploited ще издадат нов албум?</title>
		<link>http://www.cphpvb.net/fun/8063-exploited-new-album/</link>
		<comments>http://www.cphpvb.net/fun/8063-exploited-new-album/#comments</comments>
		<pubDate>Mon, 09 Apr 2012 20:48:33 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Математика]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8063</guid>
		<description><![CDATA[Любимата ми група The Exploited е много стара, но за сметка на това не може да се похвали с огромно количество студийни албуми. За вече над 30 годишното си съществуване те имат само 9. Ето ги: Punks Not Dead &#8211; 1981 Troops of Tomorrow &#8211; 1982 Let&#8217;s Start a War&#8230; (Said Maggie One Day) &#8211; [...]]]></description>
			<content:encoded><![CDATA[<p>Любимата ми група The Exploited е много стара, но за сметка на това не може да се похвали с огромно количество студийни албуми. За вече над 30 годишното си съществуване те имат само 9. Ето ги:</p>
<ol>
<li>Punks Not Dead &#8211; 1981</li>
<li>Troops of Tomorrow &#8211; 1982</li>
<li>Let&#8217;s Start a War&#8230; (Said Maggie One Day) &#8211; 1983</li>
<li>Horror Epics &#8211; 1985</li>
<li>Death Before Dishonour &#8211; 1987</li>
<li>The Massacre &#8211; 1990</li>
<li>Beat the Bastards &#8211; 1996</li>
<li>Fuck the System &#8211; 2003</li>
</ol>
<p>Кога ще излезе следващия албум?<span id="more-8063"></span></p>
<p>Можем да представим поредицата от текущите албуми като една редица. Ще се интересуваме от интервала между издаваните албуми, като първия ще го приемем за &#8220;началото на епохата&#8221;, т.е. стойност 0. Разликата между първия и втория албум е една година, разликата между втория и третия е една година, разликата между третия и четвъртия е 2 и т.н. Получава се следната редица:</p>
<p style="text-align: center;"><strong>0 1 1 2 2 3 6 7</strong></p>
<p>Ако знаем следващото число от редицата, то ще можем да кажем и през коя година ще бъде издаден албума. Какво казва математиката?</p>
<p>Търсим функция f(x) такава, че:</p>
<blockquote><p>f(1) = 0<br />
f(2) = 1<br />
f(3) = 1<br />
f(4) = 2<br />
f(5) = 2<br />
f(6) = 3<br />
f(7) = 6<br />
f(8) = 7</p></blockquote>
<p>Има два популярни начина да намерим полином, който &#8220;минава&#8221; през тези точки. Първият е интерполационния полином на Лагранж:</p>
<blockquote><p>0(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8) / ((1-2)(1-3)(1-4)(1-5)(1-6)(1-7)(1-8))+<br />
1(x-1)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8) / ((2-1)(2-3)(2-4)(2-5)(2-6)(2-7)(2-8))+<br />
1(x-1)(x-2)(x-4)(x-5)(x-6)(x-7)(x-8) / ((3-1)(3-2)(3-4)(3-5)(3-6)(3-7)(3-8))+<br />
2(x-1)(x-2)(x-3)(x-5)(x-6)(x-7)(x-8) / ((4-1)(4-2)(4-3)(4-5)(4-6)(4-7)(4-8))+<br />
2(x-1)(x-2)(x-3)(x-4)(x-6)(x-7)(x-8) / ((5-1)(5-2)(5-3)(5-4)(5-6)(5-7)(5-8))+<br />
3(x-1)(x-2)(x-3)(x-4)(x-5)(x-7)(x-8) / ((6-1)(6-2)(6-3)(6-4)(6-5)(6-7)(6-8))+<br />
6(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-8) / ((7-1)(7-2)(7-3)(7-4)(7-5)(7-6)(7-8))+<br />
7(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-7) / ((8-1)(8-2)(8-3)(8-4)(8-5)(8-6)(8-7)) = f(x)</p></blockquote>
<p>Другият е формулата на Нютон за разделените разлики):</p>
<blockquote><p>f(x) = x-1-<br />
(x-1)(x-2)/2+<br />
2(x-1)(x-2)(x-3)/(2*3)-<br />
4(x-1)(x-2)(x-3)(x-4)/(2*3*4)+<br />
8(x-1)(x-2)(x-3)(x-4)(x-5)/(2*3*4*5)-<br />
13(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)/(2*3*4*5*6)+<br />
14(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)/(2*3*4*5*6*7)</p></blockquote>
<p>И двата полинома се опростяват (интерполационния полином на Лагранж и формулата на Нютон дават като резултат едни и същи полиноми) до:</p>
<blockquote><p>f(x) = x^7/360-23x^6/240+193x^5/144-469x^4/48+28613x^3/720-1337x^2/15+1211x/12-43</p></blockquote>
<p>Вече можем да потърсим колко години ще е разликата между 8мия и 9тия албум на групата:</p>
<blockquote><p>f(9) = 9^7/360-23*9^6/240+193*9^5/144-469*9^4/48+28613*9^3/720-1337*9^2/15+1211*9/12-43</p>
<p>=&gt; f(9) = 8</p></blockquote>
<p>Това е и следващото число от поредицата. Тоест The Exploited би трябвало да издадат албума си през 2003+8 = &#8230; <strong>2011г. </strong>Е, за наше голямо съжаление групата НЕ издаде албум през 2011г., т.е. каквото и да правят в момента &#8211; хей момчета, изоставате от графика си!</p>
<p>Разбира се <span style="text-decoration: underline;">горните съждения бяха тотално погрешни</span>, защото когато и да издадат нов албум, ние винаги можем да намерим нов полином, който минава през всички точки, включително новата. Той просто ще е от по-висока с единица степен. Ако например приемем, че The Exploited ще издадат нов албум догодина (т.е. 2013г., ха дано), то разликата с последния албум ще е 10 години и редицата ще е станала <strong>0 1 1 2 2 3 6 7 10</strong>. Интерполационният полином ще е:</p>
<blockquote><p>0(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8)(x-9) / ((1-2)(1-3)(1-4)(1-5)(1-6)(1-7)(1-8)(1-9))+<br />
1(x-1)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8)(x-9) / ((2-1)(2-3)(2-4)(2-5)(2-6)(2-7)(2-8)(2-9))+<br />
1(x-1)(x-2)(x-4)(x-5)(x-6)(x-7)(x-8)(x-9) / ((3-1)(3-2)(3-4)(3-5)(3-6)(3-7)(3-8)(3-9))+<br />
2(x-1)(x-2)(x-3)(x-5)(x-6)(x-7)(x-8)(x-9) / ((4-1)(4-2)(4-3)(4-5)(4-6)(4-7)(4-8)(4-9))+<br />
2(x-1)(x-2)(x-3)(x-4)(x-6)(x-7)(x-8)(x-9) / ((5-1)(5-2)(5-3)(5-4)(5-6)(5-7)(5-8)(5-9))+<br />
3(x-1)(x-2)(x-3)(x-4)(x-5)(x-7)(x-8)(x-9) / ((6-1)(6-2)(6-3)(6-4)(6-5)(6-7)(6-8)(6-9))+<br />
6(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-8)(x-9) / ((7-1)(7-2)(7-3)(7-4)(7-5)(7-6)(7-8)(7-9))+<br />
7(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)(x-9) / ((8-1)(8-2)(8-3)(8-4)(8-5)(8-6)(8-7)(8-9))+<br />
10(x-1)(x-2)(x-3)(x-4)(x-5)(x-6)(x-7)(x-8) / ((9-1)(9-2)(9-3)(9-4)(9-5)(9-6)(9-7)(9-8)) = f(x)</p>
<p>=&gt; f(x) = x^8/20160+x^7/1008-11x^6/160+803x^5/720-8311x^4/960+2621x^3/72-419701x^2/5040+20051x/210-41</p></blockquote>
<p>Ако приемете друга година, например 2015, то редицата ще стане <strong>0 1 1 2 2 3 6 7 12 </strong>и вие лесно ще намерите друг интерполационен полином. Всъщност:</p>
<ul>
<li><strong>Което и число да сложите на края на която и да е редица, винаги ще съществува полином, който да я интерполира по показания начин!</strong></li>
</ul>
<p>Следователно задачите от типа &#8220;познайте следващото число в редицата&#8221; винаги имат лесно тривиално решение по алгоритъм с намиране на интерполационен полином. Дали обаче то е &#8220;решението, което човека търси&#8221; е напълно субективен фактор. Всъщност решение на задачата &#8220;намерете следващото число от редицата x1, x2, x3, &#8230;, xn&#8221; може да е което и да е число по ваш дори произволен избор. Затова когато някой ви даде задача от типа &#8220;намерете следващото число&#8230;&#8221; се обърнете към него и му кажете смело &#8220;следващото число е ПЕТ!&#8221;. И няма да сбъркате &#8211; ще можете да му намерите формула, по която наистина ще се получава, че числото е точно 5 :)</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/fun/8063-exploited-new-album/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>В памет на Кърт Кобейн</title>
		<link>http://www.cphpvb.net/history/8060-kurt-cobain/</link>
		<comments>http://www.cphpvb.net/history/8060-kurt-cobain/#comments</comments>
		<pubDate>Mon, 09 Apr 2012 15:35:23 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[История]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8060</guid>
		<description><![CDATA[На 05 април се навършиха 18 години от смъртта на Кърт Кобейн. Ако беше жив днес, то той щеше да е на 45 години. Това е човекът, който издигна гръндж музиката на световната сцена в началото на 90те и той е човека, след чиято смърт тя започна да залязва. Едно е сигурно &#8211; поколенията деца [...]]]></description>
			<content:encoded><![CDATA[<p>На 05 април се навършиха 18 години от смъртта на Кърт Кобейн. Ако беше жив днес, то той щеше да е на 45 години. Това е човекът, който издигна гръндж музиката на световната сцена в началото на 90те и той е човека, след чиято смърт тя започна да залязва.</p>
<div id="attachment_8061" class="wp-caption aligncenter" style="width: 248px"><a href="http://www.cphpvb.net/wp-content/uploads/2012/04/kurt-cobain.jpg"><img class="size-medium wp-image-8061" title="Kurt Cobain of Nirvana" src="http://www.cphpvb.net/wp-content/uploads/2012/04/kurt-cobain-238x300.jpg" alt="Kurt Cobain of Nirvana" width="238" height="300" /></a><p class="wp-caption-text">Снимка от <a href='http://www.listal.com/viewimage/1962460' target='_blank'>Listal</a></p></div>
<p>Едно е сигурно &#8211; поколенията деца и юноши от онези години няма да го забравим и винаги ще остане в сърцата ни! По онова време бях наистина много малък, но няма да забравя &#8220;менкането&#8221; на аудио касетки, както и по колко десетки пъти се слушаше отново и отново всяка една от тях. Бях първият човек (поне в рамките на училището), който намери запис на албума &#8220;In Utero&#8221; (не пълен, а с две-три пропуснати песни). Беше велико (момичетата ми се молеха да им го дам :P). Днес намерих аудио касетката, която даже още е &#8220;жива&#8221; и дава чудесен звук.<span id="more-8060"></span></p>
<p>Предлагам ви да изслушате няколко от най-любимите ми песни на Нирвана (аз всъщност като се замисля от Нирвана нямам &#8220;нелюбими&#8221;):</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/Qj0KeBxLy9Y?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/Qj0KeBxLy9Y?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
Lounge Act</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/pkcJEvMcnEg?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/pkcJEvMcnEg?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
Lithium</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/QD0D7IuriWQ?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/QD0D7IuriWQ?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
Aneurysm</p>
<p><object width="560" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/5BE1KRj5iiM?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="560" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/5BE1KRj5iiM?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
Sappy</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/n6P0SitRwy8?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/n6P0SitRwy8?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
Heart Shaped Box</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/psvCUWzecGo?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/psvCUWzecGo?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
Rape Me</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/AhcttcXcRYY?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/AhcttcXcRYY?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
About a girl</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/TrpiM2oKTLI?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/TrpiM2oKTLI?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
Plateau</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/zgDKHG4K7g0?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/zgDKHG4K7g0?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
Oh Me!</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/qv96yJYhk3M?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/qv96yJYhk3M?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
You know you are right</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/LhRwkC6RxcU?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/LhRwkC6RxcU?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
In Bloom</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/TP06kxW_M3I?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/TP06kxW_M3I?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
I hate myself and I wanna die</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/hkEwIemPwWI?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/hkEwIemPwWI?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
School</p>
<p><object width="420" height="315"><param name="movie" value="http://www.youtube.com/v/XBOE2CC0YYY?version=3&amp;hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/XBOE2CC0YYY?version=3&amp;hl=en_US" type="application/x-shockwave-flash" width="420" height="315" allowscriptaccess="always" allowfullscreen="true"></embed></object><br />
Milk it!</p>
<p><object width="420" height="315" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"><param name="allowFullScreen" value="true" /><param name="allowscriptaccess" value="always" /><param name="src" value="http://www.youtube.com/v/q4gMfdfRRnA?version=3&amp;hl=en_US" /><param name="allowfullscreen" value="true" /><embed width="420" height="315" type="application/x-shockwave-flash" src="http://www.youtube.com/v/q4gMfdfRRnA?version=3&amp;hl=en_US" allowFullScreen="true" allowscriptaccess="always" allowfullscreen="true" /></object><br />
Something in the way</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/history/8060-kurt-cobain/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MySQL EXPLAIN &#8211; оптимизиране на заявки</title>
		<link>http://www.cphpvb.net/db/8044-mysql-explain/</link>
		<comments>http://www.cphpvb.net/db/8044-mysql-explain/#comments</comments>
		<pubDate>Fri, 06 Apr 2012 21:00:00 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8044</guid>
		<description><![CDATA[Заявките от тип &#8220;EXPLAIN SELECT&#8230;&#8221; се използват предимно от администраторите на бази от данни, за да видят т.нар. &#8220;query execution plan&#8221; (план за изпълнение на заявките). При слагането на префикс EXPLAIN преди дадена SELECT заявка тя не се изпълнява, а вместо това се извежда информация за това по какъв начин MySQL ще я изпълни. Всяка [...]]]></description>
			<content:encoded><![CDATA[<p>Заявките от тип &#8220;EXPLAIN SELECT&#8230;&#8221; се използват предимно от администраторите на бази от данни, за да видят т.нар. &#8220;query execution plan&#8221; (план за изпълнение на заявките). При слагането на префикс EXPLAIN преди дадена SELECT заявка тя не се изпълнява, а вместо това се извежда информация за това по какъв начин MySQL ще я изпълни.<span id="more-8044"></span></p>
<p>Всяка заявка EXPLAIN връща като резултат таблица с 9 колони. Нека да разгледаме пълния им списък и възможните стойности, като дадем кратко описание:</p>
<ul>
<li><strong>id</strong> – идентификационен номер на SELECT заявката. Вървят по ред започвайки от 1. Така може да разберете коя от вложените заявки се изпълнява първа, коя втора и т.н.</li>
<li><strong>select_type</strong>– Типа на SELECT заявката, който е един от следните:
<ul>
<li>SIMPLE – проста SELECT заявка без вложени заявки или UNION.</li>
<li>PRIMARY – най-външната (обрграждаща другите) заявка</li>
<li>UNION – заявка, резултатът от която ще бъде обединен към друга</li>
<li>DEPENDENT UNION – UNION заявка, която е вътрешна за друга заявка</li>
<li>SUBQUERY – вложен SELECT</li>
<li>DEPENDENT SUBQUERY – вложен SELECT, който е зависим от външни за него данни</li>
<li>DERIVED – SELECT заявка във FROM условие</li>
</ul>
</li>
<li><strong>table</strong> – име на таблицата</li>
<li><strong>type</strong>– тип на заявката
<ul>
<li>CONST – указва, че таблицата има първичен или уникален ключ, който се използва в условие за сравнение. Това означава, че винаги ще има не повече от един ред в резултатната таблица от заявката, което впрочем прави такива заявки доста бързи &#8211; MySQL спира прелистването при намиране на съвпадение.</li>
<li>SYSTEM – const заявка върху системна таблица</li>
<li>EQ_REF – използва се UNIQUE и NOT NULL ключ. Това например може да се случи в JOIN заявка с класическа връзка 1:1. Това е най-бързият възможен JOIN, защото присъединяваната таблица винаги връща един ред и прелистването спира дотам.</li>
<li>REF – използва се неуникален ключ без NULL стойности. Това например може да е връзка 1:M.</li>
<li>REF_OR_NULL – използва се неуникален ключ, който може да има NULL стойности.</li>
<li>INDEX_MERGE – специален случай, в който MySQL сам обединява индекси в един множествен. Обикновено се случва при наличието на условия OR в WHERE частта на заявката.</li>
<li>UNIQUE_SUBQUERY – имаме вложен SELECT (извикан с оператор IN), който връща уникални стойности (без повторения).</li>
<li>INDEX_SUBQUERY – същото като предишното, но вложения SELECT връща стойности с повторения.</li>
<li>RANGE – използва се множество от стойности върху индекса. Например оператор BETWEEN, &lt;, &gt;, &lt;=, &gt;= и т.н.</li>
<li>INDEX – указва, че ще бъде преровен ЦЕЛИЯТ index, т.е. ще се прелистят всичките му стойности.</li>
<li>ALL – ще бъде направен FULL TABLE SCAN върху таблицата. Това е най-бавната възможна операция.</li>
</ul>
</li>
<li><strong>possible_keys</strong> – възможните индекси, които MySQL може да използва за конкретната заявка.</li>
<li><strong>key</strong> – кой от възможните индекси е избран за конкретната заявка.</li>
<li><strong>key_len</strong> – колко байта от индекса ще се използват.</li>
<li><strong>rows</strong> – колко реда ще бъдат прелистени. Колкото по-малко, толкова по-добре.</li>
<li><strong>Extra</strong>– допълнителна информация.
<ul>
<li>Distinct – сървъра ще направи оптимизация като спре да търси повече колони при наличие на първо съвпадение (т.е. дублиращите ще се пропуснат)</li>
<li>Not exists – сървъра ще спре да търси повече съвпадения при първо такова в JOIN клауза.</li>
<li>range checked for each record – сървърът не е намерил подходящ индекс, но въпреки това ще прави пълно обхождане на съществуващ такъв, вместо FULL TABLE SCAN.</li>
<li>Using filesort – сървърът не използва индекс при сортирането.</li>
<li>Using index – сървърът използва индекс при сортирането.</li>
<li>Using temporary – ще се наложи създаване на временна таблица.</li>
<li>Using where – в заявката е използвана WHERE клауза за лимитиране на изхода.</li>
</ul>
</li>
</ul>
<p>Преди да започнем с практически примери ще ни трябва база от данни с достатъчно голямо количество информация. Нека например вземем <a title="Примерни бази от данни на MySQL" href="http://dev.mysql.com/doc/index-other.html" target="_blank">world-innodb</a>, която е дадена като примерна на сайта на MySQL. Тя има следната структура:</p>
<blockquote>
<pre>CREATE TABLE `country` (
  `Code` char(3) NOT NULL DEFAULT '',
  `Name` char(52) NOT NULL DEFAULT '',
  `Continent` enum('Asia','Europe','North America',
                   'Africa','Oceania','Antarctica',
                   'South America') 
              NOT NULL DEFAULT 'Asia',
  `Region` char(26) NOT NULL DEFAULT '',
  `SurfaceArea` float(10,2) NOT NULL DEFAULT '0.00',
  `IndepYear` smallint(6) DEFAULT NULL,
  `Population` int(11) NOT NULL DEFAULT '0',
  `LifeExpectancy` float(3,1) DEFAULT NULL,
  `GNP` float(10,2) DEFAULT NULL,
  `GNPOld` float(10,2) DEFAULT NULL,
  `LocalName` char(45) NOT NULL DEFAULT '',
  `GovernmentForm` char(45) NOT NULL DEFAULT '',
  `HeadOfState` char(60) DEFAULT NULL,
  `Capital` int(11) DEFAULT NULL,
  `Code2` char(2) NOT NULL DEFAULT '',
  PRIMARY KEY (`Code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `city` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `Name` char(35) NOT NULL DEFAULT '',
  `CountryCode` char(3) NOT NULL DEFAULT '',
  `District` char(20) NOT NULL DEFAULT '',
  `Population` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`ID`),
  KEY `CountryCode` (`CountryCode`),
  CONSTRAINT `city_ibfk_1` FOREIGN KEY (`CountryCode`) 
                           REFERENCES `country` (`Code`)
) ENGINE=InnoDB AUTO_INCREMENT=4080 DEFAULT CHARSET=latin1;

CREATE TABLE `countrylanguage` (
  `CountryCode` char(3) NOT NULL DEFAULT '',
  `Language` char(30) NOT NULL DEFAULT '',
  `IsOfficial` enum('T','F') NOT NULL DEFAULT 'F',
  `Percentage` float(4,1) NOT NULL DEFAULT '0.0',
  PRIMARY KEY (`CountryCode`,`Language`),
  KEY `CountryCode` (`CountryCode`),
  CONSTRAINT `countryLanguage_ibfk_1` 
                FOREIGN KEY (`CountryCode`)
                REFERENCES `country` (`Code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;</pre>
</blockquote>
<p>Нека разгледаме нашата първа елементарна заявка &#8211; искаме да изведем броят на държавите, които са с управление от тип &#8216;Republic&#8217;. Ето какво ще върне EXPLAIN на една такава заявка ако нямаме създадени никакви допълнителни индекси:</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM country
    -&gt; WHERE GovernmentForm = 'Republic';
+----+---------+------+---------------+------+---------+------+------+
| id | table   | type | possible_keys | key  | key_len | ref  | rows |
+----+---------+------+---------------+------+---------+------+------+
|  1 | COUNTRY | ALL  | NULL          | NULL | NULL    | NULL |  233 |
+----+---------+------+---------------+------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Първата важна колона, която трябва да разгледаме е &#8220;possible_keys&#8221;. Тя ни указва кои са индексите, които MySQL може да използва в заявката. В случая няма нито един индекс, който може да бъде използван. Съответно MySQL няма да използва никакъв индекс (стойността под key е NULL). Другата важна за нас колона е &#8220;rows&#8221; &#8211; тя показва колко реда са били прегледани, преди да се достигне до желания резултат. В случая са били прелистени 233 реда.</p>
<p>Ако вие използвате SELECT заявка, която не използва Index, то много често това означава, че можете да подобрите бързодействието чрез създаване на такъв. Нека да видим:</p>
<blockquote>
<pre>ALTER TABLE country
ADD INDEX GovernmentFormIndex (GovernmentForm);

mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM country
    -&gt; WHERE GovernmentForm = 'Republic';
+---------+------+---------------------+---------------------+---------+-------+------+
| table   | type | possible_keys       | key                 | key_len | ref   | rows |
+---------+------+---------------------+---------------------+---------+-------+------+
| COUNTRY | ref  | GovernmentFormIndex | GovernmentFormIndex | 45      | const |  122 |
+---------+------+---------------------+---------------------+---------+-------+------+</pre>
</blockquote>
<p>Виждаме, че вече има възможен индекс, MySQL го е избрал и най-важното &#8211; прелистените редове са почти два пъти по-малко, отколкото преди &#8211; 122. Това в общия случай ще означава, че SQL заявката ще се изпълнява близо два пъти по-бързо.</p>
<p>Разбира се индексите силно зависят от данните, които се записват в тях и обемът от информация, който се използва. Ето ви друг пример &#8211; искаме да изведем броят на градовете, които имат население над 5000 души. Първата заявка ще изпълним без индекс, а втората с:</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM city
    -&gt; WHERE city.population &gt; 5000;
+-------+------+---------------+------+---------+------+------+
| table | type | possible_keys | key  | key_len | ref  | rows |
+-------+------+---------------+------+---------+------+------+
| city  | ALL  | NULL          | NULL | NULL    | NULL | 4234 |
+-------+------+---------------+------+---------+------+------+
1 row in set (0.00 sec)

mysql&gt; ALTER TABLE city
    -&gt; ADD INDEX population (population);
Query OK, 0 rows affected (0.52 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM city
    -&gt; WHERE city.population &gt; 5000;
+-------+-------+---------------+------------+---------+------+------+
| table | type  | possible_keys | key        | key_len | ref  | rows |
+-------+-------+---------------+------------+---------+------+------+
| city  | range | population    | population | 4       | NULL | 4048 |
+-------+-------+---------------+------------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Виждаме, че тук създадохме индекс, той се използва, но резултатът в не е значителен &#8211; имаме само малко намаление на броя прелистени редове. Това се получава така, понеже почти всички градове в базата от данни са с повече от 5000 души, т.е. ние използваме почти цялата таблица, което се свежда доста близко до обикновен full table scan. Нека променим условието и изведем броят на градовете с над 1 милион души:</p>
<blockquote>
<pre>mysql&gt; ALTER TABLE city
    -&gt; DROP INDEX population;
Query OK, 0 rows affected (0.16 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM city
    -&gt; WHERE city.population &gt; 1000000;
+-------+------+---------------+------+---------+------+------+
| table | type | possible_keys | key  | key_len | ref  | rows |
+-------+------+---------------+------+---------+------+------+
| city  | ALL  | NULL          | NULL | NULL    | NULL | 4234 |
+-------+------+---------------+------+---------+------+------+
1 row in set (0.00 sec)

mysql&gt; ALTER TABLE city
    -&gt; ADD INDEX population (population);
Query OK, 0 rows affected (0.22 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT COUNT(*)
    -&gt; FROM city
    -&gt; WHERE city.population &gt; 1000000;
+-------+-------+---------------+------------+---------+------+------+
| table | type  | possible_keys | key        | key_len | ref  | rows |
+-------+-------+---------------+------------+---------+------+------+
| city  | range | population    | population | 4       | NULL |  237 |
+-------+-------+---------------+------------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Тук вече виждаме огромна разлика от близо 18 пъти по-малко прелиствания! Или колкото по-ограничено е количеството на данни в резултатната таблица, толкова по-ефективен е индекса.</p>
<p>По интересен става случаят, когато имаме логическа връзка между две колони. Нека изведем Европейските държави, които имат население над 40 милиона души:</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT name
    -&gt; FROM country
    -&gt; WHERE population &gt; 40000000
    -&gt;       AND
    -&gt;       Continent="Europe";
+---------+------+---------------+------+------+
| table   | type | possible_keys | key  | rows |
+---------+------+---------------+------+------+
| country | ALL  | NULL          | NULL |  233 |
+---------+------+---------------+------+------+
1 row in set (0.00 sec)

mysql&gt; ALTER TABLE country
    -&gt; ADD INDEX populationIndex (population);
Query OK, 0 rows affected (0.47 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; ALTER TABLE country
    -&gt; ADD INDEX continentIndex (Continent);
Query OK, 0 rows affected (0.19 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT name
    -&gt; FROM country
    -&gt; WHERE population &gt; 40000000
    -&gt;       AND
    -&gt;       Continent="Europe";
+---------+------+--------------------------------+----------------+------+
| table   | type | possible_keys                  | key            | rows |
+---------+------+--------------------------------+----------------+------+
| country | ref  | populationIndex,continentIndex | continentIndex |   46 |
+---------+------+--------------------------------+----------------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Тук виждаме нещо интересно &#8211; има два възможни индекса, от които MySQL си е избрал един. Нека видим какъв ще бъде резултата, ако използваме другия индекс (можем да принудим MySQL да го използва чрез командата USE INDEX след FROM):</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT name
    -&gt; FROM country
    -&gt; USE INDEX(populationIndex)
    -&gt; WHERE population &gt; 40000000
    -&gt;       AND
    -&gt;       Continent="Europe";
+---------+-------+-----------------+-----------------+---------+------+------+
| table   | type  | possible_keys   | key             | key_len | ref  | rows |
+---------+-------+-----------------+-----------------+---------+------+------+
| country | range | populationIndex | populationIndex | 4       | NULL |   28 |
+---------+-------+-----------------+-----------------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Ясно се вижда, че в зависимост от заявката, използването на един или друг индекс ще доведе до различно бързодействие. Причината в случая от примера е, че индексът continentIndex &#8220;не е достатъчно уникален&#8221;, т.е. в колоната има доста повторения, докато при populationIndex повторенията са много малко.</p>
<p>Вече можем да си изведем първи два основни извода за оптимизиране на заявки:</p>
<ul>
<li><strong>Колкото по-уникална </strong>(с различни стойности по редове)<strong> е една колона, толкова по-ефективни ще са индексите, които се правят по нея;</strong></li>
<li><strong>При по-ограничен изход от заявките </strong>(т.е. извличат се по-малко редове от оригиналната в изходната таблица)<strong> се получава по-голяма разлика в бързодействието между използването на индекс и full table scan.</strong></li>
</ul>
<p>Обратно &#8211; ако трябва да изведем информация от всички редове в съществуваща таблица или по дадената колона имаме много повторения на стойности, то индексите започват да губят ефект.</p>
<p>Можем ли да използваме повече от един индекс върху една таблица? Отговорът за MySQL е отрицателен &#8211; не може. Това, което можем да направим обаче е да се възползваме от &#8220;множествени индекси&#8221;, т.е. индекси създадени по повече от една колона. Ето пример с предишната заявка:</p>
<blockquote>
<pre>mysql&gt; ALTER TABLE country
    -&gt; ADD INDEX pop_cont_index (Continent, population);
Query OK, 0 rows affected (0.20 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT name
    -&gt; FROM country
    -&gt; USE INDEX(pop_cont_index)
    -&gt; WHERE population &gt; 40000000
    -&gt;       AND
    -&gt;       Continent="Europe";
+---------+-------+----------------+----------------+---------+------+------+
| table   | type  | possible_keys  | key            | key_len | ref  | rows |
+---------+-------+----------------+----------------+---------+------+------+
| country | range | pop_cont_index | pop_cont_index | 5       | NULL |    6 |
+---------+-------+----------------+----------------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Виждаме, че тук имаме гигантска разлика! Оригиналната заявка (без индекси) имаше нужда от 233 прелиствания, а с използването на правилния индекс ги сведохме до само 6, което е с близо 39 пъти по-високо бързодействие!</p>
<p>Тук е много важно да отбележим нещо &#8211; <span style="text-decoration: underline;">при създаването на множествени индекси е много важна подредбата на колоните</span>. Ако например в предишната заявка ги &#8220;обърнем&#8221; и сложим колоната population преди continent, то ще се получи следното:</p>
<blockquote>
<pre>mysql&gt; ALTER TABLE country
    -&gt; DROP INDEX pop_cont_index;
Query OK, 0 rows affected (0.16 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; ALTER TABLE country
    -&gt; ADD INDEX pop_cont_index (population, Continent);
Query OK, 0 rows affected (0.52 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql&gt; EXPLAIN SELECT name
    -&gt; FROM country
    -&gt; USE INDEX(pop_cont_index)
    -&gt; WHERE population &gt; 40000000
    -&gt;       AND
    -&gt;       Continent="Europe";
+---------+-------+----------------+----------------+---------+------+------+
| table   | type  | possible_keys  | key            | key_len | ref  | rows |
+---------+-------+----------------+----------------+---------+------+------+
| country | range | pop_cont_index | pop_cont_index | 4       | NULL |   28 |
+---------+-------+----------------+----------------+---------+------+------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Виждаме, че тук изгубихме ефекта от добавянето на continent в индекса. Това е така, понеже стойностите по редове в колона population са доста различни (почти без повторения), т.е. на изолираната единична стойност няма какво допълнително да се оптимизира. Ето как можем да изкараме още една зависимост:</p>
<ul>
<li><strong>При използване на множествен индекс в общия случай ще по-добре да слагаме първо колоната с повече повторения, а след нея по-уникалната такава.</strong></li>
</ul>
<p>Това &#8220;правило&#8221; разбира се може лесно да получи впечатляващи изключения (главно тогава, когато се окаже, че въобще не е нужно да бъде използвана една от двете колони). Затова <span style="text-decoration: underline;">винаги проверявайте и правете тестове</span>. Също така имайте в предвид, че когато в условието WHERE на заявката има логическо OR ще е особено подходящо да използвате именно множествен индекс.</p>
<p>Нуждата от оптимизация на заявките идва особено важна когато ни се налага да използваме JOIN между две и повече таблици. Тук EXPLAIN ще ни дава информация за всяка една от таблиците използвани във FROM условието:</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT country.Name as country, city.Name as city
    -&gt; FROM country JOIN city ON country.code = city.countryCode
    -&gt; WHERE country.Continent = "Europe";
+---------+---------------------------------------+----------------+--------------------+------+
| table   | possible_keys                         | key            | ref                | rows |
+---------+---------------------------------------+----------------+--------------------+------+
| country | PRIMARY,continentIndex,pop_cont_index | continentIndex | const              |   46 |
| city    | CountryCode                           | CountryCode    | world.country.Code |    7 |
+---------+---------------------------------------+----------------+--------------------+------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Виждаме, че тук има няколко възможни индекса. Индексите continentIndex и pop_cont_index за country ги създадохме ние в предишни заявки. Индексите PRIMARY за country и CountryCode за city са създадени автоматично при създаването на таблиците в MySQL. Последното е така, понеже едното е primary key, а другото е foreign key. За всички ключови колони по подразбиране се създават индекси. Тук при по-сложни WHERE условия също е добре да следим индексите, които се използват и при нужда да указваме кой от тях да бъде употряван или да създаваме допълнителни множествени.</p>
<p>Освен кой индекс да се използва има и някои други неща, които трябва да следим. Важна е и колона EXTRA от резултата на EXPLAIN заявката. Там ще виждате различни стойности, сред които &#8220;using where&#8221;, &#8220;using index&#8221;, &#8220;using filesort&#8221;, &#8220;using temporarily table&#8221; и др. Ако там специално присъства &#8220;using filesort&#8221; (може да се появи когато използвате ORDER BY), то това означава, че по време на сортирането не се използва индекс. Ето един пример:</p>
<blockquote>
<pre>mysql&gt; EXPLAIN SELECT name
    -&gt; FROM city
    -&gt; ORDER BY name DESC;
+-------+------+---------------+------+---------+------+------+----------------+
| table | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+-------+------+---------------+------+---------+------+------+----------------+
| city  | ALL  | NULL          | NULL | NULL    | NULL | 4234 | Using filesort |
+-------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Използването на индекс тук ще разреши проблема:</p>
<blockquote>
<pre>ALTER TABLE city
ADD INDEX nameIndex (name);

mysql&gt; EXPLAIN SELECT name
    -&gt; FROM city
    -&gt; ORDER BY name DESC;
+-------+-------+---------------+-----------+---------+------+------+-------------+
| table | type  | possible_keys | key       | key_len | ref  | rows | Extra       |
+-------+-------+---------------+-----------+---------+------+------+-------------+
| city  | index | NULL          | nameIndex | 35      | NULL | 4234 | Using index |
+-------+-------+---------------+-----------+---------+------+------+-------------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Така можем да изведем още едно &#8220;правило&#8221;:</p>
<ul>
<li><strong>Сортирането с индекс почти винаги е по-добро от такова без</strong>. При &#8220;лош индекс&#8221; (колона с много повторения) бързината при двете сортирания може да се доближи. Но дори тогава ако изходните данни са с много голям обем, то е възможно системата да изпита недостиг на оперативна памет (големината на sort_buffer променливата в my.cnf дефинира максимален лимит) и от там нататък MySQL ще трябва да създаде временна (temporarily) таблица на хард диска. Достигането до такава ситуация винаги е &#8220;убиец на производителност&#8221;. С използването на индекси при сортирането рядко ще достигаме до такива моменти. Така, че ако видите &#8220;using index&#8221; в колона &#8220;Extra&#8221;, то би трябвало всичко да е наред, докато &#8220;using filesort&#8221; не.</li>
</ul>
<p>И накрая, но не последно &#8211; <strong>създаването на излишни или лошо работещи индекси е вредно за производителността на системата</strong>. Причината е, че те също отнемат ресурси, за да бъдат поддържани. Поради тази причина можем да изведем последното (за статията) условие:</p>
<ul>
<li><strong>Създавайте само индекси, които ще използвате често</strong></li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8044-mysql-explain/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Задача от контролна работа март 2012г. &#8211; 2</title>
		<link>http://www.cphpvb.net/db/8042-exam-2012-2/</link>
		<comments>http://www.cphpvb.net/db/8042-exam-2012-2/#comments</comments>
		<pubDate>Fri, 06 Apr 2012 12:17:30 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8042</guid>
		<description><![CDATA[Нужно е да се направи база от данни за споделяне на статии между голяма група от потребители. В нея за всички потребители се пази уникален номер и уникално име. Всеки потребител може да има три основни роли – обикновен потребител (може да чете информация), автор (може да споделя информация) и рецензент (преглежда статии и ги [...]]]></description>
			<content:encoded><![CDATA[<p>Нужно е да се направи база от данни за споделяне на статии между голяма група от потребители. В нея за всички потребители се пази уникален номер и уникално име. Всеки потребител може да има три основни роли – обикновен потребител (може да чете информация), автор (може да споделя информация) и рецензент (преглежда статии и ги одобрява или отхвърля). Разбира се авторите и рецензентите сами по себе си са обикновени потребители. Освен това не е забранено на даден автор да бъде и рецензент на други статии.<span id="more-8042"></span></p>
<p>За всяка статия се пази нейното заглавие (до 160 символа) и съдържание (количество текстова информация, което ще е подходящо за поле от тип TEXT) и дата на публикуване. Когато някой автор добави нова статия в системата тя стандартно ще отива в „опашка за рецензиране“ (moderation queue). Това означава, че трябва някой рецензент да я провери и редактира преди да бъде официално публикувана. Статии, които не успеят да преминат успешно рецензия просто ще се изтриват. За статиите, които са одобрени задължително се пази информация за това кой е бил рецензента, който ги е одобрил.</p>
<p><strong>Задача 1</strong>. Направете ER диаграма на описаната горе база от данни, създайте я в MySQL и вмъкнете примерни данни в нея.</p>
<p><strong>Задача 2</strong>. Изведете списък със статиите на автор с име „Ivan”, които все още не са рецензирани.</p>
<p><strong>Задача 3</strong>. Изведете списък с имената на авторите, общия им брой рецензирани статии и общия им брой нерецензирани статии (т.е. които все още очакват рецензия). Потребителите, които въобще нямат статии да се пропуснат.</p>
<p><em>Решение</em>: Първо да определим данните, които ще се пазят със сигурност в базата от данни. Това са:</p>
<ul>
<li>Уникални номера на потребителите;</li>
<li>Потребителски имена;</li>
<li>Заглавия на статиите;</li>
<li>Съдържания на статиите;</li>
<li>Дати на публикуване на статиите;</li>
<li>Кой потребител е автор на дадена статия;</li>
<li>Кой потребител е рецензент на дадена статия (ако е рецензирана).</li>
</ul>
<p>Очевидно зависимите данни са уникалните номера на потребителите и потребителските имена от една страна и заглавията, съдържанието и датата на публикуване за статиите от друга. Те могат да характеризират два класа обекти &#8211; Потребители (users) и Статии (articles). Ясно се вижда, че &#8220;Кой потребител е автор на дадена статия&#8221; и &#8220;Кой потребител е рецензент на дадена статия (ако е рецензирана)&#8221; са връзки между тези два класа. Тоест ER диаграмата на този етап е следната:</p>
<p><img class="aligncenter size-full wp-image-8043" title="ER диаграма" src="http://www.cphpvb.net/wp-content/uploads/2012/04/er-diag.png" alt="ER диаграма" width="476" height="323" /></p>
<p>Очевидният първичен ключ за таблица Users е атрибута id. За таблица Articles няма нито очевиден такъв, нито подходящ, затова при създаването на таблицата ще направим допълнителен атрибут &#8220;id&#8221; (редно е да се отрази и в ER диаграмата &#8211; направете го).</p>
<p>Колкото до връзките &#8211; всяка таблица задължително трябва да има автор, т.е. author_id ще бъде NOT NULL, докато някои статии ще са рецензирани, а други не &#8211; т.е. moderator_id ще може евентуално да бъде NULL.</p>
<p>В условието има още важни неща, а именно:</p>
<ul>
<li>&#8220;<em>Всеки потребител може да има три основни роли</em>&#8220;, което е допълнено с &#8220;<em>Разбира се авторите и рецензентите сами по себе си са обикновени потребители. Освен това не е забранено на даден автор да бъде и рецензент на други статии.</em>&#8220;. Това практически означава, че всеки един потребител на системата може да заема всякаква роля &#8211; в задачата никъде не е дефинирано ограничение от типа, че &#8220;някоя конкретна група потребители не може да заема дадена роля&#8221; или &#8220;само тези и тези потребители могат да бъдат автори/рецензенти&#8221;. Следователно не е нужно да правим подкласове на класа обекти Users &#8211; <span style="text-decoration: underline;">всеки един</span> потребител може потенциално да бъде както автор, така и рецензент. В случая каква роля заема за дадена статия ще се определя от връзките между статията и потребителите.</li>
<li><em>&#8220;Когато някой автор добави нова статия в системата тя стандартно ще отива в „опашка за рецензиране“ (moderation queue)</em>&#8221; &#8211; тук въпросът е дали има нужда да добавим още един атрибут в класа Articles с тип данни BIT, който да има смисъла на &#8220;одобрена&#8221; и &#8220;неодобрена&#8221; статия в зависимост от стойноста? Не е изключено да го направим (по-скоро би било полезно), но в случая можем и да си го спестим &#8211; статиите, които имат въведен moderator_id ще приемаме за &#8220;рецензирани&#8221;, а там, където moderator_id е NULL ще приемаме за &#8220;нерецензирани&#8221;. В реална ситуация по-скоро бихме приели варианта с допълнителния атрибут, понеже така даваме шанс за създаване на нови функционалности като &#8220;някой рецензент е заключил статия за себе си, но все още не я е рецензирал&#8221; или &#8220;статията е била рецензирана услешно, но рецензентът е бил изтрит от системата и вече нямаме неговото id&#8221;. В решението по-долу ще се спрем върху по-лесния вариант (без специална колона за статус в таблицата Articles), а вие в последствие преправете заявките с другия.</li>
<li>Колкото до нуждата от създаване на &#8220;опашка от нерецензирани статии&#8221; &#8211; това може много лесно да бъде реализирано, като се извадят записите с NULL стойност в колона moderator_id от таблица Articles и в последствие се сортират по датата на тяхното въвеждане. Тоест и за опашката няма да е необходимо да пазим допълнителна информация.</li>
</ul>
<p>Нека сега създадем въпросната база от данни:</p>
<blockquote>
<pre>CREATE DATABASE usersystem;
USE usersystem;

CREATE TABLE users(
  id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(255) UNIQUE NOT NULL
)ENGINE = InnoDB CHARACTER SET utf8;

CREATE TABLE articles(
  id INT UNSIGNED AUTO_INCREMENT  PRIMARY KEY,
  title VARCHAR(160) NOT NULL,
  contents TEXT NOT NULL,
  author_id SMALLINT UNSIGNED NOT NULL,
  published_on DATE NOT NULL,
  FOREIGN KEY (author_id) REFERENCES users(id)
          ON DELETE RESTRICT ON UPDATE RESTRICT,
  moderator_id SMALLINT UNSIGNED NULL DEFAULT NULL,
  FOREIGN KEY (moderator_id) REFERENCES users(id)
          ON DELETE RESTRICT ON UPDATE RESTRICT
)ENGINE = InnoDB CHARACTER SET utf8;</pre>
</blockquote>
<p>и да въведем примерни данни:</p>
<blockquote>
<pre>INSERT INTO users(name)
VALUES ("Petar"), ("Ivan"), ("Maria"), ("Philip");

INSERT INTO articles(title, contents, published_on, author_id, moderator_id)
VALUES ("Статия 1", "Съдържание към статия 1...", "2012-03-12", 2, NULL),
       ("Статия 2", "Съдържание към статия 2...", "2012-03-28", 3, NULL),
       ("Статия 3", "Съдържание към статия 3...", "2012-04-04", 3, NULL),
       ("Статия 4", "Съдържание към статия 4...", "2012-02-27", 2, NULL),
       ("Статия 5", "Съдържание към статия 5...", "2012-03-28", 3, 1),
       ("Статия 6", "Съдържание към статия 6...", "2012-04-04", 3, 2),
       ("Статия 7", "Съдържание към статия 7...", "2012-02-27", 2, 1),
       ("Статия 8", "Съдържание към статия 8...", "2012-02-27", 1, 2),
       ("Статия 9", "Съдържание към статия 9...", "2012-02-27", 1, NULL);</pre>
</blockquote>
<p>Пристъпваме към решението на задача 2. То е повече от тривиално и няма нужда от специални обяснения:</p>
<blockquote>
<pre>SELECT title, contents, published_on
FROM articles
WHERE moderator_id IS NULL
      AND author_id = ( SELECT id
                        FROM users
                        WHERE name = "Ivan"
                      );</pre>
</blockquote>
<p>Сега трябва да помислим за задача 3. Лесно можем да изведем имената на хората и броя от рецензираните им статии чрез следната заявка:</p>
<blockquote>
<pre>SELECT users.id AS user_id, users.name as user_name
COUNT(articles.id) AS moderatedcount
FROM users JOIN articles ON users.id = articles.author_id
WHERE articles.moderator_id IS NOT NULL
GROUP BY users.id;</pre>
</blockquote>
<p>Лесно можем да изведем и имената на хората и броя на нерецензираните им статии чрез следната заявка:</p>
<blockquote>
<pre>SELECT users.id AS user_id, users.name as user_name
COUNT(articles.id) AS unmoderatedcount
FROM users JOIN articles ON users.id = articles.author_id
WHERE articles.moderator_id IS NULL
GROUP BY users.id;</pre>
</blockquote>
<p>Как обаче да ги обединим в една? Очевидното решение е като направим JOIN между тези две таблици. При това забележете, че трябва да е FULL JOIN, понеже е възможно както да има потребител без нито една рецензирана статия, така и потребител без нито една нерецензирана.</p>
<p>За съжаление знаем, че MySQL не поддържа FULL JOIN и се налага да се прави сложно чрез изкуствени методи. За щастие в тази конкретна задача и двете заявки произхождат от данни на една таблица &#8211; users. Тоест ние можем да направим следното &#8211; да вземем всички записи от таблица users, за всеки един от тях да добавим moderatedcount (може да е NULL, т.е. ще използваме LEFT JOIN) и към получения резултат за всеки един ред да добавим и unmoderatedcount (отново с LEFT JOIN). Накрая, понеже в задачата се изисква &#8220;потребителите, които въобще нямат статии да се пропуснат&#8221;, от резултатната таблица трябва да премахнем редовете, в които moderatedcount и unmoderatedcount заедно са с NULL стойности. Ето как би изглеждала такава заявка:</p>
<blockquote>
<pre>SELECT users.id,
       users.name,
       moderated_articles.moderatedcount,
       unmoderated_articles.unmoderatedcount
FROM users

     LEFT JOIN 
     ( SELECT users.id AS user_id, 
              COUNT(articles.id) AS moderatedcount
        FROM users JOIN articles ON users.id = articles.author_id
        WHERE articles.moderator_id IS NOT NULL
        GROUP BY users.id
     ) AS moderated_articles
     ON users.id = moderated_articles.user_id

     LEFT JOIN 
     ( SELECT users.id AS user_id,
              COUNT(articles.id) AS unmoderatedcount
        FROM users JOIN articles ON users.id = articles.author_id
        WHERE articles.moderator_id IS NULL
        GROUP BY users.id
     ) AS unmoderated_articles
     ON users.id = unmoderated_articles.user_id

GROUP BY users.id
HAVING moderatedcount IS NOT NULL 
       OR 
       unmoderatedcount IS NOT NULL;</pre>
</blockquote>
<p>Това е валидно решение на задачата.<span style="text-decoration: underline;"> В този си вид обаче заявката въобще не е оптимална</span>. Ето някои неща, които би трябвало да ни направят впечатление:</p>
<ul>
<li>В първата вложена заявка правим обхождане на таблицата users гледайки стойностите на колоната moderator_id и във втората пак правим същото обхождане гледайки стойностите по същата колона.</li>
<li>Взимаме абсолютно всички потребители и за всички правим сметките. В зависимост от натоварването на подобна система може да се окаже така, че много от потребителите въобще не са автори, а са просто обикновени потребители или рецензенти. В такъв случай резултатната таблица (преди прилагането на HAVING) ще бъде ненужно голяма.</li>
</ul>
<p>Затова можем да помислим и за &#8220;по-хитро&#8221; решение. Един начин е да си направим два &#8220;брояча&#8221; в една SELECT заявка и да използваме оператор IF, с който да добавяме 0 или 1 към съотвeтния брояч:</p>
<blockquote>
<pre>SELECT users.id,
       users.name,
       SUM(IF(articles.moderator_id IS NOT NULL, 1, 0))
	        AS moderatedcount,
       SUM(IF(articles.moderator_id IS NULL, 1, 0))
	        AS unmoderatedcount
FROM users JOIN articles ON users.id = articles.author_id
GROUP BY users.id;</pre>
</blockquote>
<p>При това решение забележете, че потребителите с едновременно 0 рецензирани и 0 одобрени статии автоматично се премахват още от INNER JOIN условието &#8211; тези потребители, които въобще нямат статия не участват в по-нататъшните сметки. Очевидно е, че тази заявка е много по-добра и дори по-пригледна от предишната.</p>
<p><strong>Допълнителна задача</strong>: Направете така, че да не е възможно даден автор да е рецензент сам на себе си.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8042-exam-2012-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Събития в MySQL</title>
		<link>http://www.cphpvb.net/db/8040-mysql-events/</link>
		<comments>http://www.cphpvb.net/db/8040-mysql-events/#comments</comments>
		<pubDate>Tue, 03 Apr 2012 20:05:58 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8040</guid>
		<description><![CDATA[&#8220;Събитията&#8221; (events) в MySQL ни позволяват да изпълняваме процедури отложено във времето. Те може да са регулярни, еднократни или ограничени в даден интервал от време. Основната полза от тях идва тогава, когато искаме да извършваме операции, които не са жизненоважни за функционирането на системата &#8220;в момента на изпълнение&#8221;. Такива може да са заявки за обобщение [...]]]></description>
			<content:encoded><![CDATA[<p>&#8220;Събитията&#8221; (events) в MySQL ни позволяват да изпълняваме процедури отложено във времето. Те може да са регулярни, еднократни или ограничени в даден интервал от време. Основната полза от тях идва тогава, когато искаме да извършваме операции, които не са жизненоважни за функционирането на системата &#8220;в момента на изпълнение&#8221;. Такива може да са заявки за обобщение на данни (например изготвяне на статистически отчет в края на деня) или такава промяна на данни, която е хубаво да бъде правена, но не е съществено необходима за функционирането на системата и ако я правим всеки път, то бихме забавили излишно приложението.<span id="more-8040"></span></p>
<p>Събитията в MySQL се контролират от специална нишка, която работи паралелно с останалите. Преди да започнете да работите със събития трябва да проверите дали тя е включена (по подразбиране не е). Това се контролира от глобалната променлива &#8220;event_scheduler&#8221;. Ако нишката не е стартирана, то MySQL ще приема заявките ви за създаване на събития, но те няма да се изпълняват. За да проверите стойността на променливата напишете следната команда:</p>
<blockquote>
<pre>mysql&gt; SELECT @@event_scheduler;
+-------------------+
| @@event_scheduler |
+-------------------+
| OFF               |
+-------------------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>За да я включите трябва да я промените на &#8220;ON&#8221;:</p>
<blockquote>
<pre>mysql&gt; SET GLOBAL event_scheduler = "ON";
Query OK, 0 rows affected (0.00 sec)

mysql&gt; SELECT @@event_scheduler;
+-------------------+
| @@event_scheduler |
+-------------------+
| ON                |
+-------------------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>Ако искате да го включите автоматично при рестартиране на MySQL добавете в my.cnf &#8220;event_scheduler = on&#8221;. Може да проверите, че нишката е стартирана разглеждайки списъка с процесите:</p>
<blockquote>
<pre>mysql&gt; SHOW PROCESSLIST\G
*************************** 1. row
     Id: 1
   User: root
   Host: localhost:9737
     db: NULL
Command: Query
   Time: 0
  State: NULL
   Info: SHOW PROCESSLIST
*************************** 2. row
     Id: 2
   User: event_scheduler
   Host: localhost
     db: NULL
Command: Daemon
   Time: 74
  State: Waiting on empty queue
   Info: NULL
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Първият процес в списъка е този, който самите ние сме стартирали отваряйки връзка с потребител root към СУБД, а втория е въпросния &#8220;event scheduler&#8221;.</p>
<p>За да създадете събитие трябва да използвате командата CREATE EVENT. В най-елементарния си вариант тя има следния синтаксис:</p>
<blockquote>
<pre>CREATE EVENT &lt;име&gt;
ON SCHEDULE &lt;график&gt;
DO &lt;коматди&gt;</pre>
</blockquote>
<p><strong>Пример 1</strong>: Нека демонстрираме с класически пример. Нека имаме база от данни, в която съхраняваме публикации и записи за влизанията в тях (log). За всяка публикация искаме да пазим статистика за уникалните посещения (visits), която се калкулира на база на записите за влизанията:</p>
<blockquote>
<pre>CREATE TABLE articles(
   id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   title VARCHAR(255) NOT NULL,
   contents TEXT NOT NULL,
   visits INT UNSIGNED NOT NULL
)ENGINE=InnoDB CHARACTER SET=utf8;

INSERT INTO articles(title, contents, visits)
VALUES ("Статия 1", "Някакво съдържание...", 0),
       ("Статия 2", "Друго съдържание...", 0);

CREATE TABLE logs(
   article_id SMALLINT UNSIGNED,
   FOREIGN KEY (article_id) REFERENCES articles(id),
   ip INT UNSIGNED NOT NULL,
   hits INT NOT NULL,
   PRIMARY KEY(article_id,ip)
) ENGINE=InnoDB  CHARACTER SET=ascii;</pre>
</blockquote>
<p>Когато потребителите започнат да четат дадени статии, то ние ще взимаме IP адреса на всеки един от тях и ще правим запис в таблицата logs за съответното посещение.</p>
<p>Сега остава да разрешим проблема със съответствието на hits в таблицата logs и visits в таблицата articles. Разбира се вече знаем как да създадем тригер, който при добавяне на нов запис в таблицата logs ще обновява стойността на visits в таблицата articles. Ако обаче нашия сайт е достатъчно много натоварен (например имаме по няколко посещения в секунда) това може да се окаже неприятна операция (забележете, че при използване на InnoDB всяка UPDATE заявка сама по себе си е транзакция, т.е. имаме и заключване на данните). Разбира се ние можем напълно да се откажем от колона visits и всеки път когато даваме информацията на потребителите ни да си калкулираме стойността динамично, но това също не ни харесва от гледна точка на производителност.</p>
<p>Именно тук можем да използваме събитията &#8211; ще калкулираме статистиката за visits например веднъж на минута. Така потребителя няма да вижда най-актуалната статистика за посещенията към текущия момент, в който чете дадената статия, но в най-лошия случай ще вижда данните най-много от преди минута. За целта можем да добавим следното събитие:</p>
<blockquote>
<pre>CREATE EVENT visits_update
ON SCHEDULE EVERY 1 MINUTE
DO UPDATE articles
   SET visits = ( SELECT COUNT(*)
                  FROM logs
                  WHERE logs.article_id = articles.id
                );</pre>
</blockquote>
<p>Сега да вмъкнем примерни данни в logs:</p>
<blockquote>
<pre>INSERT INTO logs(article_id, ip, hits)
VALUES (1, INET_ATON('1.1.1.1'), 1),
       (2, INET_ATON('1.1.1.1'), 1),
       (1, INET_ATON('2.2.2.2'), 1),
       (1, INET_ATON('3.3.3.3'), 1),
       (1, INET_ATON('1.1.1.1'), 1),
       (2, INET_ATON('2.2.2.2'), 1),
       (2, INET_ATON('3.3.3.3'), 1),
       (2, INET_ATON('3.3.3.3'), 1),
       (2, INET_ATON('3.3.3.3'), 1),
       (1, INET_ATON('4.4.4.4'), 1)
ON DUPLICATE KEY UPDATE hits=hits+1;</pre>
</blockquote>
<p>Първоначално ще видим, че visits не е обновено:</p>
<blockquote>
<pre>SELECT * FROM articles;
+----+----------+-----------------------+--------+
| id | title    | contents              | visits |
+----+----------+-----------------------+--------+
|  1 | Статия 1 | Някакво съдържание... |      0 |
|  2 | Статия 2 | Друго съдържание...   |      0 |
+----+----------+-----------------------+--------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Ако обаче почакаме една минута, то ще видим, че visits ще се обнови:</p>
<blockquote>
<pre>SELECT * FROM articles;
+----+----------+-----------------------+--------+
| id | title    | contents              | visits |
+----+----------+-----------------------+--------+
|  1 | Статия 1 | Някакво съдържание... |      4 |
|  2 | Статия 2 | Друго съдържание...   |      3 |
+----+----------+-----------------------+--------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Можем ли да помислим за някаква оптимизация? Разбира се, че може. Защо всеки път обновяваме посещенията пресмятайки броя на всички записи в таблицата, а не просто да добавяме броя само на новите такива? Ще го направим, като от тук нататък ще разглеждаме таблицата logs като &#8220;дневни записи&#8221; (т.е. ще записваме TIME). Естествено не е проблем да се направят и &#8220;месечни&#8221; или &#8220;вечни&#8221;:</p>
<blockquote>
<pre>DROP EVENT visits_update;

TRUNCATE TABLE logs;

ALTER TABLE articles
ADD COLUMN last_visits_update DATETIME NOT NULL;

UPDATE articles
SET last_visits_update = NOW(), visits = 0;

ALTER TABLE logs
ADD COLUMN at_time TIME NOT NULL;</pre>
</blockquote>
<p>Изтрихме записите в logs и добавихме нови колони &#8211; последно обновяване в articles и време на <span style="text-decoration: underline;">първото посещение</span> на даден IP адрес в logs. Сега пренаписваме събитието така, че да удовлетворява новото условие:</p>
<blockquote>
<pre>CREATE EVENT visits_update
ON SCHEDULE EVERY 1 MINUTE
DO UPDATE articles
   SET visits = visits +
                ( SELECT COUNT(*)
                  FROM logs
                  WHERE logs.article_id = articles.id
                        AND
                        logs.at_time &gt; TIME(articles.last_visits_update)
                ),
       last_visits_update = NOW();</pre>
</blockquote>
<p>Добавяме новите записи:</p>
<blockquote>
<pre>INSERT INTO logs(article_id, ip, hits, at_time)
VALUES (1, INET_ATON('1.1.1.1'), 1, CURTIME()),
       (2, INET_ATON('1.1.1.1'), 1, CURTIME()),
       (1, INET_ATON('2.2.2.2'), 1, CURTIME()),
       (1, INET_ATON('3.3.3.3'), 1, CURTIME()),
       (1, INET_ATON('1.1.1.1'), 1, CURTIME()),
       (2, INET_ATON('2.2.2.2'), 1, CURTIME()),
       (2, INET_ATON('3.3.3.3'), 1, CURTIME()),
       (2, INET_ATON('3.3.3.3'), 1, CURTIME()),
       (2, INET_ATON('3.3.3.3'), 1, CURTIME()),
       (1, INET_ATON('4.4.4.4'), 1, CURTIME())
ON DUPLICATE KEY UPDATE hits=hits+1;</pre>
</blockquote>
<p>&#8230; и проверяваме. Първоначално статистиката няма да е обновена:</p>
<blockquote>
<pre>SELECT * FROM articles;
+----+----------+-----------------------+--------+---------------------+
| id | title    | contents              | visits | last_visits_update  |
+----+----------+-----------------------+--------+---------------------+
|  1 | Статия 1 | Някакво съдържание... |      0 | 2012-04-03 15:18:45 |
|  2 | Статия 2 | Друго съдържание...   |      0 | 2012-04-03 15:18:45 |
+----+----------+-----------------------+--------+---------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>&#8230; но след една минута вече ще бъде:</p>
<blockquote>
<pre>SELECT * FROM articles;
+----+----------+-----------------------+--------+---------------------+
| id | title    | contents              | visits | last_visits_update  |
+----+----------+-----------------------+--------+---------------------+
|  1 | Статия 1 | Някакво съдържание... |      4 | 2012-04-03 15:19:45 |
|  2 | Статия 2 | Друго съдържание...   |      3 | 2012-04-03 15:19:45 |
+----+----------+-----------------------+--------+---------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Ако в последствие добавим нови записи със съществуващи стари (не трябва да обновява visits) и нови (трябва да обнови visits) IP адреси:</p>
<blockquote>
<pre>INSERT INTO logs(article_id, ip, hits, at_time)
VALUES (1, INET_ATON('5.5.5.5'), 1, CURTIME()),
       (1, INET_ATON('1.1.1.1'), 1, CURTIME()),
       (1, INET_ATON('6.6.6.6'), 1, CURTIME())
ON DUPLICATE KEY UPDATE hits=hits+1;</pre>
</blockquote>
<p>&#8230; ще видим, че обновяването е коректно в диапазона на действие на събитието:</p>
<blockquote>
<pre>SELECT * FROM articles;
+----+----------+-----------------------+--------+---------------------+
| id | title    | contents              | visits | last_visits_update  |
+----+----------+-----------------------+--------+---------------------+
|  1 | Статия 1 | Някакво съдържание... |      4 | 2012-04-03 15:23:45 |
|  2 | Статия 2 | Друго съдържание...   |      3 | 2012-04-03 15:23:45 |
+----+----------+-----------------------+--------+---------------------+
2 rows in set (0.00 sec)

-- след известно време...

SELECT * FROM articles;
+----+----------+-----------------------+--------+---------------------+
| id | title    | contents              | visits | last_visits_update  |
+----+----------+-----------------------+--------+---------------------+
|  1 | Статия 1 | Някакво съдържание... |      6 | 2012-04-03 15:28:45 |
|  2 | Статия 2 | Друго съдържание...   |      3 | 2012-04-03 15:28:45 |
+----+----------+-----------------------+--------+---------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>Така пазим повече данни в таблиците (датите и времената), но за сметка на това броим по-малко редове на всяко обновяване.</p>
<p>Накрая да не забравим да направим събитие, което ще изчиства logs таблицата в края на всеки ден (в нея пазим колона &#8220;TIME&#8221;, т.е. тя ще е актуална само в рамките на текущия ден):</p>
<blockquote>
<pre>CREATE EVENT truncate_logs
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP '2012-04-03 00:00:00'
DO TRUNCATE TABLE logs;</pre>
</blockquote>
<p>Както виждате можем да правим събитията регулярни като &#8220;интервал от &#8230; започвайки от дадено време&#8221;. Именно затова се използва ключовата дума STARTS. Ако я нямаше, то събитието щеше да започне от текущата дата. Аналогично можем да задаваме крайна дата на събитие чрез ключова дума ENDS.</p>
<p><strong>Пример 2</strong>: Ще създадем база данни със служители (employees), клиенти (customers) и продукти (products):</p>
<blockquote>
<pre>CREATE TABLE employees(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL
)ENGINE = InnoDB;

INSERT INTO employees(name)
VALUES ("Petar"), ("Ivan"), ("Maria");

CREATE TABLE customers(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL
)ENGINE = InnoDB;

INSERT INTO customers(name)
VALUES ("Todor"), ("Mihail"), ("Angelina"), ("Mariela");

CREATE TABLE products(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL,
   price DECIMAL(5,2) NOT NULL
)ENGINE = InnoDB;

INSERT INTO products(name, price)
VALUES ("Отверки", 6.50), ("Чукове", 12.80);</pre>
</blockquote>
<p>Ще създадем таблица &#8220;дневни продажби&#8221; (dailysales), в която ще записваме кой служител на кой клиент какво е продал и в какво количество за даден ден:</p>
<blockquote>
<pre>CREATE TABLE dailysales(
   employee_id INT UNSIGNED NOT NULL,
   FOREIGN KEY (employee_id) REFERENCES employees(id),
   customer_id INT UNSIGNED NOT NULL,
   FOREIGN KEY (customer_id) REFERENCES customers(id),
   product_id INT UNSIGNED NOT NULL,
   FOREIGN KEY (product_id) REFERENCES products(id),
   quantity SMALLINT NOT NULL,
   at_date DATE NOT NULL,
   PRIMARY KEY(employee_id, customer_id, product_id, at_date)
)ENGINE=InnoDB;

INSERT INTO dailysales(employee_id, customer_id, product_id,
                       quantity, at_date)
VALUES (1, 3, 1, 3, 2012-04-02),
       (1, 3, 2, 1, 2012-04-02),
       (2, 1, 1, 4, 2012-04-02),
       (2, 2, 1, 1, 2012-04-02),
       (1, 1, 2, 1, 2012-04-03),
       (3, 4, 2, 1, 2012-04-03);</pre>
</blockquote>
<p>Сега искаме да направим таблица с обобщение за приходите получени от даден служител за даден ден. Тя може да изглежда по следния начин:</p>
<blockquote>
<pre>CREATE TABLE employees_income(
   employee_id INT UNSIGNED NOT NULL,
   FOREIGN KEY (employee_id) REFERENCES employees(id),
   at_date DATE NOT NULL,
   total_income DECIMAL(6,2),
   PRIMARY KEY(employee_id, at_date)
)ENGINE=InnoDB;</pre>
</blockquote>
<p>И да създадем регулярно дневно събитие, което ще обновява информацията в тази таблица:</p>
<blockquote>
<pre>CREATE EVENT daily_income_stats
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP '2012-04-03 00:01:30'
DO
INSERT IGNORE
INTO employees_income(employee_id, at_date, total_income)
SELECT dailysales.employee_id, dailysales.at_date,
       SUM(dailysales.quantity * products.price)
FROM dailysales JOIN products
     ON dailysales.product_id = products.id
GROUP BY dailysales.employee_id, dailysales.at_date;</pre>
</blockquote>
<p>Правим проверка:</p>
<blockquote>
<pre>SELECT * FROM employees_income;
Empty set (0.00 sec)

-- на следващия ден (в нашия случай 04-04)
-- събитието ще изчисли всичко

SELECT * FROM employees_income;
+-------------+------------+--------------+
| employee_id | at_date    | total_income |
+-------------+------------+--------------+
|           1 | 2012-04-02 |        32.30 |
|           1 | 2012-04-03 |        12.80 |
|           2 | 2012-04-02 |        32.50 |
|           3 | 2012-04-03 |        12.80 |
+-------------+------------+--------------+
4 rows in set (0.00 sec)</pre>
</blockquote>
<p>Можем да се досетим, че събитието не е направено оптимално. С всеки следващ ден ще имаме все повече и повече дублиращи се редове, които ще се пропускат (използваме INSERT IGNORE&#8230; заявка). <span style="text-decoration: underline;">Ние обаче всеки път пресмятаме сумата на парите за всеки един от тях</span>! Затова <strong>горното събитие не е добро</strong> &#8211; с напредване на времето то ще работи все по-бавно и по-бавно. Затова можем да я оптимизираме така, че да пресмята събитията само от предишния ден, а другите (вече изчислени) да не ги изчислява отново:</p>
<blockquote>
<pre>DROP EVENT daily_income_stats;

DELETE FROM employees_income;

CREATE EVENT daily_income_stats
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP '2012-04-03 00:01:30'
DO
INSERT INTO employees_income(employee_id, at_date, total_income)
SELECT dailysales.employee_id,
       DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY),
       SUM(dailysales.quantity * products.price)
FROM dailysales JOIN products ON dailysales.product_id = products.id
WHERE dailysales.at_date = DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
GROUP BY dailysales.employee_id, dailysales.at_date;</pre>
</blockquote>
<p>Проверка:</p>
<blockquote>
<pre>SELECT * FROM employees_income;
Empty set (0.00 sec)

-- на следващия ден (в нашия случай 04-04)
-- събитието ще вмъкне общите приходи
-- само за 04-03 (предишния ден)

SELECT * FROM employees_income;
+-------------+------------+--------------+
| employee_id | at_date    | total_income |
+-------------+------------+--------------+
|           1 | 2012-04-03 |        12.80 |
|           3 | 2012-04-03 |        12.80 |
+-------------+------------+--------------+
4 rows in set (0.00 sec)</pre>
</blockquote>
<p>В случая се възползвахме от функцията DATE_SUB, която връща разликана между две дати. По-конкретно DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) ни дава &#8220;вчера&#8221;.</p>
<p>* <em>Нарочно стартирахме събитието в 00:01:30, а не в 00:00:00. Не е добра идея да създавате събития, които се изпълняват точно в полунощ, защото стандартно много други приложения може правят други такива и по този начин да създавате излишен &#8220;час пик&#8221; за сървъра. Винаги гледайте събитията ви да се разминават във времето.</em></p>
<p><strong>Задача</strong>. В пример 2 ако някой служител не е направил никакви продажби в даден ден, то той ще липсва в отчета за него. Направете така, че id на такъв служител да се добавя, а неговия total_income да бъде или 0 или NULL (по ваш избор).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8040-mysql-events/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Симулиране на CHECK със SIGNAL в тригер</title>
		<link>http://www.cphpvb.net/db/8038-simulating-check-with-signal-in-trigger/</link>
		<comments>http://www.cphpvb.net/db/8038-simulating-check-with-signal-in-trigger/#comments</comments>
		<pubDate>Mon, 02 Apr 2012 18:23:47 +0000</pubDate>
		<dc:creator>Филип Петров</dc:creator>
				<category><![CDATA[Бази от Данни]]></category>

		<guid isPermaLink="false">http://www.cphpvb.net/?p=8038</guid>
		<description><![CDATA[Към версия 5.5 MySQL продължава да НЕ поддържа CHECK ограничения. В предишна статия показахме как може да се симулира CHECK с VIEW. С версия 5.5 на MySQL вече може да се използват т.нар. SIGNALS в тригери и съхранени процедури, което ни позволява &#8220;да хвърляме грешки&#8221; от тях. Пример 1: Нека покажем един пример &#8211; имаме [...]]]></description>
			<content:encoded><![CDATA[<p>Към версия 5.5 MySQL продължава да НЕ поддържа <a title="CHECK Constraints MySQL" href="http://www.cphpvb.net/db/5388-check-constraint/" target="_blank">CHECK ограничения</a>. В предишна статия показахме как може да се <a title="Симулиране на CHECK с VIEW" href="http://www.cphpvb.net/db/5671-%D1%81%D0%B8%D0%BC%D1%83%D0%BB%D0%B8%D1%80%D0%B0%D0%BD%D0%B5-%D0%BD%D0%B0-check-%D1%81-view/" target="_blank">симулира CHECK с VIEW</a>. С версия 5.5 на MySQL вече може да се използват т.нар. SIGNALS в тригери и съхранени процедури, което ни позволява &#8220;да хвърляме грешки&#8221; от тях.<span id="more-8038"></span></p>
<p><strong>Пример 1</strong>: Нека покажем един пример &#8211; имаме елементарна таблица, в която записваме данните за потребители, каквато например използваме за <a title="Пример за скрипт за автентикация" href="http://www.cphpvb.net/network-security/8005-openssl_encrypt-openssl_decrypt-php/" target="_blank">примера за скрипт за автентикация</a> в раздел ПТСК:</p>
<blockquote>
<pre>CREATE TABLE users(
 id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
 username VARCHAR(32) NOT NULL,
 password CHAR(64) NOT NULL
)ENGINE=InnoDB;</pre>
</blockquote>
<p>Искаме да направим ограничение върху потребителското име, а именно &#8211; да бъде между 4 и 32 символа. Горната граница е ясно определена от типа на полето &#8211; varchar(32). По подразбиране ако се опитате да вмъкнете повече информация от предвидената, то тя ще бъде отрязана до 32 символ и вмъкната по този начин, а заявката ще бъде успешна, но със съответния &#8220;warning&#8221; (станал е &#8220;truncate&#8221; на низа). Ако MySQL е настроен да работи в режим с включен &#8220;STRICT_ALL_TABLES&#8221; (прочетете повече за <a title="MySQL modes" href="http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html" target="_blank">режимите на MYSQL</a>), то ще се генерира грешка.</p>
<p>Долната граница обаче не е лимитирана по никакъв начин. Искаме да направим така, че да се генерира грешка ако се опитаме да вмъкнем потребителско име по-късо от 4 символа. Ето как можем да го направим с тригер в MySQL 5.5 (и евентуално по същия начин в по-новите версии):</p>
<blockquote>
<pre>DELIMITER //

CREATE TRIGGER username_min_length_check
BEFORE INSERT ON users FOR EACH ROW
BEGIN
 DECLARE errormsg VARCHAR(255);
 IF CHAR_LENGTH(NEW.username) &lt; 4
 THEN SET errormsg = CONCAT("Username too short: ", NEW.username);
      SIGNAL SQLSTATE '45000' SET message_text = errormsg;
 END IF;
END//

DELIMITER ;</pre>
</blockquote>
<p>SQL State със стойност 45000 има смисъл като &#8220;unhandled user-defined exception&#8221;, което е правилно да се използва за такива случаи. Ето две пробни заявки, които показват, че тригерът работи както очаквахме:</p>
<blockquote>
<pre>mysql&gt; INSERT INTO users(username, password)
    -&gt; VALUES ("user123", SHA2("password", 256));
Query OK, 1 row affected (0.00 sec)

mysql&gt; INSERT INTO users(username, password)
    -&gt; VALUES ("aa", SHA2("password", 256));
ERROR 1644 (45000): Username too short: aa</pre>
</blockquote>
<p>Разбира се по-добре ще бъде да валидирате данните в самото приложение и въобще да не изпълнявате заявката ако потребител се опитва да се регистрира с по-къс низ за username. Ако обаче нивата на сигурност го изискват, то може да си позволите двойна проверка.</p>
<p>Ако системата евентуално позволява промяна на потребителско име, то може да направите такъв тригер и за UPDATE заявки.</p>
<p><em>Внимание</em>: С показания тригер при вмъкването на множество редове накуп ако дори само един от тях предизвика въпросната грешка, то тя ще важи за <span style="text-decoration: underline;">цялата</span> заявка и нито един от редовете няма да бъде вмъкнат, дори някои от тях да отговарят на условието.</p>
<p><strong>Пример 2</strong>: Нека имаме две таблици &#8211; отдели и служители. Между тях ще има две връзки. Едната ще има смисъл, че &#8220;даден служител работи в даден отдел&#8221;, а другата &#8220;даден служител е мениджър на даден отдел&#8221;:</p>
<blockquote>
<pre>CREATE TABLE departments(
 id TINYINT UNSIGNED PRIMARY KEY,
 name VARCHAR(255) NOT NULL UNIQUE,
 manager INT UNSIGNED NULL DEFAULT NULL UNIQUE
)ENGINE = InnoDB;

CREATE TABLE employees(
 id INT UNSIGNED PRIMARY KEY,
 name VARCHAR(255) NOT NULL,
 d_id TINYINT UNSIGNED,
 FOREIGN KEY (d_id) REFERENCES departments(id)
    ON DELETE CASCADE ON UPDATE CASCADE
)ENGINE = InnoDB;</pre>
</blockquote>
<p>Сега създаваме два тригера &#8211; един за INSERT и един за UPDATE заявки, в които проверяваме дали стойността на &#8220;manager&#8221; в таблица &#8220;departments&#8221; е валидна:</p>
<blockquote>
<pre>DELIMITER //

CREATE TRIGGER managercheck_insert
BEFORE INSERT ON departments FOR EACH ROW
BEGIN
  IF NEW.manager IS NOT NULL
     AND
     NEW.manager NOT IN (SELECT id
                         FROM employees
                         WHERE employees.d_id = NEW.id)
  THEN SIGNAL SQLSTATE '45000'
       SET message_text = "The manager must be employee from the department";
  END IF;
END//

CREATE TRIGGER managercheck_update
BEFORE UPDATE ON departments FOR EACH ROW
BEGIN
  IF NEW.manager IS NOT NULL
     AND
     NEW.manager NOT IN (SELECT employees.id
                         FROM employees
                         WHERE employees.d_id = NEW.id)
  THEN SIGNAL SQLSTATE '45000'
       SET message_text = "The manager must be employee from the department";
  END IF;
END//

DELIMITER ;</pre>
</blockquote>
<p>Вмъкваме примерни данни:</p>
<blockquote>
<pre>INSERT INTO departments (id, name, manager)
VALUES (1, "Support", NULL),
       (2, "Sales", NULL);

INSERT INTO employees(id, name, d_id)
VALUES (1, "Petar", 1),
       (2, "Ivan", 1),
       (3, "Maria", 2);</pre>
</blockquote>
<p>И изпробваме дали тригерите работят:</p>
<blockquote>
<pre>mysql&gt; UPDATE departments
    -&gt; SET manager = 2
    -&gt; WHERE id = 1;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql&gt; UPDATE departments
    -&gt; SET manager = 3
    -&gt; WHERE id = 1;
ERROR 1644 (45000): The manager must be employee from the same department

mysql&gt; INSERT INTO departments(id, name, manager)
    -&gt; VALUES (3, "Billing", 2);
ERROR 1644 (45000): The manager must be employee from the same department</pre>
</blockquote>
<p>Забележете, че по този начин дори не се налага да правим колоната &#8220;manager&#8221; като foreign key към employees. В действителност написаният тригер припокрива тази функционалност. Ето какво ще се случи ако се опитаме да въведем несъществуваща стойност в тази колона:</p>
<blockquote>
<pre>mysql&gt; INSERT INTO departments(id, name, manager)
    -&gt; VALUES (3, "Billing", 12);
ERROR 1644 (45000): The manager must be employee from the same department</pre>
</blockquote>
]]></content:encoded>
			<wfw:commentRss>http://www.cphpvb.net/db/8038-simulating-check-with-signal-in-trigger/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

