Работа с большими объёмами данных в Yii

Правило первое - используйте консольные команды не только для задач по CRON`у, но и для выполнения длительных операций, например для импорта/экспорта данных.
Разница, между запуском задач из браузера и из консоли, ощутима: в консоли время выполнения не лимитировано и в процессе можно отображать информацию о статусе выполнения.

Задача: Сгенерировать файл sitemap.xml со всеми новостями проекта.

Правило второе - не используйте ActiveRecord для работы с большими данными, его использование занимает слишком много памяти, и вместо 1000 операций в секунду, у вас сможет выполниться только 10.

Тут кто-то упомянул про память? Правильно! Всякий раз, когда вы пишите News::model()->findAll() результат попадает в память сервера, которая лимитирована параметром memory_limit в файле php.ini, обычно она имеет значение 128M, значит, мы можем сохранить в памяти текст, длиной 134 217 728 символа, а это примерно 2 000 текстовых записей по 65000 символов каждая. А если у вас имеется более 10 000 записей? Вы сразу получите от PHP ошибку, что лимит памяти исчерпан:
  1. Fatal error: Allowed memory size of *** bytes exhausted (tried to allocate ** bytes)

Решение только одно - нужно разбивать большие данные на маленькие порции.

Хватит теории, переходим к практике:
  1. <?php
  2. $app = Yii::app();
  3. // Наше PDO подключение к БД
  4. $db = $app->db;
  5. // Размер одной части обрабатываемых данных
  6. $part_size = 500;
  7. // Несколько пробелов или табуляций для красивого выравнивания файла
  8. $tab = ' ';
  9. $tab2 = $tab.$tab;
  10. // Файл, в который будем записывать результат (в корне сайта)
  11. $f = @fopen(Yii::app()->basePath.'/../test.xml', 'w');
  12. $xml_header = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL.
  13. '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
  14. // Записываем в начало файла заголовок для sitemap-файла
  15. fwrite($f, $xml_header.PHP_EOL);
  16. // Название таблицы, в которой хранятся новости
  17. $tbl = Publications::model()->tableName();
  18. // Команда, которая будет делать порционную выборку новостей
  19. $command = $db->createCommand()->select('id, title, date')->from($tbl);
  20. // Определяем количество данных, которое нам нужно обработать
  21. $all_count = (int)$db->createCommand("SELECT COUNT(id) FROM $tbl")->queryScalar();
  22. // Устанавливаем лимит, сколько новостей надо выбрать из таблицы
  23. $command->limit($part_size);
  24. // Перебираем все части данных
  25. for ($i = 0; $i < ceil($all_count / $part_size); $i++) {
  26. // Сюда будем складывать порции данных, для записи в файл, каждый
  27. // элемент массива - это одна строка
  28. $xml = array();
  29. // Вычисляем отступ от уже обработанных данных
  30. $offset = $i * $part_size;
  31. // Устанавливам отступ
  32. $command->offset($offset);
  33. // Находим очередную часть данных
  34. $rows = $command->queryAll();
  35. // Перебираем найденные данные
  36. foreach ($rows as $row) {
  37. // Открываем тег <url> - начало описания элемента в sitemap-файле
  38. $xml[] = $tab.'<url>';
  39. $xml[] = $tab2.'<loc>'.$app->createAbsoluteUrl('news/view', array('id'=>$row['id'])).'</loc>';
  40. // Закрываем тег <url>
  41. $xml[] = $tab.'</url>';
  42. }
  43. // Убираем из памяти найденную часть данных
  44. unset($rows);
  45. // Добавляем в наш файл обработанную часть данных
  46. if (count($xml)) {
  47. // Здесь мы объединяем все элементы массива $xml в строки
  48. fwrite($f, implode(PHP_EOL, $xml).PHP_EOL);
  49. }
  50. unset($xml);
  51. }
  52. // Заканчиваем XML-файл
  53. fwrite($f, '</url></url></urlset>');
  54. // Заканчиваем работу с файлом
  55. fclose($f);

Comments (1)

  • checker
    checker Reply #

    Раз уж заговорили о больших данных, предположим нужно выгрузить хотя бы 10гб данных из бд, сколько это займёт времени?
    Реализуйте многопоточность (в php 7.2 с этим вроде стало лучше чем было) и будет вам и многим соискателям счастье.


Leave a comment