Skip to content

Commit 9a839d1

Browse files
committed
19117: fix performance leak in salesrule collection
Github Issue: #19117 Refactored sql query that created a huge temporary table for each request, when a greater amount of salesrules and coupon codes exists in database. The sorting of this table took a lot of cpu time. The statement now consists of two subselects that drill down the remaining lines as far as possible, so that the remaining temporary table is minimal and easily sorted. example: for 2,000 salesrules and 3,000,000 coupon codes the original query took about 2.4 seconds (mbp, server, aws). the optimized query takes about 5ms (about 100ms on aws).
1 parent a089cfe commit 9a839d1

File tree

1 file changed

+101
-84
lines changed

1 file changed

+101
-84
lines changed

app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php

Lines changed: 101 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,15 @@ protected function mapAssociatedEntities($entityType, $objectField)
107107

108108
$associatedEntities = $this->getConnection()->fetchAll($select);
109109

110-
array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) {
111-
$item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]);
112-
$itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField);
113-
$itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']];
114-
$item->setData($objectField, $itemAssociatedValue);
115-
}, $associatedEntities);
110+
array_map(
111+
function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) {
112+
$item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]);
113+
$itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField);
114+
$itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']];
115+
$item->setData($objectField, $itemAssociatedValue);
116+
},
117+
$associatedEntities
118+
);
116119
}
117120

