优化每天查看特定时间窗口的查询

Posted

技术标签:

【中文标题】优化每天查看特定时间窗口的查询【英文标题】:Optimizing query that looks at a specific time window each day 【发布时间】:2019-02-25 01:06:36 【问题描述】:

这是对我之前问题的跟进

Optimizing query to get entire row where one field is the maximum for a group

我将更改我在那里使用的名称以使它们更容易记住,但这些并不代表我的实际用例(所以不要估计它们的记录数量)。

我有一张表,其架构如下:

OrderTime           DATETIME(6),
Customer            VARCHAR(50),
DrinkPrice          DECIMAL,
Bartender           VARCHAR(50),
TimeToPrepareDrink  TIME(6),
...

我想从表中提取代表每天欢乐时光(下午 3 点至下午 6 点)期间每位客户最昂贵的饮料订单的行。因此,例如,我想要这样的结果

Date   | Customer | OrderTime   | MaxPrice   | Bartender | ...
-------+----------+-------------+------------+-----------+-----
1/1/18 |  Alice   | 1/1/18 3:45 | 13.15      | Jane      | ...
1/1/18 |  Bob     | 1/1/18 5:12 |  9.08      | Jane      | ...
1/1/18 |  Carol   | 1/1/18 4:45 | 20.00      | Tarzan    | ...
1/2/18 |  Alice   | 1/2/18 3:45 | 13.15      | Jane      | ...
1/2/18 |  Bob     | 1/2/18 5:57 |  6.00      | Tarzan    | ...
1/2/18 |  Carol   | 1/2/18 3:13 |  6.00      | Tarzan    | ...
 ...

该表的索引为OrderTime,包含数百亿条记录。 (我的顾客是酗酒者)。

感谢上一个问题,我可以很容易地提取特定日期的内容。我可以这样做:

SELECT * FROM orders b
INNER JOIN (
    SELECT Customer, MAX(DrinkPrice) as MaxPrice
    FROM orders
    WHERE OrderTime >= '2018-01-01 15:00' 
      AND OrderTime <= '2018-01-01 18:00'
    GROUP BY Customer
) AS a
ON a.Customer = b.Customer
AND a.MaxPrice = b.DrinkPrice
WHERE b.OrderTime >= '2018-01-01 15:00'
  AND b.OrderTime <= '2018-01-01 18:00';

此查询在不到一秒的时间内运行。解释计划如下所示:

+---+-------------+------------+-------+---------------+------------+--------------------+--------------------------------------------------------+
| id| select_type | table      | type  | possible_keys | key        | ref                | Extra                                                  |
+---+-------------+------------+-------+---------------+------------+--------------------+--------------------------------------------------------+
| 1 | PRIMARY     | b          | range | OrderTime     | OrderTime  | NULL               | Using index condition                                  |
| 1 | PRIMARY     | <derived2> | ref   | key0          | key0       | b.Customer,b.Price |                                                        |
| 2 | DERIVED     | orders     | range | OrderTime     | OrderTime  | NULL               | Using index condition; Using temporary; Using filesort |
+---+-------------+------------+-------+---------------+------------+--------------------+--------------------------------------------------------+

我还可以获取有关我的查询的相关行的信息:

SELECT Date, Customer, MAX(DrinkPrice) AS MaxPrice
FROM
        orders
    INNER JOIN
        (SELECT '2018-01-01' AS Date 
         UNION
         SELECT '2018-01-02' AS Date) dates
WHERE   OrderTime >= TIMESTAMP(Date, '15:00:00')
AND OrderTime <= TIMESTAMP(Date, '18:00:00')
GROUP BY Date, Customer
 HAVING MaxPrice > 0;

此查询也可以在不到一秒的时间内运行。以下是其解释计划的外观:

+------+--------------+------------+------+---------------+------+------+------------------------------------------------+
| id   | select_type  | table      | type | possible_keys | key  | ref  | Extra                                          |
+------+--------------+------------+------+---------------+------+------+------------------------------------------------+
|    1 | PRIMARY      | <derived2> | ALL  | NULL          | NULL | NULL | Using temporary; Using filesort                |
|    1 | PRIMARY      | orders     | ALL  | OrderTime     | NULL | NULL | Range checked for each record (index map: 0x1) |
|    2 | DERIVED      | NULL       | NULL | NULL          | NULL | NULL | No tables used                                 |
|    3 | UNION        | NULL       | NULL | NULL          | NULL | NULL | No tables used                                 |
| NULL | UNION RESULT | <union2,3> | ALL  | NULL          | NULL | NULL |                                                |
+------+--------------+------------+------+---------------+------+------+------------------------------------------------+

现在的问题是从表中检索剩余的字段。我尝试改编之前的技巧,如下所示:

SELECT * FROM
        orders a
    INNER JOIN
        (SELECT Date, Customer, MAX(DrinkPrice) AS MaxPrice
        FROM
                orders
            INNER JOIN
                (SELECT '2018-01-01' AS Date
                 UNION
                 SELECT '2018-01-02' AS Date) dates
        WHERE   OrderTime >= TIMESTAMP(Date, '15:00:00')
            AND OrderTime <= TIMESTAMP(Date, '18:00:00')
        GROUP BY Date, Customer
        HAVING MaxPrice > 0) b
    ON     a.OrderTime >= TIMESTAMP(b.Date, '15:00:00')
       AND a.OrderTime <= TIMESTAMP(b.Date, '18:00:00')
       AND a.Customer = b.Customer;

但是,由于我不明白的原因,数据库选择以一种永远耗时的方式执行此操作。解释计划:

+------+--------------+------------+------+---------------+------+------------+------------------------------------------------+
| id   | select_type  | table      | type | possible_keys | key  | ref        | Extra                                          |
+------+--------------+------------+------+---------------+------+------------+------------------------------------------------+
|    1 | PRIMARY      | a          | ALL  | OrderTime     | NULL | NULL       |                                                |
|    1 | PRIMARY      | <derived2> | ref  | key0          | key0 | a.Customer | Using where                                    |
|    2 | DERIVED      | <derived3> | ALL  | NULL          | NULL | NULL       | Using temporary; Using filesort                |
|    2 | DERIVED      | orders     | ALL  | OrderTime     | NULL | NULL       | Range checked for each record (index map: 0x1) |
|    3 | DERIVED      | NULL       | NULL | NULL          | NULL | NULL       | No tables used                                 |
|    4 | UNION        | NULL       | NULL | NULL          | NULL | NULL       | No tables used                                 |
| NULL | UNION RESULT | <union3,4> | ALL  | NULL          | NULL | NULL       |                                                |
+------+--------------+------------+------+---------------+------+------------+------------------------------------------------+

问题:

    这是怎么回事? 我该如何解决?

【问题讨论】:

我是否正确假设您的实际查询可能不止两个日期联合在一起?它可能包含任意数量的日期? 是的,也许几年的价值。只要日期数的性能为 O(n) 就没有问题,因为获取单个日期信息的查询在几分之一秒内运行。 基本上我只需要知道如何强制它作为“按记录检查的范围”进行连接。我知道在这种情况下性能是可以接受的。 您使用的是什么版本的 MariaDB? 谢谢。请将SHOW CREATE TABLE orders 的输出也添加到您的问题中。您可以用该输出替换问题中的“我有一个具有如下架构的表:”部分。我将使用这些新信息更好地了解您的 EXPLAIN 信息中使用的索引。 【参考方案1】:

这个任务似乎是一个“groupwise-max”问题。这是一种方法,仅涉及 2 个“查询”(内部查询称为“派生表”)。

SELECT  x.OrderDate, x.Customer, b.OrderTime,
        x.MaxPrice, b.Bartender
    FROM  
    (
        SELECT  DATE(OrderTime) AS OrderDate,
                Customer,
                Max(Price) AS MaxPrice
            FROM  tbl
            WHERE  TIME(OrderTime) BETWEEN '15:00' AND '18:00'
            GROUP BY  OrderDate, Customer 
    ) AS x
    JOIN  tbl AS b
       ON  b.OrderDate = X.OrderDate
      AND  b.customer = x.Customer
      AND  b.Price = x.MaxPrice
    WHERE  TIME(b.OrderTime) BETWEEN '15:00' AND '18:00'
    ORDER BY  x.OrderDate, x.Customer

理想指数:

INDEX(Customer, Price)

(没有充分的理由使用 MyISAM。)

每天有数十亿条新行

这增加了新的皱纹。每天需要超过 1 TB 的额外磁盘空间?

是否可以汇总数据?这里的目标是在新数据进入时添加摘要信息,而不必重新扫描数十亿的旧数据。这可能还可以让您删除 Fact 表上的所有二级索引。

规范化将有助于缩小表大小,从而加快查询速度。 BartenderCustomer 是此类的主要候选者 - 前者可能是 SMALLINT UNSIGNED(2 个字节;65K 值),后者可能是 MEDIUMINT UNSIGNED(3 个字节,16M)。这可能会缩小您当前显示的 5 列的 50%。标准化后,您可能会在许多操作上获得 2 倍的加速。

规范化最好通过“分段”数据来完成——将数据加载到临时表中,在其中规范化,汇总,然后复制到主 Fact 表中。

