Skip to content

Commit 85bf1c7

Browse files
committed
ACP2E-1961: Cron job aggregate_sales_report_bestsellers_data is slow and affects on performance
- adjusted report query
1 parent 1d0483e commit 85bf1c7

File tree

2 files changed

+191
-125
lines changed

2 files changed

+191
-125
lines changed

app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php

Lines changed: 186 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,39 @@
55
*/
66
namespace Magento\Sales\Model\ResourceModel\Report;
77

8+
use Magento\Catalog\Model\ResourceModel\Product;
9+
use Magento\Framework\App\ObjectManager;
10+
use Magento\Framework\DB\Select;
11+
use Magento\Framework\Exception\LocalizedException;
12+
use Magento\Framework\Model\ResourceModel\Db\Context;
13+
use Magento\Framework\Stdlib\DateTime\DateTime;
14+
use Magento\Framework\Stdlib\DateTime\Timezone\Validator;
15+
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
16+
use Magento\Reports\Model\FlagFactory;
17+
use Magento\Sales\Model\ResourceModel\Helper;
18+
use Magento\Store\Model\Store;
19+
use Magento\Store\Model\StoreManagerInterface;
20+
use Psr\Log\LoggerInterface;
21+
822
/**
923
* Bestsellers report resource model
1024
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1125
*/
1226
class Bestsellers extends AbstractReport
1327
{
14-
const AGGREGATION_DAILY = 'daily';
28+
public const AGGREGATION_DAILY = 'daily';
1529

16-
const AGGREGATION_MONTHLY = 'monthly';
30+
public const AGGREGATION_MONTHLY = 'monthly';
1731

18-
const AGGREGATION_YEARLY = 'yearly';
32+
public const AGGREGATION_YEARLY = 'yearly';
1933

2034
/**
21-
* @var \Magento\Catalog\Model\ResourceModel\Product
35+
* @var Product
2236
*/
2337
protected $_productResource;
2438

2539
/**
26-
* @var \Magento\Sales\Model\ResourceModel\Helper
40+
* @var Helper
2741
*/
2842
protected $_salesResourceHelper;
2943

@@ -37,29 +51,36 @@ class Bestsellers extends AbstractReport
3751
];
3852

