11

我想将我所做的更改存储在我的“实体”表上。这应该像一个日志。目前它在 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_identity= 我的表的主键。
  • 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。

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

我的方法是:

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

8 回答 8

5

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

  1. EntityName:(字符串)正在被操纵的实体。(强制)
  2. ObjectId: 被操作的实体,主键。
  3. FieldName:(字符串)实体字段名称。
  4. OldValue:(字符串)实体字段旧值。
  5. NewValue:(字符串)实体字段新值。
  6. UserCode:应用程序用户唯一标识符。(强制的)
  7. TransactionCode:任何更改实体的操作都需要有唯一的事务代码(如 GUID)(强制),
    如果实体上的更新更改了多个字段,这些列将是跟踪更新中所有更改的关键点(事务)
  8. ChangeDate: 交易日期。(强制的)
  9. FieldType:枚举或显示字段类型的文本,如 TEXT 或 Double。(强制的)

采用这种方法
可以跟踪任何实体(表)
报告将是可读的
仅记录更改。
事务代码将是通过单个操作检测更改的关键点。

顺便提一句

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)的有用比较

希望有所帮助。

于 2013-06-15T21:02:55.777 回答
3

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


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

于 2013-06-11T16:29:03.703 回答
2

这被称为时态数据库,20 多年来,研究人员一直在努力寻找存储和查询时态数据的最佳方法。

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

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

我已经开始阅读Anchor Modeling,它承诺处理结构和内容的时间变化。但是我还没有很好地理解它来解释它。我只是链接到它,也许它对你有意义。

这里有几本书包含对时间数据库的讨论:

于 2013-06-16T11:48:10.797 回答
2

这里有两个主要挑战:

  1. 如何有效地存储数据,即占用更少的空间并采用易于使用的格式

2-3。管理大表:归档、便于备份和恢复

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

高效存储数据

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

    • 由于数据类型的原因,使用 N<255 将每行节省 1 个字节。
    • 对该字段使用其他数据类型:无论值是多少,固定类型都使用空间,通常每行 8 个字节(日期时间、长整数、char (8)),其他可变数据类型对于该字段来说太大了。
    • 数据类型也会TEXT导致性能损失:(来自BLOB 和 Text 数据类型的手册)

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

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

基本上TEXT是为存储大字符串和一段文本而设计的,而VARCHAR()设计的是相对较短的字符串。

  1. id场地。(更新,感谢@steve)我同意这个字段不包含任何有用的信息。使用 3 列作为主键:entity_identitypewhenTIMESTAMP将很好地保证不会有重复。相同的列也将用于分区/子分区。

表可管理性 有两个主要选项:MERGE 表和分区。MERGE存储引擎基于My_ISAM,据我了解正在逐步淘汰。这是一些关于 [MERGE Storage Engine] 的阅读材料。2

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

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

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

优化

插入:

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

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

  • 最少的日志记录和表锁可能会有所帮助,但通常不太可能使用

选择:

  • TextVarchar绝对应该改进的东西。

  • 有一个包含最近数据的当前表 - 过去 15 天,然后通过分区切换移动到存档。在这里,您可以选择对不同于存档表的表进行分区(例如,首先按日期,然后是 entity_id),并通过将少量(1 天)数据移动到临时表并更改其分区来更改分区方式。

您也可以考虑按日期分区,您对日期范围有很多查询。首先使用您的数据及其部分,然后决定哪种模式最能支持它。

至于您的第三个问题,我看不出使用 MongoDB 将如何特别有益于这种情况。

于 2013-06-14T22:08:05.127 回答
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引擎。对于这个(这些)不受约束的、只插入的表,您不需要事务。

于 2013-06-16T22:36:09.070 回答
1

