在实体上存储更改:MySQL是否是正确的解决方案?

时间:2013-06-11 11:15:00

标签: mysql database-design entity-attribute-value database-partitioning temporal-database

我想存储我在“实体”表上所做的更改。这应该像一个日志。目前,它在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天内特定实体和实体类型的变化。例如:在过去15天内,最后一次更改SOMEDOUBLE为entity_id 123

现在,有两件事我不喜欢:

  1. 所有数据都存储为TEXT - 虽然大多数(小于1%)不是真正的文本,但就我而言,大多数值都是DOUBLE。这是一个大问题吗?
  2. 插入表时,表格变得非常非常慢,因为表格已经有2亿行。目前我的服务器负载高达10-15。

  3. 我的问题:我如何解决这两个“瓶颈”?我需要扩展。

    我的方法是:

    1. 像这样存储:http://sqlfiddle.com/#!2/df9d0(点击浏览) - 将更改存储在entitychange表中,然后根据其entitychange_[bool|timestamp|double|string]
    2. 中的数据类型存储值
    3. HASH(entity_id)使用分区 - 我想到了~50个分区。
    4. 我应该使用其他数据库系统,也许是MongoDB?

8 个答案:

答案 0 :(得分:5)

如果我遇到你提到的问题,我会设计如下表所示的LOG表:

  1. EntityName :(字符串)正被操纵的实体。(必填)
  2. ObjectId:被操纵的实体,主键。
  3. FieldName :(字符串)实体字段名称。
  4. OldValue :(字符串)实体字段旧值。
  5. NewValue :(字符串)实体字段新值。
  6. UserCode:应用程序用户唯一标识符。 (强制)
  7. TransactionCode:任何更改实体的操作都需要有一个唯一的事务代码(如GUID)(必填),
    如果更改多个字段的实体更新,这些列将是跟踪更新(交易)中所有更改的关键点
  8. ChangeDate:交易日期。 (强制)
  9. FieldType:显示字段类型(如TEXT或Double)的枚举或文本。 (强制)
  10. 采用这种方法
    可以跟踪任何实体(表格)。报告将是可读的。只记录更改。
    事务代码将是检测更改的关键点通过一个动作。
    顺便说一句

    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 could be found here

    )的有用比较

    希望对你有所帮助。

答案 1 :(得分:3)

现在我想我明白了你需要的东西,一个可更改记录历史的可版本表。这可能是实现相同目标的另一种方法,您可以轻松地进行一些快速测试,以确定它是否比您当前的解决方案提供更好的性能。它是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))
);


可能是提高性能的另一个步骤,可能是将所有历史记录日志记录保存在单独的表中,每月一次左右。这样你就不会在每个表中都有很多记录,按日期搜索会非常快。

答案 2 :(得分:2)

这里有两个主要挑战:

  1. 如何有效地存储数据,即占用更少的空间和易于使用的格式
  2. 2-3。管理大表:归档,便于备份和恢复

    2-3。性能优化:更快的插入和选择

    有效存储数据

    1. value已提交。我建议你做VARCHAR (N)。 原因:

      • 由于数据类型的原因,使用N <255将每行节省1个字节。
      • 使用此字段的其他数据类型:固定类型使用空间,无论值是什么,通常每行8个字节(datetime,long integer,char(8)),其他变量数据类型对于此字段来说太大
      • 同样TEXT数据类型会导致性能下降:(来自BLOB and Text data types上的manaul)
    2.   

      使用临时表处理的查询结果中TEXT列的实例会导致服务器在磁盘而不是内存中使用表,因为MEMORY存储引擎不支持这些数据类型。使用磁盘会导致性能下降,因此只有在确实需要时才在查询结果中包含BLOB或TEXT列。例如,避免使用选择所有列的SELECT *。

           

      每个BLOB或TEXT值在内部由单独分配的对象表示。这与所有其他数据类型形成对比,在打开表时,每列分配一次存储。

      基本上TEXT用于存储大字符串和拼接文本,而VARCHAR()设计为相对较短的字符串。

      1. id字段。 (更新,感谢@steve)我同意这个字段没有任何有用的信息。使用3列作为主键:entity_identitype以及whenTIMESTAMP将很好地保证您不会重复。同样的列也将用于分区/子分区。
      2. 表格可管理性 有两个主要选项:MERGE表和分区。 MERGE存储引擎基于My_ISAM,据我所知,它正在逐步淘汰。以下是对[MERGE存储引擎]的一些解读。2

        主要工具是分区,它提供两个主要好处: 1.分区切换(通常是对大块数据的即时操作)和滚动窗口场景:在一个表中插入新数据,然后立即将所有数据切换到存档表中。 2.按排序顺序存储数据,启用分区修剪 - 仅查询包含所需数据的分区。 MySQL允许子分区进一步分组数据。

        entity_id分区是有道理的。如果您需要长时间查询数据,或者在查询表时有其他模式 - 请使用该列进行子分区。除非在该级别切换分区,否则不需要对所有主键列进行子分区。

        分区数取决于您希望该分区的db文件有多大。子分区数量取决于核心数量,因此每个核心可以搜索自己的分区,N-1子分区应该没问题,所以1核心可以做整体协调工作。

        <强>优化

        插入内容:

        • 在没有索引的情况下,表格上的插入更快,因此插入大块数据(进行更新),然后创建索引(如果可能)。

        • Text更改Varchar - 数据库引擎需要一些压力

        • 最小的日志记录和表锁可能会有所帮助,但通常无法使用

        选择

        • TextVarchar肯定会有所改善。

        • 拥有包含最新数据的当前表 - 过去15天,然后通过分区切换进行归档。在这里,您可以选择将表分区与归档表不同(例如,先按日期,然后是entity_id),并通过将小(1天)数据移动到临时表以及更改分区来更改分区方式。

          < / LI>

        此外,您可以考虑按日期分区,您在日期范围内有很多查询。首先使用您的数据及其部分,然后确定哪种模式最适合它。

        至于你的第三个问题,我不知道MongoDB的使用将如何特别有利于这种情况。

