问题 在实体上存储更改:MySQL是否是正确的解决方案?
我想存储我在“实体”表上所做的更改。这应该像一个日志。目前它在MySQL中使用此表实现:
CREATE TABLE `entitychange` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`entity_id` int(10) unsigned NOT NULL,
`entitytype` enum('STRING_1','STRING_2','SOMEBOOL','SOMEDOUBLE','SOMETIMESTAMP') NOT NULL DEFAULT 'STRING_1',
`when` TIMESTAMP NOT NULL,
`value` TEXT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
entity_id
=我的主键 entity
表。
entitytype
=在中更改的字段 entity
表。有时只改变一个字段,有时多个。一个变化=一行。
value
=字段“新值”的字符串表示形式。
更改字段时的示例 entity.somedouble
从3到2,我运行这些查询:
UPDATE entity SET somedouble = 2 WHERE entity_id = 123;
INSERT INTO entitychange (entity_id,entitytype,value) VALUES (123,'SOMEDOUBLE',2);
我需要 select
过去15天内特定实体和实体类型的变化。例如:最后一次更改 SOMEDOUBLE
for entity_id 123
在过去15天内。
现在,有两件事我不喜欢:
- 所有数据都存储为
TEXT
- 虽然大多数(小于1%)不是真正的文本,但就我而言,大多数值都是 DOUBLE
。这是一个大问题吗?
- 由于表已经有2亿行,因此插入时表变得非常非常慢。目前我的服务器负载高达10-15。
我的问题: 我如何解决这两个“瓶颈”?我需要扩展。
我的方法是:
- 像这样存储: http://sqlfiddle.com/#!2/df9d0 (点击浏览) - 将更改存储在
entitychange
表然后根据其数据类型存储该值 entitychange_[bool|timestamp|double|string]
- 使用分区
HASH(entity_id)
- 我想到了~50个分区。
- 我应该使用其他数据库系统,也许是MongoDB?
1572
2018-06-11 16:29
起源
答案:
如果我遇到你提到的问题,我会像下面那样设计LOG表:
EntityName
:(String)正在被操纵的实体。(必填)
ObjectId
:正在被操纵的实体,主键。
FieldName
:(字符串)实体字段名称。
OldValue
:(String)实体字段旧值。
NewValue
:(String)实体字段的新值。
UserCode
:应用程序用户唯一标识符(强制)
TransactionCode
:任何更改实体的操作都需要具有唯一的事务代码(如GUID)(强制),
如果实体更新了多个字段,这些列将成为跟踪更新中所有更改(交易)的关键点
ChangeDate
: 交易日期。 (强制)
FieldType
:枚举或文本显示字段类型,如TEXT或Double。 (强制)
有这种方法
可以跟踪任何实体(表)
报告是可读的
仅记录更改。
事务代码将是通过单个操作检测更改的关键点。
BTW
Store the changes in the entitychange table and then store the value
according to its datatype in entitychange_[bool|timestamp|double|string]
不需要,在单个表中您将拥有更改和数据类型
Use partitioning by HASH(entity_id)
我更喜欢通过ChangeDate进行分区或为changeDate创建备份表,这些备份表已经足够大,可以从主LOG表中备份和卸载
Should I use another database system, maybe MongoDB?
任何数据库都有自己的概率和缺点,您可以在任何RDBMS上使用该设计。
基于文档的数据库(如MongoDB)的有用比较 可以在这里找到
希望有所帮助。
5
现在我想我明白了你需要的东西,一个可更改记录历史的可版本表。这可能是实现相同目标的另一种方法,您可以轻松地进行一些快速测试,以确定它是否比您当前的解决方案提供更好的性能。它是Symfony PHP Framework在Doctrine中使用Versionable插件的方式。
请记住,有两个键的主键唯一索引,版本和fk_entity。
另请查看保存的值。您将在未更改的字段中保存0值,并在更改的值中保存更改的值。
CREATE TABLE `entity_versionable` (
`version` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`fk_entity` INT(10) UNSIGNED NOT NULL,
`str1` VARCHAR(255),
`str2` VARCHAR(255),
`bool1` BOOLEAN,
`double1` DOUBLE,
`date` TIMESTAMP NOT NULL,
PRIMARY KEY (`version`,`fk_entity`)
) ENGINE=INNODB DEFAULT CHARSET=latin1;
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "a1", "0", "0", "0", "2013-06-02 17:13:16");
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "a2", "0", "0", "0", "2013-06-11 17:13:12");
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "0", "b1", "0", "0", "2013-06-11 17:13:21");
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "0", "b2", "0", "0", "2013-06-11 17:13:42");
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "0", "0", "1", "0", "2013-06-16 17:19:31");
/*Another example*/
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
VALUES ("1", "a1", "b1", "0", "0", CURRENT_TIMESTAMP);
SELECT * FROM `entity_versionable` t WHERE
(
(t.`fk_entity`="1") AND
(t.`date` >= (CURDATE() - INTERVAL 15 DAY))
);
可能是提高性能的另一个步骤,可能是将所有历史记录日志记录保存在单独的表中,每月一次左右。这样你在每个表中都不会有很多记录,按日期搜索会非常快。
3
这里有两个主要挑战:
- 如何有效地存储数据,即占用更少的空间和易于使用的格式
2-3。管理大表:归档,便于备份和还原
2-3。性能优化:更快的插入和选择
有效地存储数据
value
提起。我建议去做 VARCHAR (N)
。
原因:
- 由于数据类型的原因,使用N <255将每行节省1个字节。
- 使用此字段的其他数据类型:固定类型使用空间,无论值是什么,通常每行8个字节(日期时间,长整数,字符(8)),其他变量数据类型对于此字段来说太大。
- 也
TEXT
数据类型会导致性能损失:(来自manaul on BLOB和Text数据类型)
实例 TEXT
使用临时表处理的查询结果中的列会导致服务器在磁盘而不是内存中使用表,因为MEMORY存储引擎不支持这些数据类型。使用磁盘会导致性能下降,因此只有在确实需要时才在查询结果中包含BLOB或TEXT列。例如,避免使用选择所有列的SELECT *。
每个BLOB或TEXT值在内部由单独分配的对象表示。这与所有其他数据类型形成对比,在打开表时,每列分配一次存储。
基本上 TEXT
用于存储大字符串和拼接文本,而 VARCHAR()
是设计相对较短的字符串。
id
领域。 (更新,感谢@steve)我同意这个字段没有任何有用的信息。使用3列作为主键: entity_id
和 entitype
和 when
。 TIMESTAMP
我会很好地保证你不会重复。同样的列也将用于分区/子分区。
表可管理性
有两个主要选项:MERGE表和分区。 MERGE存储引擎基于My_ISAM,据我所知,它正在逐步淘汰。以下是[MERGE存储引擎]的一些阅读。2
主要工具是分区,它提供两个主要好处:
1.分区切换(通常是对大块数据的即时操作)和滚动窗口场景:在一个表中插入新数据,然后立即将所有数据切换到存档表中。
2.按排序顺序存储数据,启用分区修剪 - 仅查询包含所需数据的分区。 MySQL允许子分区进一步分组数据。
分区 entity_id
说得通。如果您需要长时间查询数据,或者在查询表时有其他模式 - 请使用该列进行子分区。除非在该级别切换分区,否则不需要对所有主键列进行子分区。
分区数取决于您希望该分区的db文件的大小。子分区数量取决于核心数量,因此每个核心可以搜索自己的分区,N-1子分区应该可以,所以1核心可以做整体协调工作。
优化
插入:
在没有索引的情况下,表上的插入更快,因此插入大块数据(执行更新),然后创建索引(如果可能)。
更改 Text
对于 Varchar
- 数据库引擎需要一些压力
最小的日志记录和表锁可能有所帮助,但通常不可能使用
选择:
您还可以考虑按日期分区,您在日期范围内有很多查询。首先使用您的数据及其部件,然后确定哪种架构最能支持它。
至于你的第三个问题,我不知道如何使用MongoDB特别有利于这种情况。
2
这被称为a 时态数据库20多年来,研究人员一直在努力寻找存储和查询时态数据的最佳方法。
尝试存储EAV数据时效率很低,因为在TEXT列中存储数字数据会占用大量空间,并且您的表越来越长,正如您所发现的那样。
另一个有时被称为第六范式的选项(虽然有6NF的多个不相关的定义),是存储一个额外的表来存储修订 对于每一列 你想暂时跟踪。这类似于@ xtrm的答案提出的解决方案,但它不需要存储未更改的列的冗余副本。但它确实导致了桌子数量的激增。
我已经开始阅读了 锚建模,它承诺处理结构和内容的时间变化。但我还不太清楚它的解释。我只是链接到它,也许它对你有意义。
以下是一些包含时态数据库讨论的书籍:
1
在a中存储整数 TEXT
专栏是禁止的! TEXT
是最昂贵的类型。
我会为每个要监视的字段创建一个日志表:
CREATE TABLE entitychange_somestring (
entity_id INT NOT NULL PRIMARY KEY,
ts TIMESTAMP NOT NULL,
newvalue VARCHAR(50) NOT NULL, -- same type as entity.somestring
KEY(entity_id, ts)
) ENGINE=MyISAM;
实际上是对它们进行分区。
注意我建议使用 MyISAM
发动机。您不需要此(这些)无约束,仅插入表的事务。
1
为什么INSERTing如此缓慢,你可以做些什么来加快速度。
这些是我要看的东西(大致按照我将通过它们的顺序):
创建一个新的AUTO_INCREMENT-id并将其插入主键需要一个锁(InnoDB中有一个特殊的AUTO-INC锁,它在语句结束前一直保持,有效地充当了 桌锁 在你的场景中)。这通常不是问题,因为这是一个相对较快的操作,但另一方面,当(Unix)加载值为10到15时,您可能会有进程等待释放该锁。根据您提供的信息,我认为您的代理键'id'没有任何用处。查看删除该列是否会显着改变性能。 (顺便说一句,没有规则表需要一个主键。如果你没有一个,那没关系)
对于INSERT,InnoDB可能相对昂贵。这是为了允许诸如交易之类的附加功能而进行的权衡,可能会或可能不会影响您。由于您的所有操作都是原子的,因此我认为不需要进行交易。也就是说,试试MyISAM吧。注意:对于大型表,MyISAM通常是一个糟糕的选择,因为它只支持表锁定而不是记录级别锁定,但它确实支持 并发插入,所以它可能是一个选择(特别是如果你放弃主键,见上文)
您可以使用数据库存储引擎参数。 InnoDB和MyISAM都有可以改变的选项。其中一些对TEXT数据的实际存储方式有影响,另一些则具有更广泛的功能。你应该特别注意的是 的innodb_flush_log_at_trx_commit。
如果(并且仅当)它们具有非NULL值,则TEXT列相对昂贵。您目前正在存储 所有 TEXT列中的值。值得尝试下面的内容:添加额外的字段 value_int
和 value_double
到您的表并将这些值存储在相应的列中。是的,这将浪费一些额外的空间,但可能会更快 - 但这在很大程度上取决于数据库存储引擎及其设置。请注意,很多人对TEXT列性能的看法并不正确。 (看到 我对VARCHAR与TEXT相关问题的回答)
您建议在多个表中传播信息。如果您的表完全独立,这只是一个好主意。否则,对于任何更改,您最终都会有多个INSERT操作,并且您很可能会使事情变得更糟。虽然归一化数据通常是好的(tm),但这可能会损害性能。
你能做些什么来让SELECT快速运行
正确的钥匙。和正确的钥匙。以防我忘记提及:正确的钥匙。您没有详细说明您的选择是什么样的,但我认为它们类似于“SELECT * FROM entitychange WHERE entity_id = 123 AND ts> ...”。 entity_id和ts上的单个复合索引应该足以使此操作快速。由于必须使用每个INSERT更新索引,因此可能值得尝试两者的性能 entity_id, ts
和 ts, entity_id
:它可能会有所作为。
分区。如果你没有在问题中提问,我甚至不会提起这个问题。你没有说你为什么要分区表。在性能方面,如果你有合适的密钥,它通常没有区别。有一些特定的设置可以提高性能,但你需要适当的硬件设置来配合这一点。如果您决定对表进行分区,请考虑使用entity_id或TIMESTAMP列进行分区。使用时间戳,最终可能会将归档系统与旧数据放在归档驱动器上。然而,这样的分区系统需要一些维护(随着时间的推移添加分区)。
在我看来,你并不关心查询性能和原始插入速度,所以我不会详细介绍SELECT性能。如果您对此感兴趣,请提供更多详细信息。
1
我建议你在深度测试中做很多工作,但是从我的测试中我用INSERT和SELECT获得了非常好的结果,我之前发布了表定义。我将详细介绍我的测试,以便任何人都可以轻松地重复并检查它是否会获得更好的结果。 在任何测试之前备份您的数据。
我必须说这些只是测试,可能不会反映或改善你的实际情况,但它是一种很好的学习方式,可能是一种寻找有用信息和结果的方法。
我们在这里看到的建议非常好,你肯定会注意到通过使用大小而不是TEXT的预定义类型VARCHAR来提高速度。但是你可以获得速度,我建议不要因为数据完整性原因而使用MyISAM,请留在InnoDB。
测试:
1.设置表并插入2亿个数据:
CREATE TABLE `entity_versionable` (
`version` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`fk_entity` INT(10) UNSIGNED NOT NULL,
`str1` VARCHAR(255) DEFAULT NULL,
`str2` VARCHAR(255) DEFAULT NULL,
`bool1` TINYINT(1) DEFAULT NULL,
`double1` DOUBLE DEFAULT NULL,
`date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`version`,`fk_entity`)
) ENGINE=INNODB AUTO_INCREMENT=230297534 DEFAULT CHARSET=latin1
为了在约35分钟的表格中插入+200万行,请检查我的其他问题在哪里 peterm 已回答其中一个 填写表格的最佳方法。它完美地运作。
执行以下查询2次,以插入2亿行无随机数据(每次更改数据以插入随机数据):
INSERT INTO `entity_versionable` (fk_entity, str1, str2, bool1, double1, DATE)
SELECT 1, 'a1', 238, 2, 524627, '2013-06-16 14:42:25'
FROM
(
SELECT a.N + b.N * 10 + c.N * 100 + d.N * 1000 + e.N * 10000 + f.N * 100000 + g.N * 1000000 + h.N * 10000000 + 1 N FROM
(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) c
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) d
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) e
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) f
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) g
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) h
) t;
*由于您已拥有包含2亿行真实随机数据的原始表,因此您可能不需要填充它,只需导出表数据和模式并将其导入具有相同模式的新测试表中。这样,您将在新表中使用您的真实数据进行测试,并且您获得的改进也适用于原始数据。
2.更改新测试表的性能 (或者在步骤1中使用我上面的示例来获得更好的结果)。
一旦我们有了新的测试表设置并填充了随机数据,我们应该检查上面的建议,并更改表格以加快它:
- 将TEXT更改为VARCHAR(255)。
- 选择并创建一个包含两个或三个的主键唯一索引
列。在第一个版本中使用版本自动增量和fk_entity进行测试
测试。
- 必要时对表进行分区,并检查它是否提高了速度。一世
建议不要在你的第一次测试中对它进行分区,以便
通过更改数据类型和mysql来检查实际性能增益
组态。检查以下链接 分区和
改进提示。
- 优化和修复您的桌子。索引将再次进行,并将
速度搜索很多:
优化表 test
。entity_versionable
;
修理表 test
。entity_versionable
;
*制作一个脚本来执行优化并使您的索引保持最新,每晚启动它。
3.改进MySQL和硬件配置 仔细阅读以下主题。他们值得一读,我相信你会得到更好的结果。
- 轻松提高您的数据库硬盘配置花费一点
钱:如果可能的话,使用SSD作为主MySQL数据库,以及a
独立机械硬盘用于备份目的。设置MySQL日志
保存在另一个第三个硬盘上以提高你的速度
插入。 (记得在一些之后对机械硬盘进行碎片整理
周)。
- 绩效链接: 一般与多芯, 组态,
优化IO, Debiancores, 最好的配置,
配置48gb ram..
- 分析SQL查询: 如何配置查询, 检查查询中是否存在瓶颈
- MySQL非常耗费内存,如果使用低延迟CL7 DDR3内存
可能。有点偏离主题,但如果你的系统数据很关键,你可能会寻找ECC内存,但价格昂贵。
4.最后,测试测试表中的INSERT和SEARCH。 我使用上面的表模式测试了带有200万随机数据的测试,它花了0,001秒来插入新行,大约2分钟来搜索和选择1亿行。然而它只是一个测试,似乎是好结果:)
5.我的系统配置:
- 数据库: MySQL 5.6.10 InnoDB数据库(测试)。
- 处理器: AMD Phenom II 1090T X6核心,每核心3910Mhz。
- 内存: 16GB DDR3 1600Mhz CL8。
- 高清: SSD中的Windows 7 64位SP1,安装在SSD中的mySQL,用机械硬盘写的日志。
可能我们应该得到更好的结果,其中一个最新的英特尔i5或i7轻松超频到4500Mhz +,因为 MySQL只为一个SQL使用一个核心。核心速度越高,执行速度越快。
6.阅读有关MySQL的更多信息:
O'Reilly高性能MySQL
MySQL优化SQL语句
7.使用其他数据库:
MongoDB或 Redis的 对于这种情况将是完美的,可能比MySQL快很多。两者都很容易学习,两者都有其优点:
- MongoDB: MongoDB日志文件增长
Redis的
我肯定会去 Redis的。如果您学习如何在Redis中保存日志,那么这将是以极快的速度管理日志的最佳方式:
redis用于记录
如果您使用Redis,请记住以下建议:
Redis是用C编译的,它存储在内存中,有一些不同
自动将信息保存到磁盘的方法
(坚持),你可能不必担心它。 (万一发生灾难
场景你将结束大约1秒的日志记录)。
Redis用于许多管理数TB数据的站点,
有很多方法可以处理疯狂的信息量
它意味着它的安全(在这里用于stackoverflow,暴雪,推特,你的...)
由于你的日志非常大,它需要适合内存
无需访问硬盘即可获得速度。你可以
保存不同日期的不同日志,并仅设置其中一些日志
记忆。在达到内存限制的情况下,你不会有任何错误,一切仍然可以正常工作,但检查 Redis Faqs 了解更多信息。
我完全相信Redis会比这更快
MySQL的。您将需要了解如何玩 lists
和
sets
更新数据和查询/搜索数据。如果您可能需要真正高级的查询搜索,那么您应该使用MongoDB,但在这种情况下,简单的日期搜索将非常适合Redis。
Nice Redis的文章 Instagram博客。
1
在工作中,由于客户条件(金融部门),我们几乎在每张桌子上都有日志。
我们这样做了:两个表(“普通”表和日志表)然后触发正常表的插入/更新/删除,它们存储关键字(I,U,D)和旧记录(更新时) ,删除)或新的一个(插入时)在logtable中
我们在同一个数据库模式中有两个表
0