3500万行+表的有效mysql表/索引设计,200+对应列(双列),任意组合均可查询

dye*_*ryn 17 database-design partitioning mysql-5.5 index-tuning

我正在寻找有关以下情况的表/索引设计的建议:

我有一个带有复合主键(assetid (int),date (date))的大表(股票价格历史数据、InnoDB、3500 万行并且还在增长)。除了定价信息之外,我还有 200 个双值需要对应于每条记录。

CREATE TABLE `mytable` (
`assetid` int(11) NOT NULL,
`date` date NOT NULL,
`close` double NOT NULL,
`f1` double DEFAULT NULL,   
`f2` double DEFAULT NULL,
`f3` double DEFAULT NULL,   
`f4` double DEFAULT NULL,
 ... skip a few …
`f200` double DEFAULT NULL, 
PRIMARY KEY (`assetid`, `date`)) ENGINE=`InnoDB` DEFAULT CHARACTER SET latin1 COLLATE
    latin1_swedish_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0 
    PARTITION BY RANGE COLUMNS(`date`) PARTITIONS 51;
Run Code Online (Sandbox Code Playgroud)

为了便于更新和检索,我最初将 200 个双列直接存储在该表中,这一直工作正常,因为在该表上进行的唯一查询是通过资产 ID 和日期(这些都被虔诚地包含在针对该表的任何查询中) ),并且只读取了 200 个双列。我的数据库大小约为 45 Gig

但是,现在我需要能够通过这 200 列(名为 f1、f2、...f200)的任意组合来查询此表,例如:

select from mytable 
where assetid in (1,2,3,4,5,6,7,....)
and date > '2010-1-1' and date < '2013-4-5'
and f1 > -0.23 and f1 < 0.9
and f117 > 0.012 and f117 < .877
etc,etc
Run Code Online (Sandbox Code Playgroud)

我以前从来没有处理过如此大量的数据,所以我的第一直觉是这 200 列中的每一列都需要索引,否则我会以大表扫描等结束。对我来说,这意味着我需要一个包含主键、值和索引值的 200 列的表。所以我去了。

CREATE TABLE `f1` (
`assetid` int(11) NOT NULL DEFAULT '0',
`date` date NOT NULL DEFAULT '0000-00-00',
`value` double NOT NULL DEFAULT '0',
PRIMARY KEY (`assetid`, `date`),
INDEX `val` (`value`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci ROW_FORMAT=COMPACT CHECKSUM=0 DELAY_KEY_WRITE=0;
Run Code Online (Sandbox Code Playgroud)

我填写并索引了所有 200 个表。我保留了所有 200 列的主表,因为它会定期查询资产 ID 和日期范围,并选择所有 200 列。我认为将这些列保留在父表中(未索引)用于读取目的,然后另外将它们索引到它们自己的表中(用于连接过滤)将是最高效的。我跑了对查询的新形式的解释

select count(p.assetid) as total 
from mytable p 
inner join f1 f1 on f1.assetid = p.assetid and f1.date = p.date
inner join f2 f2 on f2.assetid = p.assetid and f2.date = p.date 
where p.assetid in(1,2,3,4,5,6,7)
and p.date >= '2011-01-01' and p.date < '2013-03-14' 
and(f1.value >= 0.96 and f1.value <= 0.97 and f2.value >= 0.96 and f2.value <= 0.97) 
Run Code Online (Sandbox Code Playgroud)

确实达到了我想要的结果,解释告诉我这个查询扫描的行要小得多。然而,我结束了一些不良的副作用。

1) 我的数据库从 45 Gig 增加到 110 Gig。我不能再将数据库保存在 RAM 中。(不过,我有 256Gig 的 RAM)

2) 现在每晚插入新数据需要执行 200 次而不是一次

3) 新的 200 个表的维护/碎片整理比仅 1 个表花费的时间长 200 倍。不可能在一夜之间完成。

4) 对 f1 等表的查询不一定是高性能的。例如:

 select min(value) from f1 
 where assetid in (1,2,3,4,5,6,7) 
 and date >= '2013-3-18' and date < '2013-3-19'
Run Code Online (Sandbox Code Playgroud)

上面的查询,而解释显示它查看 < 1000 行,可能需要 30+ 秒才能完成。我认为这是因为索引太大而无法放入内存。

由于这是很多坏消息,我进一步查看并发现了分区。我在主表上实现了分区,每 3 个月按日期进行分区。每月一次对我来说似乎很有意义,但我读过一旦你得到超过 120 个分区,性能就会受到影响。在接下来的 20 年左右的时间里,按季度进行分区将使我处于这种状态。每个分区都低于 2 Gig。我运行了解释分区,一切似乎都在正确修剪,所以不管我觉得分区是一个很好的步骤,至少为了分析/优化/修复目的。

我花了很多时间看这篇文章

http://ftp.nchu.edu.tw/MySQL/tech-resources/articles/testing-partitions-large-db.html

我的表目前已分区,主键仍在其上。文章提到主键可以让分区表变慢,但是如果你有一台可以处理它的机器,分区表上的主键会更快。知道我在路上有一台大机器(256 G RAM),我把钥匙开着。

所以在我看来,这是我的选择

选项1

