Browsing "Older Posts"

Page Designer bug in combination with APEX plugin settings

Von Tobias Arnhold → 10.28.2015
Yesterday evening I struggled with a strange issue inside the Page Designer.

One APEX application created on each page of the Page Designer the following javascript parsing error:

Error: parsererror - SyntaxError: JSON.parse: unexpected end of data at line 1 column 1 of the JSON data

It only happened in 1 of several applications I currently check after an APEX 5.0.2 migration.

What I did to find the issue (Thanks to Denes for some really good tips):
 - Copied the application into a new ID --> error could be reproduced
 - I dropped every page in the application --> error still occurred
 - My application had only one new empty page (page 1) and I switched to Universal Theme --> error still occurred
 - I checked several "Supporting Objects" and found the issue in one plugin
   - After deleting the plugin the parsing error disappeared
   - Unfortunately this plugin was referenced in 8 pages so I looked deeper into it
 - The issue was a wrongly referenced "Label appearance"



 - After I changed the setting to "- Not Depending -" the Page Designer run as expected

jQuery dialog z-index problem after migrating to APEX 5

Von Tobias Arnhold →
After migrating an APEX 4 application to APEX 5.0.2 I got an issue with the plugin called "Dialog Region".


The modal dialog wasn't accessible because the overlay effect was on top of my dialog.

Luckily some CSS code fixed the issue:

body div.ui-widget-overlay {
    z-index: 1;
}


The issue comes from the plugin in combination with an old theme. Actually it is easy to fix and no reason not to upgrade to APEX 5.0.2. :)

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

Btw.: The return to page function don't work in the "edit page" view.

Custom CSV Export as ZIP file

Von Tobias Arnhold → 10.23.2015
At the beginning of this year I wrote an article about "Custom CSV Export in APEX".

Now I want to extend the solution by an optional ZIP export.

Why?
You can not export several files at the same time with standard APEX features. For that you have to create a ZIP file including all the files you want to download.

I made an example application "Multi CSV Download as ZIP file" where you see a report including all employee columns and the department name. Above the report is a select list where you can filter for departments.

When you select a department and click on EXPORT then you get an CSV file including all employees for the selected department. In case you don't filter you get an ZIP file including a CSV file for each department.



To create the ZIP file I used the AS_ZIP Package created by Anton Scheffer.

Here is code I used to create the CSV/ZIP file.
DECLARE
    -- Blob Conversion Parameter
    L_BLOB           BLOB;
    L_BLOB_FINAL     BLOB;
    L_NAME           VARCHAR2(200); 
    L_NAME_DOWNLOAD  VARCHAR2(200); 
    L_CLOB           CLOB;
    L_DEST_OFFSET    INTEGER := 1;
    L_SRC_OFFSET     INTEGER := 1;
    L_LANG_CONTEXT   INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;
    L_WARNING        INTEGER;
    L_LENGTH         INTEGER;
    
    
    -- CSV Select
    CURSOR c1 IS
      SELECT
       DNAME,
       '"EMPNO"¡"ENAME"¡"JOB"¡"MGR"¡"HIREDATE"¡"SAL"¡"COMM"' 
         || CHR(13)|| CHR(10) 
         || DBMS_XMLGEN.CONVERT(XMLAGG(XMLELEMENT(E,COL_VALUE||CHR(13)||CHR(10))).EXTRACT('//text()').GETCLOBVAL(),1) AS CLOB_VAL
      FROM  (
            SELECT
              DNAME,
              '"'||EMPNO||'"¡"'||ENAME||'"¡"'||JOB||'"¡"'||MGR||'"¡"'||HIREDATE||'"¡"'||SAL||'"¡"'||COMM||'"' as COL_VALUE
            FROM EMP E
            JOIN DEPT D 
            ON (E.DEPTNO = D.DEPTNO)
            WHERE :P1_DEPT IS NULL
            OR    D.DEPTNO = :P1_DNAME
      ) 
      GROUP BY 
        DNAME
      ;
