Работа с JSON в MySQL и PHP

Почему «просто сохранить JSON» — это путь к боли
Часто можно услышать: «Давайте сложим всё в JSON-поле, потом вытащим на PHP и разберем». Это ловушка. Основная ошибка новичков — думать, что MySQL здесь лишь тупое хранилище. На практике, если вы используете MySQL 5.7+ или 8.0, вы упускаете мощнейший инструмент для работы прямо на уровне запросов. Эксперты сразу замечают: если вы достаете JSON из базы, чтобы в PHP сделать json_decode для простой фильтрации, значит, вы неправильно проектируете архитектуру. MySQL может фильтровать и агрегировать данные внутри JSON без переноса всего объема на сторону PHP.
Заблуждение 1: «Поддерживается» означает «работает как реляционная таблица»
Самый частый провал — думать, что функции типа JSON_EXTRACT или оператор ->> медленные, потому что «парсят строку». Да, они работают не как бинарные индексы InnoDB. Но профессиональные DBA знают обходной манёвр: индексы на вычисляемых колонках. Вы можете создать виртуальную колонку, извлечь нужное значение, а затем наложить на неё индекс. Без этого любой поиск WHERE по ключу в JSON — это полный сканирующий проход. Никогда не делайте WHERE JSON_EXTRACT(col, '$.id') = 123 на таблице с миллионом записей без такого индекса. В MySQL 8.0 это лечится так:
- Создаете виртуальную колонку:
ALTER TABLE items ADD COLUMN item_id INT GENERATED ALWAYS AS (col->>'$.id') STORED. - Далее навешиваете обычный индекс на эту колонку.
- Теперь условие
WHERE col->>'$.id' = 123(илиitem_id = 123) будет работать по индексу, а не сканом.
Неочевидный нюанс: если вы используете -> (получение значения с кавычками) vs ->> (без кавычек), индекс не сработает, если типы не совпадают. Значение, извлекаемое через ->>, это строка. Сравнивая её с целым числом, MySQL делает неявное приведение и игнорирует индекс.
Заблуждение 2: PHP-парсинг JSON всегда прозрачен
Профессиональная боль: json_decode действительно быстр. Но только если JSON валиден. Сломанный JSON — это null без единой ошибки, если вы не используете JSON_THROW_ON_ERROR (PHP 7.3+). Еще один граббер — кодировка. На проектах с хостингом и разными кодировками соединений часто приходит строка в Windows-1251, которая превращается в некорректный JSON. Тихая ошибка: json_last_error() возвращает код, но разработчики его игнорируют. Совет: в production всегда оборачивайте декодинг в функцию с логированием ошибки и проверкой глубины рекурсии (параметр $depth). Стандартная глубина 512, но если у вас вложенные 600 уровней (признак плохой схемы), получите null без предупреждения, если явно не укажете лимит.
Нюанс: NULL vs пустой массив vs отсутствие ключа
Когда вы работаете с JSON в MySQL, путаница с NULL — классика. В MySQL есть три сущности: NULL самого столбца (значение колонки отсутствует), JSON null (значение ключа равно null) и отсутствие ключа (JSON_EXTRACT вернет NULL SQL). Эксперты всегда проверяют через JSON_CONTAINS_PATH или IS NOT NULL после JSON_EXTRACT для отлова отсутствующих ключей. Иначе вы получите ложные срабатывания.
Производительность: когда JSON вреден
Даже с индексами, JSON — не серебряная пуля. Если ваша структура данных стабильна, нормализованные таблицы будут быстрее для JOIN и агрегации. JSON оправдан, когда: 1) схема динамическая (разные провайдеры, API), 2) данные — это профили с огромным числом редких полей, 3) вы храните логи или метаданные, которые редко фильтруются. Распространенная ошибка — хранить в JSON связанные внешние ключи и пытаться JOINить через JSON_CONTAINS. Я видел проекты, где это убивало производительность сервера на порядок. Используйте JSON только для атрибутов сущности, а не для связей.
Профессиональные трюки
- Форматирование для дебага: В консоли MySQL используйте
SELECT JSON_PRETTY(col) FROM table, чтобы не ловить экранированные слэши глазами. - Обновление одного поля: Никогда не доставайте весь JSON, не редактируйте его в PHP и не пишите обратно целиком — вы теряете параллелизм и нагружаете сеть. Используйте
JSON_SET,JSON_REPLACEилиJSON_ARRAY_APPENDна стороне MySQL. Это атомарная операция. - Стандартный валидатор: В PHP 8+ появился флаг
JSON_INVALID_UTF8_IGNOREилиJSON_INVALID_UTF8_SUBSTITUTE. Используйте их при работе с пользовательским вводом, чтобы не обрушить скрипт из-за битых данных. - Сжатие для SELECT: Если вы храните большие JSON-блоки (логи, результаты работы парсеров), но редко читаете их целиком, используйте компрессию на уровне PHP (gzcompress) перед записью и распаковку при чтении. MySQL не умеет автоматически сжимать JSON поля, в отличие от TEXT с опцией COMPRESS.
Резюме: проверьте себя
Если вы пишете код, который выбирает 10 тысяч строк, гоняет их в PHP, делает json_decode для каждого, фильтрует по одному полю и возвращает результат — вы зря едите хлеб. Перепишите это на один SQL-запрос с JSON_TABLE (MySQL 8.0+), который превратит JSON в виртуальную таблицу прямо в запросе, с фильтром и JOIN-ом. Обработка на PHP должна оставаться только там, где нужна сложная бизнес-логика, а не просто вытащить «название из объекта». Разница в производительности на объёме в 100 тысяч записей — минуты против миллисекунд.
Добавлено: 07.05.2026
