数据库 - 设计一个“事件”表

30

Nettuts+文章中阅读到的建议后,我设计了一个表模式来将高度不稳定的数据与经常被查询的其他表分离,并同时降低整个数据库模式所需的表数目。然而,由于它不遵循规范化的规则,我不确定这是否是一个好主意,我想听听你的建议,以下是一般想法:


在一个 Class Table Inheritance 结构中,我对四种用户进行了建模,在主要的“用户”表中,我存储所有用户的共同数据(id, username, password, 几个 flags等)以及一些时间戳字段(date_created, date_updated, date_activated, date_lastLogin等)。

引用上面提到的Nettuts+文章中的第16条技巧:

例2: 表中有一个 “last_login” 字段。当用户登录网站时,它会更新。但是,每次在表上更新都会导致该表的查询缓存被清除。您可以将该字段放入另一个表中,以最小化对用户表的更新。

现在变得更加复杂了,我需要追踪一些用户统计信息,例如:

  • 用户配置文件被查看的唯一次数
  • 特定类型的用户 中点击广告唯一次数
  • 特定类型的用户 中查看帖子唯一次数
  • 等等……

在我的完全规范化的数据库中,这会增加大约8到10个额外的表,虽然不多,但如果可能的话,我想保持简单,所以我想出了以下“events”表:

|------|----------------|----------------|---------------------|-----------|
| ID   | TABLE          | EVENT          | DATE                | IP        | 
|------|----------------|----------------|---------------------|-----------|
| 1    | user           | login          | 2010-04-19 00:30:00 | 127.0.0.1 |
|------|----------------|----------------|---------------------|-----------|
| 1    | user           | login          | 2010-04-19 02:30:00 | 127.0.0.1 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | created        | 2010-04-19 00:31:00 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | activated      | 2010-04-19 02:34:00 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | approved       | 2010-04-19 09:30:00 | 217.0.0.1 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | login          | 2010-04-19 12:00:00 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | created        | 2010-04-19 12:30:00 | 127.0.0.1 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | impressed      | 2010-04-19 12:31:00 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | clicked        | 2010-04-19 12:31:01 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | clicked        | 2010-04-19 12:31:02 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | clicked        | 2010-04-19 12:31:03 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | clicked        | 2010-04-19 12:31:04 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 15   | user_ads       | clicked        | 2010-04-19 12:31:05 | 127.0.0.2 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | blocked        | 2010-04-20 03:19:00 | 217.0.0.1 |
|------|----------------|----------------|---------------------|-----------|
| 2    | user           | deleted        | 2010-04-20 03:20:00 | 217.0.0.1 |
|------|----------------|----------------|---------------------|-----------|

基本上,ID是指TABLE表中的主键字段(id),我相信其余部分应该很简单。这种设计中我喜欢的一点是,我可以跟踪所有用户登录,而不仅仅是最后一个登录,因此可以利用该数据生成一些有趣的指标。

由于events表的不断增长,我还考虑进行一些优化,例如:

  • #9:由于表的数量和事件的数量是有限的(并且预先确定的),TABLEEVENTS列可以设置为ENUM,而不是VARCHAR,以节省一些空间。
  • #14:使用INET_ATON()IP存储为UNSIGNED INT,而不是VARCHAR
  • DATE存储为TIMESTAMP,而不是DATETIME
  • 使用ARCHIVE引擎(CSV)而不是InnoDB/MyISAM
    • 仅支持INSERTSELECT,并且数据在传输过程中进行了压缩。

总的来说,每个事件只会消耗14个(未压缩)字节,这对于我的流量来说还可以接受。

优点:

  • 能够存储更详细的数据(例如登录)。
  • 不需要设计(和编写)近十个附加表(日期和统计信息)。
  • 减少了每个表的一些列并将易变的数据分离。

缺点:

  • 非关系型(仍不如EAV糟糕):
    • SELECT * FROM events WHERE id = 2 AND table = 'user' ORDER BY date DESC();
  • 每个事件的开销为6个字节(IDTABLEEVENT)。

我更倾向于采用这种方法,因为优点似乎远远超过了缺点,但我仍然有些犹豫...我有什么遗漏的吗?您对此有什么想法?

谢谢!


@coolgeek:

我做的一件稍微有些不同的事情是维护一个entity_type表,并在对象类型列(在您的情况下为“TABLE”列)中使用其ID。 对于event_type表,您还需要执行相同的操作。

只是为了明确,您的意思是我应该添加一个额外的表来映射哪些事件在表中允许,并在事件表中使用该表的主键,而不是使用 TABLE / EVENT 对吗?


@ben:

这些都是从现有数据派生出来的统计数据,是吗?

额外的表大多与统计数据相关,但我没有已经存在的数据,以下是一些示例:

user_ad_stats                          user_post_stats
-------------                          ---------------
user_ad_id (FK)                        user_post_id (FK)
ip                                     ip
date                                   date
type (impressed, clicked)

如果我删除这些表,就没有办法跟踪谁、什么和何时发生了什么事情,不确定视图在这里能起到什么作用。

我同意应该分开处理,但更多的是因为它们是基本不同的数据。一个人是什么,一个人做什么是两回事。我认为波动性并不那么重要。

我听说过两种方法,并且我在MySQL手册中找不到任何说明哪个是正确的。无论如何,我同意您的观点,它们应该是分离的表,因为它们代表了不同类型的数据(与正常方法相比具有更加描述性)。

我觉得你对局面看得太细了。

