„Isotopp“ zu Zeitreihendaten

2009/10/28

Kristian Köhntopp hat ein paar Gedanken zu Zeitreihendaten niedergeschrieben, besonders für die Verwendung von MySQL, wie hier ja auch beschrieben.

Advertisements

Datenpunkt-Tabellen und SQL für das KPI- und Verbrauchsdatenarchiv

2009/04/07

Wie sieht die Datenbank für das Verbrauchszahlen-Archiv aus? Zuerst die Tabelle mit den eigentlichen Werten.

CREATE TABLE  TagValues
(
tagname varchar(128) NOT NULL, -- Name des Datenpunktes
time varchar(20) NOT NULL, -- Zeit als String
timetype varchar(10),  -- Zeitliche Auflösung
value varchar(50) NOT NULL, -- Wert
PRIMARY KEY  (tagname,time,timetype),
KEY tagvalue_time (time,timetype)
);

Alles Strings? Bei MySQL als Datenbank und PHP als Skriptsprache ja, denn da ist die Konvertierung einfach. Der Speicherplatz stellt bei der Anzahl der Zeilen auch kein gravierendes Problem dar. Man gewinnt aber eine Menge Komfort bei den Abfragen aus dem Report-Tool. Der Trick steckt ausserdem in der Kodierung der Zeit als ISO-8601-String, siehe [ISO-Zeitkodierung].

Die Felder im Einzelnen:

  • tagname – der Name des Datenpunktes. Wir genehmigen uns ausreichend Platz für sprechende Namen, auch wenn wir gleich noch weitere Ordnungsmerkmale einführen.
  • time – Zeitstempel in ISO-8601-Schreibweise.
  • timetype – Zeitintervall des Werts. Ich verwende einfach die Buchstaben der PHP date()-Funktion, also zum Beispiel d = Tageswert, m = Monatswert, H = Stundenwert, usw. Man braucht dieses Feld nicht unbedingt, wenn man einen Mustervergleich mit like auf das Feld time einfügt. Aber die Abfrage auf „timetype=’d'“ ist schneller und übersichtlicher.
  • value – der eigentliche Wert. Bei Fliesskommawerten muss auf Punkt statt Komma für die Dezimalstellen geachtet werden, dann kann MySQL mit den Werten sofort rechnen. Genauer: Ein Komma in einer Zahl beendet die Zahl. Für MySQL ist ‚2,5‘ = 2, aber ‚2.5‘ bleibt beim Rechnen zweieinhalb.

Wie groß kann die Wertetabelle werden? Das wurde in https://openmes.wordpress.com/2008/12/18/wieviel-reporting-braucht-der-mensch-teil-ii/ schon mal ausgerechnet: Bei 400 Datenpunkten mit den Auflösungen Tag, Woche, Monat, Jahr insgesamt 172.800 Werte pro Jahr. Für meine Experimente nehme ich fast das 40-fache: 1000 Datenpunkte, 3 Jahre. Macht: 432*1000*3 = 1.296.000 Zeilen. Durchschnittliche Zeilenlänge ist 52 Byte, also ist eine Gesamtgröße der Tabelle von ungefähr 67,4MB zu erwarten.

Die größten Kosten der Abfragen werden in der Selektion der gewünschten Werte liegen, weil wir ja die Werte aller Datenpunkte und ihrer Intervalle in eine Tabelle packen. Die Wochenwerte des vergangenen Jahres für 10 bestimmte Datenpunkte sind 53*10 = 530 Werte aus 1,3 Mio. Zeilen. Daher der Index, der timetype und time kombinieren.

Kritisierbar ist natürlich, bei jedem Wert den Tagname als String abzuspeichern, weil er meistens mehrfach mehr Speicherplatz benötigen wird als der eigentliche Wert. Normalerweise würde man eine Beschreibungstabelle für Datenpunkte erstellen und jedem Datenpunkt eine eindeutige ID zuordnen. Kann man machen, wenn man das will. Vorteil der String-Methode ist jedoch, dass die Tabelle auch für sich alleine funktioniert, zum Beispiel auch nach dem Export nach Excel oder CSV. Die Definitionstabelle machen wir jedoch trotzdem.

CREATE TABLE  TagDefs
(
tagid int(10) unsigned NOT NULL auto_increment,
tagname varchar(128) NOT NULL,
physunit varchar(25),
description varchar(25),
label varchar(128),
PRIMARY KEY  USING BTREE (tagid,tagname)
);

Hier spendieren wir eine Unique ID, schaden kann’s nicht. Ausserdem hinterlegen wir die physikalische Einheit der Messwerte und einen Beschreibungstext, der zum Beispiel den Ort des Messgebers enthält oder eine innerbetriebliche Referenznummer. Das Label hingegen soll der Text sein, der zu diesem Datenpunkt in Reports angezeigt wird.

Für kleinere Anwendungen dürften diese zusätzlichen Attribute zu Tags ausreichend sein. Wenn man aber die gleichen Messwerte aus unterschiedlichen Perspektiven zusammenstellen möchte braucht es meist mehr Ordnungskriterien, die man in den Berichten zur Selektion benutzen kann.

Exkurs:

Equipment und Properties. Die meisten MES-Pakete lehnen sich an die ISA-S95 an, die eine Produktionslandschaft in eine bestimmte Hierarchie aufteilt, genannt Enterprise / Site / Area / Cell / Unit. Eine vollständige Angabe von Enterprise bis Unit nennt sich Equipmentpath und bezeichnet eine einzelne Produktionseinheit. Equipmentpfade werden in der Regel durch ‚/‘ oder ‚.‘ getrennt. „Hummel-Honig/Werk_Hintersen/Bereich_Hinten/Abfüllung_1/Füller_3“ wäre ein vollständiger Equipmentpfad zu einem Objekt „Füller 3“.

Jedem so beschriebenen Objekt können Properties – Eigenschaften bzw Attribute – zugeordnet werden. Das gilt auch für Zwischenebenen wie eine Area oder Site. Properties werden dann mit realen Datenpunkten verknüpft. Zum Beispiel wäre die aktuelle Auftragsnummer am „Füller 3“ eine Property dieser Unit.

Ende Exkurs.

Energie- und Verbrauchsdaten stehen allerdings etwas quer zur S95-Struktur, weil viele ihrer Elemente nicht direkt Teil der Produktion sondern der Infrastruktur sind: Trafos, Lüfter, Brauchwasserleitungen, usw. Deshalb erlauben wir, dass jedem Datenpunkt mehrere Equipmentpfade und Properties zugeordnet werden können. Nur die Kombination Pfad+Property muss unique sein.

CREATE TABLE  TagEquipment
(
tagname varchar(128) NOT NULL,
eqpath varchar(255) NOT NULL,
eqproperty varchar(128) NOT NULL,
description varchar(255),
label varchar(128),
PRIMARY KEY  (eqpath,eqproperty)
);

Wie bei der Tagdefinition erlauben wir pro Satz je einen beschreibenden Text und ein Label für die Anzeige.

Jetzt können wir zusätzlich zu den durch die Produktion definierten Equipmentpfaden zusätzliche Ordnungen über unsere Fabrik legen. Zum Beispiel könnte „Hummel-Honig/Werk_Hintersen/Wasser/Entsorgung/Abwasser“ das Selektionskriterium für alle Abwassermessungen sein. Wir sind aber nicht an die Site/Area/Cell/Unit-Struktur gebunden, sondern können den Pfad beliebig verkürzen oder erweitern. Beispiel: „Wasser/Entsorgung/Abwasser“ wird als Prefix in allen Pfaden verwendet, die Abwasser messen. Beim Füller 3 von oben wird auch Abwasser gemessen. Der Equipmentpfad könnte also zum Beispiel „Wasser/Entsorgung/Abwasser/Abfüllung_1“ sein und die Property „Füller_3“. Ob man Enterprise und Site vorschaltet hängt davon ab, ob man tatsächlich Werte aus mehreren Werken in der gleichen Datenbank hat.

Welche Abfragen sind dadurch möglich?

Zum Beispiel „alle Abwasser-Messwerte im ganzen Werk“:

"select * from tagequipment where eqpath like 'Wasser/Entsorgung/Abwasser/%'"

Oder „alle Wasser-Messwerte von Füller 3“:

"select * from tagequipment where eqpath like 'Wasser/%' and property = 'Füller_3'"