为什么 INSERTing 这么慢,你能做些什么来让它更快。

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

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

  2. 对于 INSERT,InnoDB 可能相对昂贵。这是为了允许交易等附加功能而做出的权衡,可能会也可能不会影响您。由于您的所有操作都是原子的,我认为不需要事务。也就是说,试试 MyISAM。注意:对于大表来说,MyISAM 通常是一个糟糕的选择,因为它只支持表锁定而不支持记录级锁定,但它确实支持并发插入,所以这里可能是一个选择(特别是如果你确实删除了主键,见上文)

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

  4. TEXT 列在(且仅当)它们具有非 NULL 值时相对昂贵。您当前将所有值存储在该 TEXT 列中。值得尝试以下操作:将额外的字段value_int和添加value_double到您的表中,并将这些值存储在相应的列中。是的,这会浪费一些额外的空间,但可能会更快——但这在很大程度上取决于数据库存储引擎及其设置。请注意,很多人对 TEXT 列性能的看法是不正确的。(请参阅我对 VARCHAR vs TEXT 相关问题的回答

  5. 您建议将信息分散在一张以上的桌子上。如果您的表完全相互独立,这只是一个好主意。否则,对于任何更改,您最终都会有不止一个 INSERT 操作,而且您很可能会使事情变得更糟。虽然规范化数据通常是好的(tm),但它可能会损害这里的性能。

你可以做些什么来让 SELECT 运行得更快

  1. 正确的键。和正确的钥匙。以防万一我忘了提:正确的钥匙。您没有详细说明您的选择是什么样的,但我认为它们类似于“SELECT * FROM entitychange WHERE entity_id=123 AND ts>...”。entity_id 和 ts 上的单个复合索引应该足以使此操作快速进行。由于每次插入都必须更新索引,因此可能值得尝试两者的性能entity_id, ts:ts, entity_id它可能会有所作为。

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

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

于 2013-06-17T20:20:24.020 回答
1

我会建议您进行大量深入测试,但从我的测试中,我使用我之前发布的表定义的 INSERT 和 SELECT 都取得了非常好的结果。我将详细介绍我的测试,以便任何人都可以轻松地重复并检查它是否得到更好的结果。在进行任何测试之前备份您的数据。
我必须说这些只是测试,可能无法反映或改善您的真实案例,但它是一种很好的学习方式,可能是一种找到有用信息和结果的方式。

我们在这里看到的建议非常好,您肯定会注意到通过使用带有大小而不是 TEXT 的预定义类型 VARCHAR 可以大大提高速度。但是你可以提高速度,出于数据完整性的原因,我建议不要使用 MyISAM,而是使用 InnoDB。

测试:

1.设置表和INSERT 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已经回答了填充表格的最佳方法之一。它完美地工作。

执行以下查询 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 配置来检查实际性能增益。检查以下链接以获取一些分区和改进提示
  • 优化和修复您的桌子。将再次建立索引,并将大大加快搜索速度:

优化表testentity_versionable;
维修表testentity_versionable;
*制作一个脚本来执行优化和保持你的索引是最新的,每晚启动它。


3.通过仔细阅读以下线程来改进您的 MySQL 和硬件配置。它们值得一读,我相信你会得到更好的结果。

  • 花一点钱轻松改进您的数据库硬盘配置
    :如果可能的话,为您的主 MySQL 数据库使用 SSD,并
    为备份目的使用独立的机械硬盘。将 MySQL 日志设置为保存在另一个第三个硬盘上,以提高
    INSERT 的速度。(记住几周后对机械硬盘进行碎片整理)。
  • 性能链接:general&multiple-cores , configuration , optimization IO , Debiancores , best configuration , config 48gb ram ..
  • 分析 SQL 查询:如何分析查询检查查询中可能存在的瓶颈
  • MySQL 非常占用内存,如果可能,请使用低延迟 CL7 DDR3 内存。有点跑题了,但是如果您的系统数据很关键,您可能会寻找 ECC 内存,但它很贵。

4. 最后,在测试表中测试您的 INSERT 和 SEARCH。我使用上面的表模式对 +2 亿随机数据进行了测试,它花费 0,001 秒来插入新行,大约 2 分钟来搜索和选择 1 亿行。然而它只是一个测试,似乎是好的结果:)


5.我的系统配置:

  • 数据库: MySQL 5.6.10 InnoDB 数据库(测试)。
  • 处理器: AMD Phenom II 1090T X6 核心,每个核心 3910Mhz。
  • 内存: 16GB DDR3 1600Mhz CL8。
  • HD: Windows 7 64bits SP1 in SSD,mySQL 安装在 SSD,日志写入机械硬盘。
    也许我们应该使用最新的英特尔 i5 或 i7 之一轻松超频至 4500Mhz+ 获得更好的结果,因为MySQL 只为一个 SQL 使用一个内核。核心速度越高,执行的速度就越快。

6. 阅读有关 MySQL 的更多信息:
O'Reilly 高性能 MySQL
MySQL 优化 SQL 语句


7. 使用另一个数据库: MongoDB 或Redis非常适合这种情况,并且可能比 MySQL 快很多。两者都非常容易学习,并且都有各自的优势:
- MongoDB:MongoDB 日志文件增长

雷迪斯

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

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

  • Redis 用于管理 TB 级数据的许多站点,有很多方法可以处理大量信息,这意味着它是安全的(在 stackoverflow、blizzard、twitter、youporn 中使用......)

  • 由于您的日志将非常大,因此它需要放入内存中以便在无需访问硬盘的情况下获得速度。您可以为不同的日期保存不同的日志,并且只在内存中设置其中的一些。在达到内存限制的情况下,您不会有任何错误,一切仍然可以完美运行,但请查看Redis 常见问题以获取更多信息。

  • 我完全确定 Redis 会比 MySQL 快得多。您将需要了解如何使用listssets更新数据以及查询/搜索数据。如果您可能需要真正高级的查询搜索,您应该使用 MongoDB,但在这种情况下,简单的日期搜索将非常适合 Redis。

Instagram 博客中不错的 Redis 文章。

于 2013-06-18T10:57:08.483 回答
0

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

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

我们在同一个数据库模式中有两个表

于 2013-06-16T09:00:27.150 回答