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)来获得当前的日期和时间最接近的小时:
SELECT SUM(cnt) FROM msg_per_hr
WHERE hr BETWEEN
'00:00') - INTERVAL 23 HOUR CONCAT(LEFT(NOW(), 14),
'00:00') - INTERVAL 1 HOUR; AND CONCAT(LEFT(NOW(), 14),
SELECT COUNT(*) FROM message
WHERE posted >= NOW() - INTERVAL 24 HOUR
'00:00') - INTERVAL 23 HOUR; AND posted < CONCAT(LEFT(NOW(), 14),
SELECT COUNT(*) FROM message
'00:00'); WHERE posted >= CONCAT(LEFT(NOW(), 14),
不管是哪种方法——不严格的计数或通过小范围查询填满间隙的严格计数——都比计算message表的所有行要有效得多。这是建立汇总表的最关键原因。实时计算统计值是很昂贵的操作,因为要么需要扫描表中的大部分数据,要么查询语句只能在某些特定的索引上才能有效运行,而这类特定索引一般会对UPDATE操作有影响,所以一般不希望创建这样的索引。计算最活跃的用户或者最常见的“标签”是这种操作的典型例子。
缓存表则相反,其对优化搜索和检索查询语句很有效。这些查询语句经常需要特殊的表和索引结构,跟普通OLTP操作用的表有些区别。
例如,可能会需要很多不同的索引组合来加速各种类型的查询。这些矛盾的需求有时需要创建一张只包含主表中部分列的缓存表。一个有用的技巧是对缓存表使用不同的存储引擎。例如,如果主表使用 InnoDB,用MyISAM作为缓存表的引擎将会得到更小的索引占用空间, 并且可以做全文搜索。有时甚至想把整个表导出MySQL,插入到专门的搜索系统中获得更高的搜索效率,例如Lucene或者Sphinx搜索引擎。
在使用缓存表和汇总表时,必须决定是实时维护数据还是定期重建。哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引(这会更加高 效)。
当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用。这就需要通过使用“影子表”来实现,“影子表”指的是一张在真实表“背后”创建的表。当完成了建表操作后,可以通过一个原子的重命名操作切换影子表和原表。例如,如果需要重建my_summary,则可以先创建my_summary_new,然后填充好数据,最后和真实表做切换:
DROP TABLE IF EXISTS my_summary_new, my_summary_old;
CREATE TABLE my_summary_new LIKE my_summary;
-- populate my_summary_new as desired
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;
UPDATE hit_counter SET cnt = cnt + 1;
mysql> CREATE TABLE hit_counter (
-> slot tinyint unsigned not null primary key,
-> cnt int unsigned not null
-> ) ENGINE=InnoDB;
UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND() * 100;
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;
INSERT INTO daily_hit_counter(day, slot, cnt)
VALUES(CURRENT_DATE, RAND() * 100, 1)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
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);
DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
更快地读,更慢地写
完
以上是关于Schema优化——缓存表和汇总表(物化视图计数器表)的主要内容,如果未能解决你的问题,请参考以下文章
MySQL《高性能MySQL》学习笔记,第四章,Schema与数据类型优化