118121
/**
@@ -144,6 +147,7 @@ protected function _afterLoad()
144147
* @use $this->addWebsiteGroupDateFilter()
145148
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
146149
* @return $this
150+
* @throws \Zend_Db_Select_Exception
147151
*/
148152
public function setValidationFilter(
149153
$websiteId,
@@ -153,32 +157,21 @@ public function setValidationFilter(
153157
Address $address = null
154158
) {
155159
if (!$this->getFlag('validation_filter')) {
156-
/* We need to overwrite joinLeft if coupon is applied */
157-
$this->getSelect()->reset();
158-
parent::_initSelect();
159160

160-
$this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now);
161-
$select = $this->getSelect();
161+
$this->prepareSelect($websiteId, $customerGroupId, $now);
162162

163-
$connection = $this->getConnection();
164-
if (strlen($couponCode)) {
165-
$noCouponWhereCondition = $connection->quoteInto(
166-
'main_table.coupon_type = ?',
167-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
168-
);
169-
$relatedRulesIds = $this->getCouponRelatedRuleIds($couponCode);
170-
171-
$select->where(
172-
$noCouponWhereCondition . ' OR main_table.rule_id IN (?)',
173-
$relatedRulesIds,
174-
Select::TYPE_CONDITION
175-
);
163+
$noCouponRules = $this->getNoCouponCodeSelect();
164+
165+
if ($couponCode) {
166+
$couponRules = $this->getCouponCodeSelect($couponCode);
167+
$allAllowedRules = $this->getConnection()->select();
168+
$allAllowedRules->union([$noCouponRules, $couponRules], \Zend_Db_Select::SQL_UNION_ALL);
169+
170+
$this->_select = $allAllowedRules;
176171
} else {
177-
$this->addFieldToFilter(
178-
'main_table.coupon_type',
179-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
180-
);
172+
$this->_select = $noCouponRules;
181173
}
174+
182175
$this->setOrder('sort_order', self::SORT_ORDER_ASC);
183176
$this->setFlag('validation_filter', true);
184177
}
@@ -187,72 +180,96 @@ public function setValidationFilter(
187180
}
188181

189182
/**
190-
* Get rules ids related to coupon code
183+
* Recreate the default select object for specific needs of salesrule evaluation with coupon codes.
191184
*
192-
* @param string $couponCode
193-
* @return array
185+
* @param $websiteId
186+
* @param $customerGroupId
187+
* @param $now
194188
*/
195-
private function getCouponRelatedRuleIds(string $couponCode): array
189+
private function prepareSelect($websiteId, $customerGroupId, $now)
196190
{
197-
$connection = $this->getConnection();
198-
$select = $connection->select()->from(
199-
['main_table' => $this->getTable('salesrule')],
200-
'rule_id'
191+
$this->getSelect()->reset();
192+
parent::_initSelect();
193+
194+
$this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now);
195+
}
196+
197+
/**
198+
* Return select object to determine all active rules not needing a coupon code.
199+
*
200+
* @return Select
201+
*/
202+
private function getNoCouponCodeSelect()
203+
{
204+
$noCouponSelect = clone $this->getSelect();
205+
206+
$noCouponSelect->where(
207+
'main_table.coupon_type = ?',
208+
Rule::COUPON_TYPE_NO_COUPON
201209
);
202-
$select->joinLeft(
203-
['rule_coupons' => $this->getTable('salesrule_coupon')],
204-
$connection->quoteInto(
205-
'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?',
206-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON,
207-
null
208-
)
210+
211+
$noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]);
212+
213+
return $noCouponSelect;
214+
}
215+
216+
/**
217+
* Determine all active rules that are valid for the given coupon code.
218+
*
219+
* @param $couponCode
220+
* @return Select
221+
*/
222+
private function getCouponCodeSelect($couponCode)
223+
{
224+
$couponSelect = clone $this->getSelect();
225+
226+
$this->joinCouponTable($couponCode, $couponSelect);
227+
228+
$notExpired = $this->getConnection()->quoteInto(
229+
'(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)',
230+
$this->_date->date()->format('Y-m-d')
209231
);
210232

211-
$autoGeneratedCouponCondition = [
212-
$connection->quoteInto(
213-
"main_table.coupon_type = ?",
214-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO
215-
),
216-
$connection->quoteInto(
217-
"rule_coupons.type = ?",
218-
\Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED
219-
),
220-
];
221-
222-
$orWhereConditions = [
223-
"(" . implode($autoGeneratedCouponCondition, " AND ") . ")",
224-
$connection->quoteInto(
225-
'(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)',
226-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC
227-
),
228-
$connection->quoteInto(
229-
'(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)',
230-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC
231-
),
232-
];
233-
234-
$andWhereConditions = [
235-
$connection->quoteInto(
236-
'rule_coupons.code = ?',
237-
$couponCode
238-
),
239-
$connection->quoteInto(
240-
'(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)',
241-
$this->_date->date()->format('Y-m-d')
242-
),
243-
];
244-
245-
$orWhereCondition = implode(' OR ', $orWhereConditions);
246-
$andWhereCondition = implode(' AND ', $andWhereConditions);
247-
248-
$select->where(
249-
'(' . $orWhereCondition . ') AND ' . $andWhereCondition,
233+
$isAutogeneratedCoupon =
234+
$this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO)
235+
. ' AND ' .
236+
$this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED);
237+
238+
$isValidSpecificCoupon =
239+
$this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC)
240+
. ' AND (' .
241+
'(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)'
242+
. ' OR ' .
243+
'(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)'
244+
. ')';
245+
246+
$couponSelect->where(
247+
"$notExpired AND ($isAutogeneratedCoupon OR $isValidSpecificCoupon)",
250248
null,
251249
Select::TYPE_CONDITION
252250
);
253-
$select->group('main_table.rule_id');
254251

255-
return $connection->fetchCol($select);
252+
return $couponSelect;
253+
}
254+
255+
/**
256+
* @param $couponCode
257+
* @param Select $couponSelect
258+
*/
259+
private function joinCouponTable($couponCode, Select $couponSelect)
260+
{
261+
$couponJoinCondition =
262+
'main_table.rule_id = rule_coupons.rule_id'
263+
. ' AND ' .
264+
$this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON)
265+
. ' AND ' .
266+
$this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode);
267+
268+
$couponSelect->joinInner(
269+
['rule_coupons' => $this->getTable('salesrule_coupon')],
270+
$couponJoinCondition,
271+
[Coupon::KEY_CODE]
272+
);
256273
}
257274

258275
/**

0 commit comments

Comments
 (0)