你的表达式是“User ID from IP IP at time DATE EVENTed to TABLE”,这似乎是合理的,但存在问题。

我所指的“不像EAV那么糟”是指所有记录都遵循线性结构,并且它们非常容易查询,没有分层结构,因此所有查询都可以通过简单的SELECT完成。

关于您的第二个声明,在这里我认为您误解了我的意思;IP地址不一定与用户相关联。表结构应该读取如下:

IP地址(IP)对表的PK(ID)进行了某些操作(EVENT)(TABLE),日期为(DATE)。

例如,在上面的示例的最后一行中,它应该读取IP 217.0.0.1(某个管理员)在2010-04-20 03:20:00删除了用户#2(其最后已知的IP是127.0.0.2)。

您仍然可以将用户事件与用户连接起来,但无法实现外键约束。

确实,这是我的主要关注点。但是我不完全确定这种设计可能出现什么问题,而传统的关系设计不能出现。我可以发现一些警告,但只要处理数据库的应用程序知道它在做什么,我想就不应该有任何问题。

另外一件事也算是这个论点的一部分,那就是我将存储更多的事件,并且每个事件将比原始设计增加一倍以上,使用ARCHIVE存储引擎在这里是很合理的选择,唯一的问题是它不支持FK(也不支持UPDATEDELETE)。

3个回答

5
我强烈推荐这种方法。由于您可能在OLTP和OLAP中使用同一个数据库,因此通过添加一些星型和雪花模式,您可以获得显着的性能优势。
我有一个社交网络应用程序,目前有65个表。我维护一个单独的表来跟踪对象(博客/帖子、论坛/线程、画廊/相册/图像等)的浏览量,另一个表用于对象推荐,第三个表用于总结其他十几个表中的插入/更新活动。
我略微有所不同的做法是维护一个实体类型表,并在对象类型列(在您的情况下为“TABLE”列)中使用其ID。您需要使用事件类型表进行相同的操作。
澄清给Alix - 是的,您要维护一个对象参考表和一个事件参考表(这将是您的维度表)。您的事实表将具有以下字段:
id
object_id
event_id
event_time
ip_address

谢谢您的输入,看到有人走过同样的路而没有遇到问题,让我感到安慰。我在我的回答中添加了一个后续问题,如果您能评论一下,我将不胜感激。=) - Alix Axel
1
谢谢,我会实现这个设计。这将为我节省相当多的工作。 =) - Alix Axel

3
看起来这是一个相当合理的设计,所以我想挑战一下你的一些假设,以确保你对自己正在做的事情有具体的理由。
在我的完全规范化的数据库中,这大约需要增加8到10个附加表。这些都是从现有数据中得出的统计数据,不是吗?为什么这些不能只是视图,甚至是材料化的视图?
收集这些统计数据可能看起来很慢,但是:
- 适当的索引可以使其非常快速 - 这不是常见的操作,因此速度并不重要 - 消除冗余数据可能会使其他常见操作变得快速和可靠
我提出了一个表模式,将高度不稳定的数据与经过重读处理的其他表分开。
我猜你说的是用户事件(只是选择其中一个表),它们可能会非常不稳定,与用户数据分开。我同意应该将其分开,但更多的是因为这是基本上不同的数据。一个人是谁,一个人做了什么是两码事。
我认为不稳定性并不那么重要。数据库管理系统应该已经允许您将日志文件和数据库文件放在不同的设备上,这样就可以实现相同的功能,并且争用不应该是行级锁定的问题。
非关系型(仍不像EAV那么糟糕)。
我认为你在某种程度上忽略了整体,可以这么说。
您的表的谓词将是“用户ID从IP IP传递到TABLE时的时间DATE EVENT” ,这似乎是合理的,但存在问题。(更新:好吧,它有点像那样。)
您仍然可以将用户事件与用户连接起来,但无法实现外键约束。这就是为什么EAV通常有问题的原因;是否确切地是EAV并不重要。在模式中实现约束只需要一两行代码,但在应用程序中可能需要几十行代码,如果多个应用程序在多个位置访问相同的数据,则可以轻松地将其乘以数千行代码。因此,通常,如果您可以使用外键约束防止错误数据,那么您可以保证没有应用程序会这样做。
您可能认为事件并不重要,但例如,广告展示次数是金钱。我肯定希望尽早在设计过程中捕获与广告展示相关的任何错误。
进一步评论:
我可以看出一些注意事项,但只要处理数据库的应用程序知道自己在做什么,我想就不会有任何问题。

有一些注意事项,你可以创建一个非常成功的系统。通过适当的约束系统,你可以说,“如果任何与数据库交互的应用程序不知道自己在做什么,DBMS将标记错误。”这可能需要比你拥有的时间和金钱更多的投入,因此,拥有一些更简单的东西可能比你无法拥有的更完美的东西更好。这就是生活。


谢谢,本!你的回答让我有些困惑,我在我的原始帖子中添加了一个后续问题,如果你能看一下就太好了。 - Alix Axel

0

我无法在Ben的回答中添加评论,所以有两件事情...

首先,在独立的OLAP/DSS数据库中使用视图是一回事;在您的交易数据库中使用它们则完全不同。《高性能MySQL》的作者建议在性能要求高的情况下避免使用视图

关于数据完整性,我同意,并且这是使用星型或雪花模式以“事件”作为中心事实表(以及使用多个事件表,就像我所做的那样)的另一个优点。但是,您不能围绕IP地址设计引用完整性方案


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接