答案 3 :(得分:1)

这被称为temporal database,研究人员一直在努力寻找存储和查询时态数据超过20年的最佳方式。

尝试存储EAV数据的效率很低,因为在TEXT列中存储数字数据会占用大量空间,而且您的表越来越长,正如您所发现的那样。

另一个有时称为第六范式的选项(虽然有6NF有多个不相关的定义),是存储一个额外的表来存储您想要暂时跟踪的每列的修订。这类似于@ xtrm的答案提出的解决方案,但它不需要存储未更改的列的冗余副本。但它确实导致了桌子数量的激增。

我开始阅读Anchor Modeling,它承诺处理结构和内容的时间变化。但我还不太清楚它是否足以解释它。我只是链接到它,也许它对你有意义。

以下是一些包含时态数据库讨论的书籍:

答案 4 :(得分:1)

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引擎。您不需要此(这些)无约束,仅插入表的事务。

答案 5 :(得分:1)

为什么INSERTing如此缓慢,你可以做些什么来加快速度。

这些是我要看的东西(大致按照我将通过它们的顺序):

  1. 创建一个新的AUTO_INCREMENT-id并将其插入主键需要一个锁(InnoDB中有一个特殊的AUTO-INC锁,它在语句结束前一直保持,有效地充当表在你的场景中锁定)。这通常不是问题,因为这是一个相对较快的操作,但另一方面,如果(Unix)加载值为10到15,您可能会有进程等待该锁被释放。根据您提供的信息,我认为您的代理键'id'没有任何用处。查看删除该列是否会显着改变性能。 (顺便说一句,没有规则表需要一个主键。如果你没有主表,那没关系)

  2. 对于INSERT,InnoDB可能相对昂贵。这是为了允许诸如交易之类的附加功能而进行的权衡,可能会或可能不会影响您。由于您的所有操作都是原子操作,因此我认为不需要进行交易。也就是说,试试MyISAM吧。注意:对于大型表,MyISAM通常是一个糟糕的选择,因为它只支持表锁定而不是记录级别锁定,但它支持concurrent inserts,因此它可能是一个选择(特别是如果你删除了主键,见上文)

  3. 您可以使用数据库存储引擎参数。 InnoDB和MyISAM都有可以改变的选项。其中一些对TEXT数据的实际存储方式有影响,另一些则具有更广泛的功能。你应该特别注意的是innodb_flush_log_at_trx_commit

  4. 如果(并且仅当)它们具有非NULL值,则TEXT列相对昂贵。您当前正在该TEXT列中存储所有值。值得尝试以下操作:在表中添加额外的字段value_intvalue_double,并将这些值存储在相应的列中。是的,这将浪费一些额外的空间,但可能会更快 - 但这在很大程度上取决于数据库存储引擎及其设置。请注意,很多人对TEXT列性能的看法并不正确。 (见my answer to a related question on VARCHAR vs TEXT

  5. 您建议在多个表格上传播信息。如果您的表完全独立,这只是一个好主意。否则,对于任何更改,您最终都会有多个INSERT操作,并且您很可能会使事情变得更糟。虽然规范化数据通常是好的(tm),但这可能会损害性能。

  6. 如何使SELECT快速运行

    1. 正确的密钥。和正确的钥匙。以防我忘记提及:正确的钥匙。您没有详细说明您的选择是什么样的,但我认为它们类似于“SELECT * FROM entitychange WHERE entity_id = 123 AND ts&gt; ...”。 entity_id和ts上的单个复合索引应足以使此操作快速。由于必须使用每个INSERT更新索引,因此可能值得尝试entity_id, tsts, entity_id的性能:它可能会产生影响。

    2. 分区。如果您没有在问题中提出问题,我甚至不会提起这个问题。你没有说你为什么要分区表。在性能方面,如果你有合适的密钥,它通常没有区别。有一些特定的设置可以提高性能,但你需要适当的硬件设置来配合这一点。如果您决定对表进行分区,请考虑使用entity_id或TIMESTAMP列进行分区。使用时间戳,最终可能会将归档系统与旧数据放​​在归档驱动器上。但是,这样的分区系统需要一些维护(随着时间的推移添加分区)。

    3. 在我看来,你并不关心查询性能和原始插入速度,所以我不会详细介绍SELECT性能。如果您对此感兴趣,请提供更多详细信息。

答案 6 :(得分: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分钟的表格中插入2亿行,请查看peterm已回答best ways to fill a table之一的其他问题。它完美无缺。

执行以下查询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来检查实际性能增益 组态。请查看以下链接,了解一些partition and improvement tips
  • 优化并修复您的桌子。索引将再次发布并且将会 速度搜索很多:
  

OPTIMIZE TABLE testentity_versionable;
  修理表testentity_versionable;   
  *制作一个脚本来执行优化并使您的索引保持最新,每晚启动它。


第3。仔细阅读以下主题,改进您的MySQL和硬件配置。他们值得一读,我相信你会得到更好的结果。

<强> 4。最后,在测试表中测试你的INSERT和SEARCH。我的测试是用上面的表模式的+200万随机数据,它花费0,001秒来插入新行,大约2分钟搜索和SELECT 100百万行。然而它只是一个测试,似乎是好结果:)

<强> 5。我的系统配置:

  • 数据库: MySQL 5.6.10 InnoDB数据库(测试)。
  • 处理器: AMD Phenom II 1090T X6核心,每核心3910Mhz。
  • RAM: 16GB DDR3 1600Mhz CL8。
  • 高清: SSD中的Windows 7 64位SP1,安装在SSD中的mySQL,用机械硬盘写的日志。
    可能我们应该得到更好的结果,其中一个最新的英特尔i5或i7轻松超频到4500Mhz +,自MySQL only uses one core for one SQL以来。核心速度越高,执行速度越快。

<强> 6。阅读更多关于MySQL的信息:
O'Reilly High Performance MySQL
MySQL Optimizing SQL Statements


7。使用其他数据库: MongoDB或Redis对于这种情况将是完美的,并且可能比MySQL快得多。两者都很容易学习,两者都有其优点:
- MongoDB:MongoDB log file growth

Redis的

我肯定会选择 Redis 。如果您学习如何在Redis中保存日志,那么这将是以极快的速度管理日志的最佳方式: redis for logging
如果您使用Redis,请记住以下建议:

  • Redis是用C编译的,它存储在内存中,有一些不同 自动将信息保存到磁盘的方法 (持久性),你可能不必担心它。 (万一发生灾难 您将结束大约1秒的日志记录)。

  • Redis用于管理数TB数据的很多站点, 有很多方法可以处理疯狂的信息量 它意味着它的安全性(在这里用于stackoverflow,暴雪,推特,youporn ......)

  • 由于您的日志非常大,因此需要适合内存 无需访问硬盘即可获得速度。你可以 保存不同日期的不同日志,并仅设置其中一些日志 记忆。如果达到内存限制,您将不会有任何错误,一切仍然可以正常工作,但请查看Redis Faqs以获取更多信息。

  • 我完全相信Redis会比这更快 MySQL的。您需要了解如何使用lists和。{ sets更新数据和查询/搜索数据。如果您可能需要非常高级的查询搜索,那么您应该使用MongoDB,但在这种情况下,简单的日期搜索将非常适合Redis。

Instagram Blog.

中的

Nice Redis文章

答案 7 :(得分:0)

在工作中,由于客户条件(金融部门),我们几乎每张桌子上都有日志。

我们这样做了:两个表(“普通”表和日志表)然后触发插入/更新/删除正常表,它们存储关键字(I,U,D)和旧记录(在更新,删除时)或在日志表中的新文件(插入时)

我们在同一个数据库架构中有两个表