Компания
857,62
рейтинг
21 августа 2014 в 19:41

Разработка → mysqlnd — проводник между PHP и MySQL перевод



Расширение mysqlnd появилось ещё в PHP 5.3, но до сих пор малоизвестно среди разработчиков. Однако оно незаменимо, если ваша система основана на MySQL. Если вы хотите узнать, почему это расширение так важно, что оно собой представляет, как его использовать и какие оно даёт преимущества — читайте статью.

Схема взаимодействия

PHP взаимодействует с MySQL через клиентские библиотеки. Их существует две — libmysql и mysqlnd. Первая лицензирована Oracle, в то время как mysqlnd распространяется под лицензией PHP. При этом обе по большей части поддерживаются сотрудниками Oracle. Если пользователю нужно с помощью PHP взаимодействовать с MySQL, то для этого можно использовать один из трёх API: ext/mysql, ext/mysqli и ext/pdo_mysql.

Библиотеки

libmysql. Исторически сложилось так, что PHP нужна клиентская библиотека, написанная на С, для взаимодействия с MySQL. Она также известна как libmysql, и ее можно установить в систему с помощью команды apt-get install libmysql. Она внедряет API для работы с MySQL:

#include <stdio.h>
#include <stdlib.h>
#include <mysql/mysql.h>
#include "mysql_example.h" /* Pour MYSQL_HOST, MYSQL_USER, MYSQL_PASS */

int main(int argv, char *argc[])
{
    MYSQL_RES *results = NULL; MYSQL_ROW row; MYSQL *db = NULL;

    db = (MYSQL *)malloc(sizeof(MYSQL));
    mysql_init(db);
    mysql_options(db, MYSQL_INIT_COMMAND, "SET NAMES UTF8;");
    if(!mysql_real_connect(db, MYSQL_HOST, MYSQL_USER, MYSQL_PASS, NULL, 0, NULL, 0)) {
        fprintf(stderr, "Failed to connect to host %s (%s)", MYSQL_HOST, mysql_error(db));
        exit(EXIT_FAILURE);
    }

    mysql_set_character_set(db, "utf8");
    mysql_select_db(db, "my_database");
    mysql_query(db , "SELECT user_id AS id FROM users WHERE user_description='member' LIMIT 10000");
    results = mysql_use_result(db);

    while(row = mysql_fetch_row(results)) {
        printf("%s\n", row[0]);
    }

    mysql_free_result(results);
    exit(EXIT_SUCCESS);
}

Вы можете выполнить этот код, связав свой бинарный файл с libmysql, передав GCC параметр '-lmysql'. Вся информация по работе с libmysql содержится в документации.

Как вы уже могли заметить, PHP-расширения mysql и mysqli повторяют API для простоты использования. Однако использование libmysql может принести некоторые проблемы:
  • Лицензирование довольно сложное. Если вы хотите создать закрытый коммерческий продукт, то вам придётся оплачивать лицензию.
  • Обновление libmysql влечёт за собой и обновление MySQL-сервера в случае с некоторыми дистрибутивами. А это обстоятельство обычно мало кого радует.

mysqlnd. Начиная с PHP 5.3, код libmysql был полностью переписан, и на свет появилось расширение mysqlnd, нативный драйвер расширения mysql. Эта библиотека имеет PHP-лицензию, что предпочтительнее лицензии Oracle.

Также был переработан код библиотеки, не являвшейся частью PHP (libmysql), что позволило улучшить взаимодействие между MySQL и PHP. Необходимо помнить, что mysqlnd не передаёт в PHP классы и функции (это не совсем так, но об этом ниже). Однако оно может служить прочной основой для других PHP-расширений, — mysql, mysqli и pdo_mysql, — при взаимодействии с MySQL-серверами. Начиная с PHP 5.4, mysqlnd становится библиотекой по умолчанию.

Расширения

Существует три расширения, с помощью которых пользователь PHP может общаться с MySQL-серверами.

mysql. Это самое первое и самое старое расширение, свою историю оно ведёт с конца 90-х. Расширение передаёт функции mysql_ в PHP с помощью С API, которое было взято разработчиками MySQL от версии 3.23. Сегодня оно считается устаревшим и не рекомендуется к использованию, причём в последних версиях PHP приводит к появлению ошибок E_DEPRECATED. Пожалуйста, не используйте его в своих проектах

mysqli. «i» на конце означает improved (улучшенное). Это расширение появилось в PHP 5.0 и предназначалось для замены старого API ext/mysql, поэтому основывается на более поздней версии MySQL – 4.1 и выше. Сейчас оно поддерживает хранимые процедуры, безопасный протокол аутентификации, подготовленные выражения (prepared statements) и многое другое. Расширение также предоставляет PHP-пользователю процедурное и объектно-ориентированное API. Благодаря очень высокой схожести, миграция с ext/mysql на ext/mysqli не вызывает затруднений.

