ITDumka
Зачем нужен prepared statements API в PHP mysqli extension?

Так получилось, что я задал себе этот вопрос и решил немножко покопаться в нём. Результаты не заставили себя долго ждать и привели к некоторым вполне обычным необычным выводам. Этот текст не является точкой отправки, он всего лишь ставит вопрос и оглашает некоторые фаты, относительно него. Поэтому комментарии были бы как никогда кстати. Ну что, суть вся в следующем.

Однажды в PHP появилось расширение под названием mysqli, которое пришло на замену привычному mysql и победило его словом Improved (улучшенный), расставив некоторые точки над i. С тех пор, если вы используете MySQL-сервер версии 4.1.3 и выше, разработчики PHP строго рекомендуют использовать mysqli-расширение. Основными аргументами в пользу последнего были улучшенный протокол обмена между клиентов и сервером, а так же поддержка следующих и не только фич:

  1. Самое, что было крутое так это поддержка объектно-ориентированного интерфейса. Коннект вешался на объект - и все становились счастливыми. Мы можем наследовать от класса mysqli, перезагружать методы и т. д. Стало намного удобнее, не надо тягать за собой ресурс и всё такое.
  2. Поддержка Multiple Statements - фактически можно выполнить несколько запросов за одно обращение. Да, это хорошо - уменьшение обращений к серверу открывает новые возможности обращения )). Это позволило открыть некоторым небольшую дыру, но у кого руки не крюки - у тех всё сработало.
  3. Поддержка Prepared Statements - третья основная фича в mysqli. Надеюсь, вы знаете, что это за механизм prepared statements, а если не знаете, то, может быть, узнаете.

Конечно, было еще несколько нововведений, но я их упущу чтобы конкретнее остановиться на последнем пункте. Итак, сторона разработчиков оперирует следующими фактами об использовании механизма prepared statement. Следующие три абзаца - это почти прямое цитирование Харисона Фиска (ссылка на оригинал) .

Использование prepared statements дает множество преимуществ как для безопасности так и для производительности. Prepared statements могут способствовать повышению безопасности путём отделения логики SQL запроса, от данных, подставляемых в него. Такое разделение логики и данных может помочь предотвратить внедрение SQL-инъекций. Обычно, когда вы используете запросы, в которых используются данные пришедшие от пользователей, вы должны быть очень осторожными. Для этого вы используете функции, которые экранируют проблемные символы, такие как одинарные и двойные кавычки, обратный слешь. Эти операции не обязательно необходимо выполнять, когда вы будете использовать prepared statements. Отделение данных от логики запроса SQL позволяет MySQL автоматически обработать эти символы и не прибегать к помощи специальных функций.

Увеличение производительности при использовании prepared statements может быть связано со следующими некоторыми факторами. Во-первых разбор строки SQL запроса происходит лишь единожды. На начальном этапе prepared statements (этап подготовки параметров) MySQL обработает каждый параметр, для проверки синтаксиса, а затем создаст запрос для непосредственного запуска. Затем, если мы будет выполнять этот запрос много раз, это уже не будет требовать расходов на новый синтаксический анализ его логики. Такой предварительный анализ, приводит к увеличению скорости, если нужно выполнить один и тот же запрос много раз, например, когда производиться вставка нескольких строк с помощью INSERT. Во-вторых, при использовании механизма prepared statements применяется совсем другой, так называемый, двоичный протокол обмена. Стандартный протокол MySQL перед отправкой данных по сети всегда преобразует их в строки. Это означает, что клиент преобразует данные в строки, которые зачастую становятся больше чем исходные данные, отправляет их серверу, который, в свою очередь, декодирует пришедшие строки обратно в правильный тип данных. Двоичный протокол отправляет все данный в родной бинарной форме, тем самым уменьшая использование ресурса процессора на конвертации данных и, естественно, сокращает затраты на передачу по сети.

Использование prepared statements не носит общий характер, так как может не поддерживать как некоторые типы SQL, так и положение в логике SQL строки. По словам разработчиков, эти недочеты будут дорабатываться, чтобы prepared statements API носило более общий характер. Также использование prepared statements может быть медленнее, чем обычный запрос без prepared statements, при котором фактически происходит лишь одно обращение к серверу. При использовании prepared statements, клиент, упрощенно говоря, сперва готовить запрос, а потом запускает его с определенными параметрами. Поэтому, если запрос используется всего один раз, то тут необходимо решать спорный вопрос, что предпочтительнее быстродействие либо возможность использования prepared statements.

