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

Дерево руководства
Глава 46 · После подключения и первых SELECT
MySQL: буфер результата, потоковый CSV и чтение порциями
Получаем строки, завершаем выдачу, повторно используем соединение и выбираем размер порции по задаче.
Буфер или поток: что меняется для памяти и соединения
Нужно вывести страницу товаров или выгрузить большой файл. В обоих случаях SELECT возвращает строки, но способ их получения определяет время жизни памяти и занятость соединения. Начинаем после первого подключения и согласования типов/кодировки. SQL и временные входные таблицы включены в архивы: читатель видит, откуда появляются данные.
- Буфер: сервер → вся выдача в память mysqlnd → PHP читает/перечитывает
- Поток: сервер → очередная строка → обработка → следующая
- Пока поток не окончен, это соединение занято
- После работы → free/close; итоговый CSV или HTML остаётся
| Свойство | Buffered / store | Unbuffered / use |
|---|---|---|
| Память PHP | Весь результат, плюс пользовательские структуры | Без полного клиентского буфера; код всё равно может сам накопить массив |
| Повторное чтение | data_seek по буферу | Дочитанные строки не вернуть перемещением указателя |
| Число строк заранее | num_rows уже доступно | До полного чтения не используйте как окончательный размер |
| Новый SQL на том же соединении | После приёма буфера возможен | Сначала завершить текущую выдачу |
| Типичная задача | Небольшая страница, повторная обработка | Последовательный экспорт, обработка большого результата |
Обычный mysqli query по умолчанию buffered; prepared statement по умолчанию unbuffered. PDO_MYSQL обычно buffered, режим меняется атрибутом PDO::MYSQL_ATTR_USE_BUFFERED_QUERY; завершение курсора — closeCursor. Эти имена не взаимозаменяемы с MYSQLI_USE_RESULT. Серверная сортировка, план, locks и объём ответа существуют при обоих режимах; unbuffered не делает любой запрос автоматически быстрым. С mysqlnd память буфера входит в PHP memory_limit; исторический libmysqlclient учитывал её иначе.
Повторно прочитать страницу каталога из буфера
Нужно показать четыре товара, вывести число строк и ещё раз прочитать первую. Для небольшого результата удобен буфер: mysqli получает всю выдачу в память клиента, после чего PHP может перемещаться по ней. Входные строки с ID, названием и остатком создаёт helper из архива; ниже показана таблица, которую вернёт SELECT.
- SELECT → сервер отдаёт 4 строки
- MYSQLI_STORE_RESULT → буфер mysqlnd в PHP
- num_rows = 4; fetch → строка 1
- data_seek(0) → повторное чтение с начала
- free → освободить буфер
Полученный результат
| ID | Title | QTY |
|---|---|---|
| 1 | Чай; зелёный | 12 |
| 2 | Кружка "Дом" | 0 |
| 3 | Кофе | 7 |
| 4 | Фильтр | 3 |
$result = $db->query('SELECT id,title,qty FROM mysql106_result_items ORDER BY id', MYSQLI_STORE_RESULT);
$count = $result->num_rows;
$first = $result->fetch_assoc();
$result->data_seek(0);my106Connect открывает учебное соединение, my106Seed создаёт TEMPORARY TABLE и заполняет её четырьмя строками; оба исходника есть в архиве. В основной операции query получает SQL и режим MYSQLI_STORE_RESULT. Свойство num_rows показывает размер уже принятого результата. fetch_assoc возвращает одну строку как массив с именами колонок; data_seek(0) возвращает указатель к первой. Знак -> обращается к свойству или методу объекта. Цикл читает строки до null. Второй SELECT можно выполнить до free первого: сервер уже отдал его полную выдачу.
Открыть таблицу · JSON результата · Измерения и диагностика.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
try {
$result = $db->query('SELECT id,title,qty FROM mysql106_result_items ORDER BY id', MYSQLI_STORE_RESULT);
try {
$count = $result->num_rows;
$first = $result->fetch_assoc();
$result->data_seek(0);
$rows = [];
while ($row = $result->fetch_assoc()) { $rows[] = [(int)$row['id'], $row['title'], (int)$row['qty']]; }
// Новый SQL допустим: весь предыдущий результат уже принят в клиентский буфер.
$totalResult = $db->query('SELECT SUM(qty) AS total FROM mysql106_result_items');
$total = (int)$totalResult->fetch_assoc()['total']; $totalResult->free();
} finally { $result->free(); }
} finally { $db->close(); }
my106Report('mysql106-buffered', ['ID','Title','QTY'], $rows, ['num_rows' => $count, 'first_id_before_seek' => (int)$first['id'], 'total_qty' => $total], ['count_and_repeat' => $count === 4 && $rows[0][0] === 1, 'second_query' => $total === 22]);
echo "buffered: 4 rows; repeat from ID1; total=22\n";
Ожидаемый результат
buffered: 4 rows; repeat from ID1; total=22
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Буферизация — не кэш между HTTP-запросами. Память занята до явного free или освобождения всех ссылок на результат; конец запроса также завершает его жизнь. С mysqlnd буфер учитывается memory_limit. Здесь четыре строки: не распространяйте удобство data_seek на миллион больших строк без оценки объёма. Нужные столбцы, WHERE и LIMIT обычно полезнее произвольного увеличения memory_limit.
Записать CSV по строке и прочитать готовую выгрузку
Нужно выгрузить каталог CSV, который корректно хранит точку с запятой в названии и кавычки в тексте. Читаем SQL по одной строке и сразу пишем её в файл. После закрытия снова читаем CSV, поэтому в таблице показаны именно восстановленные значения.
Полученный результат
| ID | Title | QTY |
|---|---|---|
| 1 | Чай; зелёный | 12 |
| 2 | Кружка "Дом" | 0 |
| 3 | Кофе | 7 |
| 4 | Фильтр | 3 |
$result = $db->query('SELECT id,title,qty FROM mysql106_result_items ORDER BY id', MYSQLI_USE_RESULT);
while ($row = $result->fetch_assoc()) {
fputcsv($file, [$row['id'],$row['title'],$row['qty']], ';', '"', '', "\n");
}MYSQLI_USE_RESULT выбирает выдачу без полного клиентского буфера. Это уменьшает необходимость держать весь результат в PHP, но не означает, что сервер не выполняет сортировку или другую работу. fputcsv получает открытый файл, массив полей, разделитель ;, кавычку, пустой escape и перевод строки. Она сама заключает нужные поля в кавычки и удваивает внутренние кавычки. Полный пример закрывает и результат, и файл в finally. Служебное повторное чтение CSV собирает четыре строки только для наглядной проверки; большой экспорт не должен затем целиком читать файл в память.
Открыть таблицу · JSON результата · Измерения и диагностика.
Скачать созданный CSV. UTF-8, разделитель точка с запятой, без BOM; при открытии в табличной программе явно укажите эти параметры.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
$file = fopen(__DIR__ . '/catalog.csv', 'wb');
if ($file === false) { $db->close(); throw new RuntimeException('Cannot create CSV'); }
try {
fputcsv($file, ['id','title','qty'], ';', '"', '', "\n");
$result = $db->query('SELECT id,title,qty FROM mysql106_result_items ORDER BY id', MYSQLI_USE_RESULT);
try {
$written = 0;
while ($row = $result->fetch_assoc()) {
if (fputcsv($file, [$row['id'],$row['title'],$row['qty']], ';', '"', '', "\n") === false) { throw new RuntimeException('CSV write failed'); }
$written++;
}
} finally { $result->free(); }
} finally { fclose($file); $db->close(); }
$reader = fopen(__DIR__ . '/catalog.csv', 'rb');
if ($reader === false) { throw new RuntimeException('Cannot reopen CSV'); }
try {
$header = fgetcsv($reader, 0, ';', '"', ''); $rows = [];
while (($row = fgetcsv($reader, 0, ';', '"', '')) !== false) { $rows[] = [(int)$row[0], $row[1], (int)$row[2]]; }
} finally { fclose($reader); }
my106Report('mysql106-stream-csv', ['ID','Title','QTY'], $rows, ['written' => $written, 'csv_roundtrip' => true], ['csv_data' => $written === 4 && $rows[0][1] === 'Чай; зелёный' && $rows[1][1] === 'Кружка "Дом"', 'header' => $header === ['id','title','qty']]);
echo "stream CSV: 4 rows; delimiter and quotes preserved\n";
Ожидаемый результат
stream CSV: 4 rows; delimiter and quotes preserved
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Пока unbuffered-результат не закончен, на этом соединении нельзя отправить новый SQL. Не помещайте SQL-запрос внутри этого цикла для каждого товара; подготовьте JOIN заранее, дочитайте результат или обоснованно используйте отдельное соединение. Для HTTP-экспорта заголовки и поток ответа добавляются отдельно: этот пример создаёт файл на диске, не изображает браузерную загрузку. Значения здесь собственные; при выгрузке пользовательского текста для Excel отдельно решают риск формул вроде =SUM(...), это не задача CSV-экранирования.
Распознать занятый результат и закончить чтение
Разработчик прочитал одну строку и сразу отправил второй запрос. Получилась ошибка Commands out of sync. Воспроизведём её на своём соединении, сохраним код 2014, закончим чтение первой выдачи и повторим второй запрос уже в корректном состоянии.
- Unbuffered SELECT → прочитана только строка 1
- Второй SELECT → код 2014
- Дочитать строки 2–4 → free
- Новый SELECT → число 99
Полученный результат
| Step | Outcome |
|---|---|
| query before drain | 2014 |
| rows consumed | 4 |
| query after drain | 99 |
try { $db->query('SELECT 99 AS number'); }
catch (mysqli_sql_exception $error) { $errorCode = $error->getCode(); }
while ($row = $result->fetch_assoc()) { $seen[] = (int)$row['id']; }
$result->free();mysqli_report включает исключения для ошибок. В узком try/catch перехватывается mysqli_sql_exception ожидаемого второго вызова, getCode возвращает числовой код. После отказа цикл дочитывает предыдущий результат, затем free завершает его использование. Второй SELECT после этого получает 99. Код 2014 означает неправильную последовательность клиентских операций, а не неверный пароль или повреждение таблицы.
Открыть таблицу · JSON результата · Измерения и диагностика.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
try {
$result = $db->query('SELECT id FROM mysql106_result_items ORDER BY id', MYSQLI_USE_RESULT);
$seen = [];
try {
$seen[] = (int)$result->fetch_assoc()['id']; $errorCode = 0;
try { $db->query('SELECT 99 AS number'); }
catch (mysqli_sql_exception $error) { $errorCode = $error->getCode(); }
while ($row = $result->fetch_assoc()) { $seen[] = (int)$row['id']; }
} finally { $result->free(); }
$next = $db->query('SELECT 99 AS number'); $number = (int)$next->fetch_assoc()['number']; $next->free();
} finally { $db->close(); }
my106Report('mysql106-busy', ['Step','Outcome'], [['query before drain',$errorCode],['rows consumed',count($seen)],['query after drain',$number]], ['ids' => $seen], ['busy_detected' => $errorCode === 2014, 'recovered' => $seen === [1,2,3,4] && $number === 99]);
echo "busy=2014; consumed=4; next query=99\n";
Ожидаемый результат
busy=2014; consumed=4; next query=99
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Не повторяйте запрос в цикле, пока состояние не исправлено. Случай CALL или multi_query сложнее: может оставаться несколько наборов результатов, которые надо обработать через more_results/next_result. Одно free текущего набора не доказывает отсутствие следующих. Обсуждение Stack Overflow ниже содержит CALL; наша самостоятельная программа намеренно изолирует базовый случай одной недочитанной выдачи, не выдаёт его за полный разбор процедур. Для новых задач часто достаточно одного SELECT с JOIN.
Разобранная ситуация: Why is mysqli giving a "Commands out of sync" error?. Новый SQL выполняется при ещё активном результате; сначала нужно закончить его жизненный цикл. Обсуждение использовано как источник реальной задачи; объяснения и программы здесь собственные, правила сверены с PHP/MySQL.
Найти нужный товар и освободить незавершённую выдачу
Нужный товар найден до конца выдачи. Нужно выйти из цикла, но оставить соединение пригодным для следующего SQL. Покажем break вместе с finally, затем сравним с запросом, который сразу просит только один нужный товар.
- Читаем строку 1, затем 2
- break → код цикла закончен
- finally free → завершить оставшуюся выдачу
- Следующий SQL работает
- WHERE id=2 LIMIT1 → сразу нужная строка
Полученный результат
| Method | Read by application | Title |
|---|---|---|
| break + finally | 2 | Кружка "Дом" |
| WHERE + LIMIT | 1 | Кружка "Дом" |
try {
while ($row = $result->fetch_assoc()) {
if ((int)$row['id'] === 2) { $found = $row['title']; break; }
}
} finally { $result->free(); }break прекращает PHP-цикл, но сам не освобождает результат и не сообщает серверу, что вся выдача обработана. finally выполняет free даже при раннем выходе. Драйвер может дочитать оставшиеся данные для очистки протокола: это полезная уборка, но не бесплатная отмена серверного запроса. Поэтому полная программа показывает улучшение SQL с WHERE и LIMIT. Число «прочитано приложением» не равно числу строк, переданных драйверу по сети.
Открыть таблицу · JSON результата · Измерения и диагностика.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
try {
$result = $db->query('SELECT id,title FROM mysql106_result_items ORDER BY id', MYSQLI_USE_RESULT);
$found = null; $read = 0;
try {
while ($row = $result->fetch_assoc()) {
$read++;
if ((int)$row['id'] === 2) { $found = $row['title']; break; }
}
} finally { $result->free(); }
$next = $db->query('SELECT COUNT(*) AS n FROM mysql106_result_items'); $count = (int)$next->fetch_assoc()['n']; $next->free();
// Если условие известно заранее, лучше не запрашивать лишние строки.
$limited = $db->query('SELECT title FROM mysql106_result_items WHERE id=2 LIMIT 1'); $direct = $limited->fetch_assoc()['title']; $limited->free();
} finally { $db->close(); }
my106Report('mysql106-early-stop', ['Method','Read by application','Title'], [['break + finally', $read, $found], ['WHERE + LIMIT',1,$direct]], ['next_query_count' => $count], ['same_product' => $found === $direct && $found === 'Кружка "Дом"', 'connection_ready' => $count === 4]);
echo "early stop: read2; free completed; WHERE returns same product\n";
Ожидаемый результат
early stop: read2; free completed; WHERE returns same product
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Для живого пользовательского поиска заранее сформулируйте условие в SQL и примените индекс по задаче. free не является универсальной командой отмены долгого запроса. Поведение сетевого сбоя и таймаута нужно обработать отдельно; в нашем примере сервер доступен и данные малы. Не закрывайте соединение после каждого fetch ради обхода ошибки: правильно завершите выдачу.
Прочитать подготовленный SELECT двумя способами
Нужно выбрать товары с остатком не меньше трёх и передать порог параметром. У подготовленного SELECT два привычных способа чтения: получать значения через связанные переменные либо получить обычный объект результата. Оба должны вернуть одинаковые три строки.
- prepare: WHERE qty >= ?
- bind_param i → minimum=3
- execute → bind_result → fetch до конца
- Повторный prepare/execute → get_result → массив
- Сравнить ID1,3,4 и количества
Полученный результат
| ID | QTY |
|---|---|
| 1 | 12 |
| 3 | 7 |
| 4 | 3 |
$stmt->execute();
$stmt->bind_result($id, $qty);
while ($stmt->fetch()) { $boundRows[] = [$id, $qty]; }
$stmt->free_result();В SQL вопросительный знак — место для параметра значения; i в bind_param задаёт целое. bind_result связывает переменные $id и $qty с колонками результата: при каждом fetch они обновляются, и мы добавляем их значения в учебный список. По умолчанию результат подготовленного запроса не буферизуется целиком; stmt->store_result включил бы буфер для такого чтения. Во второй части get_result, доступный с mysqlnd, получает buffered mysqli_result и привычный fetch_all. Это два отдельных выполнения для сравнения, не два способа читать один stmt одновременно.
Открыть таблицу · JSON результата · Измерения и диагностика.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
try {
$minimum = 3;
$stmt = $db->prepare('SELECT id,qty FROM mysql106_result_items WHERE qty >= ? ORDER BY id');
try {
$stmt->bind_param('i', $minimum); $stmt->execute();
$stmt->bind_result($id, $qty); $boundRows = [];
while ($stmt->fetch()) { $boundRows[] = [$id, $qty]; }
$stmt->free_result();
} finally { $stmt->close(); }
$stmt = $db->prepare('SELECT id,qty FROM mysql106_result_items WHERE qty >= ? ORDER BY id');
try {
$stmt->bind_param('i', $minimum); $stmt->execute();
$result = $stmt->get_result();
try { $bufferedRows = $result->fetch_all(MYSQLI_NUM); }
finally { $result->free(); }
} finally { $stmt->close(); }
} finally { $db->close(); }
my106Report('mysql106-prepared-results', ['ID','QTY'], $boundRows, ['both_methods_equal' => $boundRows === $bufferedRows, 'minimum' => $minimum], ['same_rows' => $boundRows === [[1,12],[3,7],[4,3]] && $boundRows === $bufferedRows]);
echo "prepared: bind_result and get_result give the same 3 rows\n";
Ожидаемый результат
prepared: bind_result and get_result give the same 3 rows
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Без необходимости не выполняйте SELECT дважды в рабочем коде: выберите подходящий интерфейс. store_result и get_result не комбинируют на одном выполнении как последовательные обязательные шаги. get_result возвращает false и для успешных команд без набора строк, поэтому контракт зависит от SQL; здесь везде SELECT. При большом результате массив boundRows тоже накопит память, несмотря на unbuffered-получение: обрабатывайте одну строку и отпускайте её.
Выгрузить каталог ограниченными порциями без OFFSET
Экспорт должен идти небольшими порциями и не захватывать новые товары, добавленные после начала обхода. Зафиксируем верхний ID, будем читать не больше двух строк с ID больше последнего обработанного и покажем, что поздняя строка 5 не вошла.
- MAX(id) → верхняя граница 4
- Добавилась строка 5
- id > last AND id <= 4 ORDER BY id LIMIT2
- Порция 1:1,2 → last2
- Порция 2:3,4 → last4
- Пустая порция → остановка
Полученный результат
| ID | Title | QTY |
|---|---|---|
| 1 | Чай; зелёный | 12 |
| 2 | Кружка "Дом" | 0 |
| 3 | Кофе | 7 |
| 4 | Фильтр | 3 |
$stmt = $db->prepare('SELECT id,title,qty FROM mysql106_result_items WHERE id > ? AND id <= ? ORDER BY id LIMIT 2');
$stmt->bind_param('ii', $last, $upper);
$stmt->execute();
$result = $stmt->get_result();Вместо растущего OFFSET используется ключ: WHERE id > ? AND id <= ?. Оба значения передаются параметрами, ORDER BY задаёт устойчивый порядок, LIMIT ограничивает размер текущего буфера. Последний ID меняется только после обработки строки. Каждый get_result полностью заканчивается и освобождается до следующего execute. В программе rows копит четыре строки для проверяемой таблицы; в реальном экспорте каждую порцию сразу записывают в CSV.
Открыть таблицу · JSON результата · Измерения и диагностика.
<?php
declare(strict_types=1);
require __DIR__ . '/my106-results-helper.php';
$db = my106Connect(); my106Seed($db);
try {
$maxResult = $db->query('SELECT MAX(id) AS maximum FROM mysql106_result_items');
$upper = (int)$maxResult->fetch_assoc()['maximum']; $maxResult->free();
$db->query("INSERT INTO mysql106_result_items VALUES (5,'Поздняя строка',1)");
$stmt = $db->prepare('SELECT id,title,qty FROM mysql106_result_items WHERE id > ? AND id <= ? ORDER BY id LIMIT 2');
$last = 0; $rows = []; $pages = 0;
try {
$stmt->bind_param('ii', $last, $upper);
while (true) {
$stmt->execute(); $result = $stmt->get_result();
try { $batch = $result->fetch_all(MYSQLI_ASSOC); }
finally { $result->free(); }
if ($batch === []) { break; }
$pages++;
foreach ($batch as $row) { $last = (int)$row['id']; $rows[] = [$last, $row['title'], (int)$row['qty']]; }
}
} finally { $stmt->close(); }
} finally { $db->close(); }
my106Report('mysql106-keyset', ['ID','Title','QTY'], $rows, ['upper_id' => $upper, 'nonempty_pages' => $pages, 'late_id_excluded' => $last === 4], ['bounded_complete' => array_column($rows,0) === [1,2,3,4] && $pages === 2]);
echo "keyset: 2 pages, IDs1..4; late ID5 excluded\n";
Ожидаемый результат
keyset: 2 pages, IDs1..4; late ID5 excluded
Комплект содержит example.php, helper и README. Задайте переменные окружения PHPMAN_MYSQL_* для отдельного локального сервера и выполните php -d mysqlnd.collect_statistics=1 -d mysqlnd.collect_memory_statistics=1 example.php. Расширение mysqli/mysqlnd обязательно; пароль в исходник и отчёт не записывается. Учебные TEMPORARY-таблицы создаёт программа, их входные строки видны в helper. Результаты — result.html, result.json и diagnostics.json. Таблицы принадлежат только учебной сессии; обычные таблицы пользователя не удаляются.
Верхний ID ограничивает новые ID, но не замораживает изменения старых строк и не защищает от вставки меньшего свободного ID. Это не согласованный снимок и не exactly-once доставка. Для точного снимка нужна отдельная договорённость об изоляции/транзакции, для продолжения после сбоя — сохранение последнего успешно доставленного ключа и идемпотентность. Здесь PRIMARY KEY id уникален и порядок однозначен; по неуникальному времени понадобится составной ключ.
Выбрать следующий шаг
Для задачи «показать 20 товаров» начните с WHERE/ORDER BY/LIMIT и небольшого буфера. Для выгрузки пишите каждую строку сразу в файл; для возобновляемой обработки выберите стабильный ключ и храните подтверждённый прогресс. Проверьте свой вариант: вывести только товары QTY>0 и сравнить набор ID1,3,4. Предложенное решение — перенести условие в SELECT и оставить потоковый CSV-цикл; новая проверка строится по реально прочитанному CSV, а не только сообщению «готово». Дальше: измерить запросы, память и состояние соединения.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


