4.3 重构查询方式
设计查询的时候一个需要考虑的重要问题是,是否需要将一个复杂的查询分成许多简单的查询。MySQL内部每秒能够扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢的多了。所以,有时候将一个大的查询分解为多个小查询是有必要的。
4.3.1 切分查询
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
例如,将一个大的DELETE语句切分成多较小的查询可以尽可能小的影响MySQL性能,同时还可以减少MySQL复制的延迟。
例如我们需要每个月运行一次下面的查询:
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
那么可以用类似下面的办法完成同样的工作:
rows_affected = 0 do { rows_affected = do_query( "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000") } while rows_affected > 0
一次删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法。同时,如果每次删除数据后,都暂停一会再做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间。
4.3.2 分解关联查询
很多高性能的应用都会对关联查询进行分解。简单地,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。
例如下面这个查询:
SELECT * FROM tab JOIN tag_post ON tag_post.tag_id=tag.id JOIN post ON tag_post.post_id=post.id WHERE tag.tag='mysql';
可以分解成下面的这些查询来代替:
SELECT * FROM tag WHERE tag='mysql'; SELECT * FROM tag_post WHERE tag_id=1234; SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
用分解关联查询的方式重构查询有如下的优势:
- 让缓存更高效。许多应用程序可以方便的缓存单表查询对应的结果对象。例如,上面查询中的tag已经被缓存了,那么应用就可以跳过第一个查询。对于关联查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复的访问一部分数据。从这一点看,这样的重构还可能会减少网络和内存的消耗。
- 将查询分解后,执行单个查询可以减少锁竞争;
- 在应用层进行关联,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
- 查询本身效率也可能会有所提升。例如上面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
- 更进一步讲,这样做相当于在应用层实现了哈希关联,而不是使用MySQL的嵌套循环关联。