7

I've asked a few questions lately regarding database design, probably too many ;-) However I beleive I'm slowly getting to the heart of the matter with my design and am slowly boiling it down. I'm still wrestling with a couple of decisions regarding how "alerts" are stored in the database.

In this system, an alert is an entity that must be acknowledged, acted upon, etc.

Initially I related readings to alerts like this (very cut down) : -

[Location]
LocationId

[Sensor]
SensorId
LocationId
UpperLimitValue
LowerLimitValue

[SensorReading]
SensorReadingId
Value
Status
Timestamp

[SensorAlert]
SensorAlertId

[SensorAlertReading]
SensorAlertId
SensorReadingId

The last table is associating readings with the alert, because it is the reading that dictate that the sensor is in alert or not.

The problem with this design is that it allows readings from many sensors to be associated with a single alert - whereas each alert is for a single sensor only and should only have readings for that sensor associated with it (should I be bothered that the DB allows this though?).

I thought to simplify things, why even bother with the SensorAlertReading table? Instead I could do this:

[Location]
LocationId

[Sensor]
SensorId
LocationId

[SensorReading]
SensorReadingId
SensorId
Value
Status
Timestamp

[SensorAlert]
SensorAlertId
SensorId
Timestamp

[SensorAlertEnd]
SensorAlertId
Timestamp

Basically I'm not associating readings with the alert now - instead I just know that an alert was active between a start and end time for a particular sensor, and if I want to look up the readings for that alert I can do.

Obviously the downside is I no longer have any constraint stopping me deleting readings that occurred during the alert, but I'm not sure that the constraint is neccessary.

Now looking in from the outside as a developer / DBA, would that make you want to be sick or does it seem reasonable?

Is there perhaps another way of doing this that I may be missing?

Thanks.

EDIT: Here's another idea - it works in a different way. It stores each sensor state change, going from normal to alert in a table, and then readings are simply associated with a particular state. This seems to solve all the problems - what d'ya think? (the only thing I'm not sure about is calling the table "SensorState", I can't help think there's a better name (maybe SensorReadingGroup?) : -

[Location]
LocationId

[Sensor]
SensorId
LocationId

[SensorState]
SensorStateId
SensorId
Timestamp
Status
IsInAlert

[SensorReading]
SensorReadingId
SensorStateId
Value
Timestamp

There must be an elegant solution to this!

Mark
  • 1,296
  • 13
  • 28
  • The bounty appears to have gone walk-about. As per [FAQ Bounty](http://stackoverflow.com/faq#bounty), I think you may have to unchoose/choose the question again to apply the bounty. – PerformanceDBA Dec 14 '10 at 06:51

3 Answers3

8

Revised 01 Jan 11 21:50 UTC

Data Model

I think your Data Model should look like this:▶Sensor Data Model◀. (Page 2 relates to your other question re History).

Readers who are unfamiliar with the Relational Modelling Standard may find ▶IDEF1X Notation◀ useful.

Business (Rules Developed in the Commentary)

I did identify some early business Rules, which are now obsolete, so I have deleted them

These can be "read" in the Relations (read adjacent to the Data Model). The Business Rules and all implied Referential and Data Integrity can be implemented in, and thus guaranteed by, RULES, CHECK Constraints, in any ISO SQL database. This is a demonstration of IDEF1X, in the development of both the Relational keys, and the Entities and Relations. Note the Verb Phrases are more than mere flourish.

Apart from three Reference tables, the only static, Identifying entities are Location, NetworkSlave, and User. Sensor is central to the system, so I ahve given it its own heading.

Location

  • A Location contains one-to-many Sensors
  • A Location may have one Logger

NetworkSlave

  • A NetworkSlave collects Readings for one-to-many NetworkSensors

User

  • An User may maintain zero-to-many Locations
  • An User may maintain zero-to-many Sensors
  • An User may maintain zero-to-many NetworkSlaves
  • An User may perform zero-to-many Downloads
  • An User may make zero-to-many Acknowledgements, each on one Alert
  • An User may take zero-to-many Actions, each of one ActionType

Sensor

  • A SensorType is installed as zero-to-many Sensors

  • A Logger (houses and) collects Readings for one LoggerSensor

  • A Sensor is either one NetworkSensor or one LoggerSensor

    • A NetworkSensor records Readings collected by one NetworkSlave
      .
  • A Logger is periodically Downloaded one-to-many times
    • A LoggerSensor records Readings collected by one Logger
      .
  • A Reading may be deemed in Alert, of one AlertType
    • An AlertType may happen on zero-to-many Readings
      .
  • An Alert may be one Acknowledgement, by one User .
  • An Acknowledgement may be closed by one Action, of one ActionType, by one User
    • An ActionType may be taken on zero-to-many Actions

Responses to Comments

  1. Sticking Id columns on everything that moves, interferes with the determination of Identifiers, the natural Relational keys that give your database relational "power". They are Surrogate Keys, which means an additional Key and Index, and it hinders that relational power; which results in more joins than otherwise necessary. Therefore I use them only when the Relational key becomes too cumbersome to migrate to the child tables (and accept the imposed extra join).

  2. Nullable keys are a classic symptom of an Unnormalised database. Nulls in the database is bad news for performance; but Nulls in FKs means each table is doing too many things, has too many meanings, and results is very poor code. Good for people who like to "refactor" their databases; completely unnecessary for a Relational database.

  3. Resolved: An Alert may be Acknowledged; An Acknowledgement may be Actioned.

  4. The columns above the line are the Primary Key (refer Notation document). SensorNo is a sequential number within LocationId; refer Business Rules, it is meaningless outside a Location; the two columns together form the PK. When you are ready to INSERT a Sensor (after you have checked that the attempt is valid, etc), it is derived as follows. This excludes LoggerSensors, which are zero:

    INSERT Sensor VALUES (
        @LocationId,
        SensorNo = ( SELECT ISNULL(MAX(SensorNo), 0) + 1
            FROM Sensor
            WHERE LocationId = @LocationId
            )
        @SensorCode
        )
  5. For accuracy or improved meaning, I have changed NetworkSlave monitors NetworkSensor to NetworkSlave collects Readings from NetworkSensor.

  6. Check Constraints. The NetworkSensor and LoggerSensor are exclusive subtypes of Sensor, and their integrity can be set by CHECK constraints. Alerts, Acknowledgements and Actions are not subtypes, but their integrity is set by the same method, so I will list them together.

    • Every Relation in the Data Model is implemented as a CONSTRAINT in the child (or subtype) as FOREIGN KEY (child_FK_columns) REFERENCES Parent (PK_columns)

    • A Discriminator is required to identify which subtype a Sensor is. This is SensorNo = 0 for LoggerSensors; and non-zero for NetworkSensors.

    • The existence of NetworkSensors and LoggerSensors are constrained by the FK CONSTRAINTS to NetworkSlave and Logger, respectively; as well as to Sensor.
    • In NetworkSensor, include a CHECK constraint to ensure SensorNo is non-zero
    • In LoggerSensor, include a CHECK constraint to ensure SensorNo is zero

    • The existence of Acknowledgements and Actions are constrained by the identified FK CONSTRAINTS (An Acknowledgement cannot exist without an Alert; an Action cannot exist without an Acknowledgement). Conversely, an Alert with no Acknowledgement is in an unacknowledged state; an Alert with and Acknowledgementbut no Action is in an acknowledged but un-actioned state. .

  7. Alerts. The concept in a design for this kind of (live monitoring and alert) application is many small programs, running independently; all using the database as the single version of the truth. Some programs insert rows (Readings, Alerts); other programs poll the db for existence of such rows (and send SMS messages, etc; or hand-held units pick up Alerts relevant to the unit only). In that sense, the db is a may be described as an message box (one program puts rows in, which another program reads and actions).

    The assumption is, Readings for Sensors are being recorded "live" by the NetworkSlave, and every minute or so, a new set of Readings is inserted. A background process executes periodically (every minute or whatever), this is the main "monitor" program, it will have many functions within its loop. One such function will be to monitor Readings and produce Alerts that have occurred since the last iteration (of the program loop).

    The following code segment will be executed within the loop, one for each AlertType. It is a classic Projection:

    -- 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
    So an Alert is definitely a fact, that exists as a row in the database. Subsequently that may be Acknowledged by an User (another row/fact), and Actioned with an ActionType by an User.

    Other that this (the creation by Projection act), ie. the general and unvarying case, I would refer to Alert only as a row in Alert; a static object after creation.

  8. Concerns re Changing Users. That is taken care of already, as follows. At the top of my (revised yesterday) Answer, I state that the major Identifying elements are static. I have re-sequenced the Business Rules to improve clarity.

    • For the reasons you mention, User.Name is not a good PK for User, although it remains an Alternate Key (Unique) and the one that is used for human interaction.

    • User.Name cannot be duplicated, there cannot be more than one Fred; there can be in terms of FirstName-LastName; two Fred Bloggs, but not in terms of User.Name. Our second Fred needs to choose another User.Name. Note the identified Indices.

    • UserId is the permanent record, and it is already the PK. Never delete User, it has historical significance. In fact the FK constraints will stop you (never use CASCADE in a real database, that is pure insanity). No need for code or triggers, etc.

    • Alternately (to delete Users who never did anything, and thus release User.Name for use) allow Delete as long as there are no FK violations (ie. UserId is not referenced in Download, Acknowledgement, Action).

    To ensure that only Users who are Current perform Actions, add an IsObsolete boolean in User (DM Updated), and check that column when that table is interrogated for any function (except reports) You can implement a View UserCurrent which returns only those Users.

    Same goes for Location and NetworkSlave. If you need to differentiate current vs historical, let me know, I will add IsObsolete to them as well.

    I don't know: you may purge the database of ancient Historical data periodically, delete rows that are (eg) over 10 years old. That has to be done from the bottom (tables) first, working up the Relations.

Feel free to ask Questions.

Note the IDEF1 Notation document has been expanded.

PerformanceDBA
  • 32,198
  • 10
  • 64
  • 90
  • @PerformanceDBA: Thanks for your answer, I can't see the sensor data model though - I can see "Mark's Sensor Data Model", and then nothing below it except a link to the IDEF1X notation and then the notes.. – Mark Dec 05 '10 at 21:35
  • @Mark: Changed the Inline to a visible Link. – PerformanceDBA Dec 05 '10 at 22:34
  • @PerformanceDBA: Great thanks for that. I'll study it and get back to you. I do have one question from my first glance though: Regarding the SensorNo; how do I derive it? In other words where does it come from? – Mark Dec 05 '10 at 23:03
  • @PerformanceDBA: Okay got it. If a Sensor gets deleted, it shouldn't matter that there's a gap should it? – Mark Dec 05 '10 at 23:25
  • @PerformanceDBA: Regarding (5): Alert Acknowledgement and Alert Action are 1::0-1, and when they do exist they must contain a user reference. Basically a user logs on and acknowledges the alarm. Once the alarm is acknowledged, it can then be actioned. Only one action can be applied. The user applying the action may be different to the user that acknowledged the alarm. – Mark Dec 05 '10 at 23:39
  • 1
    @Mark. Why would we care about a gap ? The other `SensorNos` remain unchanged. When we do not use `Ids`, we do not suffer all the madness and limitations of IDENTITY columns. Generally speaking, if you need an Identifier with no gaps, then it has meaning, and you cannot use an IDENTITY column. – PerformanceDBA Dec 05 '10 at 23:43
  • @Mark: Updated the DM. Pending your answers, for now, for evaluation, I have duplicated the Limits. Think about `LocationCode` as a meaningful Identifier. – PerformanceDBA Dec 06 '10 at 00:10
  • @PerformanceDBA: Thanks - I'll have to pick this up again in the morning as its getting very late here. Regarding the updated DM - I think I'd have to move Alarm Acknowledgement and Alert Action to their own table because it is a 1::0-1 relationship. If I were to keep it in the Alarm table, I'd have to make the UserId nullable (unless I'm missing something). One final thing; I may be able to use user defined names for the Sensor in each location - If so would it be sensible to use that in place if SensorNo? I could then perhaps use "logger" instead of SensorNo=0. Thanks again. – Mark Dec 06 '10 at 00:32
  • @Mark: 1) Updated the Data Model; no Nullable FKs (yes, that would be a mistake). 2) The DM reads beautifully now: Location, Sensor, SensorReading, top to bottom; and Alerts across, as they progress through States. 3) Sure, SensorName is a great idea, especially if it is short (let me know); but it is still within a Location. 4) Likewise, think about a `LocationCode`. – PerformanceDBA Dec 06 '10 at 01:21
  • @PerformanceDBA: Thanks, this has definitely opened my eyes. May I throw some proverbial spanners in the work though? When readings are taken from a logger (via a handset), it needs to be known who took those readings. Initially I was storing that information with each reading (making it look more like a spreadsheet again) - which is one of the reasons I kept the logger readings table separate. On top of that, Live sensors need to relate to a Network Slave table. As the new schema combines loggers and live sensors, both of these issues are now relevant to the discussion. – Mark Dec 06 '10 at 09:56
  • @PerformanceDBA: One other issue would be the SensorName - if the user changes it for whatever reason, then I'd need to change it in the readings table, alarms table, etc. I think - so maybe keeping SensorNo would be better? I think that's always been one of my worries about not using surrogates, but I am starting to understand why surrogates have caused problems for me. – Mark Dec 06 '10 at 09:58
  • @PerformanceDBA: Some examples of what SensorName might be: "air", "air 2", "food", "water", "humidity", etc. – Mark Dec 06 '10 at 10:06
  • 1
    @PerformanceDBA: I'm going to take a guess regarding the logger readings download. I'm thinking I need a new table along the lines of this: LoggerDownload (LocationId[PK], SensorName[PK], DownloadTimestamp[PK], UserId[FK], FirstReadingTimestamp, LastReadingTimestamp).. Basically for each reading (from a logger) I need to be able to determine whom downloaded the logger and the download time. Apologies, I hope this doesn't break the design too much! – Mark Dec 06 '10 at 11:44
  • @Mark: See, now you are doing the right type of thinking. Excellent. It is not for me to **tell** you what is right and wrong; it is for me to guide you, to make informed and considered decisions. You tell me what you've decided and I will whack it in the model. 1) you have not answered the questn re limits (duplicated for now); ie. confirm the SensorTypelookup table ... Or is it SensorModel ? – PerformanceDBA Dec 06 '10 at 12:08
  • 2) No problem with spanners or a new table; db design must take the whole context, not one table at a time. 3) What is the diff between FirstReadingTimestamp, LastReadingTimestamp ? 4) Why is new table not a child of Logger (is the data specific to Sensor or Logger) ? 5) Why don't we have a NetworkSlave table, and if so, is it just another parent of Sensor ? – PerformanceDBA Dec 06 '10 at 12:12
  • @PerformanceDBA: Very much appreciate the guidance, it's making me rethink things a lot! Each Sensor has independant upper and lower limits, so I think we can remove the limits from SensorType and from Logger and place them in Sensor. Their could be a problem though - and I wish I'd mentioned this earlier too (apologies again): Live sensors can also be used to sense switches, i.e. whether something is ON or OFF (like a door sensor for instance). The FirstReadingTimestamp and LastReadingTimestamp are to identify the range of readings taken on that particular download (continued below).. – Mark Dec 06 '10 at 12:24
  • @PerformanceDBA: Yes their should be a NetworkSlave table - and yes it's another parent of Sensor, though a Logger never has a NetworkSlave. – Mark Dec 06 '10 at 12:28
  • @PerformanceDBA: Apologies about the rather broken up way I'm giving the information to you. Initially I was trying to solve a subset of the model but it's clear to me now that I need to look at it again as a whole. I hope you don't get frustrated with me and stop guiding because it's a real eye opener and I appreciate it very much. – Mark Dec 06 '10 at 12:33
  • Apology not necessary. Paper is cheap. I will stay with you to the end. Give me a bounty if you think I deserve it. 4) ? 6) So Logger is a child of NetowrkSlave & a subtype of Location. NetworkSlave has not relation to Location ? 7) Tell me what you think of the VerbPhrases; they are important to understanding and resolution (of the modelling process we are engaged in). – PerformanceDBA Dec 06 '10 at 12:35
  • Are verb phrases statements of fact? - To clear up the Logger issue: A logger is basically a self contained peice of hardware. It has it's own power source, an internal sensor, and basically sits there taking readings all day until a user comes along and downloads the readings (via a handset). These readings are then fed into the system by docking the handset. A NetworkSlave on the other hand is part of the live system; several sensors can be plugged into it and it is polled by the Network Monitoring Unit hardware which feeds data back to the system. – Mark Dec 06 '10 at 12:45
  • So basically I have 2 different ways of getting data back to the system; via the handset from a Logger, or via the live network system (hence the NetworkSlave). – Mark Dec 06 '10 at 12:46
  • Addendum: A NetworkSlave is not associated with a Location because the Sensors that plug into it are on long cables so it could have one Sensor at one location, and another Sensor at another Location - hence why a NetworkSlave does not have a Location as a parent. – Mark Dec 06 '10 at 12:54
  • Ok. 8) But regardless, all reading are Values from Sensors (includg Sensor 0 inside Logger), and stored in SensorReading ? 9) Therefore LoggerDownload has nothing but the fact of a download; bracketing (but not duplicating) all readings for all Sensors for Location ? 10) in which case you do not need a SensorNo in it. 11) Network slave is a parent of Logger. – PerformanceDBA Dec 06 '10 at 12:56
  • Re 8: Yes, 9: Yes except the LoggerDownload is only for the Logger, not for the "live" Sensors. The data from live sensors do not require a user to download them; the data is fed to the system automatically via a network. – Mark Dec 06 '10 at 13:02
  • Re 11: NetworkSlave and Logger are entirely independant. The NetworkSlave exists to plug sensors into. A logger does not use the NetworkSlave hardware. – Mark Dec 06 '10 at 13:07
  • To put it another way: A Logger requires user intervention, it requires a download via the handset; that is the data's "mode of transport" from the hardware to the system. Sensors attached to a NetworkSlave do not require any user intervention, they are part of the live system and the readings are automatically fed to the system. The NetworkSlave is the mode of transport for the live part of the system. – Mark Dec 06 '10 at 13:16
  • 9) Ok. So where is the download data stored ? 11) Got that, so there is no relation between NetworkSlave & Logger ? between NetworkSLave & Location ? here must be something beyond Location such as Customer ? 12) VerbPhrases are sttmts of fact; supposed to be definitive; please correct supplied VPs in the next DM. – PerformanceDBA Dec 06 '10 at 13:16
  • Re 9: The logger itself stores it's data until the user downloads it onto the handset. At that point the handset contains the data until it is docked, where the data is then fed into the system and into the database. The handset also stores the user id of who downloaded the readings from the logger (the user logs onto the handset first). 11: Yes and yes. 12: Okay. – Mark Dec 06 '10 at 13:21
  • 9) After the data is "fed into the db", where is it stored ? SensorReading ? 13) DM ready for your review. – PerformanceDBA Dec 06 '10 at 13:28
  • DM Updated again. Remember to read the Notation doc re VerbPhrases; how to "read" them. ___DTM is short for DateTime (quite diff to TimeStamp) – PerformanceDBA Dec 06 '10 at 13:37
  • Okay having a read now and studying DM. Re 9: Yes SensorReading - initially I had a separate table but looks like that is no longer neccessary (because SensorNo 0 is a Logger Sensor). – Mark Dec 06 '10 at 13:44
  • Yeah, but now that we have a LoggerDownload table, maybe it should go in there. The question that has not been answered (you have given me a lot of description, but not the specific answer alone) is about that. Try this 21) how many LoggerSensor readings per Download ? – PerformanceDBA Dec 06 '10 at 13:54
  • Re 21: It depends how long between downloads: the Logger will log a reading on an interval. This interval is configurable so should be a column in the Logger table. The logger is also configured with the upper and lower limit so the reading from the logger will have an attribute to say if that reading is in alarm or not. Once in the system, the user will again be able to acknowledge the alarm and action it. For instance, if the LoggingInterval is 10 minutes and the user downloads it one hour after the previous download, there will be 6 readings. – Mark Dec 06 '10 at 14:01
  • 21) Excellent. We are getting there. Are the readings fromLoggerSensor treated any diff from non-LoggerSensors ? If the answer is "no" then I am going to bed. DM typos fixed up. Please check over the Comments again and see if we missed anything. I will catch your new comments (VerbPhrases; anything we missed) in the morning. You did well, normalising on the fly like that. Really helps that you know your data. – PerformanceDBA Dec 06 '10 at 14:17
  • Re 21: No they should be treated the same, other than reports will show a download date and the user who downloaded the readings on Logger readings. – Mark Dec 06 '10 at 14:19
  • Okay I will go over everything again and study the DM some more. Thanks for all your help so far. Good night and speak to you tomorrow. – Mark Dec 06 '10 at 14:28
  • I think the NetworkSlaveCode will have to come out of the Sensor table because it would have to be NULL when the Sensor represents a logger. Would this then mean adding a table called NetworkSensor with a 1::0-1 relationship with Sensor? The column SensorNo in NetworkSensor could even check that SensorNo is NOT 0 (to make sure it's not a Logger Sensor).. – Mark Dec 06 '10 at 16:25
  • @Mark. You're most welcome. Sure. But, for learning purposes at least, notice how we are making special cases; then special handling for special cases. At some point it becomes abominable. We should really evaluate going back to Logger, and creating the LoggerReading table as a child of Download (satisfy all FDs). I just drew it so that I could evaluate it for myself, and I felt much better. No special cases; no special handling; all FDs handled by constraints. So I saved it. Cleaned up the naming as well. Pls check & let me know how you feel. No problem to reverse it. – PerformanceDBA Dec 06 '10 at 18:30
  • @PerformanceDBA: Yes it feels better to me too. The only problem now is the Alerts; their are no Alerts for LoggerReadings, as far as I can tell anyway. I'm staring at the DM now trying to see how the current alerts table could work for the LoggerReadings too, but I can't so far - not without repeating all of the Alert tables. – Mark Dec 06 '10 at 19:54
  • @Mark. Ouch, I missed that; very politely stated (I can handle direct correction). Reversed. Updated. Please Check. – PerformanceDBA Dec 06 '10 at 21:42
  • @PerformanceDBA: It's a shame as it does feel cleaner with the seperate table, but I think without repeating tables there are few other options. One other idea was to have an Alert, a LoggerAlert which "is-a" Alert and a SensorAlert which "is-a" Alert too. The SensorAlert would have an FK to Sensor (much like the current Alert does) and the LoggerAlert would have an FK to the Logger. Maybe SensorNo=0 is better though - there's no anomolies then like an Alert existing without either a LoggerAlert or a SensorAlert - OR.. see next comment.. – Mark Dec 06 '10 at 22:04
  • Or there could be an Alert with an FK pointing to a Location, which only becomes a SensorAlert IF an entry in SensorAlert exists.. much like NetworkSensor existing or not existing. Sorry thinking out loud! Probably getting ahead of myself. – Mark Dec 06 '10 at 22:05
  • @Mark. Have you looked at the DM updated 2240 UTC 06 Dec, as per 3 comments above this ? – PerformanceDBA Dec 07 '10 at 08:46
  • Yes, I was looking over it last night just before bed. It's looking good. It looks like there's no totally clean way around this issue though with the logger. Trust me to get the tricky requirements! I think there might be a typo with that sentence next to the Sensor "If Logger.Sensor exists, it does so a valid Sensor row, with SensorNo zero". – Mark Dec 07 '10 at 10:00
  • @Mark: Are you comfortable with the Verb Phrases ? – PerformanceDBA Dec 07 '10 at 10:26
  • Yes they seem right. There's a few that don't read right (to me at least) - Like "Sensor is deemed Alert"; should I read that "Sensor is deemed IN Alert"? – Mark Dec 07 '10 at 10:41
  • Should the 1::0-1 relationship between Alert and Acknowledgement read "May have" rather than "Is" ? – Mark Dec 07 '10 at 10:46
  • One thing I've noticed - An alert must be acknowledged before it can be actioned. The database however allows an action to exist for an alert even if it has not been acknowledged. I can take care of this on the application side, but if the database is to be 100% strict on the rules, then there'd have to be a constraint of some sort. Hmm. – Mark Dec 07 '10 at 11:00
  • Do you still feel unsure about whether the SensorNo=0 being the best way to do this or does it feel right to you? – Mark Dec 07 '10 at 11:23
  • @Mark. No worries. "May be [Acknowledged]", I had that earlier, but it seemed you were comfortable with the OO "Is", meaning optional, as distinct from the Relational imperative. Will change. Action/Ack: no it doesn't, the Action FK should be to the Ack PK (not Alert PK). You will find many little things like that, usually resolved with a CHECK constraint. I don't need to tell you, you are on the ball, strong type casting, rules and checks. – PerformanceDBA Dec 07 '10 at 11:59
  • Sensor=0: I am sanguine: we walked it this way; and worked it that; and settled back down here. That's modelling. More important there is no untidiness, no loose ends. Logger.Sensor is not a big deal. It is one little twist, and we have not done some secret insertion, we have done it cleanly. You did well, you really understand your data. DM Updated. Would you like it on A4 ? – PerformanceDBA Dec 07 '10 at 12:15
  • That seems fair enough. Okay I will have another good look over the schema after lunch before closing off the question. I may be asking another question soon (probably best a new question rather than continuing here) about historical data.. – Mark Dec 07 '10 at 12:15
  • Thanks, I've been playing with the data for some time so I'm very familiar with it! I've learnt a lot through the process too (especially from your input here). It seems to print fine on A4 as is (it did yesterday anyway) so it's probably fine. – Mark Dec 07 '10 at 12:17
  • PS - what software did you use to draw the schema? I currently use MySQL workbench but find it a bit clunky sometimes and it crashes fairly often. – Mark Dec 07 '10 at 16:30
  • @Mark: Thanks, and it has been a pleasure working with you, we got past the main obstacle in the first iteration! ERwin for a job of any substance, connected directly to the enterprise db (my custs do not use freeware). But for simple one-offs like this, and for **all** my architectural drawings, OmniGraffle on a Mac. The doc re IDEF1X Notation has links, one is for the IDEF1X Stencil for OG. – PerformanceDBA Dec 07 '10 at 22:55
  • @Mark: Elevating the question to a bounty like that, demonstrates an integrity that is rare these days. – PerformanceDBA Dec 09 '10 at 06:01
  • @Mark: The Logger.SensorNo = 0 issue was nagging me, so I put it to bed. All business rules are now expressed 100% via RI and CHECK Constraints. The semicircle is the IDEF1X symbol for supertype-subtype; the X through the centre means the subtypes are exclusive. If you define the FK constraints exactly as shown, plus a CHECK in SensorNetwork (SensorNo must not = 0) and SensorLogger (SensorNo must = 0). Each of them cannot exist without the existence of the parents. Also a number small naming ameliorations. – PerformanceDBA Dec 09 '10 at 12:15
  • @PeformanceDBA: Oddly I just looked at the DM again and noticed the change, so came on here to see if you'd commented, and yes you had! I must admit I was still mulling it over too in case there was a different way of looking at it, but it looks like you have it figured. One thought I had was changing Sensor to LocationAttribute - my thinking being that (e.g) "Air" is an attribute of Location, and we are using a sensor (of some sort) to monitor it. I could be barking up the wrong tree, and in terms of the layout makes no difference really. I just thought maybe conceptually it made more sense. – Mark Dec 09 '10 at 13:06
  • @PerformanceDBA: One comment I've had is that alerts are "calculated" as in you can derive them from the readings table, however they also have state so I can't see how that can work. – Mark Dec 09 '10 at 13:07
  • @PerformanceDBA: Thanks for looking at it again, really appreciate it. It's nice having someone to talk to about it who is actually interested in the problem. – Mark Dec 09 '10 at 13:40
  • @Mark: 1) The question is: is the Logger.Sensor a true sensor that is built-in to the logger, for which Readings are recorded ? 2) I do not understand "alerts are 'calculated' as in you can derive them from the readings table, however they also have state". They are calculated in order to produce them, before they exist; after that, they exist as Alerts, they have a state (expressed as Acknowledgement, Action). See my answer. 3) I am waiting to see the requirement for Historical data, so that the DM is complete. – PerformanceDBA Dec 09 '10 at 21:40
  • @Mark: I reformatted & cleaned up the Answer. In so doing, I clarified the DM further (minor changes, sorry). – PerformanceDBA Dec 10 '10 at 08:33
  • @PerformanceDBA: Nothing to be sorry about! I truley appreciate your help and the time you've put into helping me. – Mark Dec 10 '10 at 11:32
  • @PerformanceDBA: Yep got that - basically a sensor is a thermister on the end of a couple of wires (though apparently the logger uses a different type, but yes at the end of the day it's a temperature sensor plugged into a circuit board). Just looking at historical requirements again now. – Mark Dec 10 '10 at 15:57
  • @PerformanceDBA: Basically when a report of readings is created (or alerts) the user names and sensor names, etc, that existed at the time of the reading should be displayed on the report. In other words, if user Fred acknowledged an alert - and then his user name was changed to Jim, the report should still show "Fred" for the acknowledgement. I've had this working before on a previous project by having a ValidFrom and ValidTo on all the relevant tables. A ValidFrom of 9999/12/31 meant it was the latest version of the record. It did however cause a complexity: – Mark Dec 10 '10 at 16:01
  • I had to use triggers to enforce foreign key constraints. Not very pretty, but it worked surprisingly well. – Mark Dec 10 '10 at 16:02
  • @Mark. 1) UserName: answered in detail in my post. Already taken care of. If any of that isn't completely understood, ask a specific question. 2) Is that all the "Historic" need was ? 3) Is the Alerts issue closed ? – PerformanceDBA Dec 10 '10 at 21:54
  • @PerforanceDBA: Basically if it's editable then it has to be historical (so that excludes readings and alerts). The problem I find with the isObselete flag is.. Say if you change the Location, the Sensor foreign key will now point to an obselete record, meaning you will have to duplicate every sensor record. This problem gets exponentially worse as the hierachy gets bigger. There seems to be two main approaches to historical databases; the ValidFrom/ValidTo approach using triggers - or using separate historical tables. – Mark Dec 10 '10 at 23:54
  • @PerformanceDBA: Plus with the historical data, it means we can create audit trails.. Hmm, it seems a shame to pollute the DM at this point! – Mark Dec 10 '10 at 23:56
  • @PerformanceDBA: I'm thinking rather than changing the current schema (if you're up for tackling the historical question) - how about having 2 versions of it? The current schema answers the original question clearly after all. – Mark Dec 11 '10 at 00:20
  • @PerformanceDBA: Off to bed now and have a busy weekend so catch up Sunday or Monday. Thanks again. – Mark Dec 11 '10 at 00:20
  • @Mark. Yes, this is finished and integral. Let's close this question and open a new one for the historical requirement. FWIW, re `IsObsolete`, I meant nothing of the sort. But let's take it up elsewhere. My pleasure. – PerformanceDBA Dec 11 '10 at 01:31
  • +1: awesome document link. I'd give a lot more pluses if I could, for this truly awesome, detailed and professional answer! – RBarryYoung Dec 11 '10 at 14:09
  • @PerformanceDBA: I've now asked the question RE historical / auditable data if you fancy a look at it. – Mark Dec 20 '10 at 16:36
  • @Mark. I fancied a look. Provided an Answer. – PerformanceDBA Dec 21 '10 at 10:30
2

Here are my two cents on the problem.

alt text

AlertType table holds all possible types of alerts. AlertName may be something like high temperate, low pressure, low water level, etc.

AlertSetup table allows for setup of alert thresholds from a sensor for a specific alert type. For example, TresholdLevel = 100 and TresholdType = 'HI' should trigger alert for readings over 100.

Reading table holds sensor readings as they are streamed into the server (application).

Alert table holds all alerts. It keeps links to the first reading that triggered the alert and the last one that finished it (FirstReadingId, LastReadingId). IsActive is true if there is an active alert for the (SensorId, AlertTypeId) combination. IsActive can be set to false only by reading going below the alert threshold. IsAcknowledged means that an operator has acknowledged the alert.

  1. The application layer inserts the new reading into the Reading table, captures the ReadingId.

  2. Then application checks the reading against alert setups for each (SensorId, AlertTypeId) combination. At this point a collection of objects {SensorId, AlertTypeId, ReadingId, IsAlert} is created and the IsAlert flag is set for each object.

  3. The Alert table is then checked for active alerts for each object {SensorId, AlertTypeId, ReadingId, IsAlert} from the collection.

    • If the IsAlert is TRUE and there are no active alerts for the (SensorId, AlertTypeId) combination, a new row is added to the Alert table with the FirstReadingID pointing to the current ReadingId. The IsActive is set to TRUE, the IsAcknowledged to FALSE.

    • If the IsAlert is TRUE and there is an active alert for the (SensorId, AlertTypeId) combination, that row is updated by setting the LastReadingID pointing to the current ReadingId.

    • If the IsAlert is FALSE and there is an active alert for the (SensorId, AlertTypeId) combination, that row is updated by setting the IsActive FALSE.

    • If the IsAlert is FALSE and there are no active alerts for the (SensorId, AlertTypeId) combination, the Alert table is not modified.