Я решил немного измерить процесс работы. Для теста я написал две функции - одна работает чисто без prepared statements, а другая только с prepared statements. Так же я взял "небольшие" несколько запросов и комбинировал их в разных вариациях друг с другом. Вот один из вариантов тестирования:


Listing №1 (PHP)
  1. error_reporting(E_ALL);
  2.  
  3. function workWithoutPS(mysqli $db)
  4. {
  5.   $startTime = microtime(TRUE);
  6.   /**
  7.    * Select 1
  8.    */
  9.   $login = "mylogin'mm";
  10.   $email = "myemail%@em.com";
  11.   $login = "'" . $db->real_escape_string($login) . "'";
  12.   $email = "'" . $db->real_escape_string($email) . "'";
  13.   $result = $db->query('Select ID from USERS where LOGIN = ' . $login . ' and EMAIL = ' . $email);
  14.   while($row = $result->fetch_assoc()) {
  15.     $a = $row;
  16.   }
  17.   $result->close();
  18.   /**
  19.    * Select 2
  20.    *
  21.   $login = "mylogin'mm";
  22.   $email = "myemail%@em.com";
  23.   $login = "'" . $db->real_escape_string($login) . "'";
  24.   $email = "'" . $db->real_escape_string($email) . "'";
  25.   $result = $db->query('Select ID from USERS where LOGIN = '. $login . ' and EMAIL = ' . $email . ' LIMIT 10 OFFSET 100' );
  26.   while($row = $result->fetch_assoc()) {
  27.     $a = $row;
  28.   }
  29.   $result->close();
  30.   /**
  31.    * Select 3
  32.    *
  33.   $login = "mylogin'mm";
  34.   $login = "'" . $db->real_escape_string($login) . "'";
  35.   $result = $db->query('Select ID from USERS where LOGIN = '. $login . '  LIMIT 30 OFFSET 100');
  36.   while($row = $result->fetch_assoc()) {
  37.     $a = $row;
  38.   }
  39.   $result->close();*/
  40.   /**
  41.    * Insert 1
  42.    */
  43.   $login = "mylogin'mm";
  44.   $email = "myemail%@em.com";
  45.   $login = "'" . $db->real_escape_string($login) . "'";
  46.   $email = "'" . $db->real_escape_string($email) . "'";
  47.   $db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');
  48.   /**
  49.    * Insert 2
  50.    *
  51.   $login = "mylogin'mm";
  52.   $email = "myemail%@em.com";
  53.   $login = "'" . $db->real_escape_string($login) . "'";
  54.   $email = "'" . $db->real_escape_string($email) . "'";
  55.   $db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');*/
  56.   return microtime(TRUE) - $startTime;
  57. }
  58.  
  59. function workWithPS(mysqli $db)
  60. {
  61.   $startTime = microtime(TRUE);
  62.   /**
  63.    * Select 1
  64.    */
  65.   $login = "mylogin'mm";
  66.   $email = "myemail%@em.com";
  67.   $stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ?');
  68.   $stmt->bind_param('ss', $login, $email);
  69.   $result = $stmt->execute();
  70.   $stmt->bind_result($id);
  71.   while($stmt->fetch()) {
  72.     $a = $id;
  73.   }
  74.   $stmt->close();
  75.   /**
  76.    * Select 2
  77.    *
  78.   $login = "mylogin'mm";
  79.   $email = "myemail%@em.com";
  80.   $stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ?  LIMIT 10 OFFSET 100');
  81.   $stmt->bind_param('ss', $login, $email);
  82.   $result = $stmt->execute();
  83.   $stmt->bind_result($id);
  84.   while($stmt->fetch()) {
  85.     $a = $id;
  86.   }
  87.   $stmt->close();
  88.   /**
  89.    * Select 3
  90.    *
  91.   $login = "mylogin'mm";
  92.   $stmt = $db->prepare('Select ID from USERS where LOGIN = ? LIMIT 30 OFFSET 100');
  93.   $stmt->bind_param('s', $login);
  94.   $result = $stmt->execute();
  95.   $stmt->bind_result($id);
  96.   while($stmt->fetch()) {
  97.     $a = $id;
  98.   }
  99.   $stmt->close();*/  
  100.   /**
  101.    * Insert 1
  102.    */
  103.   $login = "mylogin'mm";
  104.   $email = "myemail%@em.com";
  105.   $stmt = $db->prepare('Insert into USERS (LOGIN, EMAIL) values (?, ?)');
  106.   $stmt->bind_param('ss', $login, $email);
  107.   $result = $stmt->execute();
  108.   /**
  109.    * Insert 2
  110.    *
  111.   $login = "mylogin'mm";
  112.   $email = "myemail%@em.com";
  113.   $stmt->bind_param('ss', $login, $email);
  114.   $result = $stmt->execute();*/
  115.   return microtime(TRUE) - $startTime;
  116. }
  117.  
  118. $db = new mysqli('localhost', 'test', 'test', 'test');
  119. $count = 1000;
  120. $time = 0;
  121. for ($i = 0; $i < $count; $i++) {
  122.   $time += workWithoutPS($db); //workWithPS($db)
  123. }
  124. $time /= $count;
  125. echo  $time . '</br>';


Как видите, я мог комментировать отдельные части кода и получать нужные результаты. В основном результаты проводились тремя последовательными обновлениями браузера так, что постепенно выбиралось всё больше и больше строк. Вот некоторые результаты:


Listing №2 (unknown)
  1.         workWithoutPS                   workWithPS
  2.  
  3.   $count=1000
  4.                       Select 1 + Insert 1
  5.  libmysql         mysqlnd          libmysql        mysqlnd
  6.   0.0150          0.0153            0.0172         0.0178
  7.   0.0248          0.0277            0.0249         0.0277
  8.   0.0368          0.0415            0.0334         0.0383
  9.   ______          ______            ______         ______
  10.   0.0255          0.0282            0.0252         0.0279
  11.  
  12.                 Select 1  + Insert 1 + Insert 2
  13.  libmysql         mysqlnd          libmysql        mysqlnd
  14.   0.0261          0.0287            0.0275         0.0298
  15.   0.0507          0.0566            0.0460         0.0513
  16.   0.0738          0.0834            0.0636         0.0731
  17.   ______          ______            ______         ______  
  18.   0.0502          0.0562            0.0457         0.0514
  19.  
  20.   $count=10000
  21.                       Select 2 + Select 3
  22.  libmysql         mysqlnd          libmysql        mysqlnd
  23.      -            0.00659              -           0.01000
  24.      -            0.00653              -           0.01040
  25.      -            0.00649              -           0.01010
  26.   ______          _______           _______        _______  
  27.      -            0.00654              -           0.01020


Из таких результатов я делаю следующий вывод: использование повсеместно запросов с prepared statements может замедлить работу с базой данных в полтора раза. Реальная выгода происходить только лишь при запуске подряд одинаковых запросов с разными параметрами. Также я вижу, что mysqlnd не совсем оправдывает ожидания. Как говорят разработчики, mysqlnd пришел заменить libmysql в связи с тем, что libmysql оптимизирована для работы с приложениями, написанными на С, а не PHP. И настоятельно предлагают использовать mysqlnd, потому что он оптимизирован для работы из PHP и по сравнению с libmysql имеет множество усовершенствований по работе с памятью и скоростью работы. Но, как мы можем наблюдать, mysqlnd все время проигрывает libmysql, хотя стоит заметить, что я не измерял используемую память и ресурс процессора. Это что касается производительности.

Что же с нашей безопасностью? Что лучше использовать prepared statements или real_escape_string? Многие говорят что real_escape_string это костыль, он что-то там недофильтровывает и всё такое. Я считаю, что он такой же костыль как и prepared statements, который не может подставлять переменные абсолютно во все места SQL запроса. Обычно логика вокруг real_escape_string такая же костыльная как и вокруг prepared statements, причём вокруг последних бывает побольше всякой хрени типа call_user_func_array. В связи с эти, я делаю следующий вывод: что безопасность, обеспечиваемая при использовании расширения mysqli, одинаково удобна, костыльна и "безопасна" как при real_escape_string так и при prepared statements. Другое дело использование prepared statements в PDO, где они поддерживаются на клиентской части и эмулируются, если сервер базы данных не поддерживает prepared statements. Слой абстракции PDO явно оправдывает подстановку параметров в запросы к разным СУБД.