BEGIN
    if :P1_DNAME is null then
   
      for rec in c1 loop    
        DBMS_LOB.createtemporary(L_BLOB, FALSE);
        L_DEST_OFFSET    := 1;
        L_SRC_OFFSET     := 1;
        L_LANG_CONTEXT   := DBMS_LOB.DEFAULT_LANG_CTX;
        L_WARNING        := null;
      
        -- tranform the input CLOB into a BLOB of the desired charset
        DBMS_LOB.CONVERTTOBLOB( DEST_LOB     => L_BLOB,
                                SRC_CLOB     => rec.CLOB_VAL,
                                AMOUNT       => DBMS_LOB.LOBMAXSIZE,
                                DEST_OFFSET  => L_DEST_OFFSET,
                                SRC_OFFSET   => L_SRC_OFFSET,
                                BLOB_CSID    => NLS_CHARSET_ID('WE8MSWIN1252'),
                                LANG_CONTEXT => L_LANG_CONTEXT,
                                WARNING      => L_WARNING
                              );
    
         L_NAME := 'EXPORT_' || trim(to_char(rec.DNAME)) || '.csv';
    
         AS_ZIP.add1file (  P_ZIPPED_BLOB => L_BLOB_FINAL,
                  P_NAME => L_NAME,
                  P_CONTENT => L_BLOB
                  ) ;  
    
         -- dbms_lob.freetemporary(L_BLOB);
      end loop; 
  
      L_NAME_DOWNLOAD := 'EXPORT_ALL.zip';
      AS_ZIP.finish_zip (  P_ZIPPED_BLOB => L_BLOB_FINAL) ;  
    
    else
      for rec in c1 loop    
        DBMS_LOB.createtemporary(L_BLOB_FINAL, FALSE);
      
        -- tranform the input CLOB into a BLOB of the desired charset
        DBMS_LOB.CONVERTTOBLOB( DEST_LOB     => L_BLOB_FINAL,
                                SRC_CLOB     => rec.CLOB_VAL,
                                AMOUNT       => DBMS_LOB.LOBMAXSIZE,
                                DEST_OFFSET  => L_DEST_OFFSET,
                                SRC_OFFSET   => L_SRC_OFFSET,
                                BLOB_CSID    => NLS_CHARSET_ID('WE8MSWIN1252'),
                                LANG_CONTEXT => L_LANG_CONTEXT,
                                WARNING      => L_WARNING
                              );
    
         L_NAME_DOWNLOAD := 'EXPORT_' || trim(to_char(rec.DNAME)) || '.csv';
    
         exit;
      end loop; 
    end if;

    -- determine length for header
    L_LENGTH := DBMS_LOB.GETLENGTH(L_BLOB_FINAL);  

    -- first clear the header
    htp.flush;
    htp.init;

    -- create response header
    OWA_UTIL.MIME_HEADER( 'application/zip', FALSE);
    htp.p('Content-length: ' || L_LENGTH);
    htp.p('Content-Disposition: attachment; filename="'||L_NAME_DOWNLOAD||'"');
    htp.p('Set-Cookie: fileDownload=true; path=/');

    OWA_UTIL.HTTP_HEADER_CLOSE;
    -- download the BLOB
    WPG_DOCLOAD.DOWNLOAD_FILE( L_BLOB_FINAL );

    -- stop APEX
    APEX_APPLICATION.STOP_APEX_ENGINE;
  EXCEPTION
    WHEN OTHERS THEN
      if :P1_DNAME is null then
        DBMS_LOB.FREETEMPORARY(L_BLOB);
      end if;
      DBMS_LOB.FREETEMPORARY(L_BLOB_FINAL);
      RAISE;
END;


Die APEX Community gibt Vollgas!

Von Tobias Arnhold → 10.22.2015
Am Montag dem 19.10. wurde getrieben von Jürgen Schuster und weiteren APEX Entwicklern die apex.world Plattform veröffentlicht.


Eine nur von der APEX Community entwickelte und betriebene Plattform, die die Kommunikation und Zusammenarbeit zwischen Entwicklern drastisch vereinfachen und verbessern will.

Die Highlights:
 - eine LIVE Integration von Slack und Twitter
 - Die APEX Job Börse
 - Plug-In Bereitstellung mit Hilfe von GitHub
 - APEX Terminübersicht zu Meetups, Konferenzen und Live Webinars
 - APEX Einsteigerbereich (noch in Arbeit) und eine Link-Zusammenfassung über APEX Themen

