<img height="1" width="1" style="display:none;" alt="" src="https://dc.ads.linkedin.com/collect/?pid=412841&amp;fmt=gif">

Data Vault 2.0 und SAP HANA I: Hashwerte

31.08.2017  •  SAP Analytics, Data Vault, ETL

In dieser Blog-Reihe bespreche ich einige Aspekte zum Thema Data Vault 2.0 mit SAP HANA. Dabei geht es nicht darum, eine weitere Einführung zum Theme Data Vault 2.0 zu geben, Interessenten verweise ich auf das Buch von D. Linstedt und M. Olschimke [Building a Scalabe Data Warehouse with Data Vault 2.0, Morgan Kaufmann Publishers, 2016].
Stattdessen diskutiere ich, wie eine Data Warehouse Lösung mit einem Core Data Warehouse, konzeptioniert nach Data Vault 2.0, in SAP HANA umgesetzt werden kann.

Ein wichtiges Konzept in Data Vault 2.0 ist die Verwendung von Hash-Werten als künstliche Schlüssel, vor allem um eine Parallelisierung bei der Beladung zu ermöglichen. Nun sind wir nach Jahrzehnten der DWH-Entwicklung darauf getrimmt worden, dass der einzig wahre Datentyp für einen künstlichen Schlüssel eine Zahlentyp sein muss, weil nur so Joins effizient gebildet werden können. Deshalb wollen wir hier mit ein paar Testdaten untersuchen, wie effizient solche Joins auf SAP HANA ausgeführt werden können.

Alle folgenden Tests werden auf einer SAP HANA Trial Instanz ausgeführt (HANA MDC (), Version 1.00.122.08.1490178281, also HANA SPS 12).

Eine Hashfunktion ist eine Funktion, die eine Zeichenfolge beliebiger Länge auf eine Zeichenfolge mit fester Länge abbildet. Dabei ist die Wahrscheinlichkeit sehr hoch, dass verschiedene Zeichenfolgen zu verschiedenen Hashwerten führen. Die Wahrscheinlichkeit ist sogar so hoch, dass wir in der Praxis eine solche Kollision de facto ausschließen können. Beliebte Algorithmen zur Berechnung solcher Hashwerte sind MD5 (Message-Digest Algorithm 5) und SHA-1 (Secure Hash Algorithm 1).

Die entsprechenden Funktionen in SAP HANA sind HASH_SHA256 und HASH_MD5. Da beide Funktionen einen binären Datentyp benötigen, muss ein String zunächst in ein Binärformat umgewandelt werden. Den Rückgabewert wandeln wir wieder in einen String um. Im folgenden Screenshot sehen wir ein paar Beispiele zur Berechnung von Hashwerten.

SELECT_HASHKEY

Nun benötigen wir Testdaten. Da diese Daten inhaltlich keinen Sinn ergeben müssen, legen wir eine Tabelle mit einer Schlüsselspalte von Typ Integer und einer Textspalte an, in die wir alle Zeichenfolgen
aaaaa bis ttttt schreiben. Das sind 5^20 = 3.200.000 Datensätze. Hier sehen wir die Tabellendefinition:

namespace de.bimanu.blog.model;
@Schema: 'GENERIC'

context hash {

type HashType: String(64);

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_1 {
key ID: Integer;
TEXT: String(5);
};
};

Die etwas stupide Aufgabe, diese Tabelle zu befüllen, kann mit folgender Prozedur erreicht werden:

PROCEDURE "GENERIC"."de.bimanu.blog.script::FILL_SAMPLE_1" ( )
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER
DEFAULT SCHEMA GENERIC AS
BEGIN
DECLARE i, j, k, l, m INTEGER;
DECLARE counter INTEGER default 0;

FOR i IN 97 .. 116 DO
FOR j IN 97 .. 116 DO
FOR k IN 97 .. 116 DO
FOR l IN 97 .. 116 DO
FOR m IN 97 .. 116 DO
INSERT INTO "de.bimanu.blog.model::hash.HASH_SAMPLE_1" (ID, TEXT)
values(counter, CHAR(i) || CHAR(j) || CHAR(k)
|| CHAR(l) || CHAR(m));
counter := counter + 1;
END FOR;
END FOR;
END FOR;
END FOR;
END FOR;

COMMIT;
END;

Da jeder Datensatz mit einem eigenen INSERT-Statement geladen wird, dauert es einige Minuten bis diese Prozedur durchgelaufen ist.

Nun legen wir zwei weitere Tabellen an, die denselben Inhalt haben, aber Hashwerte als Schlüssel verwenden:

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_1_MD5 {
key HASHKEY: String(32) ;
TEXT: String(5);
};

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_1_SHA256 {
key HASHKEY: String(64) ;
TEXT: String(5);
};

 

Diese Tabellen können jeweils mit einem einzigen INSERT-Statement gefüllt werden, dies geht auch viel schneller:

INSERT_HANA

Nun benötigen wir eine weitere Tabelle als Gegenpart zur ersten Tabelle, um verschiedene Arten von Joins testen zu können:

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_2 {
TEXT: String(5);
MEASURE: Decimal(3,0);
};

Wir laden 100.000 Datensätze in diese Tabelle, indem wir in die erste Spalte zufällig erzeugte Zeichenfolgen, die zu dem Feld TEXT oben passen, laden, und in die zweite Spalte schreiben wir eine zufällig erzeugte Zahl zwischen 0 und 999. Dazu kann die folgende Prozedur verwendet werden:

PROCEDURE "GENERIC"."de.bimanu.blog.script::FILL_SAMPLE_2" ( )
LANGUAGE SQLSCRIPT
SQL SECURITY INVOKER
DEFAULT SCHEMA GENERIC
AS
BEGIN
DECLARE i, j, k, l, m INTEGER;

FOR i IN 1 .. 100000 DO
INSERT INTO "de.bimanu.blog.model::hash.HASH_SAMPLE_2" (TEXT, MEASURE)
values(CHAR(CEIL(20 * RAND()) + 96) ||
CHAR(CEIL(20 * RAND()) + 96) ||
CHAR(CEIL(20 * RAND()) + 96) ||
CHAR(CEIL(20 * RAND()) + 96) || CHAR(CEIL(20 * RAND()) + 96), RAND() * 999);
END FOR;

COMMIT;
END;

Von dieser Tabelle legen wir wieder Varianten an, die auf unsere verschiedenen Schlüsselarten eingehen:

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_2_ID {
ID: Integer;
MEASURE: Decimal(3,0);
};

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_2_MD5 {
HASHKEY: String(32);
MEASURE: Decimal(3,0);
};

@Catalog.tableType:#COLUMN
entity HASH_SAMPLE_2_SHA256 {
HASHKEY: String(64);
MEASURE: Decimal(3,0);
};

Diese Tabellen können wieder einfach jeweils mit einem INSERT-Statement füllen:

INSERT INTO "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_ID" (id, measure)
SELECT sample_1.id, sample_2.measure
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1" sample_1
ON sample_1.text = sample_2.text;

COMMIT;

INSERT INTO "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_MD5" (hashkey, measure)
SELECT sample_1.hashkey, sample_2.measure
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1_MD5" sample_1
ON sample_1.text = sample_2.text;

COMMIT;

INSERT INTO "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_SHA256" (hashkey, measure)
SELECT sample_1.hashkey, sample_2.measure
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1_SHA256" sample_1
ON sample_1.text = sample_2.text;

COMMIT;

Zum Test wird zunächst folgende Abfrage verwendet:

SELECT sum(measure) FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2"
WHERE text LIKE '%ab%';

Diese Abfrage auf eine einzige Tabelle ist sehr schnell. Ich habe sie zehnmal ausgeführt und hatte in der Regel eine Verarbeitungszeit (server processing time) von 4 Millisekunden, nie über 7 Millisekunden.

Jetzt verwenden wir folgende Abfrage:

SELECT sum(sample_2.measure)
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_ID" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1" sample_1
ON sample_1.id = sample_2.id
WHERE sample_1.text LIKE '%ab%';

Diese Abfrage sieht schon eher nach einer Abfrage auf ein dimensionales Modell aus: Es wird über eine Dimension (HASH_SAMPLE_1) eingegrenzt, für die Kennzahl wird entsprechend dieser Filterung ein aggregierter Wert berechnet (dass die Dimension mehr Einträge als die Faktentabelle hat, schadet bei unserem Test nicht). Der Join zwischen beiden Tabellen läuft über einen Integer-Datentyp. Auch hier führten wir die Abfrage zehnmal durch. Die Abfrage lief im Durchschnitt 50 Millisekunden lang, nie über 90 Millisekunden.

Schließlich fragen wir die Daten mit Statements ab, bei denen die Joins die erzeugten Hashkeys verwenden.

SELECT sum(sample_2.measure)
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_MD5" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1_MD5" sample_1
ON sample_1.hashkey = sample_2.hashkey
WHERE sample_1.text LIKE '%ab%';

SELECT sum(sample_2.measure)
FROM "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_2_SHA256" sample_2
INNER JOIN "GENERIC"."de.bimanu.blog.model::hash.HASH_SAMPLE_1_SHA256" sample_1
ON sample_1.hashkey = sample_2.hashkey
WHERE sample_1.text LIKE '%ab%';

Bei Beide Abfragen erhalten wir beinahe identische Verarbeitungszeiten wie bei den Integer-Datentypen, eine Verschlechterung der Performance konnte nicht beobachtet werden. Mit Sicherheit erfüllt dieser Test keine wissenschaftlichen Kriterien. Trotzdem können wir mit der Erkenntnis schließen, dass Joins über aus Hash-Funktionen erzeugten Strings in einer In-Memory-Datenbank so effizient sind, dass deren Verwendung für einen Data
Vault 2.0 Ansatz nichts im Wege steht. So werde ich es auch in dieser Blog-Reihe halten und ohne weitere Bedenken Hashwerte als Schlüssel verwenden.

Jon Nedelmann, 31.08.2017