见http://mysql.rjweb.org/doc.php/summarytables 和http://mysql.rjweb.org/doc.php/staging_table

在回到优化一个查询的问题之前,我们需要看看架构,数据流,是否可以规范化,汇总表是否有效等。我希望有'答案'使查询大部分在汇总表中被消化。有时这会导致 10 倍的加速。

【讨论】:

除了 MyISAM 之外,我找不到任何可以以可接受的速度处理批量插入的存储引擎,也找不到在磁盘上占用空间可接受的存储引擎。 @DanielMcLaury - 你如何进行批量插入?请提供整个SHOW CREATE TABLE,架构中可能有一些东西会减慢批量插入的速度?您是否需要多次进行批量插入? 每天,我将数十亿条记录批量插入到该表中。我在这里手动翻译所有字段名称,但实际上您在 SHOW CREATE TABLE 中看到的唯一内容是 OrderTime 上有一个索引。我买不起磁盘空间来添加另一个索引。 @DanielMcLaury - 每天有超过 1,000,000,000 行新行?您是否还删除了一些行?请讨论更多细节——你已经进入了如何收集大量数据的领域。我们需要(或可能同时)处理查询优化之前解决这个问题。 是的,每天有超过 10 亿行新行。不,数据永远不会被删除。【参考方案2】:

为了从表中提取代表每个客户在欢乐时光(下午 3 点至下午 6 点)每天最昂贵的饮料订单的行,我会在 case expression 中使用 row_number() over() 来评估一天中的时间,如下所示:

CREATE TABLE mytable(
   Date      DATE 
  ,Customer  VARCHAR(10)
  ,OrderTime DATETIME 
  ,MaxPrice  NUMERIC(12,2)
  ,Bartender VARCHAR(11)
);

注意对 OrderTime 进行了更改

INSERT INTO mytable(Date,Customer,OrderTime,MaxPrice,Bartender) 
VALUES 
  ('1/1/18','Alice','1/1/18 13:45',13.15,'Jane')
, ('1/1/18','Bob'  ,'1/1/18 15:12', 9.08,'Jane')
, ('1/2/18','Alice','1/2/18 13:45',13.15,'Jane')
, ('1/2/18','Bob'  ,'1/2/18 15:57', 6.00,'Tarzan')
, ('1/2/18','Carol','1/2/18 13:13', 6.00,'Tarzan')
;

建议的查询是这样的:

select
    *
from (
    select
        *
        , case when hour(OrderTime) between 15 and 18 then 
                row_number() over(partition by `Date`, customer
                                      order by MaxPrice DESC)
                else null 
          end rn
    from mytable
    ) d
where rn = 1
;

结果将允许您访问派生表中包含的所有列。

日期 |客户 |订购时间 |最高价格 |调酒师 | rn :--------- | :------- | :----------------- | --------: | :-------- | -: 0001-01-18 |鲍勃 | 0001-01-18 15:12:00 | 9.08 |简 | 1 0001-02-18 |鲍勃 | 0001-02-18 15:57:00 | 6.00 |泰山 | 1

为了帮助显示其工作原理,运行派生表子查询:

select
*
, case when hour(OrderTime) between 15 and 18 then 
        row_number() over(partition by `Date`, customer order by MaxPrice DESC)
        else null 
  end rn
from mytable
;

产生这个临时结果集:

日期 |客户 |订购时间 |最高价格 |调酒师 | rn :--------- | :------- | :----------------- | --------: | :-------- | ---: 0001-01-18 |爱丽丝 | 0001-01-18 13:45:00 | 13.15 |简 | 0001-01-18 |鲍勃 | 0001-01-18 15:12:00 | 9.08 |简 | 1 0001-02-18 |爱丽丝 | 0001-02-18 13:45:00 | 13.15 |简 | 0001-02-18 |鲍勃 | 0001-02-18 15:57:00 | 6.00 |泰山 | 1 0001-02-18 |卡罗尔 | 0001-02-18 13:13:00 | 6.00 |泰山 |

db小提琴here

【讨论】:

以上是关于优化每天查看特定时间窗口的查询的主要内容,如果未能解决你的问题,请参考以下文章

查询优化——窗口排序

在 presto 中优化窗口查询

带窗口函数的简单SQL查询优化

1.19.7.Table APISQL数据类型保留关键字查询语句指定查询执行查询语法操作符无排名输出优化去重分组窗口时间属性选择分组窗口的开始和结束时间戳模式匹配

操作超时 - BigQuery 优化窗口功能

为大型 Postgresql 表优化嵌套连接窗口函数