Einbringen kann sich ein jeder APEX Entwickler.
Interesse? Hierzu wendet ihr euch am Besten an Jürgen. :)

Imho
Eine Klasse Leistung aller Beteiligten und wieder einmal beweist die Community selbst wie Weiterentwicklung funktioniert.

Was fehlt noch?
Ich persönlich fände eine Integration des APEX Blogaggregators sehr sinnvoll, damit wäre im Prinzip alles beisammen. Eine Integration von Beispielanwendungen wäre ebenfalls eine sinnvolle Erweiterung. Ich allein hätte da eine ganze Menge zu bieten:
http://www.apex-at-work.com/p/beispielanwendungen.html

Neben apex.world gibt es weitere Neuigkeiten rund um APEX zu berichten.


Generell:
 - APEX 5.0.2 steht zum Download bereit. Die Installation sollte in 10 Minuten erledigt sein.

 - Eine Aktualisierung des Statement of Direction für APEX 5.1 wurde veröffentlicht.

 - Die Developer Choice Awards wurden vergeben und Dietmar ist dabei.
   Glückwunsch! Respekt! Absolut Verdient!

 - Die APEX Connect Seite und die Seite der MT AG (APEX only by Material Design) wurden rund erneuert.
   Ps.: Bei der APEX Connect dachte ich Vorträge über SQL Techniken in APEX und Geovisualisierung einzureichen. Was denkt Ihr?

 - Neue APEX Anwendung: livesql.oracle.com 
   Imho: Ab jetzt immer und jederzeit SQL coden!


- Die wichtigste aktuelle Programmiersprache ist: SQL!!!

- Der SQL Developer und der ORDS sind jeweils in neuen Versionen erschienen.

Events die in naher Zukunft jeder kennen sollte:
28.10. Stuttgart - Meetup Stuttgart
30.10. Frankfurt - Meetup Frankfurt
16.11. Nürnberg - APEX Treff vor der DOAG Konferenz 2015
17.11. - 19.11. Nürnberg - DOAG 2015

Seit Juli diesen Jahres gibt es auch ein monatlich stattfindendes Webseminar rund um aktuelle Oracle Themen: tinyurl.com/oradevmonthly
Aktuelle Infos dazu findet ihr auch in Twitter.

Und nächstes Jahr geht es gleich weiter:
Im Januar 2016 starten Karin Patenge, Rainer Willems, Beda Hammerschmidt, Kai Donato, Dietmar Aust und Carsten Czarski - in Zusammenarbeit mit der DOAG eine Online-Videoserie zum Thema Modern Application Development - in der Praxis.

SQL Probleme und das Oracle SQL Forum

Von Tobias Arnhold →
Viele meiner Kollegen (um die 90 %) nutzen bei schwierigen SQL Problemen nicht das SQL Forum, sondern versuchen sich lieber an inperformanten PL/SQL Code.

Die Gründe sind leicht erklärt: Zeitmangel!

Der kurzfristige Vorteil kann im Nachhinein aber sehr teuer / zeitaufwendig werden. Nämlich dann, wenn die schnelle Lösung mit erhöhten Datenmengen versagt und statt ursprünglich wenigen Sekunden plötzlich Minuten an Zeit verbraucht.

Hierbei möchte ich auf die goldene Regel von Tom Kyte referenzieren:
     1.) Nutze solange SQL wie es geht
     2.) Wenn SQL nicht mehr reicht, dann nehme PL/SQL
     3.) Wenn PL/SQL nicht mehr reicht, dann nehme JAVA oder irgendetwas Anderes was weiterhilft

Meine Anmerkung wäre die folgende zu Punkt 1.)
     1.)  Nutze solange SQL wie es geht und befrage bei Problemen das Oracle SQL Forum

Wie aber stelle ich eine Frage richtig?
Um auch richtige Hilfe zu erhalten, muss eine entsprechende Vorarbeit Ihrerseits geleistet werden.
Das heißt:
     1.) Beschreiben Sie das Problem und fokussieren Sie sich dabei auf das Kernproblem
     2.) Hinterlegen Sie Quelldaten und ihre gescheiterten SQL Versuche
     3.) Beschreiben Sie das Zielszenario (Ergebnis in Tabellenform)
Sie sollten mit 30 Minuten Vorbereitungszeit rechnen (das erste Mal wird etwas länger dauern).
Info: Achten Sie auch auf den Inhalt der Quelldaten.  :)

Sie können sich dabei an meinen Beispielen inspirieren lassen:
     Beispiel 1: Generate some kind of cartesian list
     Beispiel 2: wi (dd.mm.yyyy-dd.mm.yyyy)
     Beispiel 3: Check crossing time periods between rows

Der aufwendigste Teil bleibt immer wieder die Aufbereitung der Quell- und Zieldaten. Ich bin zwar kein Fan der WITH-Klausel aber bei der Hilfe im Forum wird Sie von den Entwicklern immer wieder verwendet. Nun wäre es die einfachste Vorgehensweise die Quelldaten immer mit Hilfe der WITH-Klausel aufbereitet zur Verfügung zu stellen. Denn dann könnten die Entwickler deren Hilfe ich in Anspruch nehme, viel schneller reagieren. Wobei auch so die Reaktionszeit bei durchschnittlich 10 Minuten liegt (siehe Beispiele).

Deshalb habe ich eine Funktionen gebaut die auf Basis einer Tabelle oder View die Quelldaten als WITH-Klausel aufbereitet.
create or replace function get_with_clause(p_table_name in varchar2, p_include_with_clause number default 1, p_rows in number default 10) return clob as
/* Dynamische WITH-Klausel mit SELECT FROM DUAL auf Basis einer vorhandenen Tabelle generieren
   Übergabeparameter: 
     p_table_name = Tabellename
     p_include_with_clause = 1 oder 0, wenn 1 dann mit WITH-Klausel ansonsten nur SELECT FROM DUAL
     p_rows  = Anzahl Zeilen die zurückgegeben werden sollen, wenn NULL, dann alle
*/
 l_sql1 varchar2(4000);
 l_sql2 clob;
begin 
  select 'SELECT ' || chr(13) 
    || 
       case 
        when p_include_with_clause = 1 then
         '''WITH ' || max(table_name) || ' AS ('' ||' || chr(13) 
        else 
         null
       end
    || 'REGEXP_REPLACE(DBMS_XMLGEN.CONVERT(XMLAGG(XMLELEMENT(E, ' || chr(13) 
       || 'CHR(13) || ''SELECT '' || CHR(13) || '
       || replace(listagg(
                case 
                  when data_type = 'NUMBER' 
                    then 'nvl(trim(to_char('||column_name||')),''NULL'') || ''  as ' || column_name || ',''|| CHR(13) || '
                  when data_type = 'VARCHAR2' 
                    then 'decode('||column_name||',NULL,''NULL'',''''''''||'||column_name||'||'''''''') || ''  as ' || column_name || ',''|| CHR(13) || '
                  when data_type = 'DATE' 
                    then 'decode('||column_name||',NULL,''NULL'',''''''''||to_char('||column_name||',''dd.mm.yyyy hh24:mi'')||'''''''') || '' as ' || column_name || ',''|| CHR(13) || '
                  else 
                    null
                end
                , ' '
          ) within group (order by rownum) || 'ZZ',',''|| CHR(13) || ZZ',' '' ||')
       || chr(13) 
       || '''FROM DUAL UNION ALL '''
       || chr(13) 
    || ')).EXTRACT(''//text()'').GETCLOBVAL(),1)||''XX'','' UNION ALL XX'','''')'
    ||
       case 
        when p_include_with_clause = 1 then
          '|| CHR(13) || '') ' || 'SELECT t1.* FROM ' || max(table_name) || ' t1 ORDER BY 1'''
        else 
          null
       end
    || chr(13) 
    || 'FROM ' 
    || max(table_name)
    || chr(13) 
    || case when p_rows is null then null else 'WHERE ROWNUM BETWEEN 1 AND ' || trim(to_char(p_rows)) end
  into l_sql1
  from user_tab_columns
  where table_name = p_table_name
  and data_type in ('NUMBER','VARCHAR2','DATE');
 
  execute immediate (l_sql1) into l_sql2;
 
  return l_sql2;
