如何优化此 MySQL 查询?数百万行

How to optimise this MySQL query? Millions of Rows

我有以下查询:

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 

分析 table 有 6000 万行,交易 table 有 300 万行。

当我在此查询中 运行 一个 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'    |  ''      |  ''            |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+

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

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;

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

对于此查询:

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) 上的索引。

试试下面的方法,如果有帮助请告诉我。

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

我会尝试子查询:

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) and transactions(analytics, status)。

我将创建以下索引(b 树索引):

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

这与戈登的建议不同。

索引中列的顺序很重要。

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

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 

先分析一下...

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的映射是"one-to-many",那么你需要考虑COUNTSUM是否有正确的值或者夸大的价值观。就查询而言,它们是 "inflated"。 JOIN 发生在 聚合之前 ,因此您计算的是交易数量和已完成的交易数量。我假设这是需要的。

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

该索引处理 WHERE 并且是 "covering":

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

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

GROUP BY 可能需要也可能不需要 'sort'。 ORDER BYGROUP BY 不同,肯定需要排序。并且需要对整个分组的行集进行排序; LIMIT.

没有捷径

通常,摘要 table 是面向日期的。也就是说,PRIMARY KEY 包括一个 'date' 和一些其他维度。也许,按日期和 user_id 键入有意义吗?一般用户每天有多少笔交易?如果至少有 10 个,那么让我们考虑一个摘要 table。此外,重要的是不要成为 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 

速度来自很多因素

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

(还需要额外的排序。)

即使没有摘要 table,也可能会有一些加速...

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

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

GROUP BY analytics.source 
ORDER BY frequency DESC 

因为此查询正在使用临时 table.

进行文件排序

避免这种情况的一种方法是创建另一个 table 类似

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

你能试试下面的方法吗:

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 

试试这个

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 

我提出这个建议是因为你说 "they are massive table" 但这个 sql 只使用了很少的列。在这种情况下,如果我们只使用带有 require 列的内联视图,那么它会很好

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

我会尝试将查询与两个表分开。由于您只需要前 10 个 source,我会先获取它们,然后从 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

也可能会更快

我假设谓词 user_id = 52094 是为了说明目的,在应用中,选定的 user_id 是一个变量。

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

(1) 因此,我将使用实用程序 table 维护两个仅包含必要字段的副本 table(它类似于 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);

这里的收获是我们将读取原始 tables 的相对较小的投影 -- 希望 OS 级和数据库级缓存工作并且它们不会从较慢的位置读取辅助存储,但来自更快的 RAM。 这可以是一个很大的收获。

这是我更新两个 table 的方式(下面是 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) 现在,我将解决选择性问题以减少顺序扫描时间。我将不得不在 user_id 上构建一个 b 树索引,在 mv_anal 上构建源和 ID(按此顺序)。

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

这就是我确保在查询 时实现高选择性并解决倾斜 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

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

路径

您的问题很简单。它有两个 table 并且 "path" 非常清楚:

  • 优化器应计划首先读取 analytics table。
  • 优化器应该计划读取 transactions table 秒。这是因为您使用的是 LEFT OUTER JOIN。这个就不多讨论了。
  • 此外,analytics table 是6000万行,最好的路径应该在这一行上尽快过滤行。

访问

路径清晰后,您需要决定是使用索引访问还是 Table 访问。两者各有利弊。但是,您想提高 SELECT 性能:

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

过滤

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

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

行聚合

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

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

考虑到以上所有情况,您应该将所有提到的列都包含到索引中。以下指标应该会提高响应时间:

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

create index ix2_transactions on transactions (analytics, status);

这些索引实现了上述 "path"、"access" 和 "filtering" 策略。

索引缓存

最后——这很关键——将二级索引加载到MySQL的内存缓存中。 MySQL 正在执行 NLJ(嵌套循环连接)——MySQL 术语中的 'ref'——并且需要随机访问第二个近 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

确保你有足够的缓存 space。这是一个简短的 question/answer 来弄清楚:How to figure out if mysql index fits entirely in memory

祝你好运!哦,还有 post 结果。

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

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

最小子查询(注:无join,无sum,returns 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

这个问题肯定受到了很多关注,所以我确信所有明显的解决方案都已尝试过。不过,我没有在查询中看到解决 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 

希望对您有所帮助。