【问题标题】:How to optmize query with subqueries,WHERE IN and varchar comparison fields?如何使用子查询、WHERE IN 和 varchar 比较字段优化查询?
【发布时间】:2015-01-01 09:02:18
【问题描述】:

我正在开展一个抓取项目,以在不同的计划中抓取项目及其查看次数。计划是脚本打算运行的用户定义的时间段(日期)。

表结构如下:

CREATE TABLE IF NOT EXISTS `stats` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `schedule_id` smallint(11) NOT NULL,
  `type` smallint(11) NOT NULL,
  `name` varchar(250) COLLATE utf8_unicode_ci NOT NULL,
  `views` int(11) NOT NULL,
  `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ;

所有数据都存储在表 stats 中,稍后将对其进行分析以查看视图中的类型增长。

数据会是这样的:

样本集

抓取是分时段完成的,对于每个计划,预计会有大约 20K 条目。计划可以每天或每周进行,因此数据将在 5-6 年增长到大约 2-3 百万个月。

在这些数据上,我需要执行查询以汇总选定范围内的相同名称。

例如:

我需要汇总多个日程表中的相同项目(名称)。如果选择了时间表 1 和 2,则只会选择同时属于这两个时间表的项目。所以这里将是 ItemAItemB

应在此处计算按类型计算的视图总和。

因此对于时间表 1:(已更新)

SELECT COUNT(t.`type`) AS count, SUM(t.views) AS view_count 
FROM `stats` t 
INNER JOIN 
( 
   SELECT name,COUNT(name) as c FROM `stats` WHERE `schedule_id` IN (1,2) GROUP BY name HAVING c=2
) t2 ON t2.`name` = t.`name` 
WHERE `schedule_id`=2 GROUP BY type

这是我的预期结果。

但是我读过使用子查询,在哪里,varchar 比较字段无助于优化查询。如何优化这以获得更好的性能。

Same Type Aggregator 的规则如下:

1.在一个schedule id下,可以有同名不同类型值。schedule_id、name和type的组合不会重复。

2.Type wise aggregator - 对每种类型下的值进行求和。

我正在 Python 中进行项目 - MySQL 用于抓取目的,PHP 用于列出结果。我想知道如何正确组织此表以及查询以获得更好的性能。 请指教。

【问题讨论】:

  • 对于初学者,我会创建一个包含 id 和相应名称的字典表,例如 itemA,并且只将 id 存储在主表中。这肯定会加快查询速度,如果您想检索名称,您可以随时通过 id 加入 dict 表。
  • @ConsiderMe 像 ItemA 这样的名称只是一个虚拟值。在实际情况下,将其放入字典表并不是一种平滑的方法。如果 varchar 产生问题,是否建议将 varchar 名称字符串转换为整数。
  • 与性能无关,但您的子查询正在尝试查找 schedule_id 为 1 和 2 的记录。但是它也会匹配任何 schedule_id 为 1 的记录两次。如果这在逻辑上不可能发生,那么您可以轻松地在没有子查询的情况下重写它。

标签: mysql sql subquery query-optimization


【解决方案1】:

VARCHAR 列

正如评论中所说,将 varchars 存储在字典表中是一个好习惯。为什么?它们比 int4 需要更多的空间,因此越来越大的表只会占用更多空间,而每个名称可以在另一个表中存储一次。

查询效果

WHERE IN 实际上意味着规划器确实将schedule_id 与转换为integer[] 类型的ANY'{1,2}' 进行比较,您可以在下面注意到。

子查询

如果您需要聚合数据,有时您无法避免子查询。考虑到这一点,请记住并非所有查询都包含 1 个SELECT 语句。实际上,它们很少这样做(除非您的应用程序只有一小部分与数据库连接,例如简单的游戏,您只需要存储包含用户和积分的信息)

查询

您对给定样本数据的查询计划:

select count(type), sum(views) from tmp_test8 a join (select name,count(1) from tmp_test8 where schedule_id in (1,2) group by 1 having count(1) = 2) b
on a.name = b.name where schedule_id = 1;

                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Aggregate  (cost=23.59..23.60 rows=1 width=8)
   ->  Nested Loop  (cost=11.77..23.59 rows=1 width=8)
         Join Filter: ((a.name)::text = (tmp_test8.name)::text)
         ->  Seq Scan on tmp_test8 a  (cost=0.00..11.75 rows=1 width=524)
               Filter: (schedule_id = 1)
         ->  HashAggregate  (cost=11.77..11.79 rows=2 width=516)
               Filter: (count(1) = 2)
               ->  Seq Scan on tmp_test8  (cost=0.00..11.75 rows=2 width=516)
                     Filter: (schedule_id = ANY ('{1,2}'::integer[]))

不过,您的查询可以在没有连接的情况下重写,因此它只会扫描表一次。这是我的建议:

select count, sum(view_count) 
from( 
    select name, count(1) count, sum(case when schedule_id = 1 then views end) view_count 
    from tmp_test8 
    where schedule_id in (1,2) 
    group by 1 
    having count(1) = 2 
    ) foo 
group by 1
                               QUERY PLAN                               
------------------------------------------------------------------------
 HashAggregate  (cost=11.83..11.85 rows=2 width=16)
   ->  HashAggregate  (cost=11.78..11.80 rows=2 width=524)
         Filter: (count(1) = 2)
         ->  Seq Scan on tmp_test8  (cost=0.00..11.75 rows=2 width=524)
               Filter: (schedule_id = ANY ('{1,2}'::integer[]))

两个查询会产生相同的结果。

【讨论】:

  • 谢谢。但是在您的第二个查询中,有一个带有案例检查的子查询。我相信使用更大的数据集,它的获取速度会更慢。
  • @Surabhil 对不起,但是提供更大数据集的每个查询都会越来越慢。这很明显。您可以运行一个测试用例,但无论您的数据样本包含多少行,查询计划将始终*(除非您更改查询之外的某些内容,例如索引)保持不变。
  • 是的,当然,我可以用更大的数据理解事物。但我认为我们应该从我们的角度处理所有基本优化。
【解决方案2】:

关于我的评论。

如果一个名称可以有重复的计划 ID,那么您当前的查询将不起作用。最简单的解决方法是将其从 COUNT(name) 更改为 COUNT(DISTINCT schedule_id)

如果对于一个名称,那么 schedule_id 是唯一的,那么您可以通过为每个计划 id 加入一次统计信息来避免子查询:-

SELECT COUNT(t.`type`) AS count, SUM(t.views) AS view_count 
FROM `stats` t 
INNER JOIN stats t1 ON t.name = t1.name AND t1.schedule_id = 1
INNER JOIN stats t2 ON t.name = t2.name AND t2.schedule_id = 2
WHERE t.schedule_id = 1 

这是对您的数据的一些假设。

虽然有时需要子查询,但 MySQL 不会使用子查询结果的索引来将其与主表连接。

【讨论】:

  • 我假设 name 不会有与 schedule_id 相同的重复条目。您可以通过架构看到,views 确实会更新,updated_time 确实会在完成后更改。
  • @ConsiderMe - 这是我的希望,但不确定。如果 name 不能有相同 schedule_id 的重复条目,那么它可以很容易地避免子查询(此时覆盖 name 和 schedule_id 的索引可能会有很大帮助 - 或者更好,因为您建议拆分名称并使用它的 id加入)
  • @Kickstart 我已经更新了这个问题。关于将 //COUNT(name) 更改为 COUNT(DISTINCT schedule_id)// 在 COUNT 下使用 DISTINCT 是一个好习惯吗?在我的情况下,一个时间表下可能有多个不同类型的名称字段。
  • COUNT(name) 和 COUNT(DISTINCT name) 做两件不同的事情,不可互换。如果需要,请使用 DISTINCT。在您的情况下,您可以使用相同的时间表多次使用相同的名称吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多