end get_with_clause;
Innerhalb der Funktion werden die Spalten der übergebenen Tabelle ausgelesen. Daraus generiere ich dann ein dynamisches SQL das mir die "select * from dual" - je Zeile generiert. Das Ergebnis ist ein CLOB mit einem ausführbaren SQL Code.

Unter 12c sollte es auch ohne Funktionsaufruf abbildbar sein (Grundlagen sie Blogpost von Carsten), ich konnte es aber wegen fehlen einer 12c Umgebung noch nicht abschließend testen. :(
with function get_with_clause(p_table_name in varchar2, p_include_with_clause number default 1, p_rows in number default 10) return clob as
 L_SQL1 VARCHAR2(4000);
 L_SQL2 CLOB;
BEGIN 
  SELECT 'SELECT ' || CHR(13) 
    || 
       case 
        when p_include_with_clause = 1 then
         '''WITH ' || MAX(table_name) || ' AS ('' ||' || CHR(13) 
        else 
         null
       end
    || 'REGEXP_REPLACE(DBMS_XMLGEN.CONVERT(XMLAGG(XMLELEMENT(E, ' || CHR(13) 
       || 'CHR(13) || ''SELECT '' || CHR(13) || '
       || replace(listagg(
                case 
                  when data_type = 'NUMBER' 
                    then 'nvl(trim(to_char('||column_name||')),''NULL'') || ''  as ' || column_name || ',''|| CHR(13) || '
                  when data_type = 'VARCHAR2' 
                    then 'decode('||column_name||',NULL,''NULL'',''''''''||'||column_name||'||'''''''') || ''  as ' || column_name || ',''|| CHR(13) || '
                  when data_type = 'DATE' 
                    then 'decode('||column_name||',NULL,''NULL'',''''''''||to_char('||column_name||',''dd.mm.yyyy hh24:mi'')||'''''''') || '' as ' || column_name || ',''|| CHR(13) || '
                  else 
                    null
                end
                , ' '
          ) WITHIN GROUP (ORDER BY ROWNUM) || 'ZZ',',''|| CHR(13) || ZZ',' '' ||')
       || CHR(13) 
       || '''FROM DUAL UNION ALL '''
       || CHR(13) 
    || ')).EXTRACT(''//text()'').GETCLOBVAL(),1)||''XX'','' UNION ALL XX'','''')'
    ||
       case 
        when p_include_with_clause = 1 then
          '|| CHR(13) || '') ' || 'SELECT t1.* FROM ' || MAX(table_name) || ' t1 ORDER BY 1'''
        else 
          null
       end
    || CHR(13) 
    || 'FROM ' 
    || MAX(table_name)
    || CHR(13) 
    || case when p_rows is null then null else 'WHERE ROWNUM BETWEEN 1 AND ' || trim(to_char(p_rows)) end
  INTO L_SQL1
  FROM user_tab_columns
  WHERE table_name = p_table_name
  AND DATA_TYPE IN ('NUMBER','VARCHAR2','DATE');
 
  EXECUTE IMMEDIATE (L_SQL1) INTO L_SQL2;
 
  RETURN L_SQL2;
end get_with_clause;
select get_with_clause('DEPARTMENT') as result from dual
Ergebnis Beispiel für Tabelle DEPT:
SELECT GET_WITH_CLAUSE( P_TABLE_NAME => 'DEPT', P_INCLUDE_WITH_CLAUSE => 1, P_ROWS => 100 ) as RESULT
FROM DUAL;

WITH DEPT AS (
SELECT 
10  as DEPTNO,
'ACCOUNTING'  as DNAME,
'NEW YORK'  as LOC FROM DUAL UNION ALL 
SELECT 
20  as DEPTNO,
'RESEARCH'  as DNAME,
'DALLAS'  as LOC FROM DUAL UNION ALL 
SELECT 
30  as DEPTNO,
'SALES'  as DNAME,
'CHICAGO'  as LOC FROM DUAL UNION ALL 
SELECT 
40  as DEPTNO,
'OPERATIONS'  as DNAME,
'BOSTON'  as LOC FROM DUAL
) SELECT t1.* FROM DEPT t1 ORDER BY 1

Info: Die Lösung funktioniert nur mit den Datentypen: NUMBER, VARCHAR2, DATE. Die restlichen Datentypen werden derzeit ignoriert.

Analytische Funktionen (Teil 3): Gruppen mit zufälliger Auswahl der Datenbasis

Von Tobias Arnhold → 10.07.2015
Das folgende Beispiel bezieht sich auf die Datenbasis meines ersten Post zu "Analytischen Funktionen".
Es geht darum, dass für das Jahr 2014 drei Gruppen (mit einem Hash-Wert je Gruppe) gebildet werden müssen.
Gruppe 1:
- 3 zufällig ausgewählte Bundesländer die mit B anfangen
Gruppe 2:
- 3 zufällig ausgewählte Bundesländer die mit S anfangen
Gruppe 3:
- 3 zufällig ausgewählte Bundesländer die NICHT mit B und S anfangen

Hier das dafür notwendige SQL:
SELECT
  BUNDESLAND,
  EINWOHNER,
  GRUPPE,
  /* Hash Generierung */
  DBMS_CRYPTO.HASH (UTL_RAW.CAST_TO_RAW (GRUPPE||'XYZ'), 1) AS HASH_WERT
FROM (
  SELECT
   JAHR,
   BUNDESLAND,
   EINWOHNER,
    /* Gruppe definieren */
    CASE 
      WHEN BUNDESLAND LIKE 'B%' 
      THEN 1
      WHEN BUNDESLAND LIKE 'S%'
      THEN 2
      ELSE 3
    END  AS GRUPPE,
   /* Zufällige Sortierung innerhalb der Gruppe */
   ROW_NUMBER() OVER (
        PARTITION BY
          CASE 
            WHEN BUNDESLAND LIKE 'B%' 
            THEN 1
            WHEN BUNDESLAND LIKE 'S%'
            THEN 2
            ELSE 3
          END 
        ORDER BY DBMS_RANDOM.VALUE(1,10)
    ) AS RN
  FROM AS_EINWO_BUNDESL_JAHR
  WHERE JAHR = 2014
)
WHERE RN <= 3
ORDER BY GRUPPE, BUNDESLAND

/* 
   Nicht vergessen bei der Verwendung von DBMS_CRYPTO.HASH: 
   grant execute on sys.dbms_crypto to APEX_SCHEMA; 
*/
Ergebnis:

BUNDESLAND EINWOHNER GRUPPE HASH_WERT
Baden-Württemberg 10666000 1 C5B1DF1A52DCC34C648FC79804F49A0A
Bayern 12643000 1 C5B1DF1A52DCC34C648FC79804F49A0A
Bremen 659000 1 C5B1DF1A52DCC34C648FC79804F49A0A
Sachsen 4045000 2 38FAEEE9CD02869F273DE5A44CF75218
Sachsen-Anhalt 2238000 2 38FAEEE9CD02869F273DE5A44CF75218
Schleswig-Holstein 2819000 2 38FAEEE9CD02869F273DE5A44CF75218
Hamburg 1762000 3 7DA2D9ED3366BAEA4B1FA0D6269F7F9F
Mecklenburg-Vorpommern 1594000 3 7DA2D9ED3366BAEA4B1FA0D6269F7F9F
Rheinland-Pfalz 3996000 3 7DA2D9ED3366BAEA4B1FA0D6269F7F9F

Im Endeffekt wird mit Hilfe der ROW_NUMBER() Funktion und DBMS_RANDOM.VALUE Funktion eine zufällige Verteilung geschaffen.

DOAG 2015 noch wenige Wochen...

Von Tobias Arnhold → 10.01.2015
Noch wenige Wochen bis zur DOAG 2015.

Dieses Jahr habe ich die Ehre 2 Vorträge zu halten, nachdem ich...
 - 2012 abgelehnt wurde
 - 2013 abgelehnt wurde
 - 2014 dabei war mit: Dynamisches Arbeiten mit Grafiken innerhalb von APEX
 - und 2015...

3 Gründe warum es sich lohnt meine Vorträge anzuschauen
1.) Egal was ich mache, ich mache es zu 100%!
2.) Wenn Ihr nicht den "WOW"-Effekt habt, dann ist es für mich ne Niederlage!
3.) Ich entwickle einen Großteil meiner Präsentationen mit APEX. Weil darin die Leidenschaft eines jeden APEX Entwicklers steckt. Und diese Leidenschaft gilt es weiterzugeben!

Hier nun die Details zu meinen Vorträgen:

    APEX & SQL = THE Reporting Solution - 100% APEX Dienstag 17.11. 14:00 - Helsinki
    --> Wer meint APEX und BI passt nicht zusammen, der wird hier eines besseren belehrt.
    --> Woran scheitern BI-Projekte? Wie kann APEX helfen? Wie kann Visualisierung mit APEX aussehen?
    --> Genau die richtige Präsentation, um nach dem Mittag nicht einzuschlafen. :)


    Die APEX 5 Migration - 50% APEX / 50 % PowerPoint Mittwoch 18.11. 11:00 - Hongkong
    --> Fokus auf das Universal Theme
    --> Die Universal Theme Migration anhand einer echten Businessanwendung präsentiert.


Ps.:
Schaut euch auch mal die anderen Vorträge an: DOAG Konferenz Planer
Wie immer sind auch jede Menge APEX Themen und TOP-Speaker mit dabei:
  Denes
  Dietmar
  Peter
  Oliver
  Andreas
  Jürgen (Vote for at the APEX Developer Choice Awards)
  Carsten
  Patrick
  uvm...

Analytische Funktionen (Teil 2): LISTAGG mit eindeutiger Liste

Von Tobias Arnhold →
Die LISTAGG Funktion dient der Generierung von zusammenkonkatenierten Strings auf Basis einer Spalte. Wenn in der Spalte ein Wert mehrfach vorkommt, dann wird die Liste ebenfalls mit doppelten Werten generiert.

Im folgenden demonstriere ich ein Beispiel um dieses Problem zu lösen.

Beispieldaten - Land und Ort:
SELECT
 LAND, ORT
FROM STADT_LISTE 
ORDER BY 1,2;
Ergebnis:
LAND ORT
Belgien Bruxelles
Belgien Bruxelles
Polen Katowice
Polen Wegliniec
Schweiz Basel
Schweiz Bern
Schweiz Riehen
Schweiz Thayngen
Schweiz Thayngen

Ziel ist es nun, nur die Länder mit jeweils einer Liste an Orten auszugeben. Unglücklicherweise hat meine Tabelle die Orte Bruxelles und Thayngen doppelt hinterlegt. Mit Hilfe der ROW_NUMBER() Funktion selektiere ich diese Werte vor der LISTAGG-Funktion heraus.
SELECT
  LAND,
  REPLACE(':'||LISTAGG(ORT_UNGUELTIG,':')  WITHIN GROUP (ORDER BY ORT_UNGUELTIG)||':','::',NULL) AS ORT_LISTE_UNGUELTIG,
  REPLACE(':'||LISTAGG(ORT_GUELTIG,':')  WITHIN GROUP (ORDER BY ORT_GUELTIG)||':','::',NULL) AS ORT_LISTE_GUELTIG
FROM (
  SELECT  
   LAND, 
   ORT AS ORT_UNGUELTIG,
   CASE WHEN 
         ROW_NUMBER() OVER (PARTITION BY LAND, ORT ORDER BY ORT) = 1
         THEN ORT
         ELSE NULL
    END AS ORT_GUELTIG
  FROM STADT_LISTE 
)
GROUP BY LAND
ORDER BY LAND;
Ergebnis:
LAND ORT_LISTE_UNGUELTIG ORT_LISTE_GUELTIG
Belgien :Bruxelles:Bruxelles: :Bruxelles:
Polen :Katowice:Wegliniec: :Katowice:Wegliniec:
Schweiz :Basel:Bern:Riehen:Thayngen:Thayngen: :Basel:Bern:Riehen:Thayngen:

Weiterführende Infos zum Thema LISTAGG und Analytische Funktionen findet Ihr hier: Die besten HowTo's rund um fortgeschrittene SQL-Techniken