Но зачем нужны prepared statements в mysqli? Допустим, вы вставляете несколько записей с помощью INSERT. Но ведь есть варианты, как вставить много записей с помощью одного запроса. Допустим, вы удаляете записи, но это тоже может быть довольно специфичной ситуацией. Некоторые аргументы в пользу повсеместного использования prepared statements явно пестрят рассказами о том, что MySQL-сервер кеширует подготовленные запросы идущие не подряд. То есть подготовив и выполнив запрос, можно подготовить и выполнить другой, отличный по структуре запрос, не переживая о том, что он собственно другой по структуре. Разбор первого сохранится в каком-то кеше и подхватится при повторном его использовании. Хотя я сильно не искал, но в документации я такого не нашел, да и почему-то слабо вериться в такое, хотя с другой стороны, вроде как, MySQL-сервер должен быть довольно всемогущ.

Ладно, допустим он всё таки кеширует этот момент, но сколько у вас реально запросов повторяется при обращении к странице? Подразумеваю, что ответ равен нулю. В таком случае, нужно кешировать подготовленный запрос не в пределах одного соединения, а для нескольких соединений, чтобы достичь каких то положительных результатов с какой-то вероятностью, превышающую аргументы вышеописанного теста. Опять же, это всё представляется маловероятным и в документации я каких-то фактов, касающихся этого вопроса не нашел. Может вы найдете и откроете мне глаза? Поэтому это пост и носит некоторый вопросительных характер.

Как бы там ни было, как говориться, каждому своё, и для каждого обращения к базе данных можно выбрать свой способ. А сосредоточится на использовании лишь одного способа - вовсе не выход, хотя есть много случаев, где это будет нормальным решением. Например, не использовать prepared statements API в mysqli, потому что пока всё указывает на то, что он там не нужен и способен замедлить работу с базой данных.

Comments
By   remal
Published   28.02.2010

Тесты слишком искусственны, а их интерпретация высосана из пальца. Но это не главное. Самое главное - зачем использовать mysqli, когда есть PDO?

Published   28.02.2010

remal
Тесты слишком искусственны, а их интерпретация высосана из пальца. Самое главное - зачем использовать mysqli, когда есть PDO?
Бред. Тесты может и искусственны, но из пальца ничего не высосано. А PDO тут не причем, речь идет о mysqli.

By   remal
Published   28.02.2010

Герасимов Константин
Бред. Тесты может и искусственны, но из пальца ничего не высосано. А PDO тут не причем, речь идет о mysqli.

Да-да-да. Давайте возьмем пару простейших запросов, которые не имеют ничего общего с реальным приложением и будет делать тесты на основе их.


Правильно, зачем использовать хорошее расширение с единым API для многих баз и кучей других фич, когда есть решение явно хуже?


ЗЫ: Поправил форматирование.

Published   28.02.2010

Возьмите запросы посложнее. Разница будет не значительная, и зависеть от количества фечнутых полей, клиенту пофиг сколько соединений, подзапросов в запросе и т.п., он лишь передает результат, а запрос непосредственно обрабатывает сервер.

А про PDO я не говорю, даже не хочу говорить о его достоинствах и недостатках, потому что речь, еще раз повторяю, о нём не идёт.

By   remal
Published   28.02.2010

При использовании *только* prepared statements вы избавляетесь от рисков SQL инъекций. Забыть же где-нибудь использовать real_escape_string() 100% забудете. Одно это уже перекрывает любые недостатки в производительности, которые и не факт что есть. Это во-первых.


Во-вторых, процент времени выполнения в каждом участке кода будет совсем другим. И когда вы делаете такой вот "тест", вы никогда не сможете его интерпретировать корректно, т.к. банально не понятно что тут узкое место.


Зачем говорить о более приспособленных для web 2.0 базах данных, когда можно мучиться с mysql? Зачем использовать нормальные языки программирования, когда можно извращаться с PHP (пример)? Зачем использовать нормальное расширение для работы с базой, когда есть другое хуже него?

Такая логика у многих PHP'шников из-за того, что ничего другого не знают или я чего-то не понимаю? Самому, к сожалению, приходится писать и на PHP в том числе, но это в стиле "ежики кололись, но продолжали есть кактус". При этом лично мне не приходит в голову делать еще и какие-то исследования по корявым технологиям.

