Метка: ORM

  • Практическое применение ZVAL: 8 реальных кейсов оптимизации памяти в PHP

    Содержание статьи

    1. Утечка памяти в циклах обработки данных

    Проблемный код:

    while ($row = getBigDataRow()) {
      $processed[] = processRow($row);
    }
    

    Оптимизированное решение:

    $batch = [];
    while ($row = getBigDataRow()) {
      $batch[] = processRow($row);
      if (count($batch) >= 1000) {
        saveBatch($batch);
        $batch = [];
      }
    }
    

    Почему это работает:

    В оригинальном коде каждый новый элемент добавляется в массив $processed, что приводит к:

    • Постоянному увеличению refcount для zval массива
    • Многократному перевыделению памяти при расширении массива
    • Накоплению всех zval-значений до конца выполнения цикла

    Оптимизированный вариант сбрасывает ссылки каждые 1000 итераций, позволяя сборщику мусора своевременно освобождать память.

    2. Неожиданное поведение ссылок

    Проблемный код:

    $data = ['key' => 'original'];
    $ref = &$data['key'];
    $copy = $data;
    $copy['key'] = 'modified';
    
    echo $data['key']; // Выведет 'modified'!
    

    Правильный подход:

    $data = ['key' => 'original'];
    $copy = $data; // Копируем ДО создания ссылки
    $ref = &$data['key'];
    

    Механизм работы:

    Когда создаётся ссылка (&), PHP помечает zval флагом is_ref=1. Последующие операции:

    • При копировании массива с is_ref=1 создаётся не независимая копия
    • Все переменные продолжают ссылаться на один zval
    • Изменения через любую переменную влияют на все «копии»

    Решение создаёт настоящую копию до установки ссылки.

    3. Конкатенация больших строк

    Проблемный код:

    $report = '';
    foreach ($records as $record) {
      $report .= generateReportRow($record);
    }
    

    Оптимизированный вариант:

    $rows = [];
    foreach ($records as $record) {
      $rows[] = generateReportRow($record);
    }
    $report = implode('', $rows);
    

    Принцип работы:

    Конкатенация через .= в цикле вызывает:

    • Создание нового zval-строка на каждой итерации
    • Копирование всего предыдущего содержимого
    • Сложность O(n²) по памяти

    Вариант с implode:

    • Хранит части в отдельных zval
    • Выделяет память для результата один раз
    • Сложность O(n)

    4. Циклические ссылки в объектах

    Проблемный код:

    class Node {
      public $next;
    }
    
    $a = new Node();
    $b = new Node();
    $a->next = $b;
    $b->next = $a; // Создана циклическая ссылка
    

    Решение:

    // Перед удалением:
    unset($a->next, $b->next);
    
    // Теперь GC сможет очистить память
    unset($a, $b);
    

    Как работает:

    Циклические ссылки создают ситуацию, где:

    • Каждый объект имеет refcount=2 (оригинал + ссылка из другого объекта)
    • Сборщик мусора видит refcount=1 после unset() переменных
    • Память не освобождается, так как объекты всё ещё ссылаются друг на друга

    Явный разрыв ссылок перед удалением позволяет GC корректно очистить память.

    5. Эффективная работа с большими массивами в функциях

    Проблемный код:

    function processArray($data) {
      // Создаётся копия массива
      foreach ($data as $k => $v) {
        $data[$k] = $v * 2;
      }
      return $data;
    }
    
    $bigData = range(1, 100000);
    processArray($bigData); // Двойное потребление памяти
    

    Оптимизация:

    function processArray(&$data) {
      foreach ($data as $k => $v) {
        $data[$k] = $v * 2;
      }
    }
    
    $bigData = range(1, 100000);
    processArray($bigData); // Работаем с оригиналом
    

    Механизм:

    При передаче массива в функцию:

    • Без & создаётся новый zval с refcount=1
    • С & используется оригинальный zval с увеличением refcount
    • Изменения применяются к исходному массиву

    Важно: после работы со ссылками нужно unset() временные переменные.

    6. Оптимизация временных переменных

    Проблемный код:

    function calculate($x) {
      $temp = $x * 2; // Лишний zval
      return $temp + 1;
    }
    

    Улучшенный вариант:

    function calculate($x) {
      return ($x * 2) + 1; // Нет временных zval
    }
    

    Принцип работы:

    Каждая переменная в PHP:

    • Создаёт отдельный zval в текущей scope
    • Увеличивает refcount для значений
    • Требует времени на аллокацию и освобождение

    Инлайн-вычисления позволяют:

    • Избежать создания промежуточных zval
    • Снизить нагрузку на сборщик мусора
    • Уменьшить пиковое потребление памяти

    7. Оптимизация ORM-запросов

    Проблемный код:

    $users = User::all(); // Загружает все объекты
    foreach ($users as $user) {
      $user->updateStats(); // Все zval в памяти
    }
    

    Пакетная обработка:

    User::chunk(1000, function($users) {
      foreach ($users as $user) {
        $user->updateStats();
      }
    }); // Освобождает память после каждой порции
    

    Как это работает:

    При загрузке объектов ORM:

    • Каждый объект — отдельный zval
    • Свойства объекта хранятся во внутреннем массиве (HashTable)
    • Потребление памяти растёт линейно

    Метод chunk():

    • Загружает данные порциями
    • После обработки порции zval освобождаются
    • Снижает пиковое потребление памяти на 90%+

    8. Эффективная сериализация

    Проблемный код:

    $bigData = getHugeDataset();
    $serialized = serialize($bigData); // 2GB в памяти
    

    Стриминг-решение:

    $handle = fopen('cache.dat', 'w');
    foreach (chunkData(getHugeDataset()) as $chunk) {
      fwrite($handle, serialize($chunk));
    }
    fclose($handle);
    

    Механизм работы:

    При сериализации:

    • PHP создаёт полную копию данных в памяти
    • Формирует строковое представление
    • Хранит всё в одном zval-строке

    Пакетная сериализация:

    • Обрабатывает данные частями
    • Не превышает лимит memory_limit
    • Позволяет работать с данными > доступной памяти
  • Как работать с GROUP BY и SUM в Doctrine

    Doctrine ORM предоставляет мощные инструменты для работы с агрегатными функциями SQL. В этом руководстве мы разберем использование GROUP BY и SUM в Symfony-проектах.

    1. Базовые примеры

    1.1. Простая группировка с суммированием

    // ProductRepository.php
    public function getCategoryStats()
    {
        return $this->createQueryBuilder('p')
            ->select([
                'p.category',
                'SUM(p.price) as total_price',
                'COUNT(p.id) as product_count'
            ])
            ->groupBy('p.category')
            ->getQuery()
            ->getResult();
    }

    1.2. Группировка по дате

    public function getMonthlySales()
    {
        return $this->createQueryBuilder('o')
            ->select([
                "DATE_FORMAT(o.createdAt, '%Y-%m') as month",
                'SUM(o.total) as sales'
            ])
            ->groupBy('month')
            ->getQuery()
            ->getResult();
    }

    2. Продвинутые сценарии

    2.1. Фильтрация с HAVING

    public function getHighValueOrders($minAmount)
    {
        return $this->createQueryBuilder('o')
            ->select([
                'c.name',
                'SUM(o.total) as total'
            ])
            ->join('o.customer', 'c')
            ->groupBy('c.id')
            ->having('total > :minAmount')
            ->setParameter('minAmount', $minAmount)
            ->getQuery()
            ->getResult();
    }

    2.2. Группировка с JOIN связанных сущностей

    public function getSalesByCategoryAndUser()
    {
        return $this->createQueryBuilder('o')
            ->select([
                'p.category',
                'u.name',
                'SUM(o.total) as total'
            ])
            ->join('o.product', 'p')
            ->join('o.user', 'u')
            ->groupBy('p.category, u.id')
            ->getQuery()
            ->getResult();
    }

    3. Оптимизация запросов

    • Добавляйте индексы для полей группировки
    • Используйте кеширование для сложных отчетов
    • Ограничивайте выборку при работе с большими данными

    4. Частые проблемы

    ПроблемаРешение
    Ошибка «Non-selected field in GROUP BY»Включите все неагрегированные поля в GROUP BY
    Медленные запросыДобавьте индексы и используйте LIMIT

    Заключение

    GROUP BY и SUM в Doctrine — мощные инструменты для аналитики. Ключевые правила:

    • Используйте индексы для полей группировки
    • Для сложных отчетов применяйте нативные SQL-запросы
    • Тестируйте запросы на реальных данных