在查询中重复相同的功能

Thi*_*ark 5 mysql optimization

在下面的查询中,有重复的计算,例如对 的三个调用SUM(p.amount)。MySQL 是否为每个函数调用重新计算,或者是否有某种内存优化?如果没有,如何优化这种查询以获得最大性能?

似乎在第一次计算后通过别名获得下一个计算会更快total_payemnts,但这只会引发错误。

SELECT LEFT(c.last_name, 1) AS 'last_names',
    SUM(p.amount) AS 'total_payments', 
    COUNT(p.rental_id) AS 'num_rentals',
    SUM(p.amount) / COUNT(p.rental_id) AS 'avg_pay'
FROM customer c
JOIN payment p ON p.customer_id = c.customer_id
GROUP BY LEFT(c.last_name, 1)
ORDER BY SUM(p.amount) DESC;
Run Code Online (Sandbox Code Playgroud)

此查询在MySQL Sakila示例数据库上运行。

joa*_*olo 2

一个明确的答案确实很难。然而,一些试验可能会给我们一些提示。

我们从一些数据开始:

CREATE TABLE customers
(
    customer_id integer PRIMARY KEY,
    last_name varchar(50) NOT NULL
) ;

CREATE TABLE payments
(
    rental_id integer not null,
    customer_id integer not null, --  references customer(customer_id),
    amount numeric(10,2) not null
) ;

INSERT INTO customers 
VALUES 
  (1, 'A - Customer 1'),
  (2, 'B - Customer 2'),
  (3, 'C - Customer 3'),
  (4, 'D - Customer 4') ;

INSERT INTO payments
VALUES
  (1, 1, 900.0),
  (2, 1, 800.0),
  (3, 1, 500.0),
  (4, 1, 900.0),
  (5, 2, 500.0),
  (6, 2, 500.0) ;
Run Code Online (Sandbox Code Playgroud)

我们首先定义一个DETERMINISTIC(尽管它副作用)函数来记录调用:

CREATE FUNCTION log_numeric(_n numeric(10,2), _counter character(2))
    RETURNS numeric(10,2)
    LANGUAGE SQL
    DETERMINISTIC
    BEGIN
      UPDATE log SET n = n + 1 WHERE counter = _counter ;
      RETURN _n ;
    END ;
Run Code Online (Sandbox Code Playgroud)

我们之前定义了一个log表,用于存储计数:

CREATE TABLE log
(
    counter character(2) primary key,
    n integer default 0
) ENGINE=MyISAM;
INSERT INTO log 
VALUES
    ('x', 0),
    ('L', 0),
    ('T', 0),
    ('N', 0),
    ('L2', 0),
    ('T2', 0),
    ('N2', 0) ;
Run Code Online (Sandbox Code Playgroud)

此时,如果 MySQL 能够执行公共子表达式消除优化(对于编译器来说很常见),那么当我们发出以下命令时,它只会进行一次调用:

SELECT
    log_numeric(30, 'x') AS c1,
    log_numeric(30, 'x') AS c2 ;
Run Code Online (Sandbox Code Playgroud)

因为在两种情况下log_numeric(30, 'x')都会返回相同的值(因为该函数是确定性的)。

事实上,它对该函数进行了两次调用,如观察计数器“x”所证明的那样;

SELECT
    *
FROM
    log
WHERE
    counter = 'x' ;
Run Code Online (Sandbox Code Playgroud)

[注意:使用 MySQL 5.7.12 进行测试]

MySQL可能认为该函数有一些副作用,或者用户定义函数的工作方式与内置函数不同。内置函数可以优化,而 UDF 则不能。无论如何,如果 MySQL 只需一次调用就可以优化这两个表达式,那么很明显,这种优化是标准的。由于事实并非如此,我们可以做出有根据的猜测这与证明不同)并假设函数被调用的次数与它们出现的次数一样多。log_numeric

对于发布的案例也可以这样做:

SELECT 
    log_char(LEFT(c.last_name, 1), 'L') AS last_names,
    log_numeric(SUM(p.amount), 'T') AS total_payments, 
    log_numeric(COUNT(p.rental_id), 'N') AS num_rentals,
    log_numeric(SUM(p.amount), 'T') / log_numeric(COUNT(p.rental_id), 'N') AS avg_pay
FROM 
    customers c
    JOIN payments p ON p.customer_id = c.customer_id
GROUP BY 
    log_char(LEFT(c.last_name, 1), 'L')
ORDER BY 
    log_numeric(SUM(p.amount), 'T') DESC;
Run Code Online (Sandbox Code Playgroud)

结果表明,所有调用都会被评估多次。

您可以在http://rextester.com/ZKS91815检查所有这些测试。


注意:出于好奇,postgreSQL (9.6.1) 做得更好一点:它将 ORDER BY 和 GROUP BY 识别为 上的“已计算” SELECT,但它没有针对公共子表达式消除进行优化。有改进的余地。

避免重复函数的替代查询

如果您担心计算量超出必要范围,请使用:

SELECT
    last_names, total_payments, num_rentals,
    total_payments / num_rentals AS avg_pay
FROM
(
    SELECT 
        log_char(LEFT(c.last_name, 1), 'L') AS last_names,
        log_numeric(SUM(p.amount), 'T') AS total_payments, 
        log_numeric(COUNT(p.rental_id), 'N') AS num_rentals
    FROM 
        customers c
        JOIN payments p ON p.customer_id = c.customer_id
    GROUP BY 
        log_char(LEFT(c.last_name, 1), 'L')    
) AS q0
ORDER BY
    total_payments DESC ;
Run Code Online (Sandbox Code Playgroud)

老实说,我认为差异不会太大。整个查询所花费的时间中函数的权重可能很小。如果功能复杂的话可能会很重要。根据 MySQL 处理子查询(可能是临时表)的方式,这实际上可能会更糟。