Wie man sieht, muss man nur aufpassen, Füller 3 auch jedes Mal gleich zu benennen. Die Definitionen, Bezeichnungen, Pfade und so weiter für ein paar hundert Datenpunkte konsistent zu halten ist eine friemelige Arbeit, die häufig in großen Excel-Tabellen endet.

Wir können aber auch Konsistenzprüfungen auf unserer Datenbank durchführen. Zum Beispiel: Sind für alle Tags Definitionen verfügbar? "select distinct tagname from tagvalues where tagname not in (select tagname from tagdefs)"

Das geht genauso für Equipments.

Mit diesen Definitionen erzeugen wir eine Datenbank mit Testdaten. Ich nehme mir einfach meine echten Kundendaten, aber genauso gut kann man Zufallszahlen durch ein Skript erzeugen lassen.

Im nächsten Teil geht es dann endlich ums das Reporting selbst: Berichte für lau – Reporting mit BIRT


KPI- und Verbrauchsdaten-Archiv, eine Alternativlösung

2009/04/04

Nachdem ein größeres Historian/Reporting-Projekt mit Energie- und Verbrauchsdaten weitgehend abgeschlossen ist, sind doch einige Knackpunkte überdenkenswert.
Zunächst: In dem Projekt werden aus einer kompletten Fabrik ca. 400 Datenpunkte aufgezeichnet aus den Bereichen Strom, Wasser, (Erd)Gas, daraus erzeugter Dampf, Kondensat, Druckluft und CO2. Die Rohdaten – minütliche Aufzeichnung – werden täglich zu Tages-, Wochen- und Monatssummen verdichtet. Aus den Summen werden täglich / wöchentlich / monatlich Berichte erzeugt und als PDFs verschickt.
Das ganze wurde mit einem recht hochpreisigen, proprietären Produkt realisiert, wobei man ehrlicherweise sagen muss, dass für den Kunden nur ein „Markenprodukt“ mit entsprechend breitem und tiefem Support in Frage kam. Konzerne denken so.
Abgesehen davon, dass die Aufgabe natürlich mit dem „Markenprodukt“ gelöst werden konnte, gab es schon einige Probleme damit, namentlich:

  • Die Installation war aufwändig, kompliziert und ohne spezielle „unter der Hand“-Doku nicht zu bewerkstelligen.
  • Das Webportal läuft nur mit IIS, der wirklich nicht easy zu administrieren ist.
  • Die Software kann sehr viel mehr als was benötigt wurde. Diese Über-Features erzeugten zusätzlichen Arbeitsaufwand.
  • Das Reporting ist eigentlich sehr komfortabel (Abfragen aus Objektbaum zusammenklicken), aber die Implementation des Objektverzeichnisses (SQL-Makro-Ersetzung) führt zu sehr langwierigen Abfragen (bis über 10 Minuten Laufzeit).
  • Alle Daten (Rohdaten und verdichtete) sind in einem Topf, der damit sehr groß und unübersichtlich wird.
  • Die Datenaufzeichnung und das Reporting haben unterschiedliche Vorstellung von Zeitstempeln und UTC, was bei Sommerzeit und Jahreswechseln zu Problemen führte, die in der Zwischenschicht gelöst werden mussten.

Aber kann man die Aufgabe auch anders lösen? Mit freier Software, weniger Aufwand und weniger Problemen mit den riesigen Paketen? Ich wünsche mir:

  • Ein schlankes System, das einfach zu installieren und zu administrieren ist.
  • Möglichst große Unabhängigkeit von Betriebssystem-Features oder bestimmten Releases eines Betriebssystems
  • Schnelle Abfragen und Auswertungen
  • Trennung zwischen Rohdatenaufzeichnung und verdichteten Daten
  • Sinnvolle Datums- und Zeitstempel, die für Abfragen in den Reports geeignet sind.

Lassen wir die Datenaufzeichnung erst mal weg und gehen auf das Reporting. Diese Teilaufgabe lautet: Aus einer Datenbank mit Tages/Wochen/Monatssummen werden automatisch und per Abfrage im Webportal Berichte erzeugt. Die automatisch erzeugten Berichte werden per Mail an verschiedene Empfänger verschickt. Wir brauchen also:

  • Datenbank-Tabellen mit Definitionen der Datenpunkte und den Werten.
  • Reporting-Engine, webfähig
  • Report-Designer
  • Automatische Report-Erzeugung als PDF, zeitgesteuert
  • Mailversand für die erzeugten PDFs an verschiedene Adressen
  • Archiv für die erzeugten PDFs mit Auflistung und Ansicht/Download
  • Web-Portal für Report-Auswahl und Konfiguration des Mailversands

