【问题标题】:Opinions on sensor / reading / alert database design关于传感器/读取/警报数据库设计的意见
【发布时间】:2011-05-19 02:57:52
【问题描述】:

我最近问了一些关于数据库设计的问题,可能太多了 ;-) 但是我相信我的设计正在慢慢地触及问题的核心,并且正在慢慢地把它煮沸。关于如何将“警报”存储在数据库中,我仍在纠结几个决定。

在这个系统中,警报是一个必须被确认、采取行动等的实体。

最初我将读数与这样的警报相关联(非常减少):-

[Location]
LocationId

[Sensor]
SensorId
LocationId
UpperLimitValue
LowerLimitValue

[SensorReading]
SensorReadingId
Value
Status
Timestamp

[SensorAlert]
SensorAlertId

[SensorAlertReading]
SensorAlertId
SensorReadingId

最后一个表格将读数与警报相关联,因为读数指示传感器是否处于警报状态。

这种设计的问题在于,它允许将来自多个传感器的读数与单个警报相关联 - 而每个警报仅针对单个传感器,并且应该只具有与其关联的传感器的读数(我是否应该为数据库允许这样做吗?)。

我想简化一些事情,为什么还要麻烦 SensorAlertReading 表?相反,我可以这样做:

[Location]
LocationId

[Sensor]
SensorId
LocationId

[SensorReading]
SensorReadingId
SensorId
Value
Status
Timestamp

[SensorAlert]
SensorAlertId
SensorId
Timestamp

[SensorAlertEnd]
SensorAlertId
Timestamp

基本上我现在没有将读数与警报相关联 - 相反,我只知道警报在特定传感器的开始和结束时间之间处于活动状态,如果我想查找该警报的读数,我可以做.

显然,缺点是我不再有任何约束阻止我删除警报期间发生的读数,但我不确定约束是否必要。

现在作为开发人员/DBA 从外部看,这会让你想生病还是看起来合理?

我可能会想念另一种方法吗?

谢谢。

