Pull to refresh
VK
Building the Internet

mysqlnd — проводник между PHP и MySQL

Reading time 16 min
Views 65K
Original author: Julien Pauli


Расширение 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.
Tags:
Hubs:
+103
Comments 35
Comments Comments 35

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен