Schema优化——缓存表和汇总表(物化视图计数器表)

Posted 多栖技术控小董

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Schema优化——缓存表和汇总表(物化视图计数器表)相关的知识,希望对你有一定的参考价值。



概述

有时提升性能最好的方法是在同一张表中保存衍生的冗余数据。然 而,有时也需要创建一张完全独立的汇总表或缓存表(特别是为满足检索的需求时)。如果能容许少量的脏数据,这是非常好的方法,但是有时确实没有选择的余地(例如,需要避免复杂、昂贵的实时更新操作)。 

术语“缓存表”和“汇总表”没有标准的含义

  •  我们用术语“缓存表”来表示存储那些可以比较简单地从schema其他表获取(但是每次获取的速度比较慢)数据的表(例如,逻辑上冗余的数据)。
  • 而术语“汇总表”时,则保存的是使用GROUP BY语句 聚合数据的表(例如,数据不是逻辑上冗余的)。
  • 也有人使用术语“累积表(Roll-Up Table)”称呼这些表。因为这些数据被“累积”了。 




演示案例

仍然以网站为例,假设需要计算之前24小时内发送的消息数。在一个很繁忙的网站不可能维护一个实时精确的计数器。作为替代方案,可以每小时生成一张汇总表。这样也许一条简单的查询就可以做到,并且比实时维护计数器要高效得多。缺点是计数器并不是100%精确

如果必须获得过去24小时准确的消息发送数量(没有遗漏),有另外一种选择。以每小时汇总表为基础,把前23个完整的小时的统计表中的计数全部加起来,最后再加上开始阶段和结束阶段不完整的小时内的计数。假设统计表叫作msg_per_hr并且这样定义:

CREATE TABLE msg_per_hr ( hr DATETIME NOT NULL, cnt INT UNSIGNED NOT NULL, PRIMARY KEY(hr));

可以通过把下面的三个语句的结果加起来,得到过去24小时发送消息的总数。我们使用LEFT(NOW(),14)来获得当前的日期和时间最接近的小时:

mysql> SELECT SUM(cnt) FROM msg_per_hr-> WHERE hr BETWEEN-> CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR-> AND CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 1 HOUR;mysql> SELECT COUNT(*) FROM message-> WHERE posted >= NOW() - INTERVAL 24 HOUR-> AND posted < CONCAT(LEFT(NOW(), 14), '00:00') - INTERVAL 23 HOUR;mysql> SELECT COUNT(*) FROM message-> WHERE posted >= CONCAT(LEFT(NOW(), 14), '00:00');

不管是哪种方法——不严格的计数或通过小范围查询填满间隙的严格计数——都比计算message表的所有行要有效得多。这是建立汇总表的最关键原因。实时计算统计值是很昂贵的操作,因为要么需要扫描表中的大部分数据,要么查询语句只能在某些特定的索引上才能有效运行,而这类特定索引一般会对UPDATE操作有影响,所以一般不希望创建这样的索引。计算最活跃的用户或者最常见的“标签”是这种操作的典型例子。 

缓存表则相反,其对优化搜索和检索查询语句很有效。这些查询语句经常需要特殊的表和索引结构,跟普通OLTP操作用的表有些区别。 

例如,可能会需要很多不同的索引组合来加速各种类型的查询。这些矛盾的需求有时需要创建一张只包含主表中部分列的缓存表。一个有用的技巧是对缓存表使用不同的存储引擎。例如,如果主表使用 InnoDB,用MyISAM作为缓存表的引擎将会得到更小的索引占用空间, 并且可以做全文搜索。有时甚至想把整个表导出MySQL,插入到专门的搜索系统中获得更高的搜索效率,例如Lucene或者Sphinx搜索引擎。 

在使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建。哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这会更加高 效)。

当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用。这就需要通过使用“影子表”来实现,“影子表”指的是一张在真实表“背后”创建的表。当完成了建表操作后,可以通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换:

mysql> DROP TABLE IF EXISTS my_summary_new, my_summary_old;mysql> CREATE TABLE my_summary_new LIKE my_summary;-- populate my_summary_new as desiredmysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;

如果像上面的例子一样,在将my_summary这个名字分配给新建的表之前将原始的my_summary表重命名为my_summary_old,就可以在下一次 重建之前一直保留旧版本的数据。如果新表有问题,则可以很容易地进行快速回滚操作。





物化视图

