在关系数据库中存储(和访问)历史 1:M 关系的最佳方式是啥?
Posted
技术标签:
【中文标题】在关系数据库中存储(和访问)历史 1:M 关系的最佳方式是啥?【英文标题】:What's the best way to store (and access) historical 1:M relationships in a relational database?在关系数据库中存储(和访问)历史 1:M 关系的最佳方式是什么? 【发布时间】:2009-04-13 20:22:27 【问题描述】:假设示例:
我有汽车和车主。每辆汽车在给定时间属于一个(并且只有一个)所有者,但所有权可能会转移。车主可以随时拥有零辆或多辆汽车。我想要的是将历史关系存储在 mysql 数据库中,以便在给定任意时间时,我可以查找当前对所有者的汽车分配。
即在时间 X(X 可以是现在或过去的任何时间):
谁拥有汽车 Y? 车主 Z 拥有哪些汽车(如果有)?在 SQL 中创建一个 M:N 表(带有时间戳)很简单,但我想避免相关的子查询,因为该表会变大(因此,性能会受到影响)。有任何想法吗?我觉得有一种方法可以通过自己加入这样的表来做到这一点,但我对数据库的经验并不丰富。
更新:我想避免在每行中同时使用“start_date”和“end_date”字段,因为每次插入新行时都需要进行(可能)昂贵的查找。 (另外,它是多余的)。
【问题讨论】:
【参考方案1】:创建第三个名为 CarOwners 的表,其中包含 carid、ownerid 以及 start_date 和 end_date 字段。 购买汽车时,请填写前三个并检查表格以确保没有其他人被列为所有者。如果有,则使用该数据作为 end_date 更新记录。
要查找当前所有者:
select carid, ownerid from CarOwner where end_date is null
在某个时间点查找所有者:
select carid, ownerid from CarOwner where start_date < getdate()
and end_date > getdate()
getdate() 是特定于 MS SQL Server 的,但每个数据库都有一些返回当前日期的函数 - 只是替换。
当然,如果您还想从其他表中获取更多信息,您也可以加入它们。
select co.carid, co.ownerid, o.owner_name, c.make, c.Model, c.year
from CarOwner co
JOIN Car c on co.carid = c.carid
JOIN Owner o on o.ownerid = co.ownerid
where co.end_date is null
【讨论】:
这个问题是它需要对插入进行(可能很昂贵)查找。出于这个原因,我宁愿不使用“end_date”字段(另外,它是多余的)。 丢失 end_date,获取当前所有者:select ownerid from carowner where carid=XX order by start_date desc limit 1
在给定日期获取所有者:select ownerid from carowner where carid=XX and start_date<=YY order by start_date desc limit 1
插入有什么问题?我不明白
@Javier:我喜欢您评论中的想法,您自己的回答至少会为您赢得我的支持。
如果您正确使用了日期时间数据类型并在开始和发送日期上放置了一个索引,那么它根本不是一个昂贵的查找。我也看不出它在哪里是多余的。【参考方案2】:
我发现处理此类要求的最佳方法是只维护 VehicleEvents 的日志,其中之一就是 ChangeOwner。在实践中,您可以得出此处提出的所有问题的答案 - 至少与您收集事件一样准确。
每条记录都有一个时间戳,指示事件发生的时间。
这样做的一个好处是可以在每个事件中添加最少量的数据,但有关车辆的信息可以积累和发展。
此外,通过时间戳,可以在事后添加事件(只要时间戳准确反映事件发生的时间。
试图以我尝试过的任何其他方式来维持此类事件的历史状态会导致疯狂。 (也许我还在恢复中。:D)
顺便说一句,这里的显着特征可能是它是时间序列或事件日志,而不是 1:m。
【讨论】:
【参考方案3】:考虑到每辆车至少属于一个车主的业务规则(即车主在被分配给车之前就存在)以及表可能会变大的操作约束,我将设计架构如下:
(通用 sql 92 语法:)
CREATE TABLE Cars
(
CarID integer not null default autoincrement,
OwnerID integer not null,
CarDescription varchar(100) not null,
CreatedOn timestamp not null default current timestamp,
Primary key (CarID),
FOREIGN KEY (OwnerID ) REFERENCES Owners(OwnerID )
)
CREATE TABLE Owners
(
OwnerID integer not null default autoincrement,
OwnerName varchar(100) not null,
Primary key(OwnerID )
)
CREATE TABLE HistoricalCarOwners
(
CarID integer not null,
OwnerID integer not null,
OwnedFrom timestamp null,
Owneduntil timestamp null,
primary key (cardid, ownerid),
FOREIGN KEY (OwnerID ) REFERENCES Owners(OwnerID ),
FOREIGN KEY (CarID ) REFERENCES Cars(CarID )
)
我个人不会触及客户端应用程序中的第三个表,而只是让数据库完成工作 - 并保持数据完整性 - 在 Cars 表上使用 ON UPDATE
和 ON DELETE
触发器来填充 HistoricalCarOwners
表每当汽车更换车主(即在 OwnerId 列上提交 UPDATE 时)或汽车被删除时。
使用上述模式,选择当前车主很简单,选择历史车主很简单
select ownerid, ownername from owners o inner join historicalcarowners hco
on hco.ownerid = o.ownerid
where hco.carid = :arg_id and
:arg_timestamp between ownedfrom and owneduntil
order by ...
HTH,文斯
【讨论】:
【参考方案4】:如果您真的不想有开始和结束日期,您可以只使用一个日期并执行如下查询。
SELECT * FROM CarOwner co
WHERE co.CarId = @CarId
AND co.TransferDate <= @AsOfDate
AND NOT EXISTS (SELECT * FROM CarOwner co2
WHERE co2.CarId = @CarId
AND co2.TransferDate <= @AsOfDate
AND co2.TransferDate > co.Transferdate)
或略有不同
SELECT * FROM Car ca
JOIN CarOwner co ON ca.Id = co.CarId
AND co.TransferDate = (SELECT MAX(TransferDate)
FROM CarOwner WHERE CarId = @CarId
AND TransferDate < @AsOfDate)
WHERE co.CarId = @CarId
这些解决方案在功能上等同于 Javier 的建议,但根据您使用的数据库,一种解决方案可能比另一种更快。
但是,根据您的读写比率,如果您在关联实体中冗余更新结束日期,您可能会发现性能更好。
【讨论】:
【参考方案5】:为什么没有事务表?其中将包含汽车 ID、FROM 车主、TO 车主和交易发生的日期。
然后,您要做的就是在所需日期之前找到汽车的第一笔交易。
要查找 3 月 1 日所有者 253 拥有的汽车:
SELECT * FROM transactions
WHERE ownerToId
= 253 AND date
> '2009-03-01'
【讨论】:
【参考方案6】:cars 表可以有一个名为 ownerID 的 id,然后你可以简单地
1.select car from cars inner join owner on car.ownerid=owner.ownerid where ownerid=y
2.从owner=z的汽车中选择汽车
不是确切的语法,而是简单的伪代码。
【讨论】:
对于目前的状态来说这很好,但如果我想看一周前的作业怎么办?以上是关于在关系数据库中存储(和访问)历史 1:M 关系的最佳方式是啥?的主要内容,如果未能解决你的问题,请参考以下文章