Und eben mit freier Software, möglichst portabel und einfach zu realisieren.

Nächster Teil: Datenpunkt-Tabellen und SQL für das KPI- und Verbrauchsdatenarchiv


Ein Hoch auf die ISO 8601

2009/03/04

Immer Ärger mit der Zeit

Immer wieder sollen Produktionsdaten zeitlich eingeordnet und in Berichten zusammengestellt werden. Das sind Verbrauchswerte oder erzeugte Produkte. Und immer wieder ist wenigstens für mich der Aufwand ärgerlich, mit Datum, Uhrzeit, Schichtmodellen, Sommer/Winterzeit und so weiter rumzurechnen, bzw. die Daten per SQL irgendwo herauszufischen.

Die Ursache des Aufwands liegt für mich in der Genauigkeit. Der Datentyp Datetime oder auch die Unix time() ist immer bis auf die Sekunde genau. Aber welcher Zeitpunkt gilt für eine ganze Woche? 23:59:59 am Sonntag? 00:00:00 am Montag? Und wenn, wie beim Beispielkunden, die Woche von Sonntag bis Samstag geht und die Daten vom Werk in die Konzernzentrale in einer anderen Zeitzone berichtet werden? Gleiches gilt für Tage (Sommerzeit, Zeitzonen) und Monate.

Wenn ich einen Wochenwert abspeichern möchte, dann mit der Angabe Jahr-Woche. Schichtwerte mit der Angabe Jahr-Monat-Tag-Schicht, usw. Dann kann ich sie auch per SQL einfach selektieren.

Also wie geht das? Dafür gibt es die Schreibweisen der ISO-8601. Ausführliche Beschreibungen gibt es hinter den unten angegebenen Links. Für unsere Zwecke – Produktionsdaten mit Zeitangaben – reichen folgende Konventionen:

Tagesdatum: ’YYYY-MM-DD’ – Eigentlich ’[YY]YY[-]MM[-]DD’, aber wenn man die ’-’ und oder das Jahrhundert weglässt, kann das in einem SQL-Query missverständlich werden. Also: Jahr-Monat-Tag immer mit 4-stelligem Jahr und ’-’ als Trenner.

Die Angaben können von rechts weggelassen werden: YYYY-MM = Monatswert, YYYY = Jahreswert.

Eine Erweiterung der ISO möchte ich mir herausnehmen: ’YYYY-MM-DD"S"S’ für die Schicht, zum Beispiel 2009-02-05S2 = Zweite Schicht am 5.2.2009. Richtig nett wird es nämlich, wenn man sekundengenaue Zeitstempel bei Schichtmodellen verwendet. Dann muss man sämtliche Schichtmodelle archivieren, um vor der eigentlichen Abfrage herauszufinden, welches Schichtmodell an welchem Tag gültig war. Die Beantwortung der Frage, wie hoch die Ausschussrate in allen ersten Schichten der vergangenen 12 Monate war ist damit nicht mehr trivial: Sonderschichten, Sommer/Winterzeit, ausgefallene Schichten („Montags fangen wir immer erst zur Spätschicht an“), usw.

Wochendatum ’YYYY-"W"WW[-D]’ – Das ist eine Kalenderwoche mit optionalem Tag in der Woche. Beispiel: 2009-W07 = Kalenderwoche 7 in 2009 oder 2009-W07-3 = Der dritte Tag in KW7 in 2009. Dabei ist die ISO-Kalenderwoche sehr genau definiert; Montag – Sonntag, erste Kalenderwoche ist die mit mindestens 4 Tagen. Das kollidiert allerdings manchmal mit der Arbeitswoche einiger Firmen.

Die Wochentagskodierung mit angehängtem -D zu verwenden sollte man sich gut überlegen. Hier ist die Umrechnung oder der Vergleich mit den anderen Tages-Kodierungen wie YYYY-MM-DD oder YYYY-DDD nicht so einfach. Am besten verwendet man so häufig wie möglich die gleiche Kodierung, um sich Umrechnungen und kombinierte Abfragen (UNION) zu sparen.

Uhrzeiten ’HH[:MM[:SS]]’ – Uhrzeiten können laut ISO mit einem Leerzeichen oder „T“ dazwischen an jedes Tagesdatum angehängt werden. Wieder kann von rechts weggelassen werden: HH:MM = Minute, HH = Stunde.

Zeitspannen, „P“ – Zwei Datum/Zeitangaben können laut ISO mit einem „P“ kombiniert werden, um die Zeitspanne zwischen ihnen zu kodieren. Beispiele: 2000P2008 = Jahre 2000 bis 2008, 2007-01P2007-03 = Erstes Quartal 2007, 2008-W10P2008-W19 = Wochen 10-19 in 2008 komplett.

Als Zeitangabe in einer Datenbank ist mir das noch nicht begegnet. Ich würde es auf den ersten Blick auch ungern verwenden, weil ungeschickte Abfragen mit Mustervergleichen versehentlich auch diese Werte liefern könnten.

SQL-Abfragen für ISO-8601 Datums- und Zeitangaben

Wenn wir jetzt eine große Tabelle mit gemischten Datumswerten haben, zum Beispiel meine geliebten Energie- und Verbrauchszahlen als Tages-, Wochen- und Monatssummen; wie kriegen wir dann die gewünschten Daten heraus? Das hängt von der verwendeten Datenbank ab.

Am komfortabelsten ist MySQL, weil es automatisch von String in Datum umrechnet. Wir nehmen eine Tabelle mit den Spalten tagname, zeit und wert. Zeit ist als varchar() definiert und kann alle oben angegebene Datumsformate enthalten.

Beispiele:

(1) Alle Tageswerte der letzten sieben Tage:

select * from isozeit
where zeit like ’____-__-__’
and zeit > subdate(now(), interval 7 day)

Der Mustervergleich ’____-__-__’ selektiert das Tagesformat, das nach obigen Konventionen eindeutig ist. Deshalb die Pflicht, den ’-’ zwischen die Felder zu schreiben.

(2) Wochenwerte dieses Jahres:

select * from isozeit
where zeit like concat(year(now()), ’-W__’)

Die Abfrage ist sicher, weil sie optional angehängte Wochentage (’-D’) ausblendet.

(3) Werte mehrerer Wochen, z.B. Woche 10-19, wie oben erwähnt:

select * from isozeit
where time like ’____-W__’
and zeit between concat(year(now()), ’-W10’) and concat(year(now()), ’-W19’)

Die Wochennummern kann man natürlich auch relativ berechnen. Leider ist das „-W“ im between-Teil allein nicht ausreichend, um angehängte Wochentage auszuschliessen, daher das zusätzliche like ’____-W__’.

(4) Wochenwerte der vergangenen Woche:

select * from isozeit
where zeit = date_format(subdate(now(), interval 1 week), ’%x-W%v’)

Hier glänzt MySQL richtig. Date_format() kann nämlich verschiedene Wochenzählweisen und liefert dann Jahr und Woche entsprechend.

Beispiel:

select date_format(’2008-12-31’, ’%x-W%v’), date_format(’2009-01-01’, ’%x-W%v’),
date_format(’2008-12-31’, ’%X-W%V’), date_format(’2009-01-01’, ’%X-W%V’)

liefert:

’2009-W01’, ’2009-W01’, ’2008-W52’, ’2008-W52’

’%x-W%v’ ist also die europäische (und ISO-)Zählung; ’%X-W%V’ die US-/UK-Zählung.

Bei anderen DBs wird es etwas komplizierter. MS-SQL kennt datepart(), aber die Art der Wochenzählung kann nur global eingestellt werden. Könnte man den Wochencode auch aus year() und week() zusammensetzen, zum Beispiel mit

concat(year(subdate(now(), interval 1 week)), ’-W’, week(subdate(now(), interval 1 week)))

Leider nicht. Wenn die Woche 1 des aktuellen Jahres im vergangenen Jahr beginnt, liefert year() möglicherweise bereits das aktuelle Jahr und week() 0. Beweis:

select yearweek(’2009-01-01’), year(’2009-01-01’), week(’2009-01-01’)

(5) Tageswerte der vergangenen Woche, z.B. für einen Wochenreport:

select * from isozeit
where zeit like ’____-__-__’
and zeit
between from_days(to_days(now()) - (7 + weekday(now())))
and from_days(to_days(now()) - (1 + weekday(now())))

Uff. Das ist nicht ganz einfach, aber die scheinbar einfachere Lösung,

where date_format(zeit, ’%x%v’) = date_format(subdate(now(), interval 1 week), ’%x%v’)

würde bewirken, dass der erste date_format()-Term für jeden Datensatz durchgeführt würde.

Noch etwas flinker wäre vielleicht

zeit between date_format(from_days(to_days(now()) - (7 + weekday(now()))), ’%Y-%m-%d’) and date_format(from_days(to_days(now()) - weekday(now())), ’%Y-%m-%d’)

aber wahrscheinlich wandelt MySQL die from_days()-Terme schon gleich vor dem ersten Vergleich zu Strings. Ich konnte jedenfalls keinen Unterschied messen.

(6) Monatssummen aus Tageswerten bilden:

select tagname, year(zeit), month(zeit), sum(wert) from isozeit
where time like ’____-__-__’
group by 1,2,3
order by 1,2,3

Das ist einfach.

Allerdings weiss man noch nicht, ob auch für alle Tage des jeweiligen Monats Daten vorliegen, die Summe also korrekt ist. Das kann man durch eine zusätzliche Spalte

count(wert) = day(last_day(zeit))

ermitteln. Hier steht 1 für korrekt und 0 für fehlerhaft. Last_day() ist wieder ein Pluspunkt für MySQL. Es liefert das Datum des letzten Tages im angegebenen Monat.

(7) Wochensumme aus Tageswerten? Okay.

select tagname, date_format(zeit, ’%x-W%v’), sum(wert), count(wert) = 7 from isozeit
where zeit like ’____-__-__’
group by 1,2
order by 1,2

Gut, dass Wochen immer sieben Tage haben.

(8) und zum Schluss: Sind die Wochenwerte gleich der Summe der Tagswerte der Woche? Das sieht zunächst einfach aus:

select a.tagname, a.woche, a.summe, b.summe, a.korrekt, a.summe=b.summe
from
( select tagname, date_format(zeit, ’%x-W%v’) as woche, sum(wert) as summe, (count(wert) = 7) as korrekt
from isozeit where zeit like ’____-__-__’ group by 1,2 order by 1,2
) a
join
( select tagname, zeit as woche, wert as summe, 1 as korrekt
from isozeit where zeit like ’____-W__’ group by 1,2 order by 1,2
) b
on a.tagname=b.tagname and a.woche=b.woche

Subselect a kennen wir aus (7). Subselect b sind die Wochenwerte. Nur macht mir auf meinem Rechner MySQL einen Strich durch die Richtung, weil es zwar bei Typen tolerant ist, aber nicht bei Zeichensätzen. Die stimmen bei den Subselects wegen der Berechnung nicht überein, deshalb musste ich bei mir folgendes schreiben:

select a.tagname, a.woche, a.summe, b.summe, a.korrekt, a.summe=b.summe
from
( select
convert(tagname using latin1) as tagname,
convert(date_format(zeit, ’%x-W%v’) using latin1) as woche,
convert(sum(wert) using latin1) as summe,
convert((count(wert) = 7) using latin1) as korrekt
from isozeit where zeit like ’____-__-__’ group by 1,2 order by 1,2
) a
join
( select
convert(tagname using latin1) as tagname,
convert(zeit using latin1) as woche,
convert(wert using latin1) as summe,
convert(1 using latin1) as korrekt
from isozeit where zeit like ’____-W__’ group by 1,2 order by 1,2
) b
on a.tagname=b.tagname and a.woche=b.woche

Das soll an Beispielen reichen. Ähnliche Abfragen für die anderen Formate können ziemlich direkt abgeleitet werden.

Links

Wikipedia (dt): ISO-8601
Wikipedia (en): ISO-8601
The Mathematics of the ISO 8601 Calendar
http://www.dmoz.org/Science/Reference/Standards/Individual_Standards/ISO/ISO_8601/