Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 55 · После SQLSRV-подключения, параметров, чтения строк и основ транзакций

SQLSRV: отмена результата, повтор команды и ограничение ожидания

Повтор подготовленного поиска после cancel, отличие отмены команды от rollback, отказ из-за pending results при выключенном MARS и контролируемый QueryTimeout. Четыре самостоятельных опыта с видимым состоянием базы.

Команда, её результат и данные — разные вещи

У результата SQL есть жизнь после успешного запроса: строки ещё читаются, команда может оставаться активной, а приложение уже хочет другую выдачу. Эта глава помогает управлять именно этим состоянием. Сначала отменим ненужный остаток и повторим поиск, затем отделим отмену команды от отката данных, разберём занятое соединение и ограничим ожидание медленного запроса.

Четыре разных решения
  1. Получить данные: query или prepare → execute
  2. Прочитать нужное: fetch; больше не нужно → cancel
  3. Ресурс больше не нужен → free_stmt
  4. Данные группы сохранить/отменить → commit/rollback
  5. Не ждать команду бесконечно → 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; четыре строки базы останутся на месте.

Путь результата
  1. prepare: один SELECT, параметр &$minimum = 1000
  2. execute → выдача ID 1, 2, 3, 4
  3. fetch читает только ID 1
  4. cancel отбрасывает непрочитанную старую выдачу
  5. minimum = 3000 → execute того же ресурса → ID 3, 4
  6. 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 после повторного execute3, 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.

Путь результата
  1. Подтверждённый остаток: 10
  2. begin → UPDATE списывает 2; rows_affected = 1
  3. cancel($stmt) завершает использование команды
  4. SELECT на том же $db → 8; @@TRANCOUNT → 1
  5. rollback($db) отменяет изменения группы
  6. 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
Транзакций после cancel1
Явный rollback успешенда
После rollback доступно10
Транзакций после rollback0
Полная программа и архив для самостоятельного запуска
Отличить отмену команды от отмены резерваСамостоятельный пример
<?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.

Путь результата
  1. Отдельное соединение: MultipleActiveResultSets = false
  2. SELECT возвращает 200 строк с полем по 4000 байт
  3. Прочитан ID 1; оставшийся результат активен
  4. Новый COUNT на том же соединении → pending results
  5. Сохраняем диагностику → cancel старого результата
  6. Повтор 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 в учебном соединениинет
Прочитан первый ID1
Байт в поле первой строки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 обычным запросом. Это не пустой список и не подтверждение успешного чтения.

Путь результата
  1. В таблице одно задание: id = 1, status = ready
  2. Запрос: WAITFOR 3 секунды → SELECT
  3. Опция команды QueryTimeout = 1 секунда
  4. Драйвер сообщает таймаут; причина сразу копируется
  5. Обычный SELECT на том же соединении → ready
  6. Таблица осталась с одной строкой; новое чтение проверено отдельно

Сначала короткий вызов

$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. Это три разных исхода, и интерфейс должен их различать.

Справочник управления остатком результата

sqlsrv_cancel: параметр, возврат и две ситуации

Собрать понятный жизненный цикл запроса

Перед следующим запросом решите, нужен ли оставшийся результат: дочитать его, отменить или окончательно освободить ресурс. Отдельно завершите свою транзакцию. Если выдача не нужна с самого начала, уменьшите её SQL-запросом: конкретные поля, WHERE, ограничение числа строк и стабильный порядок уменьшают передачу данных лучше, чем поздняя отмена ненужного объёма.

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

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

sqlsrv_cancelОтбросить ненужные результаты команды и сохранить возможность повторить подготовленный запрос.

Первая версия в PHP Manual не указана; точный проверенный профиль приведён в README · sqlsrv

sqlsrv_cancel(resource $stmt): bool

Оставшиеся результаты команды больше не нужны, но подготовленный запрос хочется выполнить ещё раз. sqlsrv_cancel отбрасывает непрочитанную часть текущего выполнения; сам ресурс sqlsrv_prepare остаётся пригодным для нового execute.

Что получает и что меняет cancel
  1. Вход: $stmt подготовленного SELECT; прочитан ID 1
  2. cancel($stmt) отбрасывает оставшуюся выдачу
  3. true подтверждает отмену результата; false требует диагностики
  4. Ресурс 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.