PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 55 · После SQLSRV-подключения, параметров, чтения строк и основ транзакций
SQLSRV: отмена результата, повтор команды и ограничение ожидания
Повтор подготовленного поиска после cancel, отличие отмены команды от rollback, отказ из-за pending results при выключенном MARS и контролируемый QueryTimeout. Четыре самостоятельных опыта с видимым состоянием базы.
Команда, её результат и данные — разные вещи
У результата SQL есть жизнь после успешного запроса: строки ещё читаются, команда может оставаться активной, а приложение уже хочет другую выдачу. Эта глава помогает управлять именно этим состоянием. Сначала отменим ненужный остаток и повторим поиск, затем отделим отмену команды от отката данных, разберём занятое соединение и ограничим ожидание медленного запроса.
- Получить данные: query или prepare → execute
- Прочитать нужное: fetch; больше не нужно → cancel
- Ресурс больше не нужен → free_stmt
- Данные группы сохранить/отменить → commit/rollback
- Не ждать команду бесконечно → QueryTimeout до выполнения
Представьте открытый список заказов и сохранённый бланк запроса. Cancel закрывает ненужную текущую выдачу, а подготовленный бланк можно заполнить другим параметром. Free убирает сам бланк. Это модель владения результатом, не описание физической памяти SQL Server и не обещание удалять или возвращать записи в базе.
Нужны понятия соединения и ресурса команды, prepare/execute и параметров по ссылке и последовательного чтения результата. Объяснение текущего вызова начинается с одного действия:
if (sqlsrv_cancel($stmt) === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
throw new RuntimeException("Результат не отменён");
}
echo "Ненужный остаток выдачи закрыт";Это короткий контекстный фрагмент с уже открытым $stmt. Скобки передают ресурс функции; === false отделяет отказ от успеха; echo выполняется только после успешного вызова. Текст результата: «Ненужный остаток выдачи закрыт». Ни таблицу, ни соединение этот фрагмент не удаляет. Ниже каждое действие включено в полную программу с подготовкой своих данных и проверяемой таблицей результата.
| Действие | На что влияет | Чего не обещает |
|---|---|---|
| cancel($stmt) | Непрочитанные результаты текущей команды; подготовленный ресурс можно повторить. | Откат всей операции приложения или отмену другого HTTP-запроса. |
| free_stmt($stmt) | Освобождение ресурса; повторять его нельзя. | Автоматическое правильное завершение всех бизнес-транзакций. |
| rollback($db) | Незавершённая явная группа этого соединения. | Отмену ранее успешного commit или отправленного письма. |
| QueryTimeout | Настроенное до запуска ожидание конкретной SQL-команды. | Ускорение запроса, ровно измеренную задержку или полный частичный ответ. |
Закрыть ненужную выдачу и повторить подготовленный поиск
Внутренний обработчик начал искать заказы от 1000 копеек, прочитал первый и получил новое требование: нужны заказы от 3000. Оставшаяся старая выдача больше не нужна. Отменим её, сохраним подготовленную команду и выполним поиск заново. Получим ID 3 и 4; четыре строки базы останутся на месте.
- prepare: один SELECT, параметр &$minimum = 1000
- execute → выдача ID 1, 2, 3, 4
- fetch читает только ID 1
- cancel отбрасывает непрочитанную старую выдачу
- minimum = 3000 → execute того же ресурса → ID 3, 4
- free_stmt завершает жизнь ресурса; таблица всё ещё содержит 4 строки
Сначала короткий вызов
if (sqlsrv_cancel($stmt) === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
throw new RuntimeException("Старая выдача не закрыта");
}
$minimum = 3000;
if (sqlsrv_execute($stmt) === false) {
throw new RuntimeException("Новый поиск не выполнен");
}$stmt — уже подготовленная и выполненная команда, а не соединение $db и не строка SQL. В полном файле &$minimum связывает параметр со значением переменной: следующий execute использует новый порог. === false проверяет отказ функции. cancel не меняет WHERE и не удаляет заказы; он прекращает использование оставшихся результатов текущего выполнения. После free_stmt этот ресурс больше не используют.
Какой результат получится
| Наблюдение | Результат |
|---|---|
| Первый порог, копейки | 1000 |
| Прочитан ID первого результата | 1 |
| Остаток результата отменён | да |
| Новый порог, копейки | 3000 |
| ID после повторного execute | 3, 4 |
| Повторён тот же подготовленный ресурс | да |
| Ресурс окончательно освобождён | да |
| Строк в самой таблице | 4 |
Полная программа и архив для самостоятельного запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
$orders=s151OwnTable($db,$owned,'cancel_reuse','(id int NOT NULL PRIMARY KEY, total_minor int NOT NULL)');
$q=s151Qualified($orders);
s151Command($db,"INSERT INTO $q (id,total_minor) VALUES (1,1000),(2,2000),(3,3000),(4,4000)");
$minimum=1000;
$params=[&$minimum];
$stmt=sqlsrv_prepare($db,"SELECT id,total_minor FROM $q WHERE total_minor>=? ORDER BY id",$params,['QueryTimeout'=>10]);
if ($stmt===false) {s151Failure('Подготовка поиска');}
try {
$resourceId=get_resource_id($stmt);
if (sqlsrv_execute($stmt)===false) {s151Failure('Первый поиск');}
$first=sqlsrv_fetch_array($stmt,SQLSRV_FETCH_ASSOC);
if ($first===false) {s151Failure('Первая строка поиска');}
if ($first===null) {throw new RuntimeException('В учебной выдаче должна быть строка');}
// The caller now needs another filter; rows 2, 3 and 4 of this result are not used.
$cancelled=sqlsrv_cancel($stmt);
if ($cancelled===false) {s151Failure('Отмена остатка первого поиска');}
$minimum=3000;
if (sqlsrv_execute($stmt)===false) {s151Failure('Повторный поиск с новым порогом');}
$again=s151Rows($stmt);
$sameStatement=$resourceId===get_resource_id($stmt);
} finally {
$freed=sqlsrv_free_stmt($stmt);
if ($freed===false) {s151Failure('Освобождение поисковой команды');}
}
$stmt=null; // Do not use the freed statement again.
$ids=implode(', ',array_column($again,'id'));
$remaining=s151Read($db,"SELECT COUNT(*) AS total FROM $q")[0]['total'];
$rows=[['Первый порог, копейки',1000],['Прочитан ID первого результата',$first['id']],
['Остаток результата отменён',$cancelled],['Новый порог, копейки',$minimum],
['ID после повторного execute',$ids],['Повторён тот же подготовленный ресурс',$sameStatement],
['Ресурс окончательно освобождён',$freed],['Строк в самой таблице',$remaining]];
$checks=['first_row'=>$first===['id'=>1,'total_minor'=>1000],'cancel_succeeded'=>$cancelled===true,
'new_bound_filter'=>$again===[['id'=>3,'total_minor'=>3000],['id'=>4,'total_minor'=>4000]],
'same_prepared_resource'=>$sameStatement,'free_succeeded'=>$freed===true,'table_unchanged'=>$remaining===4];
$observations=['first_row'=>$first,'second_result'=>$again,'filter_values'=>[1000,3000],
'same_statement'=>$sameStatement,'table_row_count_after_cancel'=>$remaining];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-cancel-reuse','Закрыть ненужную выдачу и повторить подготовленный поиск',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первый порог, копейки — 1000
Прочитан ID первого результата — 1
Остаток результата отменён — да
Новый порог, копейки — 3000
ID после повторного execute — 3, 4
Повторён тот же подготовленный ресурс — да
Ресурс окончательно освобождён — да
Строк в самой таблице — 4
Распакуйте в отдельную папку и следуйте README.txt. lab.php содержит открытый код подключения к отдельному учебному SQL Server и сохранения результатов. example.php создаёт одну собственную таблицу, демонстрирует состояние команды и удаляет только эту таблицу после наблюдений.
Таблица сохранённого запуска · Данные и диагностические наблюдения JSON.
Границы и ошибки
Повторное выполнение после cancel относится к ресурсу sqlsrv_prepare. Ресурс, полученный одним sqlsrv_query, таким способом повторно не выполняют: для разовой команды делают новый query. Если все результаты уже прочитаны, обязательной дополнительной отмены нет. Это последовательный сценарий в одном PHP-процессе, не кнопка, которая из другого HTTP-запроса остановит зависший поиск. Если изначально нужна одна строка, задайте TOP (1), нужные поля и ORDER BY в SQL; не получайте весь каталог ради демонстрации cancel.
Измените одно требование
Поставьте второй порог 4500. Удалилась ли информация, если новая выдача пуста?
Как проверить изменение
Повторный SELECT вернёт пустой массив; в таблице по-прежнему четыре строки. Пустой результат нового фильтра — нормальный ответ, не сбой и не удаление данных. Проверка первой выдачи и COUNT помогают различить эти случаи.
Отличить отмену команды от отмены резерва
Резерв уменьшил остаток с 10 до 8. Разработчик вызывает cancel и ожидает, что товар вернётся на склад. Проверим это ожидание: выполненная команда отменена как ресурс результата, но свой SELECT всё ещё видит 8, а транзакция открыта. Только явный rollback возвращает 10.
- Подтверждённый остаток: 10
- begin → UPDATE списывает 2; rows_affected = 1
- cancel($stmt) завершает использование команды
- SELECT на том же $db → 8; @@TRANCOUNT → 1
- rollback($db) отменяет изменения группы
- SELECT → 10; @@TRANCOUNT → 0
Сначала короткий вызов
// UPDATE уже выполнен и его результат проверен.
if (sqlsrv_cancel($stmt) === false) {
throw new RuntimeException("Команда не отменена");
}
// Для отмены данных активной группы нужен другой вызов:
if (sqlsrv_rollback($db) === false) {
throw new RuntimeException("Резерв не отменён");
}Параметры различаются по смыслу: cancel получает ресурс конкретной команды $stmt, rollback — соединение $db с активной группой изменений. @@TRANCOUNT — диагностическое значение SQL Server: в данном простом опыте 1 означает одну открытую транзакцию, 0 — её отсутствие. Эта диагностика помогает увидеть состояние; приложение не заменяет ей корректное управление begin/commit/rollback.
Какой результат получится
| Наблюдение | Результат |
|---|---|
| До резерва | 10 |
| UPDATE изменил строк | 1 |
| cancel команды успешен | да |
| Свое соединение после cancel видит | 8 |
| Транзакций после cancel | 1 |
| Явный rollback успешен | да |
| После rollback доступно | 10 |
| Транзакций после rollback | 0 |
Полная программа и архив для самостоятельного запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
$stock=s151OwnTable($db,$owned,'cancel_transaction','(id int NOT NULL PRIMARY KEY, available int NOT NULL)');
$q=s151Qualified($stock);
s151Command($db,"INSERT INTO $q (id,available) VALUES (1,10)");
s151Command($db,'SET NOCOUNT OFF');
$active=false;$stmt=null;
try {
if (sqlsrv_begin_transaction($db)===false) {s151Failure('Начало учебного резерва');}
$active=true;
$stmt=s151Query($db,"UPDATE $q SET available=available-? WHERE id=? AND available>=?",[2,1,2]);
$affected=sqlsrv_rows_affected($stmt);
if ($affected===false) {s151Failure('Количество изменённых строк резерва');}
if ($affected!==1) {throw new RuntimeException('Ожидался резерв одной учебной позиции');}
// This UPDATE has completed. Cancel is not an undo of its data change.
$cancelled=sqlsrv_cancel($stmt);
if ($cancelled===false) {s151Failure('Отмена остатка команды резерва');}
if (sqlsrv_free_stmt($stmt)===false) {s151Failure('Освобождение команды резерва');}
$stmt=null;
$inside=s151Read($db,"SELECT available FROM $q WHERE id=?",[1])[0]['available'];
$transactionCount=s151Read($db,'SELECT @@TRANCOUNT AS transaction_count')[0]['transaction_count'];
$rolledBack=sqlsrv_rollback($db);
if ($rolledBack===false) {s151Failure('Явная отмена учебного резерва');}
$active=false;
} finally {
if (is_resource($stmt)) {sqlsrv_free_stmt($stmt);}
if ($active && sqlsrv_rollback($db)===false) {s151Failure('Аварийный откат учебного резерва');}
}
$after=s151Read($db,"SELECT available FROM $q WHERE id=?",[1])[0]['available'];
$afterCount=s151Read($db,'SELECT @@TRANCOUNT AS transaction_count')[0]['transaction_count'];
$rows=[['До резерва',10],['UPDATE изменил строк',$affected],['cancel команды успешен',$cancelled],
['Свое соединение после cancel видит',$inside],['Транзакций после cancel',$transactionCount],
['Явный rollback успешен',$rolledBack],['После rollback доступно',$after],['Транзакций после rollback',$afterCount]];
$checks=['one_row_updated'=>$affected===1,'cancel_succeeded'=>$cancelled===true,'cancel_not_undo'=>$inside===8,
'transaction_still_active'=>$transactionCount===1,'rollback_succeeded'=>$rolledBack===true,
'stock_restored'=>$after===10,'transaction_closed'=>$afterCount===0];
$observations=['available_before'=>10,'available_after_completed_update_and_cancel'=>$inside,
'transaction_count_after_cancel'=>$transactionCount,'available_after_rollback'=>$after];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-cancel-transaction','Отличить отмену команды от отмены резерва',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До резерва — 10
UPDATE изменил строк — 1
cancel команды успешен — да
Свое соединение после cancel видит — 8
Транзакций после cancel — 1
Явный rollback успешен — да
После rollback доступно — 10
Транзакций после rollback — 0
Распакуйте в отдельную папку и следуйте README.txt. lab.php содержит открытый код подключения к отдельному учебному SQL Server и сохранения результатов. example.php создаёт одну собственную таблицу, демонстрирует состояние команды и удаляет только эту таблицу после наблюдений.
Таблица сохранённого запуска · Данные и диагностические наблюдения JSON.
Границы и ошибки
Мы проверяем уже завершённый UPDATE внутри собственной явной транзакции, а не все разновидности прерывания SQL. Отмена выполняющейся команды, ошибки и SET XACT_ABORT могут влиять на состояние иначе. Не делайте универсальный вывод «любая отмена всегда оставляет всё как есть» и не обещайте автоматический откат всей бизнес-операции. После непредвиденного отказа контролируйте состояние и явно завершайте свою транзакцию. Ранее подтверждённое изменение не вернётся от позднего rollback; внешняя отправка письма тоже не отменится.
После кнопки отмены запроса в SSMS автор ожидал исчезновения всей транзакции, но обнаружил её активной. Исходная практическая проблема на Stack Overflow. Это вопрос о другой клиентской программе, используемый как пример распространённого смешения понятий. В PHP показываем ограниченный самостоятельный опыт с завершённым UPDATE: cancel команды отдельно от rollback группы. Советы про XACT_ABORT не объявляем универсальным контрактом SQLSRV.
Измените одно требование
Что получится, если после cancel выполнить проверенный commit вместо rollback?
Как проверить изменение
Остаток 8 будет подтверждён, транзакция завершится. Cancel не является решением о сохранении данных: его принимает код, который выбирает commit или rollback. В проверках такого изменённого опыта следует ожидать 8, а не 10.
Освободить соединение, занятое непрочитанной выдачей
Обработчик начал читать список документов, взял первый, а затем хочет выполнить другой запрос на том же соединении. При отключённом MARS непрочитанный результат может занять соединение. Воспроизведём отказ, сохраним его причину и откажемся от ненужного остатка выдачи. После cancel тот же COUNT вернёт 200.
- Отдельное соединение: MultipleActiveResultSets = false
- SELECT возвращает 200 строк с полем по 4000 байт
- Прочитан ID 1; оставшийся результат активен
- Новый COUNT на том же соединении → pending results
- Сохраняем диагностику → cancel старого результата
- Повтор COUNT → 200; данные не изменялись
Сначала короткий вызов
$options['MultipleActiveResultSets'] = false; // Опция подключения.
// $pending получен на соединении с этой опцией; остаток уже не нужен.
if (sqlsrv_cancel($pending) === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
throw new RuntimeException("Результат не закрыт");
}
// Теперь можно отправить следующую команду на это соединение.MARS — поддержка нескольких активных результатов на одном соединении; это настройка подключения, не параметр SELECT. Здесь она намеренно выключена в отдельном соединении. Forward-курсор читается последовательно. Большое текстовое поле служит только воспроизводимой учебной нагрузкой: 200 × 4000 байт не дают свести опыт к крошечному полностью полученному ответу. Текст не печатается целиком — видны его размер и нужный ID.
Какой результат получится
| Наблюдение | Результат |
|---|---|
| MARS в учебном соединении | нет |
| Прочитан первый ID | 1 |
| Байт в поле первой строки | 4000 |
| Вторая команда отклонена | да |
| Причина pending results сохранена | да |
| Ненужная выдача отменена | да |
| COUNT после cancel на том же соединении | 200 |
Полная программа и архив для самостоятельного запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
$items=s151OwnTable($db,$owned,'mars_pending','(id int NOT NULL PRIMARY KEY, payload varchar(4000) NOT NULL)');
$q=s151Qualified($items);
s151Command($db,";WITH n AS (SELECT 1 AS id UNION ALL SELECT id+1 FROM n WHERE id<200)
INSERT INTO $q (id,payload) SELECT id,REPLICATE('x',4000) FROM n OPTION (MAXRECURSION 200)");
$singleConfig=$config;
$singleConfig['options']['MultipleActiveResultSets']=false;
$single=s151Connect($singleConfig);
$pending=null;
try {
$pending=s151Query($single,"SELECT id,payload FROM $q ORDER BY id",[],['Scrollable'=>SQLSRV_CURSOR_FORWARD]);
$first=sqlsrv_fetch_array($pending,SQLSRV_FETCH_ASSOC);
if ($first===false) {s151Failure('Чтение первой строки большой выдачи');}
if ($first===null) {throw new RuntimeException('Учебная выдача неожиданно пуста');}
// Deliberate second command while most of the first result remains unread.
$blocked=sqlsrv_query($single,"SELECT COUNT(*) AS total FROM $q",[],['QueryTimeout'=>2]);
$saved=s151Diagnostics(sqlsrv_errors(SQLSRV_ERR_ALL));
$wasBlocked=$blocked===false;
if (is_resource($blocked)) {sqlsrv_free_stmt($blocked);}
if (!$wasBlocked) {throw new RuntimeException('Ожидался контролируемый отказ из-за pending results');}
$cancelled=sqlsrv_cancel($pending);
if ($cancelled===false) {s151Failure('Освобождение соединения отменой выдачи');}
$after=s151Read($single,"SELECT COUNT(*) AS total FROM $q")[0]['total'];
} finally {
if (is_resource($pending)) {sqlsrv_free_stmt($pending);}
sqlsrv_close($single);
}
$pendingCode=false;
foreach ($saved as $error) {
if ($error['SQLSTATE']==='IMSSP' && $error['code']===-44) {$pendingCode=true;}
}
$rows=[['MARS в учебном соединении',false],['Прочитан первый ID',$first['id']],
['Байт в поле первой строки',strlen($first['payload'])],['Вторая команда отклонена',$wasBlocked],
['Причина pending results сохранена',$pendingCode],['Ненужная выдача отменена',$cancelled],
['COUNT после cancel на том же соединении',$after]];
$checks=['first_id'=>$first['id']===1,'large_unread_result'=>strlen($first['payload'])===4000,
'second_command_blocked'=>$wasBlocked,'pending_diagnostic'=>$pendingCode,
'cancel_succeeded'=>$cancelled===true,'connection_reused'=>$after===200];
$observations=['mars_enabled'=>false,'seeded_rows'=>200,'first_payload_bytes'=>strlen($first['payload']),
'blocked_command_diagnostic'=>$saved,'count_after_cancel'=>$after];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-mars-pending','Освободить соединение, занятое непрочитанной выдачей',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
MARS в учебном соединении — нет
Прочитан первый ID — 1
Байт в поле первой строки — 4000
Вторая команда отклонена — да
Причина pending results сохранена — да
Ненужная выдача отменена — да
COUNT после cancel на том же соединении — 200
Распакуйте в отдельную папку и следуйте README.txt. lab.php содержит открытый код подключения к отдельному учебному SQL Server и сохранения результатов. example.php создаёт одну собственную таблицу, демонстрирует состояние команды и удаляет только эту таблицу после наблюдений.
Таблица сохранённого запуска · Данные и диагностические наблюдения JSON.
Границы и ошибки
У SQLSRV MARS по умолчанию включён; этот опыт не утверждает, что любой второй запрос обязательно упадёт. Альтернативы: дочитать все нужные результаты, освободить ненужную команду через free_stmt или осознанно использовать MARS/другое соединение. Cancel подходит только если оставшиеся данные действительно не нужны. Механически читать всю большую выдачу в массив ради освобождения соединения — лишняя память и передача; лучше пересмотреть SQL. MARS не равно несколько последовательных результатов одного batch: их читают next_result. Вложенный SELECT для каждой строки может создать проблему N+1; для экрана часто подходит один JOIN или заранее ограниченная выборка.
Человек отключил MARS и столкнулся с pending results: следующая команда не исполняется, пока предыдущий результат не завершён. Исходная практическая проблема на Stack Overflow. Вопрос задан о PDO_SQLSRV; API чужой обёртки не переносим. Собственный пример на SQLSRV изолирует непрочитанный SELECT и освобождает его документированным cancel. Разбираем также дочитывание и окончательное free_stmt.
Измените одно требование
Нужен только первый документ. Как убрать и большой результат, и необходимость читать 199 лишних строк?
Как проверить изменение
Запросить TOP (1) id с нужным ORDER BY и затем освободить результат. Для количества использовать COUNT на сервере. Не выбирать payload, если экран его не показывает. Учебное поле в 4000 байт нужно для демонстрации состояния драйвера, не как образец проектирования списка.
Ограничить ожидание команды и отличить таймаут от пустого ответа
Экран состояния задания не должен бесконечно ждать медленный запрос. Ограничим ожидание конкретной команды одной секундой и намеренно поставим перед SELECT задержку три секунды. Получим контролируемый отказ с диагностикой таймаута, затем прочитаем ready обычным запросом. Это не пустой список и не подтверждение успешного чтения.
- В таблице одно задание: id = 1, status = ready
- Запрос: WAITFOR 3 секунды → SELECT
- Опция команды QueryTimeout = 1 секунда
- Драйвер сообщает таймаут; причина сразу копируется
- Обычный SELECT на том же соединении → ready
- Таблица осталась с одной строкой; новое чтение проверено отдельно
Сначала короткий вызов
$stmt = sqlsrv_query($db, $sql, $params, ['QueryTimeout' => 1]);
if ($stmt === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
// Классифицировать причину; не показывать «ничего не найдено».
}Четвёртый аргумент sqlsrv_query — массив настроек команды; третий содержит значения вместо знаков ?. QueryTimeout задаётся положительным целым числом в секундах. Это ограничение ожидания SQL-команды, а LoginTimeout относится к созданию соединения. PHP max_execution_time и таймаут HTTP-сервера — ещё другие границы. hrtime измеряет наблюдённую длительность, но проверка не требует точного совпадения с одной секундой.
Какой результат получится
| Наблюдение | Результат |
|---|---|
| QueryTimeout этой команды, секунд | 1 |
| Искусственная задержка SQL, секунд | 3 |
| Команда вернула false | да |
| Сохранена диагностика таймаута | да |
| Новый SELECT на том же соединении | ready |
| Строк после опыта | 1 |
Полная программа и архив для самостоятельного запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
$jobs=s151OwnTable($db,$owned,'query_timeout','(id int NOT NULL PRIMARY KEY, job_status varchar(16) NOT NULL)');
$q=s151Qualified($jobs);
s151Command($db,"INSERT INTO $q (id,job_status) VALUES (1,'ready')");
$started=hrtime(true);
// A bounded read-only delay deliberately exceeds this statement's one-second timeout.
$slow=sqlsrv_query($db,"WAITFOR DELAY '00:00:03'; SELECT job_status FROM $q WHERE id=?",[1],['QueryTimeout'=>1]);
$saved=s151Diagnostics(sqlsrv_errors(SQLSRV_ERR_ALL));
$elapsed=(hrtime(true)-$started)/1000000000;
$failed=$slow===false;
if (is_resource($slow)) {sqlsrv_free_stmt($slow);}
if (!$failed) {throw new RuntimeException('Ожидался контролируемый таймаут команды');}
$timeoutDiagnostic=false;
foreach ($saved as $error) {
if ($error['SQLSTATE']==='HYT00') {$timeoutDiagnostic=true;}
}
$after=s151Read($db,"SELECT job_status FROM $q WHERE id=?",[1])[0]['job_status'];
$count=s151Read($db,"SELECT COUNT(*) AS total FROM $q")[0]['total'];
$rows=[['QueryTimeout этой команды, секунд',1],['Искусственная задержка SQL, секунд',3],
['Команда вернула false',$failed],['Сохранена диагностика таймаута',$timeoutDiagnostic],
['Новый SELECT на том же соединении',$after],['Строк после опыта',$count]];
$checks=['query_timeout_returned_false'=>$failed,'timeout_diagnostic'=>$timeoutDiagnostic,
'connection_reused'=>$after==='ready','table_unchanged'=>$count===1];
$observations=['query_timeout_seconds'=>1,'requested_wait_seconds'=>3,'elapsed_seconds'=>round($elapsed,3),
'saved_timeout_diagnostic'=>$saved,'state_after_timeout'=>$after,'row_count_after_timeout'=>$count];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-query-timeout','Ограничить ожидание команды и отличить таймаут от пустого ответа',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
QueryTimeout этой команды, секунд — 1
Искусственная задержка SQL, секунд — 3
Команда вернула false — да
Сохранена диагностика таймаута — да
Новый SELECT на том же соединении — ready
Строк после опыта — 1
Распакуйте в отдельную папку и следуйте README.txt. lab.php содержит открытый код подключения к отдельному учебному SQL Server и сохранения результатов. example.php создаёт одну собственную таблицу, демонстрирует состояние команды и удаляет только эту таблицу после наблюдений.
Таблица сохранённого запуска · Данные и диагностические наблюдения JSON.
Границы и ошибки
Задержка WAITFOR здесь короткая, намеренная и без изменения данных; она не является способом ускорять реальные запросы. Таймаут не гарантирует полезный частичный результат и не заменяет WHERE, TOP, пагинацию и индекс. В обычном синхронном PHP код, стоящий после заблокированного sqlsrv_query, не может одновременно вызвать cancel для него: QueryTimeout настраивается заранее. Успешное новое чтение доказывает пригодность именно этого соединения после этого опыта, но не обещает восстановление после любого сетевого сбоя. Если прервалась запись, отдельно выясняйте состояние своей транзакции и операции; не повторяйте платёж вслепую. Не увеличивайте все лимиты без анализа причины медленного запроса.
Ошибка Maximum execution time была принята за таймаут SQLSRV; автор также путал место настройки QueryTimeout. Исходная практическая проблема на Stack Overflow. Отделяем лимит PHP, ожидание соединения и настройку конкретной SQL-команды. Пример воспроизводит именно QueryTimeout и показывает диагностику с последующим проверенным чтением; увеличение max_execution_time не выдаётся за оптимизацию SQL.
Измените одно требование
Уберите WAITFOR и оставьте QueryTimeout = 1. Как отличить нормальный ответ от таймаута?
Как проверить изменение
При успешном запросе вернётся ресурс и строка ready; при ошибке — false с диагностикой. Если WHERE не найдёт ID, запрос может быть успешным, но fetch вернёт null. Это три разных исхода, и интерфейс должен их различать.
Справочник управления остатком результата
Собрать понятный жизненный цикл запроса
Перед следующим запросом решите, нужен ли оставшийся результат: дочитать его, отменить или окончательно освободить ресурс. Отдельно завершите свою транзакцию. Если выдача не нужна с самого начала, уменьшите её SQL-запросом: конкретные поля, WHERE, ограничение числа строк и стабильный порядок уменьшают передачу данных лучше, чем поздняя отмена ненужного объёма.
Следующий связанный материал — передача больших параметров частями: ресурс входного потока также имеет положение и срок жизни. Для нескольких ответов одного SQL-пакета используйте переход между результатами, а целостность резервирования разбирается в транзакциях.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
sqlsrv_cancelОтбросить ненужные результаты команды и сохранить возможность повторить подготовленный запрос.
Первая версия в PHP Manual не указана; точный проверенный профиль приведён в README · sqlsrv
sqlsrv_cancel(resource $stmt): boolОставшиеся результаты команды больше не нужны, но подготовленный запрос хочется выполнить ещё раз. sqlsrv_cancel отбрасывает непрочитанную часть текущего выполнения; сам ресурс sqlsrv_prepare остаётся пригодным для нового execute.
- Вход: $stmt подготовленного SELECT; прочитан ID 1
- cancel($stmt) отбрасывает оставшуюся выдачу
- true подтверждает отмену результата; false требует диагностики
- Ресурс prepare можно выполнить заново; строки таблицы не удаляются
| Параметр | Значение |
|---|---|
| $stmt | Ресурс команды SQLSRV, результат которой нужно отменить. Это не соединение и не текст запроса. |
Возвращаемое значение — bool: true означает успешную отмену, false — отказ. После false сразу сохраните sqlsrv_errors, прежде чем делать другие вызовы драйвера. Передавать заведомо недействительный или уже освобождённый ресурс не нужно: valid-resource контракт не означает, что любой неверный PHP-аргумент обязательно превратится в обычный false.
Изменился фильтр списка
if (sqlsrv_cancel($stmt) === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
throw new RuntimeException("Старая выдача не закрыта");
}
$minimum = 3000; // Ранее привязан через &$minimum.
if (sqlsrv_execute($stmt) === false) {
throw new RuntimeException("Поиск не повторён");
}Контекстный фрагмент требует существующей подготовленной команды. Полный запуск показывает переход от ID 1 к новой выдаче 3, 4 и сохранность всех четырёх строк таблицы.
Нужна другая команда при отключённом MARS
if (sqlsrv_cancel($pending) === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
throw new RuntimeException("Непрочитанный результат не закрыт");
}
// Остаток больше не нужен; теперь выполнить следующий запрос.
$stmt = sqlsrv_query($db, $sql, $params);Здесь решение отказаться от оставшихся данных уже принято приложением. Самостоятельный опыт с MARS сначала сохраняет отказ второй команды, затем после cancel получает COUNT = 200 на том же соединении.
Если все результаты потреблены, дополнительный cancel не обязателен. Повторить после него можно команду sqlsrv_prepare/sqlsrv_execute; sqlsrv_query повторно таким образом не исполняют. free_stmt окончательно освобождает ресурс, и после него нужен новый ресурс. Cancel не является обещанием отката уже выполненной бизнес-операции: резерв и явный rollback показаны отдельно. Функция не создаёт механизм асинхронной отмены другого PHP-процесса.
Связанные инструменты: подготовка и выполнение, освобождение ресурса, последующие результаты, отмена транзакции.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