PDO. Это расширение заметно отличается от mysql/mysqli, поскольку оно было разработано для поддержки систем управления реляционными базами данных (RDBMS) вообще, а не конкретно MySQL. По этой причине PDO далек от совершенства и работа с ним ожидает от пользователя большого количества предположений, что иногда выливается в странное поведение расширения. Что имеется в виду?

PDO взаимодействует с SQL-парсером, который эмулирует подготовленные выражения, если RDBMS их не поддерживает. Проблема в том, что поведение этого уровня отличается от поведения RDBMS. В случае с MySQL, уровень эмуляции PDO активен по умолчанию, когда вы готовите запрос. Но он никогда не достигнет уровня подготовленного выражения. По сути, код PDO отпарсит и соберёт ваш запрос без взаимодействия с MySQL на эту тему. Это странное поведение. Поэтому отключите этот уровень эмуляции как можно скорее:

/* Отключаем подготовленные выражения в PDO */
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, 0);

/* То же самое, обратите внимание что мы передаем действительно 0, а не 1 */
$pdo->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 0);

Если уровень эмуляции включён, PDO позаботится о сборке запроса и отправит традиционный запрос к RDBMS. Но, ввиду множества недостатков, это может стать причиной странного поведения. Поскольку PDO ничего не знает о столбцах таблицы, его уровень эмуляции заключит в кавычки каждый параметр, даже числового типа. Это приводит к подобным ошибкам:


$stmt = $pdo->prepare("SELECT user_id FROM users LIMIT :limit");
$stmt->bindValue('limit', 10);
$stmt->execute();

$result = $stmt->fetch();
var_dump($result);

/*
PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right syntax to use near ''10'' 
*/

Как видите, PDO заключил в кавычки параметр limit, хотя этого делать было не нужно. Попробуем отключить уровень эмуляции, оставив только уровень RDBMS (в данном случае MySQL):

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, 0); /* Disable prepared statement emulation layer */
$stmt = $pdo->prepare("SELECT user_id FROM users LIMIT :limit"); /* A true prepare() will be sent to the RDBMS, it has to support it */
$stmt->bindValue('limit', 10);
$stmt->execute();

$result = $stmt->fetch();
var_dump($result);
/*
array(4) {
  ["user_id"]=>
  string(7) "18"
  [0]=>
  string(7) "18"
}
*/

Теперь всё в порядке. Если же вам всё же нужен работающий уровень эмуляции, то нужно указать PDO, что не надо трогать числовые параметры:

/* Tells the PDO prepared statement emulation layer that this column is of type integer (SQL type) */
$stmt->bindValue('limit', 10, PDO::PARAM_INT);

Но на этом ещё не всё. Несмотря на то, что мы явным образом отключили уровень эмуляции подготовленных выражений в PDO, он всё ещё немного активен. У него инициирован парсинг параметров. Есть два вида синтаксиса параметров: анонимные параметры, представленные как «?» в вашем запросе на заполнение, и именованные параметры, вроде «:myparam». Не каждая RDBMS поддерживает оба синтаксиса, причём MySQL не поддерживает именованные, только анонимные. Однако наш предыдущий запрос завершён без проблем. Всё дело в том, что анализатор запросов PDO всё ещё активен, даже с отключённым уровнем эмуляции. Он вмешался и заменил каждый именованный параметр на анонимный, потому что перед этим спросил RDBMS (в данном случае MySQL), поддерживает ли тот именованный синтаксис. Поскольку MySQL не поддерживает, то PDO и заменил все «:myparamname» на «?».

Справедливости ради надо заметить, что API в PDO хорошо спроектировано и способно сильно облегчить жизнь PHP-разработчика. Но если вы не знаете, что происходит на нижних уровнях, то у вас возникнут проблемы.

Подробнее о расширении mysqli

Сегодня все используют PDO, потому что это сильно облегчает переход с одной RDBMS на другую. Однако если вы используете MySQL и не собираетесь её менять, то откажитесь от PDO, иначе потеряете много времени и сил. Лучше посмотрите, насколько функционален API mysqli:



mysqli постоянно обвиняют в том, что он не генерирует исключения, вместо них выдаёт ошибки. Это не так:


<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

try {
    $db = mysqli_connect('myserver', 'myuser', 'secret', 'unknown_database');
} catch (mysqli_sql_exception $e) {
    exit($e->getMessage());
}
try {
    mysqli_query($db, "SELECT foo FROM bar");
    /* ... */
} catch(mysqli_sql_exception $e) { }

mysqli даже может сообщить, что вы забыли про индекс:

<?php
mysqli_report(MYSQLI_REPORT_INDEX);
$db = mysqli_connect('myserver', 'myuser', 'secret', 'my_database');

mysqli_query($db, "SELECT photo FROM Users WHERE source ='web' LIMIT 1000");

/* PHP Warning:  mysqli_query(): (00000/0): No index used in query/prepared statement ... */

MySQL обеспечивает связь клиента (в нашем случае, PHP) со многими вещами. Для более подробной информации изучите документацию.

В mysqli поддерживается функция смены кодировки символов: mysqli_set_charset(). Никогда не надо использовать запрос «SET NAMES». Подробнее можно почитать об этом на php.net или dev.mysql.com.

Теперь давайте поговорим о буферизованных запросах (buffered queries). Когда вы запрашиваете результаты в MySQL, то есть когда вы обычно используете запросы SELECT, то будет сформирован набор данных, содержащих результаты. Идея буферизованных наборов с результатами заключается в организации места их хранения: в клиентской памяти (буферизованный запрос) или оставить на стороне MySQL (небуферизованный запрос). Вот и всё.

Обратите внимание, что мы говорим о прямых запросах, а не о подготовленных выражениях. По умолчанию, буферизуется каждый прямой запрос от mysqli к MySQL. Это означает, что к тому моменту, когда вы cделаете вызов mysqli_query(), весь набор результатов уже будет передан в память на стороне PHP и освобождён на стороне MySQL. Вы можете посчитать его mysqli_num_rows(), можете осуществить поиск mysqli_data_seek(), а также можете сделать другой запрос, пока набор не освобождён. Например:

$mysqli = mysqli_connect(/*...*/);

/* По умолчанию результат будет буферизован клиентом : PHP */
$result = mysqli_query($mysqli, "SELECT id, nom, email FROM members");
$line1 = mysqli_fetch_row($result);
mysqli_data_seek($result, mysqli_num_rows($result)); /* Переходим к последнему кортежу */
$last_result = mysqli_fetch_row($result); /* И получаем его  */

/* Нам больше не нужен результат, поэтому освободим его, что в свою очередь освободит память :*/
mysqli_free_result($result);

Это классический случай «по умолчанию». Не забывайте, что весь набор немедленно передаётся из MySQL в PHP, так что если он может быть большим, то объём памяти под PHP пропорционально увеличится. Однако этот объём вам не удастся увидеть с помощью memory_get_usage(), и он не будет учитываться в memory_limit до тех пор, пока вы не будете использовать mysqlnd в качестве низкоуровневого коннектора.

Если вы хотите сделать тот же запрос с использованием небуферизованного набора результатов, то используйте флаг MYSQLI_USE_RESULT. Но будьте осторожны, потому что результат будет храниться в памяти MySQL-процесса для вашего соединения, а MySQL может хранить лишь один набор на соединение. Поэтому вы не сможете сделать другой прямой запрос по этому соединению до тех пор, пока не освободите набор. Кроме того, нельзя будет осуществлять поиск или подсчитывать количество данных:

$mysqli = mysqli_connect(/*...*/);

/* Результат будет все время храниться на стороне MySQL */
$result = mysqli_query($mysqli, "SELECT id, email FROM members", MYSQLI_USE_RESULT);

$line1 = mysqli_fetch_row($result); /* Осуществляем передачу результата клиенту */

/* Это приведет к ошибке, потому что данные по-прежнему “не ваши” */
mysqli_data_seek($result, mysqli_num_rows($result));

/* Это тоже приведет к ошибке, потому что мы не можем выполнить еще один небуферизированный запрос, не освободив результаты выполнения предыдущего*/
$result2 = mysqli_query($mysqli, "SELECT name FROM membres", MYSQLI_USE_RESULT);

Освободить данные можно с помощью mysqli_free_result(). По умолчанию, любой прямой запрос делается в буферизованном режиме, потому что MySQL-серверу есть чем заняться вместо отведения памяти для хранения каждого набора.

Теперь давайте поговорим о подготовленных выражениях. Они сильно отличаются от традиционных прямых запросов:
  • Подготовленные выражения не используют тот же протокол, что и прямые запросы. Вместо этого используется бинарный протокол, который высокооптимизирован и предоставляет много возможностей, таких как привязка типов данных.
  • Наборы результатов для подготовленных выражений по умолчанию не буферизуются, в отличие от прямых запросов.

Начнём с выгрузки протокола для прямого запроса:

$m = mysqli_connect(/* params */);
$q = mysqli_query($m, 'SELECT * FROM Users LIMIT 1');
while($r = mysqli_fetch_row($q)) {
    /* do something */
}
mysqli_free_result($r);




Как видите, это текстовый протокол, то есть MySQL отправляет в ответ только текст. В запросе вы указывали колонку цифр, а в ответ получили текст. Во-первых, это означает, что в MySQL придётся осуществить дополнительные действия, чтобы конвертировать данные в текст. А во-вторых, на PHP-стороне вы получите только строчные данные.

Тот же запрос в виде подготовленного выражения:

$m = mysqli_connect(/* params */);
$ps = mysqli_prepare($m, 'SELECT * FROM Users LIMIT 1');
mysqli_stmt_execute($ps);
while(mysqli_stmt_fetch($ps)) {
    /*  */
}
mysqli_stmt_close($ps);




Каждый bind и каждая выборка переключают MySQL на приём или передачу данных по сети. Мы не видим этого на скриншоте, однако протокол был бинарным. Это значит, что данные в каждой колонке отправлены как есть: числовые как числовые, а не строчные. Также бинарный протокол позволяет экономить трафик, так как передача, например, TINYINT 200 потянет на 1 байт, а в текстовом виде — 4 байта. Также есть и другие виды сетевых замен.

$m = mysqli_connect(/* params */);
$ps = mysqli_prepare($m, 'SELECT id FROM Users LIMIT 10'); /* id INTEGER */
mysqli_stmt_execute($ps);
mysqli_stmt_bind_result($ps, $id); /* Привяжем результирующую колонку к $id */
while(mysqli_stmt_fetch($ps)) {
    var_dump($id);
}
/*
int(62)
int(64)
*/

Из приведённого примера видно, что PHP получает числовые данные, а не строковые.

Однако можно сохранять типы данных, используя и текстовый протокол. Для этого нужно, чтобы клиент (PHP) конвертировал полученные строковые данные в правильный, ожидаемый тип. И это может делать только mysqlnd, libmysql не поможет:

$m = mysqli_connect(/* */);
$q = mysqli_query($m, 'SELECT id FROM users LIMIT 10';

while($r = mysqli_fetch_row($q)) {
    var_dump($r[0]);
}
/*
string(2) "62"
string(2) "64"
*/

$m = mysqli_connect(/* */);
mysqli_options($m, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true); /* Такое возможно только используя mysqlnd */
$q = mysqli_query($m, 'SELECT id FROM users LIMIT 10');

while($r = mysqli_fetch_row($q)) {
    var_dump($r[0]);
}
/*
int(62)
int(64)
*/

Наборы с результатами для подготовленных выражений по умолчанию не буферизуются, а каждая операция fetch() будет запускать сетевой обмен данными. Однако можно буферизировать наборы с помощью mysqli_stmt_store_result().

$m = mysqli_connect(/* */);
$ps = mysqli_prepare($m, 'SELECT id, name FROM Users LIMIT 1000');
mysqli_stmt_execute($ps);
mysqli_stmt_bind_result($ps, $id, $name);

/* Одним вызовом буферизирием все кортежи */
mysqli_stmt_store_result($ps);

while(mysqli_stmt_fetch($ps)) {
    /* do something with $id and $name */
}
mysqli_stmt_close($ps);

Мы можем буферизировать наборы, но в случае с подготовленными выражениями необходимо привязывать каждую колонку с результатами к PHP-переменной, чтобы иметь возможность считать данные. И ещё, если вы используете mysqlnd, то вам доступен mysqli_stmt_get_result(), превращающий набор результатов для подготовленного выражения в mysqli_result. А это позволяет использовать API для прямых запросов, но с подготовленными выражениями:

$m = mysqli_connect(/* params */);
$ps = mysqli_prepare($m, 'SELECT id, name FROM Users LIMIT 1000');
mysqli_stmt_execute($ps);

/* Превращаем результат в mysqli_result */
$r = mysqli_stmt_get_result($ps); /* Работает только с mysqlnd */

while($result = mysqli_fetch_row($r)) { /* API прямого запроса */
    /* делаем что-нибудь */
}
mysqli_free_result($r);
mysqli_stmt_close($ps);

Подробнее о расширении mysqlnd

Мы уже разобрались, что mysqlnd работает как скрытое расширение, добавляющее много новых возможностей к существующим API, особенно mysqli (это верно и для PDO, только в меньшей степени).

Экономия памяти

Вспомним некоторые моменты:
  • Буферизованный запрос забирает все результаты из MySQL в память PHP.
  • Буферизованный набор результатов создаётся библиотекой, которая используется для связи как в mysqlnd, так и в libmysql.
  • Набор результатов не используется напрямую в PHP-среде, его нужно предварительно конвертировать в PHP-структуру в виде массива, эта операция называется выборка (fetch).

В приведённом ниже примере теряется огромная часть памяти:

$db  = mysqli_connect(/* */);
$result = mysqli_query($db, "SELECT very_huge_blob_column, lots, of, columns FROM foobar"); /* Тяжелый запрос с кучей результатов */

while($results[] = mysqli_fetch_row($result)) { }
mysqli_free_result($result); /* Про это часто забывает, что еще больше ухудшает ситуацию */
foreach($results as $foo) { /* делаем что-нибудь */ }

Докажем это утверждение:

function memory_usage()
{
    $pid = getmypid();
    $r = explode(':',shell_exec("grep VmData /proc/$pid/status"));
    return '->'.trim($r[1])."\n";
}
$db = mysqli_connect(/* */);

echo "initial memory " . memory_usage();
$result = mysqli_query($db,"SELECT very_huge_blob_column FROM foobar");
echo "resultSet stored " . memory_usage();
while($results[] = mysqli_fetch_row($result)) { }
echo "query result saved " . memory_usage();
mysqli_free_result($result);
echo "resultSet freed " . memory_usage();
unset($results);
echo "saved result freed " . memory_usage();
unset($db);
echo "Db closed " . memory_usage();

В libmysql:

> phplibmysql/bin/php poc_mysqli.php
initial memory ->3348 kB
resultSet stored ->72724 kB
query result saved ->149012 kB
resultSet freed ->81156 kB
saved result freed ->25348 kB
Db closed ->24260 kB

Как видите, как только выполняется mysqli_query(), все наборы результатов отправляются в память PHP. Её объём возрастает с 3 до 70 Мб. Это нормальная ситуация для прямых буферизованных запросов. Важно понимать, что буфер памяти был выделен библиотекой libmysql. И когда нужно конвертировать результаты в пригодный для PHP вид, все данные из набора результатов будут продублированы в память, что приводит к её раздуванию.

Если буфер выделяется libmysql, то он не будет отображаться в memory_get_usage(), и вам придётся мониторить свои процессы, чтобы это вовремя заметить.

Конвертирование всех данных из набора в PHP-переменную просто взрывает вашу память. На данной стадии буфер всё ещё выделен и данные полностью дуплицированы в PHP-массив, поэтому занято уже 140 Мб памяти. Давайте удостоверимся в этом, запустив анализатор памяти:

99.92% (257,473,815B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->52.90% (136,314,880B) 0x69A01E: _zend_mm_alloc_int (zend_alloc.c:1908)
| ->52.60% (135,528,448B) 0x69A1DE: _estrndup (zend_alloc.c:2503)
| | ->52.60% (135,528,448B) 0x533BCE: php_mysqli_fetch_into_hash (mysqli.c:1191)
| |   ->52.60% (135,528,448B) 0x53F2E1: zif_mysqli_fetch_row (mysqli_nonapi.c:352)
| |     ->52.60% (135,528,448B) 0x70186A: zend_do_fcall_common_helper_SPEC (zend_vm_execute.h:320)
| |       ->52.60% (135,528,448B) 0x6D9D96: execute (zend_vm_execute.h:107)
| |         ->52.60% (135,528,448B) 0x6B4B98: zend_execute_scripts (zend.c:1236)
| |           ->52.60% (135,528,448B) 0x663D0C: php_execute_script (main.c:2308)
| |             ->52.60% (135,528,448B) 0x73BCDC: main (php_cli.c:1184)
| |               
| ->00.31% (786,432B) in 1+ places, all below ms_print's threshold (01.00%)
| 
->45.85% (118,130,675B) 0x52DD010: my_malloc (my_malloc.c:37)
| ->45.84% (118,112,344B) 0x52E0583: alloc_root (my_alloc.c:219)
| | ->45.83% (118,096,024B) 0x5307A40: cli_read_rows (client.c:1418)
| | | ->45.83% (118,096,024B) 0x5305955: mysql_store_result (client.c:2957)
| | |   ->45.83% (118,096,024B) 0x53EF09: zif_mysqli_query (mysqli_nonapi.c:540)
| | |     ->45.83% (118,096,024B) 0x70186A: zend_do_fcall_common_helper_SPEC (zend_vm_execute.h:320)
| | |       ->45.83% (118,096,024B) 0x6D9D96: execute (zend_vm_execute.h:107)
| | |         ->45.83% (118,096,024B) 0x6B4B98: zend_execute_scripts (zend.c:1236)
| | |           ->45.83% (118,096,024B) 0x663D0C: php_execute_script (main.c:2308)
| | |             ->45.83% (118,096,024B) 0x73BCDC: main (php_cli.c:1184)

Чтобы освободить набор результатов, нужно сделать вызов mysqli_free_result(). После этого занимаемая память уменьшится до 70 Мб. А когда мы, наконец, освободим PHP-массив, содержащий копию набора, то память вернётся к своему прежнему размеру.

Дупликацию можно предотвратить, используя mysqlnd.

> phpmysqlnd/bin/php poc_mysqli.php
initial memory ->3208 kB
resultSet stored ->70452 kB
query result saved ->71220 kB
resultSet freed ->81148 kB
saved result freed ->19196 kB
Db closed ->19196 kB

Когда буферизованный набор конвертируется в PHP-массив, объём памяти не меняется. Только когда вы начнёте записывать в этот массив, то есть модифицировать его, PHP удвоит объем памяти для результата, по принципу «от случая к случаю». Также mysqlnd использует аллокатор памяти для хранения набора результатов в его собственном буфере, память используется совместно с PHP, memory_get_usage() покажет эту память и вы даже можете получить доступа к memory_limit.

Ещё один подход позволяет предотвратить любую операцию «выборки всего». В PDO $stmt->fetchAll() трансформирует все наборы результатов в PHP-переменную. Это позволяет экономнее использовать память для поиска в наборе и получения актуальных данных с последующей конвертацией любой строки в участок PHP-массива.

Статистика



mysqlnd видит всё — каждый байт, проходящий между PHP и MySQL-сервером, и может собирать очень полезную статистику. Можно легко получить ответы на многие важные вопросы без использования плагинов для мониторинга:
  • Сколько у меня активных MySQL-соединений?
  • Сколько ошибок было в соединении?
  • Сколько запросов было приготовлено, но не выполнено? (что является потерей производительности)
  • Сколько запросов было приготовлено, но использовано лишь однажды? (подготовленные выражения полезны лишь при многократном использовании)
  • Сколько колонок запрошено, а выборок не сделано?
  • Сколько было медленных MySQL-запросов?
  • Сколько запросов не используют индекс?

mysqlnd может ответить на все эти вопросы:

$db = mysqli_connect(/* */);

$result = mysqli_query($db,"SELECT user_id, email FROM users LIMIT 5");
mysqli_data_seek($result, 5);
$data = mysqli_fetch_row($result);
do_something($data);
mysqli_free_result($result);
var_dump(mysqli_get_connection_stats($db)); /* only available under mysqlnd */

/*
["buffered_sets"]=>
  string(1) "1"
["rows_fetched_from_server_normal"]=>
  string(1) "5"
["rows_buffered_from_client_normal"]=>
  string(1) "5"
["rows_fetched_from_client_normal_buffered"]=>
  string(1) "1"
["connect_success"]=>
  string(1) "1"
["connect_failure"]=>
  string(1) "0"
["connection_reused"]=>
  string(1) "0"
["reconnect"]=>
  string(1) "0"
["active_connections"]=>
  string(1) "1"
*/

Запрошено 5 результатов, в наборе проведён поиск, сделана выборка, использована, все наборы освобождены. Зачем запрашивать 5 результатов вместо одного? Из статистики видно, что мы получили их все, но rows_fetched_from_client_normal_buffered говорит нам, эффективно была проведена выборка только в одном случае. Потом началось неэффективное использование пропускной способности канала, выделенных для MySQL ресурсов процессора и PHP-памяти.

Расширим mysqli-класс, чтобы получить простой лог:

class JPMysqli extends Mysqli
{
    public function __destruct()
    {
        $stats = $this->get_connection_stats();
        $this->close();
        if($diff = $stats["rows_fetched_from_server_normal"] - ($stats["rows_fetched_from_client_normal_unbuffered"] + $stats["rows_fetched_from_client_normal_buffered"])) {
            trigger_error("Вы не использовали *$diff* из запрошенных кортежей", E_USER_NOTICE);
        }
    }
}

$db = new JPMysqli(/* */);

$result = mysqli_query($db,"SELECT user_id, email FROM users LIMIT 5");
mysqli_data_seek($result, 5);
$data = mysqli_fetch_row($result);
do_something($data);
exit();
/*
Notice : " Вы не использовали *4* из запрошенных кортежей "
*/

Очень много приложений получают тонны результатов по запросам, но эффективно используется лишь часть из них. Если вы используете Symfony2, то можете применять mysqlnd-bundle или mysqlnd-analytics.

Плагины

В mysqlnd заложена возможность расширения с помощью плагинов. Желающие могут разрабатывать их на С. Хотелось бы упомянуть некоторые из них:
  • mysqlnd_qc. Позволяет кэшировать наборы результатов в разные бэкэнды и позднее их использовать.
  • mysqlnd_ms. Master-Slave балансировщик, выбирающий подходящий сервер в зависимости от отправленного запроса. Полностью прозрачен для пользовательской среды.
  • myslnd_uh. Реализовывает хуки. Позволяет писать собственные плагины с помощью PHP (а не С). Возможность создания низкоуровневых mysqlnd-хуков для реализации логирования, защиты от SQL-инъекций, балансировщиков.

Заключение

Надеюсь, теперь вы стали лучше понимать, как PHP взаимодействует с MySQL-серверами. Также надюсь, что mysqlnd поможет вам реализовать ваши задумки эффективнее, с гораздо меньшими нерво- и трудозатратами.

Отдельное спасибо Ulf Wendel, Andrey Hristov, Georg Richter и Johannes Schlüter – основным создателям mysqlnd.
Автор: @AloneCoder Julien Pauli

Комментарии (35)

  • +3
    Спасибо, полезно! Но насчет нервов и трудозатрат — это спорный вопрос. Как сам автор отметил: «Сегодня все используют PDO...».
    • +5
      Да, хотелось бы в продолжение аналогичную статью для PDO
  • 0
    mysqlnd_ms может адекватно и нативно заменить mysql proxy?
  • 0
    Спасибо, про память было особенно интересно!
  • 0
    Очень годно! Спасибо!
  • +2
    Огромное спасибо за это:
    mysqli_options($m, MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);

    Впилил в mysqli модуль для Коханы, при выборке 100 записей из БД потребление памяти упало на 5%. Помоему неплохо для однострочного фикса.
    • +1
      Для нее появилось mysqli расширение?
      • +1
        тысячу лет назад, да, появилось
        • 0
          Прошу прощения, вы имеете в виду официальный модуль или его коммъюнити-версию, где mysql* просто поменяли на mysqli*?
          • 0
            полуофициальный, кто-то из разработчиков Коханы написал по быстрому и залил на гитхаб. Релиза не было естественно.
  • +1
    По теме статьи: моя любимая функция в PHP — как раз mysqli_real_escape_string </sarcasm>
    • 0
      Моя тоже. No sarcasm.
      Прекрасная функция, у которой нет никаких проблем, кроме пользователей.
  • 0
    Очень интересная и полезная статья. Я прочитал её недели 2 назад и сразу отправил ссылки знакомым. Могу порекомендовать остальные 3 статьи в блоге автора: jpauli.github.io/ (английский язык). Пишет про внутренности PHP и то, как это соотносится с буднями программирования на самом PHP.
    • 0
      Статей не густо, но интересно.
  • +2
    эмм возможно я вас неверно понял но функции mysqli_set_character_name() у mysqli нет, есть mysqli_set_charset
    • 0
      Автор, по-видимому, описался. Правильно mysqli_set_charset().

      Кому интересно, чем это отличается от SET NAMES и почему функция mysqli_set_charset() правильнее, вот тут есть пример того, как кодировка влияет на правила эскейпинга. Для меня было откровением. Правда, в реалиях UTF-8, насколько я понимаю, на это наткнуться невозможно (т. е. отличия SET NAMES 'utf8' от mysqli_set_charset не будет).
      • 0
        Ну и для полноты картины стоит добавить, что в PDO аналогичный эффект достигается заданием charset в DSN. Работает начиная с 5.3.6
    • 0
      Спасибо, поправил. Как-то я и сам этот момент упустил
  • 0
    del
  • 0
    Установка пакета mysqlnd в Ubuntu
    sudo apt-get install php5-mysqlnd


    Проверить, установлен ли

    php -m | grep mysqlnd
    • +1
      только аккуратней с apt-get install php5-mysqlnd, так как php5-mysql (и всё что с ним связано, например phpmyadmin) будут удалены
      • +3
        Ну и славно.
      • 0
        Вроде phpmyadmin уже может использовать пакет mysqlnd
  • +1
    Спасибо. Классная статейка. Особенно понравилось про раздувание памяти. Как-то я раньше не думал об этом.
  • 0
    Более 5 лет использую mysqli и узнал кое-что новое
    как говорится «век живи — век учись»
    а вообще-то ман в руки и с флагом по жизни!
    спасибо за статью

    интересно, а с каких пор в маил-ру есть РНР-ные проекты?
  • +5
    первая статья мейлру которую я плюсану с большим удовольствием. Спасибо за подробное описание. Даёшь меньше коммерции, больше технической информации!
    • +3
      статья действительно хорошая, тоже плюсанул, но справедливости ради — это перевод, а не авторская статья мейлру
  • 0
    Ну, как показывает практика, сама libmysql написана через Ж..., и для её использования в много-тредовом режиме приходится делать танцы с бубнами. Багрепорты не помогают…
    • 0
      Да вроде как с инитом только проблемы, не? У нас постоянно в многопоточном либмай гоняют, без проблем.
  • 0
    Боль и страдания причиняют хостеры, которые не предоставляют в пакете mysqlnd драйвер. В частности когда возникает острая нужда использовать PREPARED STATEMENTS. В то время, как mysqlnd позволяет все решить простым вызовом get_result() и fetch, отсутствие драйвера заставляет пользоваться bind_result(… уйма переменных)
    • 0
      Ну как бы, можно использовать ORM, которая будет делать все за вас, можно сделать обертку над DBAL, можно много чего придумать что бы не прописывать все руками.

      Да и что сейчас стоит взять маленький виртуальный сервачек для своего небольшого проектика и распрощаться с болью.
      • 0
        Я не стал заморачиваться с мини проектами клиентов и беру для них хостинг на регру. Они предоставляют драйвер mysqlnd по-умолчанию. Плюс резервные копии разделенные автоматически делают. Минимум головной боли с «беспокойными» клиентами
  • 0
    Вот кто и зачем делает настолько бледные схемы расцветки кода?
    Они как бы говорят мне «У твоего монитора недостаточно выкручена контрастность»)
  • 0
    Очень грустно читать статью, автор которой предубеждён против какой-либо технологии, и, за неимением фактов, пытается отстаивать свои пристрастия с помощью голословных утверждений.

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

    Это единственно правильное поведение в 99.9% случаев.
    Поддерживаемые базой подготовленные выражения ориентированы на однократную подготовку и множественное выполнение одного и того же запроса. В пхп такое поведение требуется в мизерном количестве случаев. Всё остальное время гонять запрос в БД по два раза, чтобы «достичь уровня просветления подготовленного выражения» — бессмысленно. И замена плейсхолдеров на их значения, производимая PDO, работает прекрасно.

    Да, режим эмуляции приводит к небольшим неудобствам.
    Да, параметр LIMIT без явного указания типа приведет к ошибке.
    Да, даже явное указание не спасёт для случая
    $stmt->bindValue('limit', '10', PDO::PARAM_INT);

    — PDO все равно добавит кавычки, несмотря на модификатор PDO::PARAM_INT.
    Но положа руку на сердце — это не те проблемы, ради которых стоит устраивать такой шум. Режим эмуляции — прекрасная вещь, и бежать отключать его совсем не обязательно.

    Несмотря на то, что мы явным образом отключили уровень эмуляции подготовленных выражений в PDO, он всё ещё немного активен. У него инициирован парсинг параметров. Есть два вида синтаксиса параметров: анонимные параметры, представленные как «?» в вашем запросе на заполнение, и именованные параметры, вроде «:myparam». Не каждая RDBMS поддерживает оба синтаксиса, причём MySQL не поддерживает именованные, только анонимные. Однако наш предыдущий запрос завершён без проблем. Всё дело в том, что анализатор запросов PDO всё ещё активен, даже с отключённым уровнем эмуляции. Он вмешался и заменил каждый именованный параметр на анонимный

    Я не пойму — это аргумент за «режим эмуляции» или против? Судя по тону автора, складывается впечатление, что второе.
    Однако по отзывам знакомых программистов — это чуть ли не лучшее, что есть в PDO.

    Поэтому отключите этот уровень эмуляции как можно скорее

    Куда полезнее будет включить режим критического отношения к голословным утверждениям (это же относится к заявлениям еще одного противника режима эмуляции — А.Феррары, если вам случалось читать их на Stackoverflow).
  • 0
    У меня после запуска скрипта с статьи:
    2ой столбец результат функции ( memory_usage с этого скрипта), 3ий ф. memory_get_usage() / 1000000

    libmysql

    initial memory ->79144 kB — 0.237144
    resultSet stored ->95688 kB — 0.237472
    query result saved ->170812 kB — 75.687992
    resultSet freed ->154364 kB — 75.687952
    saved result freed ->79760 kB — 0.237408
    Db closed ->79760 kB — 0.236944

    И после установки mysqlnd:

    initial memory ->5524 kB — 0.244352
    resultSet stored ->27172 kB — 2.646576
    query result saved ->84524 kB — 61.295688
    resultSet freed ->82212 kB — 75.694144
    saved result freed ->80928 kB — 0.244744
    Db closed ->80928 kB — 0.236944

    Ubuntu 14.4.1 / php 5.5.9

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка