优化解析5000万行MySQL表的请求

798qvoo8  于 2023-01-04  发布在  Mysql
关注(0)|答案(2)|浏览(145)

我有一个请求,需要在特定时间段内多次处理,我的实现正在工作,但我的用户群每天都在增长,数据库的CPU负载和执行查询所需的时间每天都在增加
请求如下:

SELECT bill.* FROM billing bill
            INNER JOIN subscriber s ON (s.subscriber_id = bill.subscriber_id) 
            INNER JOIN subscription sub ON(s.subscriber_id = sub.subscriber_id)
            WHERE s.status = 'C' 
            AND bill.subscription_id = sub.subscription_id                      
            AND sub.renewable = 1
            AND (hour(sub.created_at) > 1 AND hour(sub.created_at) < 5 )
            AND sub.store = 'BizaoStore'
            AND (sub.purchase_token = 'myservice' or sub.purchase_token = 'myservice_wait' ) 
            AND bill.billing_date > '2022-12-31 07:00:00' AND bill.billing_date < '2023-01-01 10:00:00'
            AND (bill.billing_value = 'not_ok bizao_tobe' or bill.billing_value =  'not_ok BILL010 2' or bill.billing_value =  'not_ok BILL010' or bill.billing_value = 'not_ok BILL010 3')
            AND (SELECT MAX(bill2.billing_date)
                FROM billing bill2
                WHERE bill2.subscriber_id = bill.subscriber_id
                AND bill2.subscription_id = bill.subscription_id 
                AND bill2.billing_value = 'not_ok bizao_tobe') 
            = bill.billing_date order by sub.created_at DESC LIMIT 300;

此请求在两个不同的服务器上执行,每个服务器处理一个特定的服务。在每个服务器上,请求每分钟运行8次(大约3个小时),每一次都有以下行,并具有不同的小时:

AND (hour(sub.created_at) >  1 AND hour(sub.created_at) < 5 )

我这样做是为了将我的用户群分成8个,从而更有效地处理请求。此外,我一次只需要处理300个用户,因为我必须为每个用户调用的第三方服务器不是很稳定,有时需要很长时间才能响应
计费表计数大约50.000.000个条目,下面是列和索引的模式:

订阅者表大约为2.000.000,列方案和索引:

最后是订阅表,2.500.000行,方案和索引:

作为其他一些信息,我注意到在我的优化测试中,如果我在请求中添加这样一个事实,即我希望在特定ID上使用“billing_id”的数据,它将运行得非常快。基本上,我认为花费最多时间的是解析50.000.000行的表。
我确实(或者至少我尝试过)用时间来优化我的请求,以提高效率,但到目前为止,我有点被它卡住了。
Mysql版本为5.7.38
谢谢你的帮忙

p1tboqfb

p1tboqfb1#

我认为还可以有一些改进的空间,有些更容易做到有些需要更多的努力:
1.您提到过您试图通过限制时间来提高性能,我认为这意味着AND (hour(sub.created_at) > 1 AND hour(sub.created_at) < 5 ),但是sub.created_at没有索引,在数据库中created_atupdated_at列始终具有索引是一个好主意。
1.此外,还可以在bill.billing_date上添加索引
1.有一个查找MAX(bill2.billing_date)的子查询,这也可能是一个瓶颈,因为该子查询需要为上一执行阶段的每一行运行(您可以使用explain查询检查)根据您的条件,您可以将最大字段存储在某个地方,如数据库中的列、缓存等,并在每次底层表的值改变时不断更新它。你可以用数据库触发器(有些人认为它是反模式的)或在代码中使用事务来做到这一点。
1.您可以对复制副本运行查询,而不必担心是否需要花费大量时间。
总而言之,第一点可能会大大提高性能,因此其余的就没有必要了

5t7ly7z5

5t7ly7z52#

我看到了一些加快此查询的机会。(参考:Markus Winand的https://use-the-index-luke.com/电子书。)
1.将correlated subquerySELECT MAX(bill2.billing_date)...)替换为独立的子查询。
1.试着让所有的WHERE子句sargable--能够利用索引。
1.添加适当的索引。

独立子查询获取每个订户/订阅的最新计费日期,如下所示。此查询只需运行一次,而您的相关子查询则运行多次。

SELECT MAX(billing_date) billing_date,
             subscriber_id,
             subscription_id
        FROM billing
       WHERE billing_value = 'not_ok bizao_tobe'
       GROUP BY subscriber_id, subscription_id

使用这个索引可以加快子查询的速度,这个索引可以让子查询满足于一个快得近乎奇迹的loose index scan

CREATE INDEX value_subscriber_subscription_date ON billing
   (billing_value, subscriber_id, subscription_id, billing_date DESC);

然后像这样重写整个查询以使用它。为了提高可读性,我还重写了其他一些东西:主要是将col = a OR col = b OR col = c更改为col IN (a,b,c)。我还更改了一些WHERE子句的顺序,同样是为了可读性。WHERE子句的顺序对性能没有影响。

SELECT bill.* 
  FROM billing bill
  JOIN subscriber s ON s.subscriber_id = bill.subscriber_id 
  JOIN subscription sub   ON s.subscriber_id = sub.subscriber_id
                         AND bill.subscription_id = sub.subscription_id
  JOIN (
      SELECT MAX(billing_date) billing_date,
             subscriber_id,
             subscription_id
        FROM billing
       WHERE billing_value = 'not_ok bizao_tobe'
       GROUP BY subscriber_id, subscription_id
       ) latest   ON bill.subscriber_id = latest.subscriber_id
                 AND bill.subscription_id = latest.subscription_id 
                 AND bill.billing_date = latest.billing_date                 
 WHERE s.status = 'C' 
   AND (hour(sub.created_at) > 1 AND hour(sub.created_at) < 5 )
   AND sub.renewable = 1
   AND sub.store = 'BizaoStore'
   AND sub.purchase_token IN ('myservice', 'myservice_wait' ) 
   AND bill.billing_value IN (
      'not_ok bizao_tobe', 'not_ok BILL010 2', 
      'not_ok BILL010', 'not_ok BILL010 3')
   AND bill.billing_date > '2022-12-31 07:00:00' 
   AND bill.billing_date < '2023-01-01 10:00:00'
 ORDER BY sub.created_at DESC
 LIMIT 300;

Sargability正如您所指出的,您按一天中的小时数对用户群进行细分意味着您需要此子句。

AND (HOUR(sub.created_at) >  1 AND HOUR(sub.created_at) < 5 )

该子句将HOUR()函数应用于每个符合条件的行,因此它必须扫描所有行。慢一点。将名为created_hourvirtual column添加到subscription表中。我们稍后将在该列上放置索引。

ALTER TABLE subscription 
     ADD COLUMN created_hour TINYINT 
     GENERATED ALWAYS AS (HOUR(created_at)) VIRTUAL;

然后开始使用虚拟列来划分用户。

AND (sub.created_hour >  1 AND sub.created_hour < 5)

索引复合(多列)索引是加快复杂查询速度的一种方法。列的顺序在索引中很重要。具有相等匹配的列在前面,然后是具有范围匹配的列。

首先,让我们在subscription表上放置一个符合查询要求的复合索引,我们将在此过程中为新的虚拟列建立索引,这将使查询计划器能够高效地查找批处理。
这个索引的最后一列是created_at,这样可以加快ORDER BY ... LIMIT操作的速度。

CREATE INDEX renewable_store_token_hour_created 
     ON subscription (renewable, store, purchase_token,
                      created_hour, created_at);

接下来,让我们看看如何在主查询中使用billing。(我们已经添加了一个索引来帮助子查询)。您在billing_value上匹配相等性,然后在billing_date上匹配日期范围。因此,您需要这个索引。

CREATE INDEX value_date ON billing (billing_value, billing_date);

subscriber上已经有了所需的索引。

相关问题