3953
/**
40-
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
41-
* @param \Psr\Log\LoggerInterface $logger
42-
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
43-
* @param \Magento\Reports\Model\FlagFactory $reportsFlagFactory
44-
* @param \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator
45-
* @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
46-
* @param \Magento\Catalog\Model\ResourceModel\Product $productResource
47-
* @param \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper
54+
* @var StoreManagerInterface
55+
*/
56+
protected $storeManager;
57+
58+
/**
59+
* @param Context $context
60+
* @param LoggerInterface $logger
61+
* @param TimezoneInterface $localeDate
62+
* @param FlagFactory $reportsFlagFactory
63+
* @param Validator $timezoneValidator
64+
* @param DateTime $dateTime
65+
* @param Product $productResource
66+
* @param Helper $salesResourceHelper
67+
* @param string|null $connectionName
4868
* @param array $ignoredProductTypes
49-
* @param string $connectionName
69+
* @param StoreManagerInterface|null $storeManager
5070
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
5171
*/
5272
public function __construct(
53-
\Magento\Framework\Model\ResourceModel\Db\Context $context,
54-
\Psr\Log\LoggerInterface $logger,
55-
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
56-
\Magento\Reports\Model\FlagFactory $reportsFlagFactory,
57-
\Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator,
58-
\Magento\Framework\Stdlib\DateTime\DateTime $dateTime,
59-
\Magento\Catalog\Model\ResourceModel\Product $productResource,
60-
\Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper,
61-
$connectionName = null,
62-
array $ignoredProductTypes = []
73+
Context $context,
74+
LoggerInterface $logger,
75+
TimezoneInterface $localeDate,
76+
FlagFactory $reportsFlagFactory,
77+
Validator $timezoneValidator,
78+
DateTime $dateTime,
79+
Product $productResource,
80+
Helper $salesResourceHelper,
81+
?string $connectionName = null,
82+
array $ignoredProductTypes = [],
83+
?StoreManagerInterface $storeManager = null
6384
) {
6485
parent::__construct(
6586
$context,
@@ -73,6 +94,7 @@ public function __construct(
7394
$this->_productResource = $productResource;
7495
$this->_salesResourceHelper = $salesResourceHelper;
7596
$this->ignoredProductTypes = array_merge($this->ignoredProductTypes, $ignoredProductTypes);
97+
$this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
7698
}
7799

78100
/**
@@ -92,123 +114,162 @@ protected function _construct()
92114
* @param string|int|\DateTime|array|null $to
93115
* @return $this
94116
* @throws \Exception
95-
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
96117
*/
97118
public function aggregate($from = null, $to = null)
98119
{
99120
$connection = $this->getConnection();
100-
//$this->getConnection()->beginTransaction();
101-
102-
try {
103-
if ($from !== null || $to !== null) {
104-
$subSelect = $this->_getTableDateRangeSelect(
105-
$this->getTable('sales_order'),
106-
'created_at',
107-
'updated_at',
108-
$from,
109-
$to
110-
);
111-
} else {
112-
$subSelect = null;
113-
}
114-
115-
$this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect);
116-
// convert dates to current admin timezone
117-
$periodExpr = $connection->getDatePartSql(
118-
$this->getStoreTZOffsetQuery(
119-
['source_table' => $this->getTable('sales_order')],
120-
'source_table.created_at',
121-
$from,
122-
$to
123-
)
124-
);
125-
$select = $connection->select();
126-
127-
$select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']);
128-
129-
$columns = [
130-
'period' => $periodExpr,
131-
'store_id' => 'source_table.store_id',
132-
'product_id' => 'order_item.product_id',
133-
'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'),
134-
'product_price' => new \Zend_Db_Expr(
135-
'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' .
136-
'* MIN(source_table.base_to_global_rate)'
137-
),
138-
'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'),
139-
];
140-
141-
$select->from(
142-
['source_table' => $this->getTable('sales_order')],
143-
$columns
144-
)->joinInner(
145-
['order_item' => $this->getTable('sales_order_item')],
146-
'order_item.order_id = source_table.entity_id',
147-
[]
148-
)->joinLeft(
149-
['order_item_parent' => $this->getTable('sales_order_item')],
150-
'order_item.parent_item_id = order_item_parent.item_id',
151-
[]
152-
)->where(
153-
'source_table.state != ?',
154-
\Magento\Sales\Model\Order::STATE_CANCELED
155-
)->where(
156-
'order_item.product_type NOT IN(?)',
157-
$this->ignoredProductTypes
158-
);
121+
$this->clearByDateRange($from, $to);
159122

160-
if ($subSelect !== null) {
161-
$select->having($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
162-
}
163-
164-
$select->useStraightJoin();
165-
// important!
166-
$insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
167-
$connection->query($insertQuery);
168-
169-
$columns = [
170-
'period' => 'period',
171-
'store_id' => new \Zend_Db_Expr(\Magento\Store\Model\Store::DEFAULT_STORE_ID),
172-
'product_id' => 'product_id',
173-
'product_name' => new \Zend_Db_Expr('MIN(product_name)'),
174-
'product_price' => new \Zend_Db_Expr('MIN(product_price)'),
175-
'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'),
176-
];
177-
178-
$select->reset();
179-
$select->from(
180-
$this->getMainTable(),
181-
$columns
182-
)->where(
183-
'store_id <> ?',
184-
\Magento\Store\Model\Store::DEFAULT_STORE_ID
185-
);
123+
foreach ($this->storeManager->getStores(true) as $store) {
124+
$this->processStoreAggregate($store->getId(), $from, $to);
125+
}
126+
127+
$columns = [
128+
'period' => 'period',
129+
'store_id' => new \Zend_Db_Expr(Store::DEFAULT_STORE_ID),
130+
'product_id' => 'product_id',
131+
'product_name' => new \Zend_Db_Expr('MIN(product_name)'),
132+
'product_price' => new \Zend_Db_Expr('MIN(product_price)'),
133+
'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'),
134+
];
186135

187-
if ($subSelect !== null) {
188-
$select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
189-
}
190-
191-
$select->group(['period', 'product_id']);
192-
$insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
193-
$connection->query($insertQuery);
194-
195-
// update rating
196-
$this->_updateRatingPos(self::AGGREGATION_DAILY);
197-
$this->_updateRatingPos(self::AGGREGATION_MONTHLY);
198-
$this->_updateRatingPos(self::AGGREGATION_YEARLY);
199-
$this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE);
200-
} catch (\Exception $e) {
201-
throw $e;
136+
$select = $connection->select();
137+
$select->reset();
138+
$select->from(
139+
$this->getMainTable(),
140+
$columns
141+
)->where(
142+
'store_id <> ?',
143+
Store::DEFAULT_STORE_ID
144+
);
145+
$subSelect = $this->getRangeSubSelect($from, $to);
146+
if ($subSelect !== null) {
147+
$select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period'));
202148
}
203149

150+
$select->group(['period', 'product_id']);
151+
$insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
152+
$connection->query($insertQuery);
153+
154+
$this->_updateRatingPos(self::AGGREGATION_DAILY);
155+
$this->_updateRatingPos(self::AGGREGATION_MONTHLY);
156+
$this->_updateRatingPos(self::AGGREGATION_YEARLY);
157+
$this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE);
158+
204159
return $this;
205160
}
206161

