DOAG Datenbank Kolumne: Warum Tools kaufen, wenn man selber basteln kann?

  • Erstellt von Dr. Elke Fritsch
  • Datenbank Kolumne, Datenbank

Meine QA-Kolleginnen verwendeten lange Zeit ein allgemein bekanntes Berichterstellungstool, das auf vielen verschiedenen Datenbanksystemen läuft.

Damit konnten sie sich Selects über ein User-Interface bequem zusammenklicken, Teilergebnisse pivotieren und die Ausgaben anschließend in Excel übertragen.

Da die alte Lizenz auslief, das Tool inzwischen wesentlich teurer und umfangreicher geworden war und sich auch die Bedienung verändert hatte, suchten sie nach einer Ersatzmöglichkeit.

Eine Oracle-Datenbank kann fast alles (außer Kaffee kochen) und so ließ ich mir die Abfragen schicken und versuchte selber mein Glück.

Unser Produktionsschema (hier abstrahiert und vereinfacht) enthält Tabellen für die Aufträge (orders), die Geräte (DEVICES), die Tester-Stationen (TEST_STATION), die Gerätetests (DEVICE_TESTS), die Resultate der einzelnen Messungen (RESULTS), die Parameter, die gemessen wurden (PARAMETERS), und die Verpackungsdaten (PACKAGING)

(siehe Abbildung rechts).

 

Insgesamt sollten ca. 15 verschiedene Berichte für verschiedene Produkttypen erstellt werden, die als Eingabeparameter entweder einzelne Auftragsnummern oder Geräteseriennummern akzeptierten.

Die Ergebnisse sollten als csv-Datei abrufbar sein, um sie mit Excel weiterverarbeiten zu können.

Zusätzlich zu diversen Test- und Verpackungsdetails der Geräte wünschten sich die Kolleginnen die Messergebnisse von zwischen 5 und 30 Messparametern in pivotierter Form.

Die über das Tool generierten Abfragen sahen eher abschreckend aus

Hier ein (vereinfachtes) Beispiel:

SELECT "DEVICES"."SERIAL_NO", "ORDERS"."ORDER_NO", "RESULTS"."PASS", "PARAMETERS"."PARAMETER_NAME", "PARAMETERS"."UNITY", "DEVICE_TESTS"."TEST_DATE", "DEVICE_TESTS"."PASS", "TEST_STATION"."DESCRIPTION",  "PACKAGING"."STATUS", "PRODUCT_TYPES"."DESCRIPTION", "RESULTS"."VALUE"

 FROM ((("PROD"."PACKAGING" "PACKAGING" INNER JOIN (((("PROD"."RESULTS" "RESULTS" INNER JOIN "PROD"."DEVICE_TESTS" "DEVICE_TESTS" ON "RESULTS"."DEV_TEST_ID"="DEVICE_TESTS"."ID") INNER JOIN "PROD"."PARAMETERS" "PARAMETERS" ON "RESULTS"."PARAMETER_ID"="PARAMETERS"."ID") INNER JOIN "PROD"."DEVICES" "DEVICES" ON "DEVICE_TESTS"."DEVICE_ID"="DEVICES"."ID") INNER JOIN "PROD"."TEST_STATION" "TEST_STATION" ON "DEVICE_TESTS"."TEST_STATION_ID"="TEST_STATION"."ID") ON "PACKAGING"."DEVICE_ID"="DEVICES"."ID") INNER JOIN "PROD"."ORDERS" "ORDERS" ON "DEVICES"."ORDER_ID"="ORDERS"."ID")  INNER JOIN "PROD"."PRODUCT_TYPES" "PRODUCT_TYPES" ON "DEVICES"."PROD_TYPE_ID"="PRODUCT_TYPES"."ID")

 WHERE "ORDERS"."ORDER_NO"='<auftragsnummer>' AND "TEST_STATION"."DESCRIPTION"<>'STATION3' AND ("PARAMETERS"."PARAMETER_NAME"='PAR1' OR "PARAMETERS"."PARAMETER_NAME"='PAR2' OR "PARAMETERS"."PARAMETER_NAME"='PAR3' OR "PARAMETERS"."PARAMETER_NAME"='PAR4' OR "PARAMETERS"."PARAMETER_NAME"='PAR5' OR "PARAMETERS"."PARAMETER_NAME"='PAR6' OR "PARAMETERS"."PARAMETER_NAME"='PAR7' OR "PARAMETERS"."PARAMETER_NAME"='PAR8' OR "PARAMETERS"."PARAMETER_NAME"='PAR9' OR "PARAMETERS"."PARAMETER_NAME"='PAR10' .....)

 ORDER BY "DEVICES"."SERIAL_NO", "DEVICE_TESTS"."TEST_DATE";

 

Nach Löschen der unzähligen Anführungszeichen und Klammern und ein paar Umstellungen war der Select aber schon lesbarer:

SELECT d.serial_no,

       o.order_no,

       p.parameter_name,

       p.unity,

       dt.test_date,

       dt.pass              test_result,

       r.pass                meas_result,

       t.description      test_station,

       pl.status            packing_status,

       pt.description    prod_type,

       r.value        