1) 删除额外的 200 个表并让查询执行表扫描以查找 f1、f2 等值。非唯一索引实际上会损害正确分区表的性能。在用户运行查询之前运行解释,如果扫描的行数超过我定义的某个阈值,则拒绝它们。省去庞大数据库的痛苦。哎呀,无论如何,这一切很快就会在记忆中。

子问题:

听起来我选择了合适的分区方案吗?

选项 2

使用相同的 3 个月方案对所有 200 个表进行分区。享受较小的行扫描并允许用户运行较大的查询。既然它们已分区,至少我可以一次管理它们 1 个分区以进行维护。哎呀,无论如何,这一切很快就会在记忆中。开发有效的方法来每晚更新它们。

子问题:

你有没有看到我可以避免在这些 f1,f2,f3,f4... 表上使用主键索引的原因,知道我在查询时总是有资产 ID 和日期?对我来说似乎违反直觉,但我不习惯这种大小的数据集。我认为这会使数据库缩小很多

选项 3

删除主表中的 f1、f2、f3 列以回收该空间。如果我需要阅读 200 个功能,请执行 200 个连接,也许它不会像听起来那么慢。

选项 4

你们都有比我目前想到的更好的方式来构建它。

*注意:我很快会为每个项目添加另外 50-100 个这样的双精度值,所以我需要在设计时知道即将到来的。

感谢您的任何帮助

更新 #1 - 3/24/2013

我采纳了下面评论中建议的想法,并使用以下设置创建了一个新表:

create table 'features'{
  assetid int,
  date    date,
  feature varchar(4),
  value   double
}
Run Code Online (Sandbox Code Playgroud)

我以 3 个月为间隔对表进行分区。

我炸掉了之前的 200 个表,以便我的数据库回到 45 Gig 并开始填充这个新表。一天半后,它完成了,我的数据库现在有 220 Gig!

它确实允许从主表中删除这 200 个值,因为我可以从一个连接中获取它们,但这实际上只会给我 25 Gig 左右

我要求它在资产 ID、日期、功能和价值索引上创建一个主键,经过 9 个小时的调试,它确实没有产生任何凹痕,而且似乎冻结了,所以我取消了那部分。

我重建了几个分区,但它似乎没有回收太多/任何空间。

因此,该解决方案看起来可能并不理想。我想知道行占用的空间是否比列多得多,这可能是这个解决方案占用更多空间的原因吗?

我偶然发现了这篇文章:

http://www.chrismoos.com/2010/01/31/mysql-partitioning-tables-with-millions-of-rows

它给了我一个想法。它说:

起初,我考虑了按日期进行 RANGE 分区,当我在查询中使用日期时,查询具有非常大的日期范围是很常见的,这意味着它可以轻松跨越所有分区。

现在我也按日期进行范围分区,但也将允许按大日期范围进行搜索,这会降低分区的有效性。我在搜索时总是会有一个日期范围,但是我也会总是有一个资产 ID 列表。也许我的解决方案应该是按资产 ID 和日期划分,在那里我确定通常搜索的资产 ID 范围(我可以想出,有标准列表、标准普尔 500、罗素 2000 等)。这样我几乎不会查看整个数据集。

再说一次,无论如何,我主要以 assetid 和 date 为键,所以也许这不会有太大帮助。

任何更多的想法/评论将不胜感激。

Anu*_*hah 1

巧合的是,我也在研究一种客户端支持,我们设计了键值对结构以实现灵活性,目前表超过 1.5B 行,ETL 太慢。嗯,在我的例子中还有很多其他的事情,但是你考虑过这个设计吗?您将有一行包含所有 200 列的当前值,该行将转换为键值对设计中的 200 行。通过此设计,您将获得空间优势,具体取决于给定的 AssetID 和日期,实际上有多少行包含所有 200 个 f1 到 f200 值?如果你说甚至 30% od 列都有 NULL 值,那么这就是你节省的空间。因为在键值对设计中,如果值 id 为 NULL,则该行不需要在表中。但在现有的列结构设计中,即使 NULL 也占用空间。(我不是 100% 确定,但如果表中有超过 30 列 NULL,则 NULL 占用 4 个字节)。如果您看到此设计并假设所有 35M 行在所有 200 列中都有值,那么您当前的数据库将立即在表中变为 200*35M=700M 行。但表空间不会比单个表中的所有列高很多,因为我们只是将列转置到行中。在这个转置操作中,实际上我们不会有值为 NULL 的行。因此,您实际上可以对该表运行查询,查看有多少空值,并在实际实现之前估计目标表大小。

第二个优点是读取性能。正如您提到的,查询数据的新方法是 where 子句中 f1 到 f200 列的任意组合。采用键值对设计,f1 到 f200 出现在一列中(假设为“FildName”),它们的值出现在第二列(假设为“FieldValue”)中。您可以在两列上都有聚集索引。您的查询将是这些 Select 的 UNION。

WHERE(FiledName = 'f1' 且 FieldValue 介于 5 和 6 之间)

联盟

(FiledName = 'f2' 且 FieldValue 介于 8 和 10 之间)

ETC.....

我将为您提供一些来自实际产品服务器的性能数据。每个证券代码我们有 75 个价格栏。