MySQL和NoSQL:帮我选择合适的

时间:2010-12-11 23:15:20

标签: php mysql nosql cassandra

有一个大型数据库,1,000,000,000行,称为线程(这些线程实际存在,我不是因为我喜欢它而使事情变得更难)。线程中只有一些东西,以加快速度:(int id,string hash,int replycount,int dateline(timestamp),int forumid,string title)

查询:

select * from thread where forumid = 100 and replycount > 1 order by dateline desc limit 10000, 100

由于有1G的记录,因此查询速度相当慢。所以我想,让我们在尽可能多的论坛(类别)中将这1G记录分开!这几乎是完美的。有很多表我搜索的记录较少,而且速度真快。查询现在变为:

select * from thread_{forum_id} where replycount > 1 order by dateline desc limit 10000, 100

99%的论坛(类别)真的更快,因为大多数论坛只有少数主题(100k-1M)。但是因为有一些大约有10M的记录,一些查询仍然会变慢(0.1 / .2秒,对于我的应用来说很多! 我已经在使用索引了! )。

我不知道如何使用MySQL改进这一点。有办法吗?

对于这个项目,我将使用10台服务器(12GB内存,4x7200rpm硬盘,软件raid 10,四核)

这个想法只是简单地在服务器之间拆分数据库,但是上面解释的问题仍未实现。

如果我在这10台服务器上安装cassandra(假设我找时间让它按预期工作)我应该假设性能得到提升吗?

我该怎么办?继续使用MySQL与多台机器上的分布式数据库或构建一个cassandra集群?

我被要求发布什么是索引,这里是:

mysql> show index in thread;
PRIMARY id
forumid
dateline
replycount

选择说明:

mysql> explain SELECT * FROM thread WHERE forumid = 655 AND visible = 1 AND open <> 10 ORDER BY dateline ASC LIMIT 268000, 250;
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
| id | select_type | table  | type | possible_keys | key     | key_len | ref         | rows   | Extra                       |
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+
|  1 | SIMPLE      | thread | ref  | forumid       | forumid | 4       | const,const | 221575 | Using where; Using filesort | 
+----+-------------+--------+------+---------------+---------+---------+-------------+--------+-----------------------------+

5 个答案:

答案 0 :(得分:78)

您应该阅读以下内容并了解一下设计良好的innodb表的优点以及如何最好地使用聚簇索引 - 仅适用于innodb!

http://dev.mysql.com/doc/refman/5.0/en/innodb-index-types.html

http://www.xaprb.com/blog/2006/07/04/how-to-exploit-mysql-index-optimizations/

然后按照以下简化示例的方式设计您的系统:

示例模式(简化)

重要的特性是表使用innodb引擎,而线程表的主键不再是单个auto_incrementing键,而是基于forum_id和thread_id组合的复合集群键。 e.g。

threads - primary key (forum_id, thread_id)

forum_id    thread_id
========    =========
1                   1
1                   2
1                   3
1                 ...
1             2058300  
2                   1
2                   2
2                   3
2                  ...
2              2352141
...

每个论坛行都包含一个名为next_thread_id(unsigned int)的计数器,该计数器由触发器维护,并在每次将线程添加到给定论坛时递增。这也意味着如果为thread_id使用单个auto_increment主键,我们每个论坛可以存储40亿个线程,而不是总共40亿个线程。

forum_id    title   next_thread_id
========    =====   ==============
1          forum 1        2058300
2          forum 2        2352141
3          forum 3        2482805
4          forum 4        3740957
...
64        forum 64       3243097
65        forum 65      15000000 -- ooh a big one
66        forum 66       5038900
67        forum 67       4449764
...
247      forum 247            0 -- still loading data for half the forums !
248      forum 248            0
249      forum 249            0
250      forum 250            0

使用复合键的缺点是您不能再按单个键值选择线程,如下所示:

select * from threads where thread_id = y;

你必须这样做:

select * from threads where forum_id = x and thread_id = y;

但是,您的应用程序代码应该知道用户正在浏览哪个论坛,因此实现起来并不十分困难 - 将当前查看的forum_id存储在会话变量或隐藏的表单字段等中......

这是简化的架构:

drop table if exists forums;
create table forums
(
forum_id smallint unsigned not null auto_increment primary key,
title varchar(255) unique not null,
next_thread_id int unsigned not null default 0 -- count of threads in each forum
)engine=innodb;


drop table if exists threads;
create table threads
(
forum_id smallint unsigned not null,
thread_id int unsigned not null default 0,
reply_count int unsigned not null default 0,
hash char(32) not null,
created_date datetime not null,
primary key (forum_id, thread_id, reply_count) -- composite clustered index
)engine=innodb;

delimiter #

create trigger threads_before_ins_trig before insert on threads
for each row
begin
declare v_id int unsigned default 0;

  select next_thread_id + 1 into v_id from forums where forum_id = new.forum_id;
  set new.thread_id = v_id;
  update forums set next_thread_id = v_id where forum_id = new.forum_id;
end#

delimiter ;

您可能已经注意到我已将reply_count作为主键的一部分包含在内,这有点奇怪,因为(forum_id,thread_id)复合本身是唯一的。这只是一个索引优化,它可以在执行使用reply_count的查询时节省一些I / O.有关详细信息,请参阅上面的2个链接。

示例查询

我仍在将数据加载到我的示例表中,到目前为止,我已经加载了大约。 5亿行(是系统的一半)。当加载过程完成后,我应该有大约:

250 forums * 5 million threads = 1250 000 000 (1.2 billion rows)

我故意让一些论坛包含超过500万个线程,例如论坛65有1500万个线程:

forum_id    title   next_thread_id
========    =====   ==============
65        forum 65      15000000 -- ooh a big one

查询运行时

select sum(next_thread_id) from forums;

sum(next_thread_id)
===================
539,155,433 (500 million threads so far and still growing...)
在innodb总结下,next_thread_ids给出一个总线程数比通常快得多:

select count(*) from threads;

论坛65有多少个主题:

select next_thread_id from forums where forum_id = 65

next_thread_id
==============
15,000,000 (15 million)
再次,这比通常更快:

select count(*) from threads where forum_id = 65

好了,现在我们知道到目前为止我们有大约5亿个线程,而论坛65有1500万个线程 - 让我们看看架构是如何执行的:)

select forum_id, thread_id from threads where forum_id = 65 and reply_count > 64 order by thread_id desc limit 32;

runtime = 0.022 secs

select forum_id, thread_id from threads where forum_id = 65 and reply_count > 1 order by thread_id desc limit 10000, 100;

runtime = 0.027 secs

看起来非常高效 - 所以这是一个包含500多万行(并且还在增长)的单个表,其查询在0.02秒内覆盖了1500万行(在加载时!)

进一步优化

这些包括:

  • 按范围划分

  • 分片

  • 向它投掷金钱和硬件

等...

希望您觉得这个答案有用:)

答案 1 :(得分:24)

编辑:您的单列索引是不够的。您至少需要涵盖三个相关的列。

更高级的解决方案:在replycount > 1时创建一个等于1的新hasreplies = 1字段,将hasreplies替换为replycount > 1。完成此操作后,按顺序在三列上创建索引:INDEX(forumid, hasreplies, dateline)。确保它是支持订购的BTREE索引。

您的选择基于:

  • 给定的forumid
  • 给定的hasreplies
  • dateline
  • 订购

执行此操作后,您的查询执行将涉及:

  • 向下移动BTREE以找到与forumid = X匹配的子树。这是一个对数操作(持续时间:log(论坛数))。
  • 进一步向下移动BTREE以找到与hasreplies = 1匹配的子树(同时仍匹配forumid = X)。这是一个恒定时间操作,因为hasreplies只有0或1。
  • 移动日期行排序的子树以获得所需的结果,而无需阅读和重新排序论坛中的整个项目列表。

我之前关于replycount索引的建议是不正确的,因为它本来是一个范围查询,因此无法使用dateline对结果进行排序(因此您可能选择了回复速度非常快,但是在查找所需的100个元素之前,必须完全排序得到的百万行列表。

重要:虽然这可以在所有情况下提高性能,但是你的巨大OFFSET值(10000!)会降低性能,因为尽管直接阅读了MySQL,但它似乎无法跳过BTREE。因此,OFFSET越大,请求就越慢。

我担心OFFSET问题不能通过将计算扩展到多个计算来自动解决(无论如何,你如何跳过并行偏移?)或转移到NoSQL。所有解决方案(包括NoSQL解决方案)将归结为基于dateline模拟OFFSET(基本上是dateline > Y LIMIT 100而不是LIMIT Z, 100,其中Y是偏移Z项的日期{1}})。这有效,并消除了与偏移相关的任何性能问题,但阻止直接进入200页中的第100页。

答案 2 :(得分:4)

有一些问题与NoSQL或MySQL选项有关。实际上这是隐藏在这里的一个基本内容。 SQL语言很容易为人类编写,有点难以为计算机阅读。在高容量数据库中,我建议避免使用SQL后端,因为这需要额外的步骤 - 命令解析。我已经做了大量的基准测试,有些情况下SQL解析器是最慢的。你无能为力。好的,您可以使用预解析的语句并访问它们。

BTW,它并不为人所知,但MySQL已经从NoSQL数据库发展而来。 MySQL David和Monty的作者所在的公司是数据仓库公司,他们经常不得不为不常见的任务编写自定义解决方案。这导致了大量的自制C库,用于在Oracle和其他人表现不佳时手动编写数据库函数。为了好玩,SQL于1996年被添加到这个近20年的动物园。你知道后发生了什么。

实际上,您可以避免使用MySQL的SQL开销。但通常SQL解析不是最慢的部分,但只是很好知道。要测试解析器开销,您可以为“SELECT 1”制作基准,例如;)。

答案 3 :(得分:2)

您不应该尝试将数据库架构适合您计划购买的硬件,而是计划购买适合您数据库架构的硬件。

一旦有足够的RAM来保存内存中的工作索引集,所有可以使用索引的查询都会很快。确保您的密钥缓冲区设置得足够大以容纳索引。

因此,如果12GB不够,请不要使用10个内存为12GB的服务器,使用较少的32GB或64GB内存。

答案 4 :(得分:0)

索引是必须的 - 但请记住选择正确的索引类型:当使用带有“&lt;”的查询时,BTREE更合适或“&gt;”在WHERE子句中,当您在一列中有许多不同的值并且使用“=”或“&lt; =&gt;”时,HASH更合适在你的WHERE子句中。

进一步阅读http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html