FROM prod.packaging pl JOIN prod.devices d

ON pl.device_id = d.id

      JOIN prod.orders o

ON d.order_id = orders.id

      JOIN prod.product_types  pt

ON d.prod_type_id = pt.id

      JOIN prod.device_tests dt

ON dt.device_id = d.id

      JOIN prod.results r

ON r.dev_test_id = dt.id 

      JOIN prod.parameters  p

ON r.parameter_id = p.id

      JOIN prod.test_station t

ON dt.test_station_id = t.id 

WHERE o.order_no = '?'

AND t.description <> 'STATION3'

AND ( p.parameter_name = 'PAR1'

     OR p.parameter_name = 'PAR2'

     OR p.parameter_name = 'PAR3'

     OR p.parameter_name = 'PAR4'

     OR p.parameter_name = 'PAR5'

     OR p.parameter_name = 'PAR6'

     OR p.parameter_name = 'PAR7'

     OR p.parameter_name = 'PAR8'

     OR p.parameter_name = 'PAR9'

     OR p.parameter_name = 'PAR10'

     .....)

ORDER BY devices.serial_no, device_tests.test_date;

 

Pivotiert werden sollten die Messergebnisse für die einzelnen Parameter.

In der Tabelle RESULTS standen sie neben der ID der jeweiligen Parameter, z.B.

 

ID           DEV_TEST_ID  PARAMETER_ID  VALUE

-----          --------------------  ----------------------------------

   1                                2                     150 1.512784

   2                                2                       210 9910.76

   3                                2                          90 FLAG1

 

Gewünscht war eine Folge  von Parameternamen mit dem Messergebnis darunter:

 

DEV_TEST_ID   PAR1         PAR2      PAR3

--------------------   -----------     ----------    ---------

                       2 1.512784    9910.76   FLAG1

 

Vor der Version 11g brauchte man für die Pivotierung ja noch einige Verrenkungen, hier ein Beispiel für die unverwüstliche EMP-Tabelle:

SELECT SUM(CASE TO_CHAR(hiredate, 'yyyy') WHEN '1980' THEN 1 ELSE 0 END)  "1980",

   SUM(CASE TO_CHAR(hiredate, 'yyyy') WHEN '1981' THEN 1 ELSE 0     END)  "1981",

       SUM(CASE TO_CHAR(hiredate, 'yyyy') WHEN '1982' THEN 1 ELSE 0 END)  "1982",

       SUM(CASE TO_CHAR(hiredate, 'yyyy') WHEN '1987' THEN 1 ELSE 0 END)  "1987"

FROM emp;

 

     1980        1981      1982        1987

   ---------    ---------    ---------    ---------

           1           10            1              2

 

Seit 11g gibt es die PIVOT-Klausel, die hier einiges an Schreibarbeit erspart.

SELECT * FROM (SELECT TO_CHAR(hiredate, 'yyyy') year FROM emp)

PIVOT (COUNT(*) FOR year IN (1980,1981,1982,1987));

 

Ich musste also den alten tool-generierten Select mit einer Pivotierung ergänzen, am besten etwas tunen und damit eine Art View basteln, die nach Eingabe einer Auftragsnummer die Ergebnisse auflistet.

Genau dafür gibt es Table Functions, also Funktionen, die diverse Parameter übernehmen und als Ergebnis eine Tabelle ausgeben.

Dafür braucht man als erstes einen Objekttypen. Damit alle Reportfunktionen den gleichen Typen nutzen können, umfasst er alle Felder, die in den Abfragen vorkommen können, z.B.

CREATE OR REPLACE TYPE select_obj_type AS OBJECT(

   serial_no               VARCHAR2(9),

   status                     NUMBER(1,0),

   order_no               VARCHAR2(30),

   box_no                  VARCHAR2(30),

   package_no           NUMBER(5,0),

   packing_date         DATE,

   test_date               TIMESTAMP(3),

   test_type                VARCHAR2(60),

   test_pass                NUMBER(1,0),

   test_station            VARCHAR2(70),

   meas_pass             NUMBER(1,0),

   order_start             TIMESTAMP(2),

   order_finished        TIMESTAMP(2),

   product_type          VARCHAR2(60),

   article_no                NUMBER(5,0),

   Par1                        NUMBER,

   Par2                        NUMBER,

   Par3                        NUMBER,

   Par4                        VARCHAR2(200),

   Par5                        NUMBER,

.....

   Par30                      NUMBER);

/

 

Auf Basis des Objekttyps erstellt man eine Nested Table.

CREATE OR REPLACE TYPE select_nt_type AS TABLE OF select_obj_type;

/

 

Und die Funktion sieht dann so aus:

CREATE OR REPLACE FUNCTION fnc_get_devices_per_order (p_order_no IN VARCHAR2)

-- PIPELINED besagt, dass der Inhalt der Nested Table Zeile für Zeile zurückgegeben wird

    RETURN select_nt_type PIPELINED

AS

    -- Variable für die NT

    select_nt   select_nt_type;

    l_dt_min    prod.device_tests.id%TYPE;

    l_dt_max    prod.device_tests.id%TYPE;

    l_order_id  prod.orders.id%TYPE;

BEGIN

    SELECT id INTO l_order_id

    FROM prod.orders

    WHERE order_no = p_order_no;

 

    -- kleinste und größte Test-ID bestimmen, dann geht's danach schneller

    SELECT MAX(id), MIN(id) INTO l_dt_max, l_dt_min

    FROM prod.device_tests dt

    WHERE EXISTS (SELECT 1 FROM prod.devices d

                  WHERE dt.device_id = d.id

                  AND d.order_id = l_order_id);

 

-- hier kommt die Abfrage von oben mit Anpassungen

    FOR rec IN (SELECT d.serial_no, p_order_no, r.pass meas_pass, pl.status,

                      dt.test_date,dt.test_type, r.par1, r.par4, r.par7,

                      r.par11,r.par15, r.par18, r.par22, r.par25, r.par27, r.par30

                FROM (SELECT serial_no, id FROM prod.devices

                      WHERE order_id = l_order_id) d

                JOIN (SELECT device_id, status FROM prod.packaging) pl

                ON pl.device_id = d.id

                JOIN (SELECT id, device_id, test_date, test_type, test_station_id

                      FROM prod.device_tests

                      WHERE id BETWEEN l_dt_min AND l_dt_max

                      AND test_station_id IN (12,15,77,22,9,13,72)) dt

                ON dt.device_id = d.id

                        JOIN prod.test_station t

                ON dt.test_station_id = t.id

               

-- hier werden die Messwerte pivotiert      

     

                JOIN (SELECT *

                     FROM (SELECT dev_test_id, parameter_id, value, pass

                           FROM prod.results

                           WHERE parameter_id IN (110, 410, 710, 110, 150,

                                                                      180, 220, 250, 270, 300)

                           AND dev_test_id BETWEEN l_dt_min AND l_dt_max)

                  PIVOT (MAX(value)

                         FOR parameter_id IN (110 AS par1,

                                                              410 AS par4,

                                                              710 AS par7,

                                                              110 AS par11,

                                                              150 AS par15,

                                                              180 AS par18,

                                                              220 AS par22,

                                                              250 AS par25,

                                                             270 AS par27,

                                                            300 AS par30))) r

                 ON r.dev_test_id = dt.id

                 ORDER BY d.serial_no) LOOP

       

-- und hier die Nested Table zeilenweise über den Objekttypen gefüllt

 

        PIPE ROW(select_obj_type(rec.serial_no, rec.status, p_order_no, NULL, NULL,

                              NULL, rec.test_date, rec.test_type, NULL, NULL,

                              rec.meas_pass, NULL, NULL, NULL, NULL, rec.par1,

                              NULL, NULL, rec.par4, NULL, NULL, rec.par7, NULL,

                              NULL, NULL, rec.par11, NULL, NULL, NULL, rec.par15,

                              NULL, NULL, rec.par18, NULL, NULL, NULL, rec.par22,

                              NULL, NULL, rec.par25, NULL, rec.par27, NULL, NULL,

                            rec.par30));  

   END LOOP;

EXCEPTION

   WHEN OTHERS THEN

        prod.pkg_err.log_error(

                p_errm => REGEXP_SUBSTR(SQLERRM, '.*$',1,1,'m'),

                p_proc => 'fnc_get_devices_per_order',

          p_parameters => 'Order_no: '||p_order_no);

END;

/

 

Der QA-User bekommt das Select-Recht an der Funktion und kann sie dann folgendermaßen aufrufen:

SELECT d.serial_n, p_order_no, r.pass meas_pass, pl.status, dt.test_date, r.par1,

       r.par4, r.par7, r.par11,r.par15, r.par18, r.par22, r.par25, r.par27, r.par30

FROM TABLE(fnc_get_devices_per_order('9S117022023'));

 

Da Pipelined Table Functions die Ergebnisse schon ausgeben, bevor die Nested Table komplett gefüllt ist, und relativ speicherschonend arbeiten, ging die Reporterstellung sehr zügig, auch dank der verbesserten Abfrage.

 

Da meine Kolleginnen die Funktionen im SQL-Developer aufrufen und sich dann selber als csv-File abspeichern, brauchte ich hier keinen Export oder eine Mailing-Funktionalität einzubauen. Das wäre aber mit den altbewährten Packages UTL_FILE und/oder UTL_HTTP kein Problem gewesen.

 

Fazit:

Eine Oracle-Datenbank hat auch in der einfachsten Ausgabe soviel "unter der Haube", dass man für die meisten Alltagsaufgaben keine externe Tools braucht, wenn man sich mit PL/SQL und SQL auskennt und die vordefinierten Oracle Packages nutzt.

Es wird dann nicht nur billiger, sondern meist auch schneller.

Und es macht echt Spaß!

 

Dr. Elke Fritsch

DOAG Themenverantwortliche Tuning

 

© Stefan Schweihofer