Как стать автором
Обновить

Комментарии 22

$writer->addRow($this->toXlsxRow($row));

Предполагается, что этот метод пишет сразу на диск, а не накапливает строки где-то в памяти для последующей записи, я правильно понимаю?


Ну и это… Хорошо бы все же иметь возможность расширябельности на другие СУБД стандартными средствами Доктрины.


А так — отличная идея, приятная реализация. Если бы мог — обязательно влепил бы плюсик:))

Спасибо за отзыв!

> Предполагается, что этот метод пишет сразу на диск, а не накапливает строки где-то в памяти для последующей записи, я правильно понимаю?

Да, это очень важно, чтобы при записи файла тоже не было буферизации. Как вот здесь, например.

Какая интересная библиотека! Надо будет присмотреться к ней. Большое спасибо за наводку!

Использование курсоров сомнительно, т.к. выполняя запрос штатными средствами я могу поштучно перебрать все строки выборки и переполнения памяти при этом не будет.
А курсоры дадут дополнительную нагрузку на базу данных, поэтому использование курсоров не целесообразно в описанном вами кейсе.
Штатные средства — это выкачать в память и перебрать? Норм, если у вас много ненужной памяти.

Про дополнительную нагрузку на базу с удовольствием бы почитал. Дадите ссылку?
к примеру:
www.php.net/manual/ru/pdostatement.fetch.php
извлекает одну строку, а не все данные.

результат работы запроса (что с курсорами, что без них) храниться на сервере базы данных, какие при этом выбирать средства для извлечения данных сугубо дело каждого, но открывать ещё и курсор на результат выборки это уже через чур (уж точно не экономичнее обычного извлечения)
Чтобы сделать fetch() в PDO, данные уже должны быть в памяти приложения. Именно так PDO работает по умолчанию. То есть, fetch() извлекает одну строку не из сервера, а из собственного буфера.

Можно перед fetch() сделать prepare() с атрибутом ATTR_CURSOR, тогда будет использоваться серверный курсор (ему и посвящена статья), и только тогда данные действительно будут выкачиваться с сервера строка-за-строкой.
Метод серверных курсоров также описан в книге "СУБД для программиста" в сравнении с другими способами пакетного извлечения данных из больших таблиц (глава «Постраничые выборки»).
Там есть существенная разница.

Какие шаги предпринимает Doctrine, чтобы достать из базы, например, 5000 объектов заказа?

1. Делает запрос в базу
2. Выкачивает 5000 строк
3. Делает Hydration — то есть раскладывание сырых данных в объекты (или массивы, есть разные режимы гидрации)
4. Отдает тебе массив из 5000 объектов заказа

Какой шаг позволяет оптимизировать использование iterate()? Только третий — гидрацию. Он позволяет не гидрировать 5000 объектов за раз, а делать это один за другим. Да, это дает значительную экономию памяти. Но Doctrine при этом все равно выкачивает все 5000 строк в память приложения, прежде чем начать их итерировать.

Описанный в статье подход позволяет оптимизировать не третий шаг, а второй. То есть получать данные из базы постепенно. И, само собой, гидрация в таком подходе тоже будет постепенной — будут обрабатываться только скачанные в эту итерацию строчки.
А зачем выбирать из базы все заказы, а не конкретного пользователя? Если памяти хватило чтобы сгенерировать HTML страницу, то ее должно хватить чтобы выгрузить данные в excel файл. Плюс для такого рода отчетов и выгрузок можно не использовать гидрацию.
Не нужно так уж привязываться к задаче, она наполовину выдуманная и нужна, чтобы проиллюстрировать использование курсора. Суть проблемы в том, что нужно выкачать из базы много данных и использовать при этом предсказуемое количество памяти.
Как вариант решения данной задачи использовать
copy (select * from huge_table) to '/tmp/huge_table.csv' with CSV DELIMITER ',';

или
psql -d dbname -t -A -F"," -c "select * from huge_table" > /tmp/huge_table.csv

и дальше потом преобразовывать полученный файл в то что надо
Ваше решение хорошее и рабочее и более того, но методологии LEAN, даже более правильное, если это разовая или достаточно редкая операция. Но если это часть бизнес-логики, то уже не такое удобное и надежное, так как уже есть точки интеграции, которые могут сломаться в любой момент времени.
Я оперировал не архитектурой или методологией, а обычным принципом, как бы я не старался оптимизировать запрос к большой таблице, лучше чем низкоуровневый
copy (select * from huge_table) to '/tmp/huge_table.csv' with CSV DELIMITER ',';
у меня не выйдет, так как разработчики на низком уровне оперируют на порядок большим количеством метрик, чем мне доступны.

Кроме этого такое решение может работать на отдельных нодах воркерах (с репликой базы), и после выполнения операции просто слать сообщение в общую шину про завершение задания.
Самый простой способ по сути, после выполнение дампа дернуть контроллер у приложения и передать туда какие-то параметры. Можно вставить конвертацию прямо в эту цепочку, а можно и новый таск создать.
psql -d dbname -t -A -F"," -c "select * from huge_table" > /tmp/huge_table.csv && curl -d "action=dump&table=huge_table&file=/tmp/huge_table.csv" -X POST http://app.name/jobs 

Мало того если не вынести такую тяжёлую операцию на отдельную ноду, то можно положить все приложение. Для данной задачи, сформировать огромный excel с огромной таблицы — это проще чем кажется, например:
1. конец месяца и клиенты/менеджеры побежали формировать огромный документы выгрузки, пришел OOM Killer и все сломалось.
2. кривая либа для формирования excel, потекла память даже при формировании одного файла и опять OOM Killer и все сломалось.
3. возросла нагрузка на таблицу (прилетело кучу данных), все формирование этого файла будет сильно больше чем обычно, и пользователи будут просто не дожидаться завершения задания и будут запрашивать excel документ еще раз, получили лавину запросов не только на уровне приложения, но и на уровне бд.

Ну и я вообще молчу про оптимальный выбор значения для количества записей что выбираем из базы будь то LIMIT или бегаем с использованием курсора, тут только бенчмаркинг поможет, так просто не посчитаешь сколько оптимально выбирать (надо учитывать количество оперативки, размеры буферов, и прикинуть это все при нормальной работе приложения).

На странице может использоваться постраничная навигация, что не требует выкачки всех заказов даже по конкретному пользователю. При экспорте, необходимо выгрузить все заказы конкретного пользователя.


Очень часто встречаюсь с данной задаче, при этом в качеств БД выступает MySQL

Спасибо и за понятный пример и за курсоры
Ещё бы можно было "обновлять" выборку без больших расходов

Ещё бы можно было «обновлять» выборку без больших расходов

А в чём проблема?

Если обновить данные в таблице(без объектов) то просто запрос через тот же SQL/DQL на обновление.

Если в цикле объекты обновлять — Query::iterate() и EntityManager::clear() чтобы IM/UoW почистить — manual
Я думаю главной проблемой в таких выборках и экспортах является не память, а консистентность данных, потому что когда процесс идёт долго, как обычно данные в БД изменяется и результат выходит иреллевантным.
В примере в заметке это делается через транзакцию, следовательно с блокировкой, что может быть плохо, если данных уж совсем вагон и все очень долго, но целостность в таком случае не страдает.
Когда я в прошлый раз переписывал такую ядрёную выгрузку, база не была узким местом (всего 200к строк, но с кучей связей, так что в результате мало не было). Им, внезапно, оказался twig, а все запросы суммарно отрабатывали за пренебрежимо малое время. Решалось заменой на libxml или шаблон на чистом php, но делать всё равно не стали, потому что на этом этапе (та самая выборка пачками в генераторе) скорость и потребление памяти уже всех устраивали. Ну, и использование курсоров мне там на стареньком mysql всё равно не светило. Но идея хороша.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий