Оптимизация запроса, который просматривает определенное временное окно каждый день - PullRequest
0 голосов
/ 20 сентября 2018

Это продолжение моего предыдущего вопроса

Оптимизация запроса для получения всей строки, где одно поле является максимальным для группы

Я изменю именаиз того, что я использовал там, чтобы сделать их немного более запоминающимися, но они не отражают моего фактического варианта использования (поэтому не оценивайте количество записей из них).

У меня есть таблица ссхема, подобная этой:

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

Я бы хотел извлечь строки из таблицы, представляющие самый дорогой заказ напитков каждого клиента в течение счастливого часа (15:00 - 18:00) каждый день.Так, например, я хотел бы получить результаты типа

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       |                                                |
+------+--------------+------------+------+---------------+------+------------+------------------------------------------------+

Вопросы:

  1. Что здесь происходит?
  2. Как это исправить?

Ответы [ 2 ]

0 голосов
/ 07 октября 2018

Чтобы извлечь строки из таблицы, представляющие самый дорогой заказ на напитки каждого клиента в течение счастливого часа (15:00 - 18:00) каждый день, я бы использовал row_number() over() в течение case expression, оценивая время суток, например:

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
;

и результат предоставит доступ ко всем столбцам, включенным в производную таблицу.

Date       | Customer | OrderTime           | MaxPrice | Bartender | rn
:--------- | :------- | :------------------ | -------: | :-------- | -:
0001-01-18 | Bob      | 0001-01-18 15:12:00 |     9.08 | Jane      |  1
0001-02-18 | Bob      | 0001-02-18 15:57:00 |     6.00 | Tarzan    |  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
;

производит этот промежуточный набор результатов:

Date       | Customer | OrderTime           | MaxPrice | Bartender |   rn
:--------- | :------- | :------------------ | -------: | :-------- | ---:
0001-01-18 | Alice    | 0001-01-18 13:45:00 |    13.15 | Jane      | <em>null</em>
0001-01-18 | Bob      | 0001-01-18 15:12:00 |     9.08 | Jane      |    1
0001-02-18 | Alice    | 0001-02-18 13:45:00 |    13.15 | Jane      | <em>null</em>
0001-02-18 | Bob      | 0001-02-18 15:57:00 |     6.00 | Tarzan    |    1
0001-02-18 | Carol    | 0001-02-18 13:13:00 |     6.00 | Tarzan    | <em>null</em>

db <> fiddle здесь

0 голосов
/ 07 октября 2018

Задача, похоже, является проблемой "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.)

Миллиарды новых строк в день

Это добавляет новые морщины.Это больше терабайта дополнительного дискового пространства, необходимого каждый день?

Можно ли суммировать данные?Цель здесь - добавить сводную информацию по мере поступления новых данных, и вам никогда не придется повторно сканировать миллиарды старых данных.Это может также позволит вам удалить все вторичные индексы в таблице фактов.

Нормализация поможет уменьшить размер таблицы, а значит, ускорить запросы.Bartender и Customer являются основными кандидатами на это - возможно, SMALLINT UNSIGNED (2 байта; значения 65K) для первого и MEDIUMINT UNSIGNED (3 байта, 16M) для второго.Это, вероятно, сократит на 50% те 5 столбцов, которые вы сейчас показываете.После нормализации вы можете получить двукратное ускорение для многих операций.

Нормализацию лучше всего выполнять путем «поэтапного» размещения данных - загрузить данные во временную таблицу, нормализовать ее, суммировать, затем скопируйте в основную таблицу фактов.

См. http://mysql.rjweb.org/doc.php/summarytables
и http://mysql.rjweb.org/doc.php/staging_table

Прежде чем вернуться к вопросу оптимизации одного запроса, нам нужно посмотреть схемупоток данных, могут ли все быть нормализованы, могут ли сводные таблицы быть эффективными и т. д. Я хотел бы получить «ответ» на запрос, который будет в основном перевариваться в сводной таблице.Иногда это приводит к ускорению в 10 раз.

...