2

我在为具有版本控制的动态属性设计架构时遇到了一些问题。假设以下用例:

我有一个名为的表Actor,其中包含id和 a name(为简单起见)。我的情况的上限是,该表包含大约 100 万个条目。

此外,每个演员都会获得分配给他们的属性。因为当时不知道属性,所以需要一张表来管理属性。我想到了一个Property-table。生成的 n:m 关系将通过一个包含主键和属性值(类型?)之间的表来Actor解决Property

目前,这似乎很容易处理。如果有一百万个条目,每个条目有 10 个属性,则该ActorProperty表将有一千万个节点。我相信使用btree索引 (log2(n)) 这应该没问题。

现在是我正在努力的部分。应该以某种方式跟踪属性。随着时间的推移,这些属性会发生变化,但历史不应丢失。很可能会使用时间戳来完成。请注意,多个属性会同时更新。一个例子是:我每天拍摄所有演员的快照,如果发生变化,我将同时更新所有更改的属性。这导致每年有 365 个时间戳。

如果我使用另一个表来管理版本(时间戳)并向ActorProperty表中添加另一个外键,我将获得 365 * 1000 万个条目。这应该是我能得到的最大值。大多数情况下,数据集会明显变小。

我现在的问题是更多地解决性能问题。我阅读了以下有关索引的答案:数据库索引如何工作。查询具有这么多条目的表不是很慢吗?一个示例查询是:前 100 个演员,其所有属性都在给定的时间戳 id=x 处。我也觉得我正在考虑的模式可能不是最好的。是否有人对具有更高可扩展性的模式有任何建议或想法?

顺便说一句,我目前也在评估 NoSql 方法,所以我想暂时专注于关系方法。我的目标是收集不同技术的优缺点,然后为所描述的用例建立一个理论架构或模型。在关系数据库上使用最佳模型的性能似乎很难评估或发现。

谢谢!

4

4 回答 4

1

应该以某种方式跟踪属性

究竟应该如何跟踪它们是这里重要的事情。以最简单的情况为例,您可能希望在任何给定时间查询状态 - 因此解决方案是在分解表中有多个与时间相关的记录:

create table actor_property (
  actor_id INT NOT NULL,
  property_id INT NOT NULL,
  starttime DATE NOT NULL,
  endtime DATE NOT NULL DEFAULT 99991231
  PRIMARY KEY (actor_id, property_id, starttime, endtime) 
);

这样做的结果是,当您尝试将参与者链接到属性并且链接已存在于表中时,您需要处理这种情况(您无法在触发器中更新表,但您可以检查冲突并强制一个例外)。然后您可以随时通过......查询数据的状态

SELECT a.name, property.name
FROM actor a
INNER JOIN actor_property ap
   ON a.id=ap.actor_id
INNER JOIN property p
   ON p.property_id
WHERE $snapshot_date >= ap.starttime
AND $snapshot_date <= ap.endtime

在上面使用 actor_property 中当前记录的物化视图会稍微快一些 - 取决于关系更改的频率。

查询具有这么多条目的表不是很慢吗?

并非如此,除非您需要经常分析整个数据集,否则大多数操作只查看一小部分行,并且通常数据库会发展热数据区域 - 读取缓存比 mysql 的查询缓存更有效(这是非常具体的) .

于 2012-06-21T12:57:18.007 回答
1

我在其中一个应用程序中使用了类似的设计。

首先,我认为属性集不会那么大(理论上),所以分享它是很好的。为此,我将创建一个PROPERTY_TYPE具有唯一性IDNAME列的表。这样在主PROPERTY表中您将拥有ACTOR_ID,PROPERTY_TYPE_IDVALUE列,这给您带来了 2 个好处:

  1. 由于只为所有用例存储一次属性名称,因此表的大小大大减小;
  2. 查询的性能将显着提高。

现在到财产跟踪。我喜欢及时跟踪对象实例的方法,每个实例都有它的开始和结束时间。可以使用 找到该属性的当前活动实例now() BETWEEN start_dt AND coalesce(end_dt, now()),因为打开的实例end_dt是有效的NULL

架构将如下所示:

CREATE TABLE actor (
    actor_id   integer not null,
    actor_name varchar(100) not null,
    PRIMARY KEY (actor_id)
    );
CREATE TABLE property_type (
    property_type_id   integer not null,
    property_type_name varchar(100) not null,
    PRIMARY KEY (property_type_id),
    UNIQUE (property_type_name)
    );
CREATE TABLE actor_property (
    actor_id         integer not null,
    property_type_id integer not null,
    property_value   varchar(500) not null,
    start_dt         timestamp not null,
    end_dt           timestamp
    PRIMARY KEY (actor_id, property_type_id, start_dt)
    );

实施注意事项:

  1. 更新属性实际上是一个原子关闭实例 + 创建实例操作。因此,最好将它包装到START TRANSACTION; ... COMMIT;块中,或者(我更喜欢)创建一个可以完成这项工作的函数;
  2. 无论如何,使用 DB 端函数是一种很好的风格;
  3. 所有表上的主键后面都有隐式索引,这反过来又会给你预期的性能;
  4. 表中潜在的 365e6 行actor_property对现代硬件来说并不是什么大问题。鉴于您的索引已就位且平衡良好,在最坏的情况下,您将执行最多 30 次磁盘页面读取以从该表中查询单个条目。
于 2012-06-21T13:25:33.397 回答
1

@symcbean 和 @vyegorov 的方法都是正确的——在现代硬件上,简单的查询对于您正在谈论的数据量应该没有问题。

但是,当涉及到您可能需要考虑的查询时,模式设计(通常称为“实体/属性/值”或 EAV)有一些缺点。

常见的关系语句可能会变得非常复杂——而且通常很慢。例如,假设有一个查询要查找属性“height”> 1.9、属性“age”<= 25、属性“agent”不像“sleazeball”以及当前没有出现属性“hard to work”的演员和”。

如果“property_value”列是 varchar,则数值比较往往会违反直觉。

搜索“in”、“not in”等很尴尬。

解释“代理不像 'sleazeball' 可能意味着两件事 - 有一个名为 agent 的属性,它的值不是 sleazeball,或者甚至没有一个名为 agent 的属性。

我提到所有这些问题的原因是为了让你在设计中走得更远——仅仅将性能视为假设的事情是不够的,你需要考虑现实的场景。

于 2012-06-21T13:53:21.180 回答
0

根据您的情况,如果将问题分解为“当前属性”和“过去属性”,您可能会发现性能更好。各种 ORM 都在采用这种方法来实现它们的版本化行为,因为它大大降低了表大小增加的指数成本。

因此,在您的情况下,请考虑您的Actor表与:

  • ActorProperty(fk = actor_id)
  • ActorPropertyVersionable(fk = actor_id, version_num)

因此,在为参与者编写新属性时,应首先复制现有值并将其插入到可版本控制的表中,然后将新值添加到当前表中。将其包装在事务中以确保其安全。

通常,属性查询通常对当前属性值感兴趣,而访问过去值的频率要低得多(当然,您需要对自己的用例做出判断)。每次查询数据确实需要两个不同的查询(当前值、过去值),但性能优势可能是值得的。

于 2012-06-21T17:06:10.223 回答