-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
High Database Load for Sales Rule Validation #19117
Comments
Hi @rauberdaniel. Thank you for your report.
Please make sure that the issue is reproducible on the vanilla Magento instance following Steps to reproduce. To deploy vanilla Magento instance on our environment, please, add a comment to the issue:
where @rauberdaniel do you confirm that you was able to reproduce the issue on vanilla Magento instance following steps to reproduce?
|
To be a bit more precise, the alternative recommended query that has the equal result but split into two queries that are very fast because of index usage is: SELECT `main_table`.*, `rule_coupons`.`code`
FROM `salesrule` AS `main_table`
INNER JOIN `salesrule_website` AS `website`
ON website.website_id IN ('1') AND main_table.rule_id = website.rule_id
INNER JOIN `salesrule_customer_group` AS `customer_group_ids`
ON main_table.rule_id = customer_group_ids.rule_id AND customer_group_ids.customer_group_id = 0
LEFT JOIN `salesrule_coupon` AS `rule_coupons` ON main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != 1
WHERE (from_date is null or from_date <= '2018-11-08')
AND (to_date is null or to_date >= '2018-11-08')
AND (`is_active` = '1')
AND
(
(
(main_table.coupon_type = 3 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)
)
AND
rule_coupons.code = 'COUPONCODE'
AND
(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= '2018-11-08')
)
AND (`is_active` = '1')
UNION
SELECT `main_table`.*, NULL as code
FROM `salesrule` AS `main_table`
INNER JOIN `salesrule_website` AS `website`
ON website.website_id IN ('1') AND main_table.rule_id = website.rule_id
INNER JOIN `salesrule_customer_group` AS `customer_group_ids`
ON main_table.rule_id = customer_group_ids.rule_id AND customer_group_ids.customer_group_id = 0
WHERE (from_date is null or from_date <= '2018-11-08')
AND (to_date is null or to_date >= '2018-11-08')
AND (`is_active` = '1')
AND (main_table.coupon_type = 1)
ORDER BY sort_order ASC; The first query will then use |
@rauberdaniel great finding and analysis! Are you interested in preparing a pull request maybe? |
@orlangur I absolutely would, however, I’m not very into Magento code (or PHP at all) and the class that defines this query as well as the abstract are pretty confusing to me. |
@rauberdaniel got it, no problem. Let's wait for a volunteer then. |
Hi @engcom-backlog-nazar. Thank you for working on this issue.
|
@engcom-backlog-nazar Thank you for verifying the issue. Based on the provided information internal tickets |
Hi @progreg. Thank you for working on this issue.
|
Hey @orlangur, I already have some working solution, but I'm not sure about an extension point that I've used. All conditions that should be deleted in cloned $select->where(
'/*@OPTIONAL*/ (' . $orWhereCondition . ') AND ' . $andWhereCondition,
null,
Select::TYPE_CONDITION
); So in result i have such protected function _beforeLoad()
{
if ($this->getFlag('validation_filter')) {
$connection = $this->getConnection();
$noCouponWhereCondition = $connection->quoteInto(
'main_table.coupon_type = ? ',
\Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON
);
$origin = $this->_select;
$clone = clone $this->_select;
$clone->where($noCouponWhereCondition);
// Remove coupon join
$fromPart = array_filter(
$clone->getPart(Select::FROM),
function ($item) {
if ($item['joinType'] == 'left join' && $item['tableName'] == 'salesrule_coupon') {
return false;
}
return $item;
}
);
$clone->setPart(Select::FROM, $fromPart);
// Replace coupon code column with NULL value
$columnsPart = array_map(
function ($item) use ($connection) {
if ($item[0] == 'rule_coupons' && $item[1] == 'code') {
$item[1] = new Zend_Db_Expr('NULL as ' . $connection->quote('code'));
}
return $item;
},
$clone->getPart(Select::COLUMNS)
);
$clone->setPart(Select::COLUMNS, $columnsPart);
// Remove where conditions related to coupon_type
$wherePart = array_filter(
$clone->getPart(Select::WHERE),
function ($item) {
if (preg_match('/\*@OPTIONAL\*/', $item)) {
return false;
}
return $item;
}
);
$clone->setPart(Select::WHERE, $wherePart);
// Apply UNION
$this->_select = $this->getConnection()->select();
$this->_select->union([$origin, $clone]);
}
return parent::_beforeLoad();
} This is just an idea of concrete realization, that's why I first wanna ask you what is the best solution for this issue in your opinion. Also please check maybe I miss something. Query Before: SELECT `main_table`.*, `rule_coupons`.`code`
FROM `salesrule` AS `main_table`
INNER JOIN `salesrule_website` AS `website` ON website.website_id IN (1) AND main_table.rule_id = website.rule_id
INNER JOIN `salesrule_customer_group` AS `customer_group_ids`
ON main_table.rule_id = customer_group_ids.rule_id AND customer_group_ids.customer_group_id = 1
LEFT JOIN `salesrule_coupon` AS `rule_coupons`
ON main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != 1
WHERE (from_date is null or from_date <= '2018-11-20')
AND (to_date is null or to_date >= '2018-11-20')
AND (`is_active` = '1')
AND (/*@OPTIONAL*/
(
(main_table.coupon_type = 3 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)
)
AND
rule_coupons.code = 'asdasdasdasd'
AND
(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= '2018-11-20')
) Query After: SELECT `main_table`.*, `rule_coupons`.`code`
FROM `salesrule` AS `main_table`
INNER JOIN `salesrule_website` AS `website` ON website.website_id IN (1) AND main_table.rule_id = website.rule_id
INNER JOIN `salesrule_customer_group` AS `customer_group_ids`
ON main_table.rule_id = customer_group_ids.rule_id AND customer_group_ids.customer_group_id = 1
LEFT JOIN `salesrule_coupon` AS `rule_coupons`
ON main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != 1
WHERE (from_date is null or from_date <= '2018-11-20')
AND (to_date is null or to_date >= '2018-11-20')
AND (`is_active` = '1')
AND (/*@OPTIONAL*/
(
(main_table.coupon_type = 3 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)
OR
(main_table.coupon_type = 2 AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)
)
AND
rule_coupons.code = 'asdasdasdasd'
AND
(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= '2018-11-20')
)
UNION
SELECT `main_table`.*, NULL as 'code'
FROM `salesrule` AS `main_table`
INNER JOIN `salesrule_website` AS `website` ON website.website_id IN (1) AND main_table.rule_id = website.rule_id
INNER JOIN `salesrule_customer_group` AS `customer_group_ids`
ON main_table.rule_id = customer_group_ids.rule_id AND customer_group_ids.customer_group_id = 1
WHERE (from_date is null or from_date <= '2018-11-20')
AND (to_date is null or to_date >= '2018-11-20')
AND (`is_active` = '1')
AND (main_table.coupon_type = 1) |
Hey @orlangur. Can you give me any feedback, is my solution may be good? Or I should add PR first? |
Github Issue: magento#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).
@david-fuehr Thank you very much for your work on this issue! |
@progreg Thank you for your work as well! Sorry to see you didn’t get any feedback on your work (which I’m unable to give due to little knowledge over Magento). |
@rauberdaniel, At least I've tried) Anyway, if a solution of @david-fuehr will be better then all is good. |
@rauberdaniel @progreg Thanks for the report and your thoughts on this issue. With that, we were able to quickly solve that issue as we encountered it. |
Hi @rauberdaniel. Thank you for your report.
The fix will be available with the upcoming 2.3.2 release. |
Github Issue: magento#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).
Hi @rauberdaniel. Thank you for your report.
The fix will be available with the upcoming 2.2.9 release. |
Hello, I realize that this post is now closed. But do you all know if there is a similar solution for M1 EE? |
Hello @cesmendez, I don't believe that someone implemented this (or a similar) fix for M1. But as the code hardly changed, you could try to apply these changes: https://github.com/magento/magento2/pull/20484/files to |
Preconditions (*)
Steps to reproduce (*)
Expected result (*)
Actual result (*)
Description
We’re currently having about 400 sales rules (most of them are inactive or expired) as well as about 600.000 coupons. The query, that is currently being used by the sales-rule module is extremely inefficient on the database and therefore results in massive database CPU consumption when having several orders with coupons per minute. However, this query can greatly be optimized by splitting it into two queries.
The query that is generated by the
setValidationFilter
method inmagento/vendor/magento/module-sales-rule/Model/ResourceModel/Rule/Collection.php
currently looks like this:The problematic part in this query is line 14
main_table.coupon_type = 1 OR
. This part forces the database to filter the joined tables based oncoupon_type
. By simply removing that line, therule_coupons.code
can be used as an index to find exactly the one matching sales rule without having to join and filter a massive amount of rows. The line 14 is a completely separate case than the rest in that scope (line 13 to 29) and therefore should be treated separately e.g. by a second query in aUNION
operation.Some details using the MySQL
EXPLAIN
function:Original Query:
Query without line 14/15 (not added in a second query in this example, therefore ignoring sales rules without coupons):
This is a major performance improvement especially for larger amounts of sales rules and coupons and absolutely critical.
The text was updated successfully, but these errors were encountered: