…или правильная работа с коллекциями.
Хочу рассказать вам об ошибках, которые видел практически на каждом проекте на Magento у которого были проблемы с производительностью. Работая с Magento, мне иногда приходится проводить аудит чужого кода. Поэтому я бы хотел поделиться с вами опытом, который поможет улучшить производительность ваших сайтов и избежать ошибок в дальнейшем.
В этой статье рассказано о Magento 1.*, но описанное так же подходит и для Magento 2.*.
Практически на каждом проекте, где есть проблемы с производительностью, можно встретить что-то вроде такого:
$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*');
foreach ($collection as $product) {
$product = $product->load($product->getId());
$temp[] = $product->getSku();
}
Неправильно
вместо
$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('sku');
foreach ($collection as $product) {
$temp[] = $product->getSku();
}
Правильно
Причины такого очень просты:
- После загрузки нет необходимых атрибутов
- Так делают «программисты» в интернете
- Загрузка лишних атрибутов по принципу «хуже не будет»
Для понимания, что же тут не так и что мы можем сделать с производительностью, я предлагаю сконцентрироваться на работе с коллекциями:
EAV/Flat таблицы
EAV – это такой подход хранения данных, когда сущность к которой относится атрибут, сам атрибут и его значение разнесены в разные таблицы.
В Magento к EAV сущностям относятся: продукты, категории, кастомеры и кастомер адреса. Сами же атрибуты хранятся в eav_attribute таблице.
Всего типов значений атрибутов в Magento 5: text, varchar, int, decimal и datetime. Есть еще 1 тип – static, он отличается от остальных 5ти тем, что находится в таблице с сущностью.
В таблице атрибутов указано в какой таблице или какого типа является тот или иной атрибут и Magento уже знает куда его писать и откуда читать.
Такое хранение значений позволяет иметь достаточно просто реализуемые атрибут сеты ( когда каждая сущность может иметь свой атрибут или не иметь его вовсе), добавление нового атрибута это всего лишь еще 1 строчка в БД. Добавил новое значение для 1 атрибута для другого стора – новая строчка в таблице значений этого атрибута.
Product – catalog_product_entity,
Category – catalog_category_entity,
Customer – customer_entity,
Customer address – customer_address_entity
Attribute:
eav_attribute
catalog_eav_attribute
customer_eav_attribute
Value:
*_text
*_varchar
*_int
*_decimal
*_datetime
Flat — это привычный нам всем подход, где все лежит в 1 месте и никакие дополнительные таблицы нам не нужны, чтобы получить товар и все его атрибуты без лишней работы – SELECT * FROM табличка WHERE id = какой то ид и все.
Из EAV сущностей, Flat представление можно использовать только для категорий и для товаров.
catalog_product_flat_1 // *_N store_view
Category:
catalog_category_flat_1 // *_N store_view
Magento добавит атрибут во Flat таблицу если у атрибута выставлено 1 из ниже указанных значений.
В админке System > Configuration > Catalog
Magento будет использовать Flat таблицы для сущностей указанных ниже.
Обратите внимание на следующие факты:
- Flat таблицы используются ТОЛЬКО на страницах категорий, списке продуктов в составе Group product, да и вообще везде, где используется коллекция. Они не используются на странице товаров, в админке, при использовании метода load у модели.
- После включения Flat таблиц необходимо произвести реиндексацию, иначе Magento будет и дальше использовать только EAV таблицы
- После включения Flat таблиц Magento все равно продолжает использовать EAV, но так же начинает копировать изменения во Flat таблицу при сохранении изменений
+ Более гибкая система чем Flat
+ При добавлении нового атрибута нет необходимости реиндексировать данные
+ Практически не ограниченное количество атрибутов
+ Всегда доступны все атрибуты
+ Статик атрибуты (sku, created_at, updated_at) всегда присутствуют в выборке, даже если их не указывать специально
— Fatal error: Call to a member function getBackend() при выборке/фильтрации по не существующему атрибуту
— Производительность
Flat:
+ Производительность
+ В выборку/фильтрацию могут быть применены только существующие атрибуты, которые добавлены во Flat таблицу
— Ограничение на размер строки (до 65,535 байт, т.е. 85 varchar 255) и количеству столбцов (InnoDB до 1000, некоторые до 4096)
— Используется только при работе с коллекциям (при загрузке всегда используется EAV)
— Результат отличается от выдачи запроса при EAV (отсутствуют статик атрибуты)
— После включения требуется реиндексация, в противном случае будут использованы EAV таблицы
— При добавлении нового атрибута необходимо реиндексировать Flat таблицы
Cache
Конечно каждый из вас может мне сказать, что зачем нам разбираться как ускорить запросы в БД и вообще как работают коллекции если кэш нас спасет и все будет закэшировано. Отвечу коротко – кэш вас не спасет. Ни 1 из кэшей представленных в Magento либо не кэширует коллекции автоматически либо не работает в ваших кастомных контроллерах и моделях, которые вы используете, скажем, при импорте данных или подсчете чего-то. Да и к тому же до того, как оно попадет в кэш, ведь надо это как-то туда положить и быстренько показать пользователю.
Типы кэшей в Magento 1.*:
- Configuration – кэширует конфигурационные файлы
- Layout – кэширует layout файлы
- Block HTML output – кэширует phtml шаблоны. По умолчанию используется на фронтенде только в top menu и footer.
- Translations – кэширует csv транслейт файлы
- Collections data – кэширует коллекции, которые используют ->initCache(…) метод. По умолчанию кэширует только core_store, core_store_group, core_website коллекции при инициализации.
- EAV types and attributes – должен кэшировать eav атрибуты, НО не кэширует. Используется в 1 методе, который никогда не вызывается начиная с Magneto CE 1.4
- Web services cache – кэширует api.xml файлы
- Page Cache (FPC) – кэширует весь HTML, кэширует только CMS, Category, Product страницы. Игнорируется если протокол https, гет параметр ?no_cache=1, куки NO_CACHE
- DDL Cache (Скрытый) – кэширует DESCRIBE вызовы к БД, используется в операциях записи
…и ни 1 не кэширует коллекции автоматически.
Правильная работа с коллекциями
Для того, чтобы показать более наглядно почему что-то надо делать иначе чем многие привыкли, я решил привести некоторые тесты производительности разных подходов. Начнем пожалуй с тестового стенда. Для тестирования я использовал:
Тестовый стенд:
OS X 10.10
3.1 GHz Intel Core i5 (4 cores)
8GB
Magento конфигурация:
Magento EE 1.14.0
MySQL 5.5.38
PHP 5.6.2
Контент:
3 Categories
2000 Products
2000 CMS pages
Процесс:
Для тестов был создан экстеншен с 1 контроллером и 1 экшеном, каждый тест проводился 5 раз, потом считалось среднее время. Все результаты указаны в секундах.
class Test_Test_IndexController extends Mage_Core_Controller_Front_Action
{
public function indexAction()
{
$temp = array();
$start = microtime(true);
Init values
Loop start
$temp[] = $product->getSku();
Loop end
Or
Some code snippet
$stop = microtime(true);
echo $stop - $start;
}
}
Псевдо код
Тесты
- EAV/Flat с перезагрузкой моделей и без
- Кэширование коллекций
- Правильное использование count() и getSize()
- Правильное использование getFirstItem и setPage(1,1)
EAV/Flat с перезагрузкой моделей и без
Цикл по коллекции. С load (перезагрузка) моделей внутри цикла:
$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
$product = $product->load($product->getId());
$temp[] = $product->getSku();
}
Цикл по коллекции. Без load моделей внутри:
$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
$temp[] = $product->getSku();
}
3 вида выборки данных:
- addAttributeToSelect(‘*’); // все атрибуты
- addAttributeToSelect(‘sku’); // 1 статик атрибут
- addAttributeToSelect(‘name’); // 1 стандартный атрибут
Как вы видимо заметили, время без перезагрузки моделей В РАЗЫ меньше, чем когда вы перезагружаете модельки. Так же время еще меньше, когда Flat таблицы включены (т.е. нет лишних джойнов и юнионов) и мы выбираем только необходимые атрибуты.
В 1ом случае мы выполняем загрузку с кучей джойнов… а потом делаем это снова, но для модельки и так 2000 раз.
2ой раз мы делаем это для статик атрибута (он находится в той же табличке, что и сам продукт) и Magento не надо делать джойны. Поэтому время меньше.
3ий раз Magento нужно приджойнить еще табличку где хранится этот атрибут.
С Flat таблицами все аналогично, а в 2ух случаях вcе идентично – это потому что оба атрибута находятся в 1 таблице, отсюда и время идентичное.
Думаю цифры говорят сами за себя.
Кэширование коллекций
Без кэша:
$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*');
Используя метод initCache:
$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->initCache(Mage::app()->getCache(),'our_data',array('SOME_TAGS'));
Кастомная реализация кэширования:
$cache = Mage::app()->getCache();
$collection = $cache->load('our_data');
if(!collection) {
$collection = Mage::getModel('collection/product')->getCollection()->addAttributeToSelect('*')->getItems();
$cache->save(serialize($collection),'our_data',array(Mage_Core_Model_Resource_Db_Collection_Abstract::CACHE_TAG));
} else {
$collection = unserialize($collection);
}
Рассмотрим выборку без использования кэша, с использованием метода, который нам предлагает Magento и с костылем, который я нигде не видел… сам сваял, основанный на методах модельки кэша. Обратите внимание, что для всех тестов после составления запроса я производил загрузку данных и преобразование коллекции к массиву объектов.
Без кэша собственно ничего удивительного…все как обычно.
А вот используя маджентовский кэш я лично удивился, когда увидел, что время стало больше. А про EAV кэширование вообще глупой затеей, потому что EAV коллекция грузит сначала сущности из таблицы продуктов (именно вот это и кэшируется), а потом отдельным запросом выбирает значения атрибутов и заполняет объекты. Во Flat там все из 1 таблицы гонится. Но тем не менее время больше уходится на работу с кэшем чем с БД (тестировал я причем как с файловой системой, так и с redis – отличия 4ая цифра после запятой…т.е. на 2к сущностях ее нет). Суть InitCache метода заключается в том, что он сначала соберет все данные в коллекцию сам (пагинация, фильтры, events и так далее), создаст хеш из sql запроса и его будет искать в кэше, а если там что-то есть, то он это ансерелизует, а потом происходит запуск всех events и последующих методов. Это самая медленная процедура во всем процессе, именно вот тут выходит что кэш медленнее чем простой запрос в БД. Но зато не шлет запрос в БД… что не так и страшно уже.
Отдельно стоит пример с кэшем, написанным мной на коленке, там мы кэшируем конечный результат коллекции, причем минуя все events и дозагрузку атрибутов. Это работает для EAV и для Flat коллекций.
Правильное использование count() и getSize()
getSize()
$size = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->getSize();
count()
$size = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->count();
Разница методов заключается в том, что count() производит загрузку всех объектов коллекции, а потом обычным пхпшным count’ом подсчитывает количество объектов и возвращает нам число. getSize же не производит загрузку коллекции, а генерирует еще 1 запрос в БД, где нет лимитов, ордеров и списка выбираемых атрибутов, есть только COUNT(*).
Пример использования обоих методов такой:
Если вам надо знать, есть ли вообще значения в БД или сколько их – используйте getSize, если же вам в любом случае коллекция нужна загруженная, или уже загрузилась то используйте count() – он вернет вам число элементов, загруженных в коллекцию.
Правильное использование getFirstItem и setPage(1,1)
getFirstItem()
$product = Mage::getModel('catalog/product')->getCollection()
->getFirstItem();
setPage(1,1)
$product = Mage::getModel('catalog/product')->getCollection()
->setPage(1,1)
->getFirstItem();
load()
$product = Mage::getModel('catalog/product')->load(22);
Проблема getFirstItem в том, что он загружает всю коллекцию целиком, а потом просто в foreach возвращает первый элемент, а если его нет то возвращает пустой объект.
setPage (он же $this->setCurPage($pageNum)->setPageSize($pageSize)) же ограничивает выборку ровно 1 записью, что как вы видите значительно ускоряет загрузку результата.
Даже load быстрее getFirstItem, но обратите внимание, что load медленнее оказался чем выборка из коллекции 1 элемента. Это связано с тем, что load всегда работает с EAV таблицами.
Выводы
Подводя итог всему выше написанному, хочу посоветовать всем людям, работающим с Magento:
- Никогда не вызывайте повторно load метод у объектов, полученных из коллекции
- Загружайте только необходимые атрибуты
- Если применимо к проекту, используйте Flat таблицы
- Используйте count для подсчета результатов загруженной коллекции и getSize для получения числа всех записей
- Не используйте getFirstItem метод без setPage(1,1) или аналогичных методов
Автор: Oxidant