编辑: 这是另一个想法——它以不同的方式工作。它将每个传感器状态变化存储在一个表格中,从正常到警报,然后读数与特定状态简单关联。这似乎解决了所有问题——你怎么想? (我唯一不确定的是将表称为“SensorState”,我不禁想到有一个更好的名称(也许 SensorReadingGroup?):-

[Location]
LocationId

[Sensor]
SensorId
LocationId

[SensorState]
SensorStateId
SensorId
Timestamp
Status
IsInAlert

[SensorReading]
SensorReadingId
SensorStateId
Value
Timestamp

必须有一个优雅的解决方案!

【问题讨论】:

  • 赏金似乎已经消失了。根据FAQ Bounty,我认为您可能必须再次取消选择/选择问题才能申请赏金。

标签: sql database database-design relational-database


【解决方案1】:

2011 年 1 月 1 日 21:50 UTC 修订

数据模型

我认为您的数据模型应该如下所示:▶Sensor Data Model◀。 (第 2 页与您的其他问题有关历史)。

不熟悉关系建模标准的读者可能会发现▶IDEF1X Notation◀ 很有用。

商业(评论中制定的规则)

我确实发现了一些早期的业务规则,现在已经过时了,所以我删除了它们

这些可以在关系中“读取”(与数据模型相邻读取)。业务规则和所有隐含的引用和数据完整性可以在任何 ISO SQL 数据库中的 RULES、CHECK 约束中实施并因此得到保证。这是 IDEF1X 在关系键以及实体和关系的开发中的演示。请注意,动词短语不仅仅是蓬勃发展。

除了三个参考表之外,唯一的静态标识实体是 Location、NetworkSlave 和 User。传感器是系统的核心,所以我给了它自己的标题。

位置

  • Location 包含一对多 Sensors
  • Location 可能有一个 Logger

NetworkSlave

  • NetworkSlave 收集一对多网络传感器的读数

用户

  • User 可以保持零对多 Locations
  • User 可以保持零对多 Sensors
  • User 可以保持零对多 NetworkSlaves
  • User 可以执行零对多 Downloads
  • User 可以生成零对多Acknowledgements,每个Alert
  • User 可以采用零对多 Actions,每个 ActionType

传感器

  • SensorType 安装为零对多 Sensors

  • 一个Logger(房屋和)收集Readings一个LoggerSensor

  • Sensor或者一个NetworkSensor 或者一个LoggerSensor

    • 一个NetworkSensor记录Readings被一个NetworkSlave收集
      .
  • Logger 定期Downloaded 一对多
    • 一个LoggerSensor记录Readings被一个Logger收集
      .
  • Reading 可以被视为Alert,其中一个AlertType
    • AlertType 可能发生在零对多 Readings
      .
  • 一个Alert 可能是一个Acknowledgement,由一个用户 .
  • Acknowledgement 可以被一个Action、一个ActionType、一个User 关闭
    • ActionType 可以采用零对多 Actions

评论回复

  1. 在所有移动的东西上粘贴Id 列会干扰标识符的确定,标识符是赋予数据库关系“权力”的自然关系键。它们是代理键,这意味着附加键和索引,它阻碍了这种关系能力;这导致比其他必要的连接更多。因此,我仅在关系键变得过于繁琐而无法迁移到子表(并接受强加的额外连接)时才使用它们。

  2. 可空键是非规范化数据库的典型症状。数据库中的空值对性能来说是个坏消息;但是 FK 中的 Nulls 意味着每个表都在做太多的事情,有太多的含义,结果是非常糟糕的代码。适合喜欢“重构”数据库的人;对于关系型数据库来说完全没有必要。

  3. 已解决:Alert 可能是 AcknowledgedAcknowledgement 可能是 Actioned

  4. 行上方的列是主键(请参阅 Notation 文档)。 SensorNoLocationId 内的序号;参考业务规则,在Location之外没有意义;两列一起形成PK。当您准备好插入传感器时(在您检查尝试是否有效等之后),它的推导如下。这不包括 LoggerSensors,它们为零:

    INSERT Sensor VALUES (
        @LocationId,
        SensorNo = ( SELECT ISNULL(MAX(SensorNo), 0) + 1
            FROM Sensor
            WHERE LocationId = @LocationId
            )
        @SensorCode
        )
  5. 为了准确或改进含义,我已将NetworkSlave monitors NetworkSensor 更改为NetworkSlave collects Readings from NetworkSensor

  6. 检查约束。 NetworkSensorLoggerSensorSensor 的独占子类型,它们的完整性可以通过 CHECK 约束来设置。 Alerts, AcknowledgementsActions 不是子类型,但是它们的完整性是通过相同的方法设置的,所以我将它们一起列出。

    • 数据模型中的每个关系都在子(或子类型)中作为 FOREIGN KEY (child_FK_columns) REFERENCES Parent (PK_columns) 的 CONSTRAINT 实现

    • 需要鉴别器来识别Sensor 是哪个子类型。这是SensorNo = 0 代表LoggerSensorsNetworkSensors 非零。

    • NetworkSensorsLoggerSensors 的存在分别被 FK CONSTRAINTS 约束为 NetworkSlaveLogger;以及传感器。
    • NetworkSensor 中包含一个 CHECK 约束以确保 SensorNo 不为零
    • LoggerSensor 中,包含一个 CHECK 约束以确保 SensorNo 为零

    • AcknowledgementsActions 的存在受已识别的 FK CONSTRAINTS 约束(没有 AlertAcknowledgement 不能存在;如果没有 AcknowledgementAction 不能存在)。相反,没有AcknowledgementAlert 处于未确认状态; AlertAcknowledgement 但没有 Action 处于已确认但未执行的状态。 .

  7. 警报。这种(实时监控和警报)应用程序的设计概念是许多独立运行的小程序;所有使用数据库作为单一版本的事实。一些程序插入行(Readings, Alerts);其他程序轮询数据库是否存在此类行(并发送 SMS 消息等;或手持设备仅接收与该设备相关的警报)。从这个意义上说,db 可以被描述为一个消息框(一个程序将行放入,另一个程序读取并执行操作)。

    假设是,SensorsReadings 正在被NetworkSlave“实时”记录,并且每隔一分钟左右,就会插入一组新的Readings。后台进程定期执行(每分钟或任何时间),这是主要的“监控”程序,它将在其循环中具有许多功能。一个这样的功能将是监视Readings 并生成自上次迭代(程序循环)以来发生的Alerts

    以下代码段将在循环中执行,每个 AlertType 一个。是经典投影:

    -- Assume @LoopDateTime contains the DateTime of the last iteration
    INSERT Alert
        SELECT LocationId,
               SensorNo,
               ReadingDtm,
               "L"          -- AlertType "Low"
            FROM Sensor  s,
                 Reading r
            WHERE s.LocationId = r.LocationId
            AND   s.SensorNo   = r.SensorNo
            AND   r.ReadingDtm > @LoopDtm
            AND   r.Value      < s.LowerLimit
    INSERT Alert
        SELECT LocationId,
               SensorNo,
               ReadingDtm,
               "H"          -- AlertType "High"
            FROM Sensor  s,
                 Reading r
            WHERE s.LocationId = r.LocationId
            AND   s.SensorNo   = r.SensorNo
            AND   r.ReadingDtm > @LoopDtm
            AND   r.Value      > s.UpperLimit
    所以Alert 绝对是一个事实,它作为数据库中的一行存在。随后,Acknowledged 可能是 User(另一行/事实),Actioned 可能是 ActionTypeUser

    其他这个(通过投影法创建),即。一般且不变的情况,我将Alert 仅作为Alert 中的一行引用;创建后的静态对象。

  8. 对更改 Users 的担忧。这已经处理好了,如下所示。在我(昨天修订的)答案的顶部,我声明主要的识别元素是静态的。我重新排序了业务规则以提高清晰度。

    • 出于您提到的原因,User.Name 不是 User 的好 PK,尽管它仍然是备用键(唯一)并且用于人机交互。

    • User.Name不能重复,Fred不能超过一个;可以是FirstName-LastName;两个Fred Bloggs,但不是User.Name。我们的第二个 Fred 需要选择另一个 User.Name。注意确定的指数。

    • UserId是永久记录,已经是PK了。永远不要删除User,它具有历史意义。事实上,FK 约束会阻止你(永远不要在真正的数据库中使用 CASCADE,那是纯粹的精神错乱)。无需代码或触发器等。

    • 或者(删除从未做过任何事情的Users,从而释放User.Name 以供使用)只要不违反FK(即UserId 不是Download, Acknowledgement, Action 中引用)。

    为了确保只有当前的Users 执行Actions,在用户(DM 更新)中添加一个IsObsolete 布尔值,并在查询该表以获取任何功能(报告除外)时检查该列您可以实现一个视图UserCurrent,它只返回那些Users

    LocationNetworkSlave 也是如此。如果您需要区分当前与历史,请告诉我,我也会在其中添加 IsObsolete

    我不知道:您可以定期清除古代历史数据的数据库,删除(例如)超过 10 年的行。这必须首先从底部(表格)开始,处理关系。

欢迎提问。

请注意,IDEF1 Notation 文档已扩展。

【讨论】:

  • @PerformanceDBA:感谢您的回答,但我看不到传感器数据模型 - 我可以看到“马克的传感器数据模型”,然后除了指向 IDEF1X 符号的链接和然后是笔记..
  • @Mark:将内联更改为可见链接。
  • @PerformanceDBA:非常感谢。我会研究它并回复你。乍一看,我确实有一个问题:关于 SensorNo;我如何得出它?换句话说,它来自哪里?
  • @马克。我们为什么要关心差距?其他SensorNos 保持不变。当我们不使用Ids 时,我们不会遭受 IDENTITY 列的所有疯狂和限制。一般来说,如果你需要一个没有空格的 Identifier,那么它是有意义的,你不能使用 IDENTITY 列。
  • @PerformanceDBA:我将猜测一下记录器读数下载。我想我需要一个新的表格:LoggerDownload (LocationId[PK], SensorName[PK], DownloadTimestamp[PK], UserId[FK], FirstReadingTimestamp, LastReadingTimestamp).. 基本上每个读数(从logger)我需要能够确定谁下载了记录器和下载时间。抱歉,我希望这不会破坏设计太多!
【解决方案2】:

这是我对这个问题的两分钱。

AlertType 表包含所有可能的警报类型。 AlertName可能是高温、低压、低水位等。

AlertSetup 表允许为特定警报类型设置来自传感器的警报阈值。 例如,TresholdLevel = 100 和 TresholdType = 'HI' 应触发读数超过 100 的警报。

Reading 表保存传感器读数,因为它们被流式传输到服务器(应用程序)。

Alert 表包含所有警报。它保留了触发警报的第一个读数和完成警报的最后一个读数的链接(FirstReadingIdLastReadingId)。如果 (SensorId, AlertTypeId) 组合存在活动警报,IsActive 为真。 IsActive 只能通过低于警报阈值的读数设置为 false。 IsAcknowledged 表示操作员已确认警报。

  1. 应用层将新读数插入到Reading表中,捕获ReadingId

  2. 然后应用程序根据每个(SensorIdAlertTypeId)组合的警报设置检查读数。此时会创建对象集合{SensorId, AlertTypeId, ReadingId, IsAlert},并为每个对象设置IsAlert 标志。

  3. 然后检查 Alert 表以查找集合中每个对象 {SensorId, AlertTypeId, ReadingId, IsAlert} 的活动警报。

    • 如果 IsAlert 为 TRUE,并且没有针对 (SensorId, AlertTypeId) 组合的活动警报,则会在 Alert 表中添加新行FirstReadingID 指向当前的ReadingIdIsActive 设置为 TRUE,IsAcknowledged 设置为 FALSE。

    • 1234563
  4. 如果 IsAlert 为 FALSE,并且 (SensorId, AlertTypeId) 组合存在活动警报,则通过设置 IsActive FALSE 更新该行。

    李>
  5. 如果 IsAlert 为 FALSE,并且 (SensorId, AlertTypeId) 组合没有活动警报,则不会修改 Alert 表。

【讨论】:

  • @Damir:谢谢 :-) 我对 NULLable 外键有点不舒服,因为我读过很多次它表明设计不好。这是否属实我不确定!我想这是主观的,因为该领域的很多人似乎都在使用它们。
  • @Mark -- 只有Alert.LastReadingId 可以为空,但并非必须如此。将行插入Alert 表时,可以设置为与Alert.FirstReadingId 相同的值。
  • @Damir:读数(值)不属于传感器,在读数中;读数是处于警报状态还是条件问题?
  • @PerformanceDBA -- 是的,阅读表缺少实际值字段。呃。
  • @Damir:你提供了这么好的模型,为什么不更新它,让阅读SO的人看到更正的版本,并进行合理的比较..
