按日期搜索MySQL性能

5

我有一张包含大约1亿条记录的大表,其中有两个字段start_dateend_date,类型为DATE。我需要检查某个日期范围内(比如2013-08-202013-08-30)重叠的记录数,因此我使用以下语句:

SELECT COUNT(*) FROM myTable WHERE end_date >= '2013-08-20' 
AND start_date <= '2013-08-30'

日期列已经建立了索引。

重要的一点是,我正在搜索的日期范围总是在未来,而表中大部分记录都是过去的(大约9700万至9900万)。

因此,如果我添加一个 is_future - TINYINT 列,那么只需检查该条件,这个查询会更快吗?

SELECT COUNT(*) FROM myTable WHERE is_future = 1 
AND end_date >= '2013-08-20' AND start_date <= '2013-08-30'

它将排除其余的大约9700万条记录,并仅检查剩余的100-300万条记录的日期条件?

我使用MySQL

谢谢

编辑

MySQL引擎是InnoDB,但如果是MyISAM,则会产生相当大的影响

这是创建表

CREATE TABLE `orders` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title`
  `start_date` date DEFAULT NULL,
  `end_date` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

编辑 2@Robert Co的答案后

对于这种情况,分区看起来是个好主意,但是除非我将is_future字段定义为主键,否则无法基于该字段创建分区,否则我应该删除我的主要主键 - id,而这是不可能的。因此,如果我将该字段定义为主键,那么分区有意义吗?如果我按照主键进行搜索,即通过is_future字段搜索,它不会变得更快。

编辑 3 我需要使用实际查询来选择在该日期范围内有一些空闲桌子的餐厅

SELECT r.id, r.name, r.table_count
FROM restaurants r
LEFT JOIN orders o 
ON r.id = o.restaurant_id 
WHERE o.id IS NULL 
OR (r.table_count > (SELECT COUNT(*) 
                FROM orders o2 
                WHERE o2.restaurant_id = r.id AND
                end_date >= '2013-08-20' AND start_date <= '2013-08-30'
                AND o2.status = 1
            )
) 

解决方案 经过更多的研究和测试,对于我的情况,计算行数最快的方法是只需再添加一个条件,即开始日期大于当前日期(因为搜索的日期范围总是在未来)。

 SELECT COUNT(*) FROM myTable WHERE end_date >= '2013-09-01' 
         AND start_date >= '2013-08-20' AND start_date <= '2013-09-30'

还需要一个索引 - 带有start_date和end_date字段(感谢@symcbean)。

结果,对于包含1000万行的表,执行时间从7秒降至0.050秒。

解决方案2(@Robert Co):

在这种情况下,分区也可以起到作用!也许这比索引更好。或者两者都可以同时应用。

谢谢


很好的问题...试一下并让我们知道。 ;) 为什么不使用 BETWEEN - DevlshOne
1
@DevlshOne,:D,在这种情况下如何使用between?我要比较两列,可以吗? - dav
你能发布创建表语句和存储引擎的信息吗?这很重要。 - Raymond Nijland
1
首先,你真的需要那个bigint吗?因为当你索引start_date和end_date/is_future时,InnoDB会将主键数据添加到该索引中(额外8个字节),在1亿条记录上,这将节省磁盘空间/更小的索引,理论上应该表现更好。 - Raymond Nijland
@RaymondNijland,我想你是对的,数据库会很大,但我想它不会超过40亿,所以int应该没问题。 - dav
3个回答

4
这是一个完美的应用场景,适合使用表分区。如果Oracle的间隔特性被引入到MySQL中,那么它只会增加其强大功能。

不太清楚您为什么认为“订单”表不适合分区。您可以提前创建分区,以防数据落入MAXVALUE。 - Robert Co
1
分区键不必与主键相同。如果选择分区,就不再需要is_future标志了。我会按照结束日期进行分区。不要过度分区。每月应该足够了。 - Robert Co
但是,看到每个月订单都在增加,而旧的订单则进入“过去”,所以每个月我应该添加一个新分区,不是吗?这不好吗?谢谢。 - dav
1
您可以指定适合该分区的日期范围。因此,您可以每年预先创建12个分区(或更多年)。只有在开始获取数据时才会占用空间。 - Robert Co

2

日期列已经建立索引

是什么类型的索引呢?基于哈希的索引对于范围查询没有用处。如果不是BTREE索引,则现在更改它。你还没有向我们展示*它们是如何索引的。这两列是否在同一个索引中?里面还有其他东西吗?顺序是什么(end_date必须出现在第一列)?

脚本中存在隐式类型转换-优化器应该自动处理这个问题,但检查一下也是值得的...

SELECT COUNT(*) FROM myTable WHERE end_date >= 20130820000000 
AND start_date <= 20130830235959

如果我新增一个名为 is_future 的列 - TINYINT
首先,为了有所用处,这需要未来日期在表中存储的数据总量中占比较小(少于10%)。即使如此,也只是为了比全表扫描更有效率。
其次,为了维护它,它将需要非常频繁的索引更新,除了初始填充的开销之外,还可能导致索引碎片化和性能下降(取决于索引的构建方式)。
第三,在处理300万行数据时,如果仍然通过索引查找,即使数据锁定在内存中,速度也会非常慢。
此外,优化器不太可能在没有强制使用该索引的情况下使用该索引(由于低基数)。

我更新了问题。关于索引 - 它是通过 ALTER TABLE orders ADD INDEX endDate (end_date); 创建的常规索引,对于 start_date 也是一样的,不是吗? - dav
这并没有回答关于索引类型的问题 - 但是你会发现删除它们并用一个单一的索引 (end_date, start_date, ...) 替换会更好。鉴于你提供的进一步信息,它应该是 (end_date, start_date, restaurant_id)。你正在使用的查找表的查询远非最优。 - symcbean
但是如何找出索引的类型呢?除了将索引替换为单个索引之外,我还能做什么来优化此查询?或者我可以使用其他查询查找有空桌的餐厅吗?谢谢。 - dav

0

我做了一个简单的测试,只是在tinyint列上创建了一个索引。结构可能不同,但使用索引似乎可以工作。

http://www.sqlfiddle.com/#!2/514ab/1/0 并且对于计数 http://www.sqlfiddle.com/#!2/514ab/2/0

在那里查看执行计划,以查看选择仅扫描一行,这意味着它将仅处理您的情况下较少的记录。

所以简单的答案是,是的,使用索引它会工作。


1
不,您不应该对选择性较低的列进行索引,只有两个值0-1的列永远不应该被索引。 - Raymond Nijland
好的,我学到了一些东西,但是答案是否定的,因为没有索引它选择了所有三行,我想答案显然。 - skv
1
@RaymondNijland,除非您希望该列位于覆盖索引中,否则每行都需要(大多数是随机的)磁盘IO。永远不要说永远。 - newtover
@newtover 是的,但是覆盖索引总是会导致更高的选择性。 - Raymond Nijland

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