Published   28.02.2010

remal
При использовании *только* prepared statements вы избавляетесь от рисков SQL инъекций

да, что вы говорите, они позволяют предохранить LIKE, и конечно это вы не читали?

php.net
In general, parameters are legal only in Data Manipulation Language (DML) statements, and not in Data Definition Language (DDL) statements.
может по этому вы и жалеете, что пишете на PHP? ))

By   remal
Published   28.02.2010

Автор, да я без проблем готов согласиться с тем, что облажался SQL инъекций. Вопрос в том, готовы ли вы признать, что тесты бесполезны и использование mysqli вместо PDO - ваши ошибки?


Ваш тон и переход на личности что тут, что на Хабре очень хорошо вас характеризует. За сим удаляюсь, ибо ничего ни приятного, ни полезного для себя в этом общении не вижу.

Published   28.02.2010

remal
использование mysqli вместо PDO - ваши ошибки
Причем тут PDO? Не я, а может некоторые не могут использовать PDO из-за выбранного хостинга? Вы не думаете об этом, например. Поэтому признавать мне нечего. А тон вы мой не слышите и личности со словами "вы" как то не особо согласуются.

By   Василий Кулаков
Published   28.02.2010

Согласен с автором. Он описал реальную ситуацию на одном из расширений. Выбор инструмента: PDO или mysqli - это вопрос за рамками поста и может быть обусловлен массой факторов, не имеющих отношения к prepared statements. Суть поста совершенно не в этом. А коментарии пошли в оффтоп и холивор. Ошибки какие-то признавать надо, про хабр и тон написано. В общем TC, пиши ещё.

By   Василий Кулаков
Published   28.02.2010

А по поводу тестовых примеров - их действительно надо было сделать разнообразнее и обосновать каждый из них. О выборе тестовых примеров можно почитать в книге "Рефакторинг SQL-приложений" издательства "Символ-плюс"

Published   28.02.2010

Василий Кулаков
...

Спасибо за понимание. В следующий раз постараюсь написать более подробно, чтобы не возникали лишние вопросы.

By   Kreker
Published   01.03.2010

Когда я начинал программировать, то mysqli как раз появился, поэтому сразу использовал именно его (в объектном варианте). Потом постепенно стали появляться статьи про подготавливаемые выражения, но я не вдохновился.

Когда подрос и стал переходить на ООП полностью, то первым делом решил написать нормальную оболочку для БД (ну да, мыслить объектами проще с базы - реального примера, с которым работал раньше). Я снова поднял для себя вопрос по PS, но понял, что никакой гибкости они не добавляют. Тут начинается самая шаурма: как использовать PS для динамического составления запроса? Ведь в bind_query нужно передавать аргументы ручками... Единственный выход - вызывать метод через call_user_func_array. Крутотень неимоверная: использовать продвинутый подход с помощью костылей. В общем никаких реальных плюсов я не увидел, да и сказки про то, что "быстрей" тоже меня не убедили, потому как делается больше операций, чем в обычном подходе.

remal
абыть же где-нибудь использовать real_escape_string() 100% забудете.

Конечно, когда идет лапша из html+php и запросы передаются напрямую в функцию mysql_query, то забыть про экранирование очень легко. Но, как правило, те, кто пишут такие приложения, не знают ни про PS, ни про PDO.

By   123
Published   05.03.2010

remal

вы идеот

Create comment
 
Formatting
Comment can not be edited. Please, use the button "Preview"
By
  (Enter prev char)
Comment
Categories
PHP
12
articles
Прочее
4
articles
Delphi
0
article
C/C++
0
article
C#
0
article
Java
0
article
Perl
0
article
Python
0
article
Enter
Cookie must be "ON"
Login
Password
 
Popular tags
PHP
9
articles
паттерн
5
articles
framework
5
articles
шаблон
5
articles
Template View
3
articles
Facade
3
articles
Service Stub
3
articles
Page Controller
2
articles
Singleton
2
articles
Gateway
2
articles
MySQL
2
articles
Registry
2
articles
Command
2
articles
Front Controller
2
articles
Action
2
articles
Abstract Factory
2
articles
типовое решение
2
articles
шаблоны проектирования
2
articles
Iterator
2
articles
Transaction Script
2
articles
Rambler's Top100 Правильный CSS!