162+
/**
163+
* Clear aggregate existing data by range
164+
*
165+
* @param string|int|\DateTime|array|null $from
166+
* @param string|int|\DateTime|array|null $to
167+
* @return void
168+
* @throws LocalizedException
169+
*/
170+
protected function clearByDateRange($from = null, $to = null): void
171+
{
172+
$subSelect = $this->getRangeSubSelect($from, $to);
173+
$this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect);
174+
}
175+
176+
/**
177+
* Get report range sub-select
178+
*
179+
* @param string|int|\DateTime|array|null $from
180+
* @param string|int|\DateTime|array|null $to
181+
* @return Select|null
182+
*/
183+
protected function getRangeSubSelect($from = null, $to = null): ?Select
184+
{
185+
if ($from !== null || $to !== null) {
186+
$subSelect = $this->_getTableDateRangeSelect(
187+
$this->getTable('sales_order'),
188+
'created_at',
189+
'updated_at',
190+
$from,
191+
$to
192+
);
193+
} else {
194+
$subSelect = null;
195+
}
196+
197+
return $subSelect;
198+
}
199+
200+
/**
201+
* Calculate report aggregate per store
202+
*
203+
* @param int $storeId
204+
* @param string|int|\DateTime|array|null $from
205+
* @param string|int|\DateTime|array|null $to
206+
* @return void
207+
* @throws LocalizedException
208+
*/
209+
protected function processStoreAggregate(int $storeId, $from = null, $to = null): void
210+
{
211+
$connection = $this->getConnection();
212+
213+
// convert dates to current admin timezone
214+
$periodExpr = $connection->getDatePartSql(
215+
$this->getStoreTZOffsetQuery(
216+
['source_table' => $this->getTable('sales_order')],
217+
'source_table.created_at',
218+
$from,
219+
$to
220+
)
221+
);
222+
$select = $connection->select();
223+
$subSelect = $this->getRangeSubSelect($from, $to);
224+
225+
$select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']);
226+
227+
$columns = [
228+
'period' => $periodExpr,
229+
'store_id' => 'source_table.store_id',
230+
'product_id' => 'order_item.product_id',
231+
'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'),
232+
'product_price' => new \Zend_Db_Expr(
233+
'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' .
234+
'* MIN(source_table.base_to_global_rate)'
235+
),
236+
'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'),
237+
];
238+
239+
$select->from(
240+
['source_table' => $this->getTable('sales_order')],
241+
$columns
242+
)->joinInner(
243+
['order_item' => $this->getTable('sales_order_item')],
244+
'order_item.order_id = source_table.entity_id',
245+
[]
246+
)->joinLeft(
247+
['order_item_parent' => $this->getTable('sales_order_item')],
248+
'order_item.parent_item_id = order_item_parent.item_id',
249+
[]
250+
)->where(
251+
'source_table.entity_id IN (?)',
252+
"SELECT entity_id FROM " . $this->getTable('sales_order') .
253+
" WHERE store_id = " . $storeId .
254+
" AND state != '" . \Magento\Sales\Model\Order::STATE_CANCELED . "'" .
255+
($subSelect !== null ? " AND " . $this->_makeConditionFromDateRangeSelect($subSelect, $periodExpr) : '')
256+
)->where(
257+
'order_item.product_type NOT IN(?)',
258+
$this->ignoredProductTypes
259+
);
260+
261+
$select->useStraightJoin();
262+
// important!
263+
$insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns));
264+
$connection->query($insertQuery);
265+
}
266+
207267
/**
208268
* Update rating position
209269
*
210270
* @param string $aggregation
211271
* @return $this
272+
* @throws LocalizedException
212273
*/
213274
protected function _updateRatingPos($aggregation)
214275
{

app/code/Magento/Sales/etc/db_schema.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@
294294
<index referenceId="SALES_ORDER_EMAIL_SENT" indexType="btree">
295295
<column name="email_sent"/>
296296
</index>
297+
<index referenceId="SALES_ORDER_STORE_STATE_CREATED" indexType="btree">
298+
<column name="store_id"/>
299+
<column name="state"/>
300+
<column name="created_at"/>
301+
</index>
297302
</table>
298303
<table name="sales_order_grid" resource="sales" engine="innodb" comment="Sales Flat Order Grid">
299304
<column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="false"

0 commit comments

Comments
 (0)