Damir Sudarevic
  • 21,891
  • 3
  • 47
  • 71
  • @Damir: Thanks :-) I'm slightly uncomfortable with NULLable foreign keys because I've read many times that it indicates bad design. Whether this is true or not I'm not sure! I guess it's subjective given that a lot of people out in the field seem to use them. – Mark Dec 02 '10 at 17:25
  • @Mark -- only `Alert.LastReadingId` is nullable, though does not have to be. It can be set to the same value as `Alert.FirstReadingId` when the row is inserted to the `Alert` table. – Damir Sudarevic Dec 02 '10 at 19:16
  • @Damir: Don't the Reading (values) belong to the Sensor, in Reading; whether the Reading is in Alert status or is a matter of condition ? – PerformanceDBA Dec 05 '10 at 22:29
  • @PerformanceDBA -- yes, the Reading table is missing the actual value field. DUH. – Damir Sudarevic Dec 06 '10 at 12:53
  • 2
    @Damir: You provide such good models, why don't you update it so that people reading SO see the corrected version, and can make reasonable comparsons.. – PerformanceDBA Dec 07 '10 at 22:52
  • @PerformanceDBA, the way I see it -- the posted answer and all the comments make one answer-unit. Also, each model is just a starting point, first draft, rarely the final solution. – Damir Sudarevic Dec 08 '10 at 13:21
