В PHP 8.1 появились файберы (Fibers), а в SQLite 3.38.0 — встроенная поддержка json_tree. Что будет, если объединить эти две технологии с кастомной функцией PDO? Получится весьма экзотический, но эффективный способ обработки данных с минимальным оверхедом по памяти.
Стандартный json_decode в PHP работает отлично, пока вы не сталкиваетесь с мегабайтными файлами.
Существуют стримовые парсеры, но они часто медленнее и сложнее в использовании. Наш подход — это "золотая середина": проще стриминга, но экономнее json_decode.
Идея состоит в том, чтобы передать "тяжелую" работу по обходу структуры данных внутрь SQLite (который написан на C и очень эффективен), а данные в PHP получать порционно через механизм Fiber::suspend().
Вот как это выглядит в коде:
<?php
/**
* 1. Экономия памяти: Если JSON весит 5MB, этот метод займет около 12-15MB.
* Стандартный "json_decode" часто требует x10 от размера файла.
* 2. SQLite "Shadow RAM": В PHP 8.4 отслеживание памяти, потребляемой SQLite,
* может работать иначе и не всегда учитываться в memory_limit скрипта напрямую. (Win)
*/
function example_parse(string $json): \Generator
{
// Используем in-memory базу данных
$pdo = new \PDO('sqlite::memory:');
// Регистрируем PHP-функцию, которую вызовет SQLite
// Это ключевой момент: функция приостанавливает выполнение запроса
$pdo->sqliteCreateFunction('FETCH', static function ($key, $value, $type, $fullkey) {
\Fiber::suspend([$key, $value, $type, $fullkey]);
return null; // Возвращаем null, чтобы условие WHERE не выполнялось и не формировался result set
});
$fiber = new \Fiber(static function (\PDO $pdo, string $json) {
// Запрос не должен формировать выдачу (SELECT 1 ...),
// это упрощает работу движка SQLite.
// Мы "перехватываем" данные прямо во время фильтрации.
$stmt = $pdo->prepare('
SELECT 1
FROM json_tree(:json)
WHERE [key] IS NOT NULL
AND FETCH([key], [value], [type], [fullkey]) = 1
');
$stmt->execute(['json' => $json]);
});
// Запускаем файбер и начинаем "вытягивать" данные
$value = $fiber->start($pdo, $json);
while (!$fiber->isTerminated()) {
yield $value;
$value = $fiber->resume();
}
}
// Пример использования
$json = file_get_contents('big.json');
foreach(example_parse($json) as $node){
[$key, $value, $type, $fullkey] = $node;
// Обрабатываем каждый узел по отдельности
// echo "$fullkey: $value\n";
}- SQLite как итератор: Функция
json_treeв SQLite рекурсивно обходит JSON. -
sqliteCreateFunction+FETCH: Мы создаем мост. Когда SQLite находит узел, он вызывает нашу PHP-функциюFETCH. - Fiber pause: Внутри
FETCHмы не возвращаем результат сразу. Мы говорим скрипту: "Стоп! Верни управление внешнему циклу с текущими данными" (Fiber::suspend). - Yield: Внешний цикл получает данные через генератор, обрабатывает их и командует
Fiber::resume(), заставляя SQLite продолжить работу с того места, где он остановился.
Важный нюанс: Обратите внимание на SELECT 1 ... AND FETCH(...) = 1. Мы специально пишем запрос так, чтобы SQLite оптимизация понимала что выборки нет. Мы используем его движок WHERE клаузы как итератор.
В примере выше мы использовали sqlite::memory:, но сам JSON всё ещё находится в переменной $json в памяти PHP, можно пойти дальше:
function example_parse_file(string $jsonFilePath): \Generator
{
// Создаем временную файловую базу SQLite
$file = \tmpfile();
$tempDb = \stream_get_meta_data($file)['uri']; // eg: /tmp/phpFx0513a
$pdo = new \PDO("sqlite:$tempDb");
$pdo->sqliteCreateFunction('FETCH', static function ($key, $value, $type, $fullkey) {
\Fiber::suspend([$key, $value, $type, $fullkey]);
return null;
});
// Создаем таблицу и загружаем JSON прямо из файла
$pdo->exec('CREATE TABLE data (json_content TEXT)');
// readfile() может не быть.
$stmt = $pdo->prepare('INSERT INTO data (json_content) VALUES (readfile(:path))');
$stmt->execute(['path' => $jsonFilePath]);
$stmt = null;
$fiber = new \Fiber(static function (\PDO $pdo) {
$stmt = $pdo->prepare('
SELECT 1
FROM data, json_tree(data.json_content)
WHERE [key] IS NOT NULL
AND FETCH([key], [value], [type], [fullkey]) = 1
');
$stmt->execute();
});
$value = $fiber->start($pdo);
while (!$fiber->isTerminated()) {
yield $value;
$value = $fiber->resume();
}
// Очистка
$pdo = null;
\fclose($file);
@\unlink($tempDb);
}Почему это еще экономнее:
- JSON-строка не загружается в память PHP через
file_get_contents(). - SQLite читает файл напрямую и парсит его внутри своего процесса.
JSON здесь — лишь наглядный пример. Сила подхода в том, что вы получаете прямой доступ к потоку данных внутри SQL-запроса с возможностью управлять им из PHP.
PS: Из меня очень плохой писатель, поэтому оформление статьи при помощи LLM.