如何优化这个MySQL查询?数百万行数据

30

我有以下查询:

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)上运行。


3
你的声望分数很高,因此你不是新手。你现在应该知道,在查询中应包括每个表的“SHOW CREATE TABLE”,以便我们可以看到表中的数据类型、索引和约束。帮助我们帮助你! - Bill Karwin
抱歉,比尔,它们是巨大的表格(有很多列)。在尝试戈登的建议之后会处理这个问题。 - Abs
2
我建议使用SHOW CREATE TABLE的原因是,如果有人想在沙盒实例上尝试您的表格,他们必须费力地猜测您的列和索引来重新创建表格。从您的SHOW INDEXES中可以拼凑出类似于您的表格的东西,但这需要太多的工作,而且我不能确定它是否正确。我不会花时间去做那些事情。祝你好运! - Bill Karwin
2
如果省略 GROUP BY 子句,查询性能会发生什么变化?(我知道这样做不会得到想要的结果;关键是要弄清楚 GROUP BY ... LIMIT... 是否占用了大量时间。) - O. Jones
1
你能稍微解释一下你想要什么吗?在进行a LEFT JOIN b查询时,COUNT(a.id)有点奇怪。它计算来自b的匹配行数,并对没有在b中找到匹配行的每一行a计数为1。这是你想要的吗?对我而言,这听起来像是难以向用户解释清楚的事情。在那个COUNT操作中完美无缺非常重要,因为之后你会用它进行GROUP BY ... LIMIT ...操作。 - O. Jones
显示剩余12条评论
13个回答

11

我会创建以下索引(B-Tree 索引):

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

这与Gordon的建议不同。

索引中列的顺序很重要。

您按特定的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 

1
我很想看看这个建议能带来多少性能提升 - 这似乎是一个很好的建议。 - Cowthulhu
1
@DavidCa1226,是的,我也很好奇。通常适当的索引是调整查询性能最有效的方法。只有在确认覆盖索引无法如预期般帮助后,才开始查看执行计划细节并尝试调整查询,以便优化器按预期使用索引。 - Vladimir Baranov

7
首先进行一些分析...
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 是否具有正确的值或膨胀的值。就查询而言,它们是"膨胀的"。连接发生在聚合之前,因此您正在计算交易数量以及完成了多少次交易。我假设这是所需的。
注意: 通常的模式是 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笔,那么让我们考虑一个汇总表。此外,重要的是不要更新或删除旧记录。了解更多

我可能会有:

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`有多大?
  • 对一些既臃肿又重复的字符串进行规范化处理,可以使该表格不受I/O限制。
  • 这很糟糕:KEY (transaction_id(191));请参见此处了解修复此问题的5种方法。
  • IP地址不需要255字节,也不需要utf8mb4_unicode_ci。(39)和ascii就足够了。

谢谢你提供有用的分析,Rick!我会逐一查看并决定哪些可以实现而不必太担心。 - Abs
1
@Abs - 可能需要进行实验。复制一个表格以便进行操作非常容易:CREATE TABLE copy LIKE live; INSERT INTO copy SELECT * FROM live; - Rick James

6

对于这个查询:

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

我应该提到我有索引但没有复合索引,上面的是复合索引对吗?现在正在运行,将这些应用于大型测试表可能需要一些时间。 - Abs
1
我添加了索引,但是不幸的是这并没有什么太大的区别,它仍然需要70秒才能执行。 - Abs
70秒看起来很长,但group by需要一些时间。 - Gordon Linoff
@Abs - 请提供 (1) EXPLAIN SELECT ... 和 (2) 更新后的 SHOW CREATE TABLE。那些索引应该有所帮助。 - Rick James
3
请确保您添加了Gordon提出的索引的确切顺序。假设您在问题中列出的索引 *_gordon 是您尝试的那些,您似乎添加了错误的列顺序-应该是 analytics(user_id, id, source) 而不是 analytics(id, user_id, source) 以及 transactions(analytics, status) 而不是 transactions(status, analytics) - Solarflare
显示剩余2条评论

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

2
太好了!性能提升了7%!很抱歉我不能做更多。我将更新我的原始帖子,包括“解决方案”,否则摘要表格也不是一个坏主意,我已经这样做了好几次。 - Vincent Rye
1
完成,已更改原帖,随意点赞。 - Vincent Rye
3
在MySQL中,将一个JOIN转换为子查询而不改变语义不会加快查询速度。 - Rick James
2
@Rick James - 显然是这样的。子查询中有一个where语句。它也可以不使用子查询,并在连接本身上添加额外的where。任何一种方法都应该可以正常工作。 - Vincent Rye
1
@VincentRye,您的查询产生了与原始查询完全不同的结果。为什么要使用analytics = 52094过滤transactions表?原始筛选条件是基于analytics.user_idtransactions.analyticsanalytics.user_id没有任何关联,但是您却使用相同的值进行了筛选。transactions.analyticsanalytics.id有关,而不是analytics.user_id - Vladimir Baranov
显示剩余7条评论

3

您可以尝试以下方法:

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 


3
这个查询可能会将数百万条analytics记录与transactions记录联接起来,并在数百万条记录上计算总和(包括状态检查)。 如果我们可以先应用LIMIT 10,然后再进行联接和计算总和,就可以加快查询速度。 不幸的是,在应用GROUP BY之后,我们需要使用analytics.id进行联接,但它会丢失。但也许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:我的建议修改是否加速了你的查询? - Olivier Jacot-Descombes

2

我建议尝试使用子查询:

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; 

与@Gordon的回答一样,加索引的方式是:analytics(user_id,id,source)和transactions(analytics,status)。


我不确定这个查询是否能够运行。你在select语句中使用了analytics.id,但是你没有按照它进行分组。 - Alexandr Kapshuk
@AlexandrKapshuk 当然可以运行:https://www.db-fiddle.com/f/qzk3NqwaYDHENvQpp4bNat/0。但是如果我们想要严格一些,我应该用 MIN 包装子查询。 - Lukasz Szozda
1
如果在“analytics”表中有几个具有相同“source”的不同“id”,则查询不会对所有“id”求和。我认为可以通过使用“sum”而不是“min”来进行更正。没想到MySQL也可以这样做! - Alexandr Kapshuk
选择子查询将为每个“分析”结果行执行一次。对于具有少量结果的“user_id”值,这可能看起来很快,但对于存在许多结果行的值而言,速度会很慢。 - saarp

2

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

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

2

试试这个

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只使用了很少的列。在这种情况下,如果我们只使用必要的列来使用内联视图,那么效果会更好。
注意:内存在这里也将起到重要作用。因此,在决定使用内联视图之前,请确认内存。

2

我建议将查询操作从两个表中分离出来。由于你仅需要前十个source,所以我会先获取它们,然后从transactions表中查询sales列:

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,可能会更快。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接