0

The main "triangle" you have to deal with here is Sensor, [Sensor]Reading, and Alert. Presuming you have to track activity as it is occuring (as opposed to a "load it all at once" design), your third solution is similar to something we did recently. A few tweaks and it would look like:

[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" could be SensorStateId, with an associated lookup table listing (and constraining) all possible states.

The idea is, you Sensor contains one row per sensor and shows it's current state. SensorReading is updated continuously with sensor readings. If and when a given sensors current state changes (i.e. new Reading's state differs from Sensor's current state), you change the current state and add a row to the SensorStateLog showing the change in state. (Optionally, you could update the "prior" entry for that sensor with a "state ended" timestamp, but that's fussy code to write.)

CurrentSensorState in the Sensor table is denormalized data, but if properly maintained (and if you have millions of rows) it will make querying current state vastly more efficient and so may be worth the effort.

The obvious downside of all this is that Alerts are no longer an entity, and they become that much harder to track and identify. If these must be readily and immediately identifiable and usable, your third scheme won't do what you need it to do.

Philip Kelley
  • 39,426
  • 11
  • 57
  • 92
  • Thanks for your answer. Yes after giving it more thought, it might be a problem as there are also other tables that need to relate to an Alert, such as AlertCorrectiveAction and AlertAcknowledgement. If it does turn out that I must have an Alert entity, do you see much of a problem with option 2? It's actually more similar to your approach than option3 - the SensorAlert table is much like your SensorStateLog table, except it only "logs" Alerts... – Mark Dec 02 '10 at 15:38
  • Or I guess another option would be to reintroduce the Alert entity that has an ID column that is both the primary key and a foreign key relating to the SensorState.. – Mark Dec 02 '10 at 16:09