我有一个在我们的报告系统中使用的查询,有时运行速度超过一秒,其他时间需要1到10分钟才能运行。
以下是慢查询日志中的条目:
# Query_time: 543 Lock_time: 0 Rows_sent: 0 Rows_examined: 124948974
use statsdb;
SELECT count(distinct Visits.visitorid) as 'uniques'
FROM Visits,Visitors
WHERE Visits.visitorid=Visitors.visitorid
and candidateid in (32)
and visittime>=1275721200 and visittime<=1275807599
and (omit=0 or omit>=1275807599)
AND Visitors.segmentid=9
AND Visits.visitorid NOT IN
(SELECT Visits.visitorid
FROM Visits,Visitors
WHERE Visits.visitorid=Visitors.visitorid
and candidateid in (32)
and visittime<1275721200
and (omit=0 or omit>=1275807599)
AND Visitors.segmentid=9);
它基本上统计了唯一的访问者,它通过计算今天的访问者然后减去之前在那里的访问者来做到这一点。如果您知道更好的方法,请告诉我。
我只是不明白为什么有时它可以这么快,而其他时间需要这么长时间 - 即使在同一服务器负载下使用相同的确切查询。
这是关于此查询的EXPLAIN。正如您所看到的那样,它使用了我设置的索引:
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY Visits range visittime_visitorid,visitorid visittime_visitorid 4 NULL 82500 Using where; Using index
1 PRIMARY Visitors eq_ref PRIMARY,cand_visitor_omit PRIMARY 8 statsdb.Visits.visitorid 1 Using where
2 DEPENDENT SUBQUERY Visits ref visittime_visitorid,visitorid visitorid 8 func 1 Using where
2 DEPENDENT SUBQUERY Visitors eq_ref PRIMARY,cand_visitor_omit PRIMARY 8 statsdb.Visits.visitorid 1 Using where
我几周前尝试优化查询,并提出了一直持续约2秒的变体,但实际上它花了更多的时间,因为90%的旧查询返回的速度更快。每个查询两秒钟太长,因为我们在每页加载时调用查询最多50次,具有不同的时间段。
快速行为可能是由于查询保存在查询缓存中吗?我尝试在我的基准测试之间运行'RESET QUERY CACHE'和'FLUSH TABLES',我大部分时间都在快速获得结果。
注意:昨晚运行查询时出现错误:无法保存结果集。我最初的研究表明,可能是由于需要修复的腐败表。这可能是我所看到的行为的原因吗?
如果您需要服务器信息:
MySQL配置:
key_buffer = 350M
max_allowed_packet = 16M
thread_stack = 128K
sort_buffer = 14M
read_buffer = 1M
bulk_insert_buffer_size = 400M
set-variable = max_connections=150
query_cache_limit = 1048576
query_cache_size = 50777216
query_cache_type = 1
tmp_table_size = 203554432
table_cache = 120
thread_cache_size = 4
wait_timeout = 28800
skip-external-locking
innodb_file_per_table
innodb_buffer_pool_size = 3512M
innodb_log_file_size=100M
innodb_log_buffer_size=4M
这是结构,比尔:
CREATE TABLE `Visitors` (
`visitorid` bigint(20) unsigned NOT NULL auto_increment,
`ip` int(11) unsigned default '0',
`candidateid` int(11) unsigned NOT NULL default '0',
`omit` int(11) unsigned NOT NULL default '0',
`segmentid` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`visitorid`),
KEY `cand_visitor_omit` (`candidateid`,`visitorid`,`omit`),
KEY `ip_omit` (`ip`,`omit`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2837988 ;
CREATE TABLE `Visits` (
`visitid` bigint(20) unsigned NOT NULL auto_increment,
`visitorid` bigint(20) unsigned NOT NULL default '0',
`visittime` int(11) unsigned NOT NULL default '0',
`converted` tinyint(4) NOT NULL default '0',
`superconverted` tinyint(4) NOT NULL default '0',
`clickedotheroffer` tinyint(4) NOT NULL default '0',
PRIMARY KEY (`visitid`),
KEY `visittime_visitorid` (`visittime`,`visitorid`),
KEY `visitorid` (`visitorid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3912081 ;
答案 0 :(得分:5)
@OMG小马的答案与我在询问你的桌子定义时的想法非常接近。基本上,在此查询中只需要一个访问者实例。
如果某位访问者在该时间段内有一些匹配的访问次数且且不会超过该时间段的匹配访问次数,则应计算在内:
SELECT COUNT(DISTINCT v.visitorid) AS unique_visitor_count
FROM Visitors v
JOIN Visits current ON v.visitorid = current.visitorid
AND current.visittime BETWEEN 1275721200 AND 1275807599
LEFT JOIN Visits earlier ON v.visitorid = earlier.visitorid
AND earlier.visittime < 1275721200
WHERE v.candidateid IN (32)
AND v.segmentid = 9
AND v.omit NOT BETWEEN 1 AND 1275807598
AND earlier.visitorid IS NULL;
您可能会从访问者的索引(candidateid,segmentid,omit)中受益,因为这些列在您的WHERE
子句中使用。您还可以尝试访问者的访问者(visitorid,candidateid,segmentid,omit)。
基本上,如果您可以将查询优化说成using index
,则意味着它从索引数据结构中获取所需的所有数据,而且根本不必读取表数据!
我尝试了几次索引,尝试了上面的查询。我上面建议的索引没有帮助,它仍然希望使用访客的cand_visitor_omit索引。但我通过反转列更改了访问时的visittime_visitorid索引:
CREATE INDEX visitorid_visittime ON Visits(visitorid, visittime);
这得到了优化计划,告诉我它将使用它作为两个访问联接的覆盖索引(请参阅右侧额外字段中的“使用索引”):
+----+-------------+---------+------+---------------------------+---------------------+---------+------------------+------+--------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------------------+---------------------+---------+------------------+------+--------------------------------------+
| 1 | SIMPLE | v | ref | PRIMARY,cand_visitor_omit | cand_visitor_omit | 4 | const | 1 | Using where |
| 1 | SIMPLE | current | ref | visitorid_visittime | visitorid_visittime | 8 | test.v.visitorid | 2 | Using where; Using index |
| 1 | SIMPLE | earlier | ref | visitorid_visittime | visitorid_visittime | 8 | test.v.visitorid | 2 | Using where; Using index; Not exists |
+----+-------------+---------+------+---------------------------+---------------------+---------+------------------+------+--------------------------------------+
以这种方式更改索引也会使访问者(visitorid)上的其他单列索引变得多余,因此您可以删除该索引。
答案 1 :(得分:2)
这样的查询可能会表现得更好:
SELECT count(distinct v.visitorid) as 'uniques'
FROM Visits v
inner join Visitors vr on v.visitorid = vr.visitorid
left outer join (
SELECT v1.visitorid
FROM Visits v1
inner join Visitors v2 on v1.visitorid = v2.visitorid
WHERE candidateid = 32
and visittime < 1275721200
and (omit = 0 or omit >= 1275807599)
and v2.segmentid = 9
) vo on v.visitorid = vo.visitorid
where candidateid = 32
and visittime between 1275721200 and 1275807599
and (omit = 0 or omit >= 1275807599)
and vr.segmentid = 9
and vo.visitorid is null
答案 2 :(得分:2)
以下是我对您的查询的重写:
SELECT COUNT(DISTINCT v.visitorid) AS uniques
FROM VISITS v
JOIN VISITORS vv ON vv.visitorid = v.visitorid
AND vv.segmentid = 9
LEFT JOIN VISTS pv ON pv.visitorid = v.visitorid
AND pv.visitorid = vv.visitorid
AND pv.candidateid = v.candidateid
AND pv.visittime < 1275721200
AND v.omit NOT BETWEEN 1 AND 1275807598
WHERE x.visitorid IS NULL
AND v.candidateid = 32
AND v.visittime BETWEEN 1275721200 AND 1275807599
AND v.omit NOT BETWEEN 1 AND 1275807598
为什么你会在页面上多次运行相同的查询?它应该运行 一次 - 您需要定义适合数据的GROUP BY
子句,以便为每个子句返回计数。我的假设是小组应该是candidateid
...
答案 3 :(得分:1)
如果您的问题是“为什么它有时很快”,那么我很确定答案是查询缓存。当您第一次运行此查询时,它的工作时间会更长,但会将结果存储在缓存中。然后它只是返回缓存中的结果,除非数据集已更改或缓存本身过期。你有没有考虑过这个选择?