Так получилось, что я задал себе этот вопрос и решил немножко покопаться в нём. Результаты не заставили себя долго ждать и привели к некоторым вполне обычным необычным выводам. Этот текст не является точкой отправки, он всего лишь ставит вопрос и оглашает некоторые фаты, относительно него. Поэтому комментарии были бы как никогда кстати. Ну что, суть вся в следующем.
Однажды в PHP появилось расширение под названием mysqli, которое пришло на замену привычному mysql и победило его словом Improved (улучшенный), расставив некоторые точки над i. С тех пор, если вы используете MySQL-сервер версии 4.1.3 и выше, разработчики PHP строго рекомендуют использовать mysqli-расширение. Основными аргументами в пользу последнего были улучшенный протокол обмена между клиентов и сервером, а так же поддержка следующих и не только фич:
- Самое, что было крутое так это поддержка объектно-ориентированного интерфейса. Коннект вешался на объект - и все становились счастливыми. Мы можем наследовать от класса mysqli, перезагружать методы и т. д. Стало намного удобнее, не надо тягать за собой ресурс и всё такое.
- Поддержка Multiple Statements - фактически можно выполнить несколько запросов за одно обращение. Да, это хорошо - уменьшение обращений к серверу открывает новые возможности обращения )). Это позволило открыть некоторым небольшую дыру, но у кого руки не крюки - у тех всё сработало.
- Поддержка 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)
error_reporting(E_ALL);
function workWithoutPS(mysqli $db)
{
$startTime = microtime(TRUE);
/**
* Select 1
*/
$login = "mylogin'mm";
$email = "myemail%@em.com";
$login = "'" . $db->real_escape_string($login) . "'";
$email = "'" . $db->real_escape_string($email) . "'";
$result = $db->query('Select ID from USERS where LOGIN = ' . $login . ' and EMAIL = ' . $email);
while($row = $result->fetch_assoc()) {
$a = $row;
}
$result->close();
/**
* Select 2
*
$login = "mylogin'mm";
$email = "myemail%@em.com";
$login = "'" . $db->real_escape_string($login) . "'";
$email = "'" . $db->real_escape_string($email) . "'";
$result = $db->query('Select ID from USERS where LOGIN = '. $login . ' and EMAIL = ' . $email . ' LIMIT 10 OFFSET 100' );
while($row = $result->fetch_assoc()) {
$a = $row;
}
$result->close();
/**
* Select 3
*
$login = "mylogin'mm";
$login = "'" . $db->real_escape_string($login) . "'";
$result = $db->query('Select ID from USERS where LOGIN = '. $login . ' LIMIT 30 OFFSET 100');
while($row = $result->fetch_assoc()) {
$a = $row;
}
$result->close();*/
/**
* Insert 1
*/
$login = "mylogin'mm";
$email = "myemail%@em.com";
$login = "'" . $db->real_escape_string($login) . "'";
$email = "'" . $db->real_escape_string($email) . "'";
$db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');
/**
* Insert 2
*
$login = "mylogin'mm";
$email = "myemail%@em.com";
$login = "'" . $db->real_escape_string($login) . "'";
$email = "'" . $db->real_escape_string($email) . "'";
$db->query('Insert into USERS (LOGIN, EMAIL) values ('. $login . ',' . $email . ')');*/
return microtime(TRUE) - $startTime;
}
function workWithPS(mysqli $db)
{
$startTime = microtime(TRUE);
/**
* Select 1
*/
$login = "mylogin'mm";
$email = "myemail%@em.com";
$stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ?');
$stmt->bind_param('ss', $login, $email);
$result = $stmt->execute();
$stmt->bind_result($id);
while($stmt->fetch()) {
$a = $id;
}
$stmt->close();
/**
* Select 2
*
$login = "mylogin'mm";
$email = "myemail%@em.com";
$stmt = $db->prepare('Select ID from USERS where LOGIN = ? and EMAIL = ? LIMIT 10 OFFSET 100');
$stmt->bind_param('ss', $login, $email);
$result = $stmt->execute();
$stmt->bind_result($id);
while($stmt->fetch()) {
$a = $id;
}
$stmt->close();
/**
* Select 3
*
$login = "mylogin'mm";
$stmt = $db->prepare('Select ID from USERS where LOGIN = ? LIMIT 30 OFFSET 100');
$stmt->bind_param('s', $login);
$result = $stmt->execute();
$stmt->bind_result($id);
while($stmt->fetch()) {
$a = $id;
}
$stmt->close();*/
/**
* Insert 1
*/
$login = "mylogin'mm";
$email = "myemail%@em.com";
$stmt = $db->prepare('Insert into USERS (LOGIN, EMAIL) values (?, ?)');
$stmt->bind_param('ss', $login, $email);
$result = $stmt->execute();
/**
* Insert 2
*
$login = "mylogin'mm";
$email = "myemail%@em.com";
$stmt->bind_param('ss', $login, $email);
$result = $stmt->execute();*/
return microtime(TRUE) - $startTime;
}
$db = new mysqli('localhost', 'test', 'test', 'test');
$count = 1000;
$time = 0;
for ($i = 0; $i < $count; $i++) {
$time += workWithoutPS($db); //workWithPS($db)
}
$time /= $count;
echo $time . '</br>';
Как видите, я мог комментировать отдельные части кода и получать нужные результаты. В основном результаты проводились тремя последовательными обновлениями браузера так, что постепенно выбиралось всё больше и больше строк. Вот некоторые результаты:
Listing №2 (unknown)
workWithoutPS workWithPS
$count=1000
Select 1 + Insert 1
libmysql mysqlnd libmysql mysqlnd
0.0150 0.0153 0.0172 0.0178
0.0248 0.0277 0.0249 0.0277
0.0368 0.0415 0.0334 0.0383
______ ______ ______ ______
0.0255 0.0282 0.0252 0.0279
Select 1 + Insert 1 + Insert 2
libmysql mysqlnd libmysql mysqlnd
0.0261 0.0287 0.0275 0.0298
0.0507 0.0566 0.0460 0.0513
0.0738 0.0834 0.0636 0.0731
______ ______ ______ ______
0.0502 0.0562 0.0457 0.0514
$count=10000
Select 2 + Select 3
libmysql mysqlnd libmysql mysqlnd
- 0.00659 - 0.01000
- 0.00653 - 0.01040
- 0.00649 - 0.01010
______ _______ _______ _______
- 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, потому что пока всё указывает на то, что он там не нужен и способен замедлить работу с базой данных.
Тесты слишком искусственны, а их интерпретация высосана из пальца. Но это не главное. Самое главное - зачем использовать mysqli, когда есть PDO?