许多数据库管理系统(例如Oracle或者微软SQL Server)都提供了 一个被称作物化视图的功能。物化视图实际上是预先计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新。MySQL并不原生支持物化视图(我们会在详细探讨支持这种视图的细节)。然而, 使用Justin Swanhart的开源工具 Flexviews(http://code.google.com/p/flexviews/),也可以自己实现物化视图。Flexviews比完全自己实现的解决方案要更精细,并且提供了很多不错的功能使得可以更简单地创建和维护物化视图。它由下面这些部分组成:

  • 变更数据抓取(Change Data Capture,CDC)功能,可以读取服务器的二进制日志并且解析相关行的变更。

  • 一系列可以帮助创建和管理视图的定义的存储过程。

  • 一些可以应用变更到数据库中的物化视图的工具。




对比传统的维护汇总表和缓存表的方法,Flexviews通过提取对源表的更改,可以增量地重新计算物化视图的内容。这意味着不需要通过查询原始数据来更新视图。例如,如果创建了一张汇总表用于计算每个分组的行数,此后增加了一行数据到源表中,Flexviews简单地给相应的组的行数加一即可。同样的技术对其他的聚合函数也有效,例如SUM()和 AVG()。这实际上是有好处的,基于行的二进制日志包含行更新前后的镜像,所以Flexviews不仅仅可以获得每行的新值,还可以不需要查找源表就能知道每行数据的旧版本。计算增量数据比从源表中读取数据的效率要高得多。 

因为版面的限制,这里我们不会完整地探讨怎么使用Flexviews,但 是可以给出一个概略。先写出一个SELECT语句描述想从已经存在的数据 库中得到的数据。这可能包含关联和聚合(GROUP BY)。Flexviews中有 一个辅助工具可以转换SQL语句到Flexviews的API调用。Flexviews会做完所有的脏活、累活:监控数据库的变更并且转换后用于更新存储物化 视图的表。现在应用可以简单地查询物化视图来替代查询需要检索的 表。

Flexviews有不错的SQL覆盖范围,包括一些棘手的表达式,你可能 没有料到一个工具可以在MySQL服务器之外处理这些工作。这一点对 创建基于复杂SQL表达式的视图很有用,可以用基于物化视图的简单、 快速的查询替换原来复杂的查询。





计数器表

如果应用在表中保存计数器,则在更新计数器时可能碰到并发问题。计数器表在Web应用中很常见。可以用这种表缓存一个用户的朋友数、文件下载次数等。创建一张独立的表存储计数器通常是个好主意, 这样可使计数器表小且快。使用独立的表可以帮助避免查询缓存失效, 并且可以使用本节展示的一些更高级的技巧。

应该让事情变得尽可能简单,假设有一个计数器表,只有一行数据,记录网站的点击次数:

mysql> CREATE TABLE hit_counter (-> cnt int unsigned not null-> ) ENGINE=InnoDB;
网站的每次点击都会导致对计数器进行更新:
mysql> UPDATE hit_counter SET cnt = cnt + 1;
问题在于,对于任何想要 更新这一行的事务来说,这条记录上都有 一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行。要获 得更高的并发更新性能,也可以将计数器保存在 多行中,每次随机选择 一行进行更新。这样做需要对计数器表进行如下修改:
mysql> CREATE TABLE hit_counter (-> slot tinyint unsigned not null primary key,-> cnt int unsigned not null-> ) ENGINE=InnoDB;
然后预先在这张表增加100行数据。现在 选择一个随机的槽(slot) 进行更新:
mysql> UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
要获得统计结果,需要使用下面这样的聚合查询:
mysql> SELECT SUM(cnt) FROM hit_counter;
一个常见的需求是 每隔一段时间开始一个新的计数器(例如,每天 一个)。如果需要这么做,则可以再简单地修改一下表设计:
mysql> CREATE TABLE daily_hit_counter (-> day date not null,-> slot tinyint unsigned not null,-> cnt int unsigned not null,-> primary key(day, slot)-> ) ENGINE=InnoDB;
在这个场景中,可以不用像前面的例子那样预先生成行,而用ON DUPLICATE KEY UPDATE代替:
mysql> INSERT INTO daily_hit_counter(day, slot, cnt)-> VALUES(CURRENT_DATE, RAND() * 100, 1)-> ON DUPLICATE KEY UPDATE cnt = cnt + 1;
如果希望 减少表的行数,以避免表变得太大,可以写一个周期执行的任务,合并所有结果到0号槽,并且删除所有其他的槽:
mysql> UPDATE daily_hit_counter as c-> INNER JOIN (-> SELECT day, SUM(cnt) AS cnt, MIN(slot) AS mslot-> FROM daily_hit_counter-> GROUP BY day-> ) AS x USING(day)-> SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0),-> c.slot = IF(c.slot = x.mslot, 0, c.slot);mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;


更快地读,更慢地写

为了提升读查询的速度,经常会需要建一些额外索引,增加冗余 列,甚至是创建缓存表和汇总表。这些方法会增加写查询的负担,也 需要额外的维护任务,但在设计高性能数据库时,这些都是常见的技 巧:虽然写操作变得更慢了,但更显著地提高了读操作的性能。 
然而,写操作变慢并不是读操作变得更快所付出的唯一代价,还 可能同时增加了读操作和写操作的开发难度。