Skip to content

Commit fa7fb54

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 41de8cb commit fa7fb54

File tree

1 file changed

+118
-73
lines changed

1 file changed

+118
-73
lines changed

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

Lines changed: 118 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use Magento\Framework\DB\Select;
1010
use Magento\Framework\Serialize\Serializer\Json;
1111
use Magento\Quote\Model\Quote\Address;
12+
use Magento\SalesRule\Api\Data\CouponInterface;
13+
use Magento\SalesRule\Model\Coupon;
14+
use Magento\SalesRule\Model\Rule;
1215

1316
/**
1417
* Sales Rules resource collection model.
@@ -105,12 +108,15 @@ protected function mapAssociatedEntities($entityType, $objectField)
105108

106109
$associatedEntities = $this->getConnection()->fetchAll($select);
107110

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

116122
/**
@@ -137,6 +143,7 @@ protected function _afterLoad()
137143
* @param string $couponCode
138144
* @param string|null $now
139145
* @param Address $address allow extensions to further filter out rules based on quote address
146+
* @throws \Zend_Db_Select_Exception
140147
* @use $this->addWebsiteGroupDateFilter()
141148
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
142149
* @return $this
@@ -149,84 +156,122 @@ public function setValidationFilter(
149156
Address $address = null
150157
) {
151158
if (!$this->getFlag('validation_filter')) {
152-
/* We need to overwrite joinLeft if coupon is applied */
153-
$this->getSelect()->reset();
154-
parent::_initSelect();
155159

156-
$this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now);
157-
$select = $this->getSelect();
160+
$this->prepareSelect($websiteId, $customerGroupId, $now);
158161

159-
$connection = $this->getConnection();
160-
if (strlen($couponCode)) {
161-
$select->joinLeft(
162-
['rule_coupons' => $this->getTable('salesrule_coupon')],
163-
$connection->quoteInto(
164-
'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?',
165-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
166-
),
167-
['code']
168-
);
169-
170-
$noCouponWhereCondition = $connection->quoteInto(
171-
'main_table.coupon_type = ? ',
172-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
173-
);
174-
175-
$autoGeneratedCouponCondition = [
176-
$connection->quoteInto(
177-
"main_table.coupon_type = ?",
178-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO
179-
),
180-
$connection->quoteInto(
181-
"rule_coupons.type = ?",
182-
\Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED
183-
),
184-
];
185-
186-
$orWhereConditions = [
187-
"(" . implode($autoGeneratedCouponCondition, " AND ") . ")",
188-
$connection->quoteInto(
189-
'(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)',
190-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC
191-
),
192-
$connection->quoteInto(
193-
'(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)',
194-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC
195-
),
196-
];
197-
198-
$andWhereConditions = [
199-
$connection->quoteInto(
200-
'rule_coupons.code = ?',
201-
$couponCode
202-
),
203-
$connection->quoteInto(
204-
'(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)',
205-
$this->_date->date()->format('Y-m-d')
206-
),
207-
];
208-
209-
$orWhereCondition = implode(' OR ', $orWhereConditions);
210-
$andWhereCondition = implode(' AND ', $andWhereConditions);
211-
212-
$select->where(
213-
$noCouponWhereCondition . ' OR ((' . $orWhereCondition . ') AND ' . $andWhereCondition . ')',
214-
null,
215-
Select::TYPE_CONDITION
216-
);
162+
$noCouponRules = $this->getNoCouponCodeSelect();
163+
164+
if ($couponCode) {
165+
$couponRules = $this->getCouponCodeSelect($couponCode);
166+
167+
$allAllowedRules = $this->getConnection()->select();
168+
$allAllowedRules->union([$noCouponRules, $couponRules], \Zend_Db_Select::SQL_UNION_ALL);
169+
170+
$this->_select = $allAllowedRules;
217171
} else {
218-
$this->addFieldToFilter(
219-
'main_table.coupon_type',
220-
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
221-
);
172+
$this->_select = $noCouponRules;
222173
}
174+
223175
$this->setOrder('sort_order', self::SORT_ORDER_ASC);
224176
$this->setFlag('validation_filter', true);
225177
}
226178

227179
return $this;
228180
}
229181

182+
/**
183+
* Recreate the default select object for specific needs of salesrule evaluation with coupon codes.
184+
*
185+
* @param $websiteId
186+
* @param $customerGroupId
187+
* @param $now
188+
*/
189+
private function prepareSelect($websiteId, $customerGroupId, $now)
190+
{
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
209+
);
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')
231+
);
232+
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)",
248+
null,
249+
Select::TYPE_CONDITION
250+
);
251+
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+
);
273+
}
274+
230275
/**
231276
* Filter collection by website(s), customer group(s) and date.
232277
* Filter collection to only active rules.

0 commit comments

Comments
 (0)