【问题标题】:How to optimise this MySQL query? Millions of Rows如何优化这个 MySQL 查询?数百万行
【发布时间】:2018-12-01 03:49:08
【问题描述】:

我有以下疑问:

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM analytics
LEFT JOIN transactions ON analytics.id = transactions.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 

分析表有 60M 行,事务表有 3M 行。

当我对此查询运行 EXPLAIN 时,我得到:

+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+
| # id |  select_type |      table      |  type  |    possible_keys    |        key        |        key_len       |            ref            |   rows   |   Extra   |                                                 |
+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+
| '1'  |  'SIMPLE'    |  'analytics'    |  'ref' |  'analytics_user_id | analytics_source' |  'analytics_user_id' |  '5'                      |  'const' |  '337662' |  'Using where; Using temporary; Using filesort' |
| '1'  |  'SIMPLE'    |  'transactions' |  'ref' |  'tran_analytics'   |  'tran_analytics' |  '5'                 |  'dijishop2.analytics.id' |  '1'     |  NULL     |                                                 |
+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+

我不知道如何优化这个查询,因为它已经很基础了。运行此查询大约需要 70 秒。

以下是存在的索引:

+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
|   # Table   |  Non_unique |          Key_name          |  Seq_in_index |    Column_name   |  Collation |  Cardinality |  Sub_part |  Packed |  Null  |  Index_type |  Comment |  Index_comment |
+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
| 'analytics' |  '0'        |  'PRIMARY'                 |  '1'          |  'id'            |  'A'       |  '56934235'  |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_user_id'       |  '1'          |  'user_id'       |  'A'       |  '130583'    |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_product_id'    |  '1'          |  'product_id'    |  'A'       |  '490812'    |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_affil_user_id' |  '1'          |  'affil_user_id' |  'A'       |  '55222'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_source'        |  '1'          |  'source'        |  'A'       |  '24604'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_country_name'  |  '1'          |  'country_name'  |  'A'       |  '39510'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '1'          |  'id'            |  'A'       |  '56934235'  |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '2'          |  'user_id'       |  'A'       |  '56934235'  |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '3'          |  'source'        |  'A'       |  '56934235'  |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+


+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
|    # Table     |  Non_unique |      Key_name     |  Seq_in_index |    Column_name    |  Collation |  Cardinality |  Sub_part |  Packed |  Null  |  Index_type |  Comment |  Index_comment |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
| 'transactions' |  '0'        |  'PRIMARY'        |  '1'          |  'id'             |  'A'       |  '2436151'   |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_user_id'   |  '1'          |  'user_id'        |  'A'       |  '56654'     |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'transaction_id' |  '1'          |  'transaction_id' |  'A'       |  '2436151'   |  '191'    |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_analytics' |  '1'          |  'analytics'      |  'A'       |  '2436151'   |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_status'    |  '1'          |  'status'         |  'A'       |  '22'        |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'gordon_trans'   |  '1'          |  'status'         |  'A'       |  '22'        |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'gordon_trans'   |  '2'          |  'analytics'      |  'A'       |  '2436151'   |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+

在按照建议添加任何额外索引之前简化了两个表的架构,因为它并没有改善这种情况。

CREATE TABLE `analytics` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `affil_user_id` int(11) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  `medium` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `source` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `terms` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `is_browser` tinyint(1) DEFAULT NULL,
  `is_mobile` tinyint(1) DEFAULT NULL,
  `is_robot` tinyint(1) DEFAULT NULL,
  `browser` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `mobile` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `robot` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `platform` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `referrer` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `domain` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `ip` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `continent_code` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `country_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `city` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `analytics_user_id` (`user_id`),
  KEY `analytics_product_id` (`product_id`),
  KEY `analytics_affil_user_id` (`affil_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=64821325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `transactions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `transaction_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `user_id` int(11) NOT NULL,
  `pay_key` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `sender_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `amount` decimal(10,2) DEFAULT NULL,
  `currency` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `status` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `analytics` int(11) DEFAULT NULL,
  `ip_address` varchar(46) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `session_id` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `eu_vat_applied` int(1) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `tran_user_id` (`user_id`),
  KEY `transaction_id` (`transaction_id`(191)),
  KEY `tran_analytics` (`analytics`),
  KEY `tran_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=10019356 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

如果以上无法进一步优化。任何关于汇总表的实施建议都会很棒。我们在 AWS 上使用 LAMP 堆栈。上述查询在 RDS (m1.large) 上运行。

【问题讨论】:

  • 你的声望分数很高,所以你不是新手。您现在应该知道,您应该在查询中包含每个表的SHOW CREATE TABLE,以便我们可以看到您的表中有哪些数据类型、索引和约束。帮助我们帮助您!
  • 抱歉比尔,它们是巨大的表格(很多列)。在我尝试 Gordon 的建议后会明白的。
  • 我建议使用SHOW CREATE TABLE 的原因是,如果有人想在沙箱实例上试用您的表,他们必须通过猜测您的列和索引来煞费苦心地重新创建表。可以从你的 SHOW INDEXES 中拼凑出与你的故事相似的东西,但这需要太多的工作,我不能确定它是否正确。我不会花时间做那件事。祝你好运!
  • 如果省略GROUP BY 子句,查询性能会怎样? (我知道它不会产生您想要的结果;关键是要弄清楚 GROUP BY ... LIMIT... 是否占用了很多时间。)
  • 你能解释一下你想要什么更好一点吗? COUNT(a.id) 在查询中执行 a LEFT JOIN b 有点奇怪。它计算来自b 的匹配行,并为a 中的每一行计算1,而b 中没有匹配的行。那是你要的吗?对我来说,这听起来像是很难向用户解释的事情。 COUNT 操作的完美性至关重要,因为您稍后会将它用于 GROUP BY ... LIMIT ... 操作。

标签: mysql sql query-optimization amazon-rds sql-optimization


【解决方案1】:

我会创建以下索引(b-tree 索引):

analytics(user_id, source, id) 
transactions(analytics, status)

这与戈登的建议不同。

索引中列的顺序很重要。

您按特定的analytics.user_id 进行过滤,因此该字段必须是索引中的第一个。 然后按analytics.source 分组。为避免按source 排序,这应该是索引的下一个字段。您还引用了analytics.id,因此最好将此字段作为索引的一部分,放在最后。 MySQL 是否能够仅读取索引而不接触表?我不知道,但它很容易测试。

transactions 上的索引必须以 analytics 开头,因为它将在 JOIN 中使用。我们还需要status

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM analytics
LEFT JOIN transactions ON analytics.id = transactions.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 

【讨论】:

  • 我很想看看这个建议带来了哪些性能提升 - 这似乎是一个很棒的建议。
  • @DavidCa1226,是的,我也很好奇。通常,适当的索引是调整查询的最有效方法。只有在您确认覆盖索引没有按预期提供帮助后,您才开始查看执行计划详细信息并尝试调整查询,以便优化器按预期使用索引。
【解决方案2】:

先分析一下……

SELECT  a.source AS referrer,
        COUNT(*) AS frequency,  -- See question below
        SUM(t.status = 'COMPLETED') AS sales
    FROM  analytics AS a
    LEFT JOIN  transactions AS t  ON a.id = t.analytics AS a
    WHERE  a.user_id = 52094
    GROUP BY  a.source
    ORDER BY  frequency DESC
    LIMIT  10 

如果从at 的映射是“一对多”,那么您需要考虑COUNTSUM 是否具有正确的值或膨胀的值。正如查询所代表的那样,它们是“膨胀的”。 JOIN 出现在聚合之前,因此您正在计算事务的数量以及已完成的事务数量。我会假设这是需要的。

注意:通常的模式是COUNT(*);说COUNT(x) 意味着检查x 是否为NULL。我怀疑不需要检查?

此索引处理WHERE 并且正在“覆盖”:

 analytics:  INDEX(user_id, source, id)   -- user_id first

 transactions:  INDEX(analytics, status)  -- in this order

GROUP BY 可能需要也可能不需要“排序”。 ORDER BYGROUP BY 不同,肯定需要排序。并且需要对整个分组的行集进行排序; LIMIT 没有快捷方式。

通常,汇总表是面向日期的。也就是说,PRIMARY KEY 包括“日期”和其他一些维度。也许,按日期和 user_id 键入会有意义吗?普通用户每天有多少笔交易?如果至少有 10 个,那么让我们考虑一个汇总表。此外,重要的是不要成为 UPDATEingDELETEing 旧记录。 More

我可能会有

user_id ...,
source ...,
dy DATE ...,
status ...,
freq      MEDIUMINT UNSIGNED NOT NULL,
status_ct MEDIUMINT UNSIGNED NOT NULL,
PRIMARY KEY(user_id, status, source, dy)

那么查询就变成了

SELECT  source AS referrer,
        SUM(freq) AS frequency,
        SUM(status_ct) AS completed_sales
    FROM  Summary
    WHERE  user_id = 52094
      AND  status = 'COMPLETED'
    GROUP BY source
    ORDER BY  frequency DESC
    LIMIT  10 

速度来自很多因素

  • 更小的表(查看的行数更少)
  • 没有JOIN
  • 更实用的索引

(它仍然需要额外的排序。)

即使没有汇总表,也可能有一些加速...

  • 桌子有多大? `innodb_buffer_pool_size 有多大?
  • Normalizing 一些既庞大又重复的字符串可能会使该表不受 I/O 限制。
  • 这太糟糕了:KEY (transaction_id(191));请参阅here 了解 5 种修复方法。
  • IP 地址不需要 255 字节,utf8mb4_unicode_ci 也不需要。 (39) 和 ascii 就足够了。

【讨论】:

  • 感谢 Rick 的有用分析!我将逐个检查,看看我可以实施哪些而不用担心太多。
  • @Abs - 并可能进行实验。很容易复制一张桌子来玩:CREATE TABLE copy LIKE live; INSERT INTO copy SELECT * FROM live;
【解决方案3】:

对于这个查询:

SELECT a.source AS referrer, 
       COUNT(*) AS frequency, 
       SUM( t.status = 'COMPLETED' ) AS sales
FROM analytics a LEFT JOIN
     transactions t
     ON a.id = t.analytics
WHERE a.user_id = 52094 
GROUP BY a.source 
ORDER BY frequency DESC 
LIMIT 10 ;

您想要在analytics(user_id, id, source)transactions(analytics, status) 上建立索引。

【讨论】:

  • 我应该提到我有索引但没有复合索引,以上是复合索引对吗?现在运行它,可能需要一些时间才能将这些应用于大型测试表。
  • 我添加了索引,不幸的是这并没有太大的区别,它仍然需要 70 秒才能执行。
  • 176200 匹配分析表上的user_id
  • 70 秒似乎很长,但group by 需要一些时间。
  • @Abs 确保您添加了 Gordon 建议的索引。假设您在问题中列出的索引 *_gordon 是您尝试过的索引,您似乎添加了错误的列 - analytics(id, user_id, source) 而不是 analytics(user_id, id, source)transactions(status, analytics) 而不是 transactions(analytics, status)。跨度>
【解决方案4】:

试试下面,如果这有帮助,请告诉我。

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM (SELECT * FROM analytics where user_id = 52094) analytics
LEFT JOIN (SELECT analytics, status from transactions where analytics = 52094) transactions ON analytics.id = transactions.analytics
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10

【讨论】:

  • 太棒了! 7%的性能提升!我很抱歉我不能做更多。我将更新我的原始帖子,以包含“解决方案”,否则汇总表不是一个坏主意,我不得不多次这样做。
  • 完成,更改原帖,欢迎点赞。
  • @VincentRye - 将 JOIN 更改为子查询而不进行任何语义更改将不会在 MySQL 中加快速度。
  • @Rick James - 显然它是 id。子查询中有一个 where 语句。它也可以在没有子查询的情况下编写,并在连接本身上添加一个额外的 where。两者都应该可以正常工作。
  • @VincentRye,您的查询产生的结果与原始查询完全不同。为什么用analytics = 52094 过滤transactions 表?原始过滤器位于analytics.user_idtransactions.analyticsanalytics.user_id 没有任何关系,但您使用相同的值对其进行过滤。 transactions.analyticsanalytics.id 相关,而不是 analytics.user_id
【解决方案5】:

你能试试下面的方法吗:

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(sales) AS sales
FROM analytics
LEFT JOIN(
	SELECT transactions.Analytics, (CASE WHEN transactions.status = 'COMPLETED' THEN 1 ELSE 0 END) AS sales
	FROM analytics INNER JOIN transactions ON analytics.id = transactions.analytics
) Tra
ON analytics.id = Tra.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 

【讨论】:

    【解决方案6】:

    此查询可能将数百万条 analytics 记录与 transactions 记录连接起来,并计算数百万条记录的总和(包括状态检查)。 如果我们可以先应用LIMIT 10,然后进行连接并计算总和,我们可以加快查询速度。 不幸的是,我们需要analytics.id 进行连接,在应用GROUP BY 后它会丢失。但也许analytics.source 的选择性足以提升查询。

    因此,我的想法是计算频率,由它们限制,在子查询中返回 analytics.sourcefrequency,并使用此结果过滤主查询中的 analytics,然后由其完成其余的在希望大大减少的记录数量上进行连接和计算。

    最小子查询(注:不加入,不求和,返回10条记录):

    SELECT
        source,
        COUNT(id) AS frequency
    FROM analytics
    WHERE user_id = 52094
    GROUP BY source
    ORDER BY frequency DESC 
    LIMIT 10
    

    使用上述查询作为子查询的完整查询x

    SELECT
        x.source AS referrer,
        x.frequency,
        SUM(IF(t.status = 'COMPLETED', 1, 0)) AS sales
    FROM
        (<subquery here>) x
        INNER JOIN analytics a
           ON x.source = a.source  -- This reduces the number of records
        LEFT JOIN transactions t
           ON a.id = t.analytics
    WHERE a.user_id = 52094      -- We could have several users per source
    GROUP BY x.source, x.frequency
    ORDER BY x.frequency DESC
    

    如果这没有产生预期的性能提升,这可能是由于 MySQL 以意外的顺序应用连接。正如此处"Is there a way to force MySQL execution order?" 所述,在这种情况下,您可以将连接替换为STRAIGHT_JOIN

    【讨论】:

    • @Abs:我建议的更改是否加快了您的查询速度?
    【解决方案7】:

    我在您的查询中发现的唯一问题是

    GROUP BY analytics.source 
    ORDER BY frequency DESC 
    

    因为这个查询正在使用临时表进行文件排序。

    避免这种情况的一种方法是创建另一个表格,例如

    CREATE TABLE `analytics_aggr` (
      `source` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
      `frequency` int(10) DEFAULT NULL,
      `sales` int(10) DEFAULT NULL,
      KEY `sales` (`sales`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`
    

    使用以下查询将数据插入 analytics_aggr

    insert into analytics_aggr SELECT 
        analytics.source AS referrer, 
        COUNT(analytics.id) AS frequency, 
        SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
        FROM analytics
        LEFT JOIN transactions ON analytics.id = transactions.analytics
        WHERE analytics.user_id = 52094 
        GROUP BY analytics.source 
        ORDER BY null 
    

    现在您可以使用

    轻松获取数据
    select * from analytics_aggr order by sales desc
    

    【讨论】:

      【解决方案8】:

      试试这个

      SELECT 
          a.source AS referrer, 
          COUNT(a.id) AS frequency, 
          SUM(t.sales) AS sales
      FROM (Select id, source From analytics Where user_id = 52094) a
      LEFT JOIN (Select analytics, case when status = 'COMPLETED' Then 1 else 0 end as sales 
                 From transactions) t ON a.id = t.analytics
      GROUP BY a.source 
      ORDER BY frequency DESC 
      LIMIT 10 
      

      我提出这个是因为你说“它们是巨大的表”,但是这个 sql 只使用很少的列。在这种情况下,如果我们只使用带有 require 列的内联视图,那就很好了

      注意:记忆在这里也将发挥重要作用。所以在决定内联视图之前确认内存

      【讨论】:

        【解决方案9】:

        我会尝试将查询与两个表分开。由于您只需要前 10 个sources,我会先获取它们,然后从transactionssales 列查询:

        SELECT  source as referrer
                ,frequency
                ,(select count(*) 
                  from   transactions t  
                  where  t.analytics in (select distinct id 
                                         from   analytics 
                                         where  user_id = 52094
                                                and source = by_frequency.source) 
                         and status = 'completed'
                 ) as sales
        from    (SELECT analytics.source
                        ,count(*) as frequency
                from    analytics 
                where   analytics.user_id = 52094
                group by analytics.source
                order by frequency desc
                limit 10
                ) by_frequency
        

        没有distinct 也可能更快

        【讨论】:

          【解决方案10】:

          我会尝试子查询:

          SELECT a.source AS referrer, 
                 COUNT(*) AS frequency,
                 SUM((SELECT COUNT(*) FROM transactions t 
                  WHERE a.id = t.analytics AND t.status = 'COMPLETED')) AS sales
          FROM analytics a
          WHERE a.user_id = 52094 
          GROUP BY a.source
          ORDER BY frequency DESC 
          LIMIT 10; 
          

          Plus 索引与@Gordon 的回答完全相同:analytics(user_id, id, source) 和 transactions(analytics, status)。

          【讨论】:

          • 我不确定这个查询是否会运行。您在 select 中使用了 analytics.id,但您没有按它分组。
          • @AlexandrKapshuk 当然会运行:db-fiddle.com/f/qzk3NqwaYDHENvQpp4bNat/0。但如果你想严格一点,我应该用 MIN 包装子查询
          • 如果analytics 表中有多个不同的id 和相同的source,则查询不会总结所有id。我认为可以通过使用sum 而不是min 来纠正它。不知道你可以在 MySQL 中做到这一点!
          • 对于每个analytics 结果行,选择子查询将执行一次。这对于user_id 的结果可能看起来很快,但对于结果行很多的值来说会很慢。
          【解决方案11】:

          我假设谓词 user_id = 52094 用于说明目的,并且在应用程序中,选定的 user_id 是一个变量。

          我还假设 ACID 属性在这里不是很重要。

          (1) 因此,我将使用实用程序表维护两个仅包含必要字段的副本表(类似于 Vladimir 上面建议的索引)。

          CREATE TABLE mv_anal (
            `id` int(11) NOT NULL,
            `user_id` int(11) DEFAULT NULL,
            `source` varchar(45),
            PRIMARY KEY (`id`)
          );
          
          CREATE TABLE mv_trans (
            `id` int(11) NOT NULL,
            `status` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
            `analytics` int(11) DEFAULT NULL,
            PRIMARY KEY (`id`)
          );
          
          CREATE TABLE util (
            last_updated_anal int (11) NOT NULL,
            last_updated_trans int (11) NOT NULL
          );
          
          INSERT INTO util (0, 0);
          

          这里的好处是我们将读取原始表的相对较小的投影——希望操作系统级别和数据库级别的缓存能够正常工作,并且它们不是从较慢的二级存储中读取,而是从较快的 RAM 中读取。 这可能是一个非常大的收获。

          以下是我更新这两个表的方法(以下是由 cron 运行的事务):

          -- TRANSACTION STARTS -- 
          
          INSERT INTO mv_trans 
          SELECT id, IF (status = 'COMPLETE', 1, 0) AS status, analysis 
          FROM transactions JOIN util
          ON util.last_updated_trans <= transactions.id
          
          UPDATE util
          SET last_updated_trans = sub.m
          FROM (SELECT MAX (id) AS m FROM mv_trans) sub;
          
          -- TRANSACTION COMMITS -- 
          
          -- similar transaction for mv_anal.
          

          (2) 现在,我将处理选择性以减少顺序扫描时间。我将不得不在 mv_anal 上的 user_id、source 和 id(按此顺序)上建立一个 b-tree 索引。

          注意:以上可以通过在分析表上创建索引来实现,但是构建这样的索引需要读取具有 60M 行的大表。我的方法要求索引构建只读取非常薄的表。因此,我们可以更频繁地重建 btree(以解决偏斜问题,因为表是仅追加的)。

          这是我确保在查询时实现高选择性并解决倾斜 btree 问题的方法。

          (3) 在 PostgreSQL 中,WITH 子查询总是被物化的。我希望 MySQL 也是如此。因此,作为优化的最后一公里:

          WITH sub_anal AS (
            SELECT user_id, source AS referrer, COUNT (id) AS frequency
            FROM mv_anal
            WHERE user_id = 52094
            GROUP BY user_id, source
            ORDER BY COUNT (id) DESC
            LIMIT 10
          )
          SELECT sa.referrer, sa.frequency, SUM (status) AS sales
          FROM sub_anal AS sa 
          JOIN mv_anal anal 
          ON sa.referrer = anal.source AND sa.user_id = anal.user_id
          JOIN mv_trans AS trans
          ON anal.id = trans.analytics
          

          【讨论】:

            【解决方案12】:

            聚会迟到了。我认为您需要将一个索引加载到 MySQL 的缓存中。 NLJ 可能正在扼杀性能。这是我的看法:

            路径

            您的查询很简单。它有两个表,“路径”很清楚:

            • 优化器应该首先计划读取analytics 表。
            • 优化器应该计划第二次读取transactions 表。这是因为您使用的是LEFT OUTER JOIN。关于这一点没有太多讨论。
            • 此外,analytics 表有 6000 万行,最佳路径应尽快过滤此表上的行。

            访问权限

            一旦路径清晰,您需要决定是要使用索引访问还是表访问。两者都有优点和缺点。但是,您想提高SELECT 的性能:

            • 您应该选择索引访问。
            • 避免混合访问。因此,您应该不惜一切代价避免任何表访问(获取)。翻译:将所有参与的列放在索引中。

            过滤

            同样,您希望SELECT 具有高性能。因此:

            • 您应该在索引级别执行过滤,而不是在表级别。

            行聚合

            过滤后,下一步是按GROUP BY analytics.source 聚合行。这可以通过将source 列作为索引中的第一列来改进。

            路径、访问、过滤和聚合的最佳索引

            考虑到上述所有因素,您应该将所有提到的列都包含在索引中。以下指标应该可以提高响应时间:

            create index ix1_analytics on analytics (user_id, source, id);
            
            create index ix2_transactions on transactions (analytics, status);
            

            这些索引满足上述“路径”、“访问”和“过滤”策略。

            索引缓存

            最后——这很关键——将二级索引加载到 MySQL 的内存缓存中。 MySQL 正在执行 NLJ(嵌套循环连接)——MySQL 术语中的“引用”——并且需要随机访问第二个连接近 20 万次。

            不幸的是,我不确定如何将索引加载到 MySQL 的缓存中。 FORCE 的使用可能会起作用,如下所示:

            SELECT 
                analytics.source AS referrer, 
                COUNT(analytics.id) AS frequency, 
                SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
            FROM analytics
            LEFT JOIN transactions FORCE index (ix2_transactions)
              ON analytics.id = transactions.analytics
            WHERE analytics.user_id = 52094 
            GROUP BY analytics.source 
            ORDER BY frequency DESC 
            LIMIT 10
            

            确保您有足够的缓存空间。这里有一个简短的问题/答案来弄清楚:How to figure out if mysql index fits entirely in memory

            祝你好运!哦,然后发布结果。

            【讨论】:

            • "LOAD INDEX INTO CACHE 仅用于MyISAM 表" - dev.mysql.com/doc/refman/8.0/en/load-index.html
            • 你说得对,这适用于 MyISAM。但是,将二级索引ix2_transactions 加载到缓存中仍然很重要。 FORCE 在这里可能会有所帮助,但我不确定。加载不出来,看不出来怎么提高速度。
            【解决方案13】:

            这个问题肯定受到了很多关注,所以我相信所有明显的解决方案都已经尝试过了。不过,我没有在查询中看到解决 LEFT JOIN 的内容。

            我注意到LEFT JOIN 语句通常会强制查询规划器进入哈希连接,这对于少量结果来说很快,但对于大量结果来说非常慢。正如@Rick James 的回答中所指出的,由于原始查询中的连接是在身份字段analytics.id 上,这将产生大量结果。哈希连接会产生糟糕的性能结果。下面的建议在没有任何架构或处理更改的情况下解决了这个问题。

            由于聚合是由analytics.source 进行的,我会尝试一个查询,该查询为按来源的频率和按来源的销售创建单独的聚合,并将左连接推迟到聚合完成后。这应该允许最好地使用索引(通常这是大型数据集的合并连接)。

            这是我的建议:

            SELECT t1.source AS referrer, t1.frequency, t2.sales
            FROM (
              -- Frequency by source
              SELECT a.source, COUNT(a.id) AS frequency
              FROM analytics a
              WHERE a.user_id=52094
              GROUP BY a.source
            ) t1
            LEFT JOIN (
              -- Sales by source
              SELECT a.source,
                SUM(IF(t.status = 'COMPLETED', 1, 0)) AS sales
              FROM analytics a
              JOIN transactions t
              WHERE a.id = t.analytics
                AND t.status = 'COMPLETED'
                AND a.user_id=52094
              GROUP by a.source
            ) t2
              ON t1.source = t2.source
            ORDER BY frequency DESC 
            LIMIT 10 
            

            希望这会有所帮助。

            【讨论】:

            • 哈希连接在 MySQL 中不存在。 (除了一个例外,在 MySQL 中不存在哈希索引。)
            • @RickJames - 正确的 MySQL 术语可能是 Nested Loop joins
            • 感谢您指出这一点。实际上(据我了解)NLJ(没有缓冲)使用 BTree,并且从第一天起就存在于 MySQL 中。BNL(块 NLJ)使用“连接缓冲区”来加载将被连接到的所有数据;然后使用(大概)一个专门构建的哈希索引。 InnoDB 没有永久的哈希索引。
            • 我的大部分低级查询优化 exp 都是使用 MS SQL,但核心 DB 原则是通用的。 MySQL 连接类型在sql_select.h 中。查询引擎执行内部/外部联接有多种方式,但据我了解,两种主要类型是查找/循环联接,当表之间的行数非常不同(即 1:100)和合并时,它们可以很好地工作联接在联接之前对行进行排序,这对于两个高基数、高行数的表来说效果很好。后者更适合多行(本例)。
            猜你喜欢
            • 2023-01-05
            • 1970-01-01
            • 1970-01-01
            • 2015-06-27
            • 2010-11-15
            • 1970-01-01
            • 2012-04-20
            相关资源
            最近更新 更多