【解决方案3】:

您必须在这里处理的主要“三角形”是传感器、[传感器]读数和警报。假设您必须在活动发生时对其进行跟踪(与“一次全部加载”设计相反),您的第三个解决方案类似于我们最近所做的事情。一些调整,它看起来像:

[Location] 
LocationId 

[Sensor] 
SensorId 
LocationId 
CurrentSensorState  --  Denormalized data!

[SensorReading] 
SensorReadingId 
SensorState
Value 
Timestamp 

[SensorStateLog] 
SensorId 
Timestamp 
SensorState
Status   --  Does what?
IsInAlert 
(Primary key is {SensorId, Timestamp})

“SensorState”可以是 SensorStateId,关联的查找表列出(和约束)所有可能的状态。

这个想法是,您的 Sensor 每个传感器包含一行并显示它的 当前 状态。 SensorReading 会随着传感器读数不断更新。如果给定传感器的当前状态发生变化(即新读数的状态与传感器的当前状态不同),则更改当前状态并在 SensorStateLog 中添加一行以显示状态变化。 (或者,您可以使用“状态结束”时间戳更新该传感器的“先前”条目,但编写代码很繁琐。)

传感器表中的 CurrentSensorState 是非规范化数据,但如果维护得当(并且如果您有数百万行),它将大大提高查询当前状态的效率,因此值得付出努力。

所有这一切的明显缺点是警报不再是一个实体,而且它们变得更难以跟踪和识别。如果这些必须易于立即识别和使用,那么您的第三个方案将无法满足您的需求。

【讨论】:

  • 感谢您的回答。是的,经过深思熟虑,这可能是个问题,因为还有其他表需要与警报相关,例如 AlertCorrectiveAction 和 AlertAcknowledgement。如果事实证明我必须有一个 Alert 实体,您认为选项 2 有很多问题吗?它实际上比选项 3 更类似于您的方法 - SensorAlert 表很像您的 SensorStateLog 表,除了它只“记录”警报...
  • 或者我想另一种选择是重新引入具有 ID 列的警报实体,该 ID 列既是与 SensorState 相关的主键,也是外键。
猜你喜欢
  • 1970-01-01
  • 2015-12-08
  • 2016-05-27
  • 2010-10-30
  • 2010-12-05
  • 1970-01-01
  • 2011-11-10
  • 2013-01-23
  • 1970-01-01
相关资源
最近更新 更多