Skip to Main Content
 

Search Results

Oracle Attention.log via Pipelined Table Function auslesen / auswerten

Bereich:DBA:PL/SQL, Version: ab RDBMS 21.1:RDBMS 23.1, Letzte Überarbeitung: 15.11.2023

Keywords:

Ab der Version Oracle 21c wird zusätzlich zum alert.log noch eine neue Datei ausgeliefert: das Attention.log. Hier sollen wichtige Information als Zusammenfassung der Alert.log stehen.

Naja, ich hoffe das wird nicht etwas verbessert, aber trotzdem wäre es praktisch die Datei mit einem Select auszulesen. Ich hatte gehofft, jemand hat sich die Arbeit schon gemacht (Google wo bist Du, wenn man Dich braucht…), aber leider habe ich nichts gefunden.

Ok, dann mach ich es halt …

Wir geben die Objekte dem Benuter system, sie können aber natürlich auch einen anderen Benutzer dafür einrichten.

ALTER SESSION SET current_schema=system;
col adir new_value adir
col afile new_value afile
with diag as (select sys_context('userenv','PLATFORM_SLASH') as ps,value from v$diag_info
where name='Attention Log')
select 
substr(value,1,instr(value,ps,-1)-1) as adir,
substr(value,instr(value,ps,-1)+1) as afile
from diag;
CREATE OR REPLACE DIRECTORY attention_dir as '&adir.';
GRANT READ ON DIRECTORY attention_dir to system;

 

CREATE OR REPLACE TYPE attention_type 
AS OBJECT (
   NOTIFICATION    VARCHAR2(4000), 
   ERROR           VARCHAR2(4000),
   URGENCY         VARCHAR2(4000),
   INFO            VARCHAR2(4000),
   CAUSE           VARCHAR2(4000),
   ACTION          VARCHAR2(4000),
   CLASS           VARCHAR2(4000),
   TIME            TIMESTAMP WITH TIME ZONE    
);
/
CREATE OR REPLACE TYPE attention_tab_type AS TABLE OF attention_type;
/
CREATE OR REPLACE FUNCTION read_attention_log 
RETURN attention_tab_type PIPELINED
IS
 f_handle  utl_file.file_type:=utl_file.fopen(
 location=>'ATTENTION_DIR',
 filename=>'&afile.',
 open_mode=>'r',
 max_linesize=>32767);
 text         varchar2(32767);
 v_a_log attention_type:=attention_type(null,null,null,null,null,null,null,null);
 v_i INT:=1;
BEGIN
 LOOP 
     BEGIN
     utl_file.get_line(f_handle,text); -- Neue Zeile lesen
   IF substr(text,1,16)='  "NOTIFICATION"' THEN
       v_a_log.NOTIFICATION:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
   IF substr(text,1,9)='  "ERROR"' THEN 
       v_a_log.ERROR:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
   IF substr(text,1,11)='  "URGENCY"' THEN    
       v_a_log.URGENCY:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
   IF substr(text,1,8)='  "INFO"' THEN    
       v_a_log.INFO:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;  
   IF substr(text,1,9)='  "CAUSE"' THEN
       v_a_log.CAUSE:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
   IF substr(text,1,10)='  "ACTION"' THEN
       v_a_log.ACTION:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
    IF substr(text,1,9)='  "CLASS"' THEN
       v_a_log.CLASS:=rtrim(rtrim(substr(text,21),','),'"');
   END IF;
   IF substr(text,1,8)='  "TIME"' THEN
       v_a_log.TIME:=to_timestamp_tz(rtrim(substr(text,21),'"'),'YYYY-MM-DD"T"HH24:MI:SS.FFTZH:TZM');
       PIPE ROW (v_a_log);
       v_a_log :=attention_type(null,null,null,null,null,null,null,null);
   END IF;
     EXCEPTION 
    WHEN NO_DATA_FOUND THEN EXIT; -- Keine Zeile im Attention.log mehr gefunden oder Fehler=> Schleife verlassen
    WHEN OTHERS THEN RAISE;
     END;
 END LOOP; 
 utl_file.fclose(f_handle);
END;
/

Un nun können Sie das attention.log via SQL auslesen …

SELECT * FROM table(read_attention_log);


Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


Funktion Return Boolean in SQL Problem lösen

Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 26.07.2018

Keywords:SQL, Boolean, WITH Function

Haben Sie sich auch schon darüber geärgert, dass gerade Oracle keine Boolean Datentypen in SQL erlaubt?
Wenn Sie also eine PL/SQL Funktion besitzen, die nur TRUE oder FALSE zurückgibt, muss man sich einen Wrapper in PL/SQL schreiben.

Aber in unserem Tipp des Monats, bekommen wir das mit einem Trick auch nur in SQL hin.

Sagen wir mal, wir hätten eine Funktion wie diese hier:

CREATE OR REPLACE FUNCTION my_bool
RETURN BOOLEAN IS
BEGIN
IF EXTRACT(HOUR FROM cast (systimestamp as timestamp))<=12 RETURN true;
ELSE RETURN false;
END IF;
END;

Dann könnten wir mit einer einfachen WITH Klausel das auch in reinem SQL hinbekommen.

WITH
  FUNCTION return_boolean RETURN VARCHAR2 IS
  BEGIN
   IF my_bool THEN
    RETURN 'True';
   ELSE
    RETURN 'False';
  END IF;
  END;
SELECT return_boolean FROM dual;

 

Viele weitere Tipps & Tricks bekommen Sie in einem unserer bewährten PL/SQL Kurse (PL/SQL, PL/SQL II, PL/SQL Packages, ...)



Weitere Interessente Artikel zum Thema:



Empfohlene Schulungen zum Thema:


DBMS_OUTPUT umgeleitet

Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 23.07.2018

Keywords:dbms_output, dbms_pipe, htp.p

Ich liebe das Package dbms_output, wenn es nur nicht ...

  • so einen langen Namen
  • so eine eingeschänkte Bedienung

hätte.

Aber den letzen Punkt können wir ändern. In diesem Tipp holen wir die Daten, die wir in den Puffer von dbms_output geschrieben haben und legen Sie woanders hin.

##################################################
Umleitung von dbms_output zu pipe
##################################################

CREATE OR REPLACE PROCEDURE test_ausgabe
-- Rechte: grant execute on dbms_pipe to scott;
IS
    PROCEDURE dop2pipe (pipe_name IN VARCHAR2 DEFAULT 'DBMS_OUTPUT_PIPE') IS
       lines dbms_output.chararr;
       num_lines number:=1000000;
    BEGIN
       dbms_output.get_lines(lines, num_lines);
       FOR i IN 1..num_lines LOOP
            dbms_pipe.pack_message (lines(i));
       END LOOP;
        IF (dbms_pipe.send_message (pipe_name)) <> 0 THEN
            raise_application_error(-20500,'Fehler beim Senden in Pipe '||pipe_name||' aufgetreten !');
        END IF;
    END;
BEGIN
   dbms_output.enable(null);  
   FOR i IN 1 .. 10 LOOP
     dbms_output.put_line('Zeile='||i);
   END LOOP;
dop2pipe;   -- <=######## Umwandelung von dbms_output in Pipe
END;
/

CREATE OR REPLACE PROCEDURE get_dout_from_pipe (pipe_name IN VARCHAR2 DEFAULT 'DBMS_OUTPUT_PIPE')

IS

     v_message VARCHAR2(32767);

     v_timeout NUMBER:=120;

BEGIN

     IF (DBMS_PIPE.receive_message(pipe_name,v_timeout)) <> 0 THEN

          RAISE_APPLICATION_ERROR(-20501,'Fehler beim Lesen aus Pipe '||pipe_name||' aufgetreten !');

     END IF;

     LOOP

        EXIT WHEN DBMS_PIPE.NEXT_ITEM_TYPE = 0;

         DBMS_PIPE.unpack_message(v_message);

         DBMS_OUTPUT.PUT_LINE(v_message);

     END LOOP;

END;

/

##################################################
Umleitung von dbms_output zu htp
##################################################


CREATE OR REPLACE PROCEDURE test_ausgabe
IS
    PROCEDURE dop2htp IS
       lines dbms_output.chararr;
       num_lines number:=1000000;
    BEGIN
       dbms_output.get_lines(lines, num_lines);
       FOR i IN 1..num_lines LOOP
          htp.p(lines(i)||'<BR>');
       END LOOP;
    END;
BEGIN
   dbms_output.enable(null);  
   FOR i IN 1 .. 10 LOOP
     dbms_output.put_line('Zeile='||i);
   END LOOP;
dop2htp;   -- <=######## Umwandelung von dbms_output in htp.p Ausgabe
END;

 

Ich hoffe dies konnte Ihnen weiterhelfen, bei sonstigen Fragen melden sie sich gerne bei uns und oder besuchen sie einen der Kurse. :-)



Weitere Interessente Artikel zum Thema:



Empfohlene Schulungen zum Thema:


Das PL/SQL-Berechtigungskonzept in 12c

Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 11.01.2021

Keywords:Oracle Neuerungen, PL/SQL, 12C Release 1

Oracle hat das Berechtigungskonzept zu PL/SQL in Hinblick auf zwei gegensätzliche Szenarien in 12c ausgebaut:

  • Szenario 1: Der Ausführende hat mehr Rechte als der Programmierer.
    Die Änderung zu diesem Szenario (Privileg "INHERIT [ANY] PRIVILEGES") betrifft ausschließlich Prozeduren, die mit Invoker Rights arbeiten. 
  • Szenario 2: Der Ausführende hat weniger Rechte als der Programmierer.

Die Änderung zu diesem Szenario (Vergabe von Rollen) betrifft Prozeduren, die mit Invoker Rights arbeiten oder mit dynamischem SQL.

Beiden Neuerungen gemeinsam ist, dass es um die Überprüfung von Rechten zur Laufzeit geht. Auch weiterhin benötigt ein Entwickler alle entsprechenden Berechtigungen selber, um eine Prozedur erfolgreich kompilieren zu können. Dynamisches SQL allerdings wird zur Kompilierzeit nicht ausgewertet.

Hinweis: Hier und im weiteren ist "Prozedur" als Sammelbegriff zu verstehen, der auch Funktionen und Packages mit einschließt. 

INHERIT [ANY] PRIVILEGES 

Hier kommt Szenario 1 ins Spiel:

Angenommen, der User ADMIN hat weitreichende Rechte.

Weiter angenommen, der Programmierer SCOTT hat vergleichsweise wenig Rechte.

Nun schreibt SCOTT eine Prozedur mit Invoker Rights (bei Definer Rights spielt das neue Privileg keine Rolle), die er ADMIN zur Verfügung stellt. Solange es sich um statisches SQL handelt, kann nicht viel passieren, da ja SCOTT's Berechtigungen zur Compile-Zeit überprüft werden.

Nun könnte aber SCOTT bösartigerweise Befehle in dynamisches SQL verpacken, die weit über seine eigenen Berechtigungen hinausgehen (z. B. an sich selbst Admin-Rechte vergeben), nicht jedoch über diejenigen von ADMIN. SCOTT kann so eine Prozedur problemlos erstellen - aber nicht ausführen. ADMIN dagegen kann sie ausführen.

Um einen solchen Missbrauch ggf. unterbinden zu können, wurde mit Version 12c die Berechtigung INHERIT [ANY] PRIVILEGES eingeführt. Das Konzept dabei sieht folgendermaßen aus:

Der Eigentümer der Prozedur (in obigem Szenario SCOTT) braucht, sofern er nicht über das Systemprivileg INHERIT ANY PRIVILEGES  verfügt, von dem Benutzer, der die Prozedur später ausführen soll - also hier ADMIN - , das Objektprivileg INHERIT PRIVILEGES. Hat er das nicht, so kann ADMIN später die Prozedur nicht ausführen.

Damit sich das Verhalten zwischen 11g und 12c nicht ändert, erhält PUBLIC automatisch für jeden neu angelegten User dieses Recht. Das können Sie leicht nachprüfen über dba_tab_privs:

SELECT *
  FROM dba_tab_privs
 WHERE grantee = 'PUBLIC' 
   AND privilege = 'INHERIT PRIVILEGES';


Oracle empfiehlt jedoch PUBLIC diese Rechte zu entziehen.
 

Für das oben beschriebene Szenario bedeutet dies:

Hat PUBLIC das Recht INHERIT PRIVILEGES ON USER ADMIN, wie das dem Default entspricht, so wird ADMIN die Prozedur von SCOTT ausführen können, wie auch schon in 11g. Entzieht ADMIN dieses Recht mit

REVOKE INHERIT PRIVILEGES ON USER ADMIN FROM PUBLIC;

und vergibt das Recht auch nicht explizit an SCOTT, dann kann er keine Prozedur von SCOTT mehr ausführen, die mit INVOKER RIGHTS angelegt wurde. Beim Versuch erhält er die Fehlermeldung

ORA-06598: Nicht ausreichende INHERIT PRIVILEGES-Berechtigung

ROLLEN AN PL/SQL-OBJEKTE VERGEBEN

Szenario 2 sieht so aus:

User SCOTT stellt wiederum Invoker Rights-Prozeduren zur Verfügung, in diesem Fall für den User LEHRLING. Eine der Prozeduren greift auf ein Tabelle im Schema SCOTT zu. SCOTT will aber nicht, dass LEHRLING direkten Zugriff auf diese Tabelle bekommt. Den bräuchte LEHRLING aber bis einschließlich Version 11g, um diese Prozedur ausführen zu können.

SCOTT erstellt z. B. folgende Funktion:

CREATE OR REPLACE FUNCTION deptno_exists (p_deptno IN SCOTT.DEPT.deptno%TYPE)
   RETURN BOOLEAN
   AUTHID CURRENT_USER
IS
   v_count   NUMBER;
   v_ret     BOOLEAN;
BEGIN
   SELECT COUNT (*)
     INTO v_count
     FROM scott.dept
    WHERE deptno = p_deptno;
   IF v_count > 0
   THEN
      v_ret  := TRUE;
   ELSE
      v_ret  := FALSE;
   END IF;
   RETURN v_ret;
END deptno_exists;
/
GRANT EXECUTE ON deptno_exists TO LEHRLING
/

LEHRLING wird diese Funktion bis jetzt nicht erfolgreich ausführen können:

ORA-00942: Tabelle oder View nicht vorhanden
ORA-06512: in "SCOTT.DEPTNO_EXISTS", Zeile 8

In Version 12c stellt nun ein DB-Administrator SCOTT eine Rolle zur Verfügung:

CREATE ROLE select_dept;
GRANT select_dept TO SCOTT WITH ADMIN OPTION;

SCOTT vergibt das benötigte Select-Recht and die Rolle:

GRANT SELECT ON dept TO select_dept;

Und die Rolle an die Funktion(!):

GRANT select_dept TO FUNCTION deptno_exists;

Nun kann LEHRLING die Funktion erfolgreich ausführen ohne direkten Zugriff auf SCOTT.dept zu erhalten.

An dem Grundprinzip, dass Rollen in PL/SQL unwirksam sind, ändert sich nichts: Eine Prozedur kann auch weiterhin nur dann erfolgreich kompiliert werden, wenn ihr Eigentümer alle dafür erforderlichen Rechte DIREKT gegrantet bekommen hat.

Ein Beispiel zur Anwendung dieses Rollenkonzepts bei Definer Rights und dynamischem SQL hat Tom Kyte in seinem Blog beschrieben.



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


Zeilenbegrenzung in 12c - Pagination leicht gemacht

Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 05.08.2023

Keywords:Oracle Neuerungen, SQL, PL/SQL

Wer je Daten für eine Web-Applikation bereitstellen musste, kennt das Problem der Pagination. In der Regel holt das Frontend ja nur die Daten, die auf eine Seite passen, und erst wenn mehr angefordert werden, werden auch mehr geholt. Das stellt den Programmierer vor die Schwierigkeit, die Daten entprechend "mundgerecht" zu liefern. Entscheidend dabei ist in jedem Fall eine absolut eindeutige Sortier-Reihenfolge.

Ein klassischer von Tom Kyte u. a. hier Öffnet externen Link in neuem Fenster hier beschriebener Ansatz über ROWNUM sieht dann so aus:

select *
  from ( select /*+ FIRST_ROWS(n) */
  a.*, ROWNUM rnum
      from ( your_query_goes_here,
      with order by ) a
      where ROWNUM <=
      :MAX_ROW_TO_FETCH )
where rnum  >= :MIN_ROW_TO_FETCH;

Anmerkung: Für die folgenden Beispiele wurde eine Tabelle OBJECT_TAB als Kopie von ALL_OBJECTS erstellt.

Angewendet auf diese Tabelle sähe ein Select, der die Zeilen 11 bis 20 holt, z. B. so aus:

SELECT object_id, object_name
  FROM (SELECT a.*, ROWNUM rnum
          FROM (  SELECT *
                    FROM object_tab
                ORDER BY object_id) a
         WHERE ROWNUM <= 20)
 WHERE rnum >= 11;

Der Ausführungsplan zeigt, dass der Optimizer hier mit Stopkeys arbeitet:

Execution Plan
----------------------------------------------------------
0      SELECT STATEMENT Optimizer Mode=ALL_ROWS (Cost=1872 Card=20 Bytes=1 K)
1   0    VIEW (Cost=1872 Card=20 Bytes=1 K)
2   1      COUNT STOPKEY
3   2        VIEW (Cost=1872 Card=65 K Bytes=4 M)
4   3          SORT ORDER BY STOPKEY (Cost=1872 Card=65 K Bytes=6 M)
5   4            TABLE ACCESS FULL OBJECT_TAB (Cost=290 Card=65 K Bytes=6 M)

Anmerkung: Um Zeilenumbrüche in der Darstellung zu verhindern, wurden überflüssige Leerzeichen und Schema-Angaben aus dem Ausführungsplan entfernt.

Im PL/SQL-Umfeld würde eine entsprechende Prozedur - stark vereinfacht - z. B. so aussehen:

CREATE PROCEDURE test_1 (p_from     IN     NUMBER,
                         p_until    IN     NUMBER,
                         p_result      OUT SYS_REFCURSOR)
AS
BEGIN
   OPEN p_result FOR
      SELECT object_id, object_name
        FROM (SELECT a.*, ROWNUM rnum
                FROM (  SELECT object_id, object_name
                          FROM object_tab
                      ORDER BY object_id) a
               WHERE ROWNUM <= p_until)
       WHERE rnum >= p_from;
END test_1;
/

Da man jedoch gerade bei Suchmasken in der Regel mit dynamischen SQL arbeiten muss, ist das wohl etwas näher dran:

CREATE PROCEDURE test_2 (p_from     IN     NUMBER,
                         p_until    IN     NUMBER,
                         p_result      OUT SYS_REFCURSOR)
AS
   v_sql   VARCHAR2 (4000 CHAR);
BEGIN
   v_sql  := 'SELECT object_id, object_name
              FROM (SELECT a.*, ROWNUM rnum
                      FROM (  SELECT object_id, object_name
                                FROM object_tab
                                -- WHERE .....
                            ORDER BY object_id) a
                     WHERE ROWNUM <= :1)
             WHERE rnum >= :2';
   OPEN p_result FOR v_sql USING p_until, p_from;
END test_2;
/

Das funktioniert zwar, ist aber nicht direkt intuitiv.

Mit Version 12c hat nun Oracle eine neue Klausel zur Begrenzung der Zeilen eingeführt, die die Syntax in solchen Fällen deutlich vereinfacht.

Angegeben werden können dabei:

  • OFFSET: Ein Startpunkt, ab wo geliefert werden soll; wird diese Angabe weggelassen, gilt OFFSET = 0, also Beginn beim ersten Datensatz der Ergebnismenge

 

  • FETCH: Anzahl oder Prozentsatz an Zeilen, die geholt werden sollen; wird diese Angabe weggelassen, so werden alle Zeilen ab <OFFEST + 1> geholt


Das obige Beispiel würde in 12c so aussehen:

  SELECT object_id, object_name
    FROM object_tab
ORDER BY object_id
 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;

Glaubt man dem Ausführungsplan, so ist diese Abfrage nicht nur wesentlich lesbarer, sondern auch noch performanter:

Execution Plan
----------------------------------------------------------
0      SELECT STATEMENT Optimizer Mode=ALL_ROWS (Cost=860 Card=65 K Bytes=6 M)
1   0    VIEW (Cost=860 Card=65 K Bytes=6 M)
2   1      WINDOW SORT PUSHED RANK (Cost=860 Card=65 K Bytes=1 M)
3   2        TABLE ACCESS FULL OBJECT_TAB (Cost=289 Card=65 K Bytes=1 M)

Anmerkung: Auch hier wurden überflüssige Leerzeichen und Schema-Angaben aus dem Ausführungsplan entfernt.

Eine Prozedur analog zu oben würde dann so aussehen:

CREATE OR REPLACE PROCEDURE test (p_offset   IN     NUMBER,
                                  p_lines    IN     NUMBER,
                                  p_result      OUT SYS_REFCURSOR)
AS
   v_sql   VARCHAR2 (4000 CHAR);
BEGIN
   v_sql  := '  SELECT object_id,object_name
                 FROM object_tab
                 -- WHERE .....
                ORDER BY object_id
                OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY';
   OPEN p_result FOR v_sql USING p_offset, p_lines;
END test;
/

Wesentlich lesbarer, oder?

Welche Möglichkeiten bietet die neue Klausel noch? Neben dem oben angegebenen <n> ROWS kann auch ein Prozentsatz mitgegeben werden mit <n> PERCENT ROWS. Das würde dann so aussehen:

  SELECT object_id, object_name
    FROM object_tab
ORDER BY object_id
 OFFSET 10 ROWS FETCH NEXT 10 PERCENT ROWS ONLY;

Sollte die Sortierung nicht eindeutig sein (und auch nicht sein müssen), so kann man angeben, dass alle Datenätze mit ausgegeben werden sollen, die den gleichen Wert haben wie der zuletzt geholte. Dazu gibt man statt ONLY an WITH TIES. Der Effekt sei hier gezeigt an der allseits bekannten Tabelle SCOTT.EMP:

  SELECT ename, sal
    FROM scott.emp
ORDER BY sal DESC
FETCH NEXT 2 ROWS ONLY;
ENAME             SAL
---------- ----------
KING             5000
SCOTT            3000

 


  SELECT ename, sal
    FROM scott.emp
ORDER BY sal DESC
FETCH NEXT 2 ROWS WITH TIES;
ENAME             SAL
---------- ----------
KING             5000
SCOTT            3000
FORD             3000

Diese neue Klausel ist prinzipell nicht abhängig von der ORDER BY-Klausel. Ihre vollen Möglichkeiten entfaltet sie aber nur hier.

Mehr zu diesem Thema erfahren Sie in unserer Schulung Oracle Neuerungen 12c, schauen Sie doch einfach vorbei :-)



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


Parallelisierung von DML - Operatoren mit DBMS_Parallel_Execute ab 11.2

Bereich:PL/SQL, Version: ab RDBMS 11.2, Letzte Überarbeitung: 05.08.2023

Keywords:PL/SQL, Standard Packages

Die Parallelisierung von größeren DELETE- und UPDATE-Aktionen bietet diverse Vorteile:

  • Die Performance wird durch den parallelen Zugriff erhöht.
  • Es wird weniger Platz in den Undo-Segmenten verbraucht, da die Transaktion auf mehrere kleinere Einheiten verteilt wird.
  • Die Tabellen sind nicht so lange offline, da die Sperren schneller aufgehoben werden können.


Wenn man selber prozedurale Lösungen zum parallelen Löschen oder Aktualisieren großer Datensatzmengen erstellen will, wird das allerdings schnell mühsam, weil man erst einmal sicherstellen muss, dass die Pakete so aufgeteilt werden, dass sich die einzelnen Transaktionen nicht gegenseitig behindern.
Ab der Oracle Version 11 Release 2 gibt es eine Oracle-eigene Lösung für die Parallelisierung von DML-Aktionen über den Scheduler, die den Anwendern sehr viel Arbeit abnehmen kann, das Package DBMS_PARALLEL_EXECUTE.
In diesem Fall werden die Tabellen über einen DBMS_SCHEDULER-Prozess automatisch in Teilbereiche, sog. Chunks, unterteilt (die aber nichts mit den Chunks bei der Speicherung von LOBs zu tun haben). Auf diese Abschnitte werden die DML-Befehle parallel abgesetzt. Ein COMMIT erfolgt nach jeder Fertigstellung des DML-Befehls auf dem jeweiligen Tabellenbereich. Dadurch werden die Undo-Segmente weniger stark belastet.
Um das Fehlerlogging und automatische Wiederholungen im Fehlerfall kümmert sich ebenfalls der Scheduler.
Die einzige Voraussetzung für die Nutzung ist das CREATE JOB-Recht. Das Package an sich ist an Public gegrantet. Im Gegensatz zu den meisten anderen Optionen, die mit Parallelisierung zu tun haben, ist die Nutzung des Packages nicht auf die Enterprise Edition beschränkt!!
Das Package besteht aus folgenden Prozeduren, die alle einen Commit beinhalten.

  • CREATE_TASK erstellt einen Auftrag für die parallele Abarbeitung.

 

  • Über CREATE_CHUNKS_BY_ROWID kann man die Teilbereiche der Tabelle über die  ROWID definieren. Diese Unterteilung ist die unkomplizierteste Methode. Die Abschnitte überlappen sich garantiert nicht, insofern hat man auch keine Probleme mit Sperren und die Tabelle muss für das Chunking nicht extra abgefragt werden, da die Informationen über die ROWIDs bequem aus dem Data Dictionary bezogen werden können.

 

  • Wenn man die Aufteilung der Tabelle über ein SQL-Statement bestimmen will, benutzt man CREATE_CHUNKS_BY_SQL.
  • Auch eine Stückelung anhand der Werte einer numerischen Spalte ist möglich, die entsprechende Prozedur heißt CREATE_CHUNKS_BY_NUMBER_COL.
  • Nach dem Erstellen der Chunks startet man die Abarbeitung über die Prozedur RUN_TASK.
  • TASK_STATUS liefert Informationen über den Status.
  • STOP_TASK würgt den Auftrag ab.
  • RESUME_TASK startet die Wiederaufnahme.
  • DROP_TASK löscht den Task.


Die Funktion TASK_STATUS kann zum Monitoren und zur Fehlerbehandlung genutzt werden.

 

BEISPIEL: PARALLELER UPDATE, EINTEILUNG ÜBER DIE ROWID

 


Probieren's wir mal aus. Für den ersten Test nehmen wir Tom Kytes bewährte Tabelle Initiates file downloadbig_tab mit 2 Millionen Datensätzen. Weil wir später die Session_ids bei der parallelen Ausführung ermitteln wollen, benennen wir die data_object_id-Spalte in session_id um.

Zunächst braucht Scott das Create Job-Recht:

conn / as sysdba
GRANT CREATE JOB TO scott;
conn scott/tiger
@ d:/create_bigtab.sql
ALTER TABLE big_tab RENAME COLUMN data_object_id TO session_id;

Jetzt kann Scott einen Task erstellen. Der Taskname ist einfach ein VARCHAR2-Parameter. Er muss nicht den Regeln für Oracle-Bezeichner entsprechen:

BEGIN
   DBMS_PARALLEL_EXECUTE.create_task (task_name => 'update big_tab, chunks by rowid');
END;

Ob die Erstellung geklappt hat, kann man über die View USER_PARALLEL_EXECUTE_TASKS herausfinden:

col task_name for a50
SELECT task_name, status FROM user_parallel_execute_tasks;
=>
TASK_NAME                             STATUS
------------------------------------- ----------
update big_tab, chunks by rowid       CREATED

Falls einem grad kein passender Name einfällt, kann man die Funktion GENERATE_TASK_NAME verwenden, die eine interne Sequenz hochzählt:

SELECT DBMS_PARALLEL_EXECUTE.generate_task_name FROM dual;
=> TASK$_1

Der nächste Schritt ist die Unterteilung der Tabelle. Wenn der Parameter by_row auf TRUE gesetzt wird, bezieht sich die chunk_size auf die Anzahl der Zeilen, wenn er auf FALSE steht, auf die Anzahl der Blöcke.
Optimale Werte für die chunk_size muss man selber ermitteln. Je kleiner die chunk_size, desto schneller sind die Tabellenabschnitte wieder frei von Sperren:

BEGIN
  DBMS_PARALLEL_EXECUTE.create_chunks_by_rowid(
          task_name => 'update big_tab, chunks by rowid',
        table_owner => 'SCOTT',
         table_name => 'BIG_TAB',
             by_row => TRUE,
         chunk_size => 10000);
END;
/

 

SELECT task_name, status FROM user_parallel_execute_tasks;
TASK_NAME                             STATUS
------------------------------------- -------------
update big_tab, chunks by rowid       CHUNKED

Genauere Informationen über die einzelnen Abschnitte liefert die View USER_PARALLEL_EXECUTE_CHUNKS:

SELECT chunk_id, status, start_rowid, end_rowid
FROM   user_parallel_execute_chunks
WHERE  task_name = 'update big_tab, chunks by rowid'
ORDER BY chunk_id;
=>
CHUNK_ID STATUS               START_ROWID        END_ROWID
-------- -------------------- ------------------ ------------------
       2 UNASSIGNED           AAAVbmAAEAAAGegAAA AAAVbmAAEAAAGenCcP
       3 UNASSIGNED           AAAVbmAAEAAAGeoAAA AAAVbmAAEAAAGevCcP
       4 UNASSIGNED           AAAVbmAAEAAAGewAAA AAAVbmAAEAAAGe3CcP
       5 UNASSIGNED           AAAVbmAAEAAAGe4AAA AAAVbmAAEAAAGe/CcP
       6 UNASSIGNED           AAAVbmAAEAAAGfAAAA AAAVbmAAEAAAGfHCcP
....
331 Zeilen ausgewählt.

Jetzt folgt die eigentliche Ausführung:
In diesem Beispiel werden 10 parallele Prozesse (Scheduler-Jobs) gestartet, die sich jeweils einen der nicht zugeordneten (unassigned) Abschnitte vornehmen, die über den Parameter sql_stmt vorgegebene DML-Anweisung durchführen, festschreiben und zum nächsten freien Chunk übergehen. Wenn man z. B. nur 4 Prozessoren hat, laufen die Jobs natürlich teilweise seriell.
Der Quotation-Operator (q'[...]') erlaubt die Schreibweise ohne Maskierung der inneren Hochkommata:

DECLARE
  l_stmt VARCHAR2(32767);
BEGIN
  l_stmt := q'[UPDATE big_tab
               SET data_object_id = sys_context('userenv', 'sessionid'),
                      object_type = object_type||'*',
                          created = sysdate
                 WHERE rowid BETWEEN :start_id AND :end_id]';
  DBMS_PARALLEL_EXECUTE.run_task(
           task_name => 'update big_tab, chunks by rowid',
            sql_stmt => l_stmt,
       language_flag => DBMS_SQL.NATIVE,
      parallel_level => 10);
END;
/
Abgelaufen: 00:00:48.98

Die Bindvariablen :start_id and :end_id beziehen sich auf die erste bzw. letzte Rowid jedes Chunks.
Der Eintrag der Session_id über die Sys_constext-Funktion ermöglicht auch nachträglich, festzustellen, welcher Anteil der Zeilen in welcher der parallelen Sessions erledigt wurde:

SELECT data_object_id session_id, COUNT(*)
FROM   big_tab
GROUP BY data_object_id
ORDER BY data_object_id;
=>
SESSION_ID   COUNT(*)
---------- ----------
   7491384     279693
   7491385     283494
   7491386     273875
   7491387     260867
   7491388     141022
   7491389     143615
   7491390     153054
   7491391     154875
   7491392     161342
   7491393     148163

 

STATUS INFORMATIONEN UND DIAGNOSE-MÖGLICHKEITEN


Ob alles glatt gegangen ist, erfährt man über die Data Dictionary-Views:

SELECT status,
       job_prefix, -- für die parallelen Scheduler-Jobs
       chunk_type,
       -- sql_stmt,
       parallel_level
FROM user_parallel_execute_tasks
WHERE TASK_NAME = 'update big_tab, chunks by rowid';
=>
STATUS     JOB_PREFIX     CHUNK_TYPE   PARALLEL_LEVEL
---------- -------------- ------------ --------------
FINISHED   TASK$_241      ROWID_RANGE              10
SELECT status, COUNT(*)
FROM user_parallel_execute_chunks
GROUP BY status;
STATUS                 COUNT(*)
-------------------- ----------
PROCESSED                   331

Aufschlussreich ist auch die Scheduler-DD-View user_scheduler_job_run_details. Hierfür muss man nur das Job-Präfix aus der View user_parallel_execute_tasks für die Job-Namen einsetzen:

SELECT job_name,
       status,
       error#,
       SUBSTR(run_duration, INSTR(run_duration,':')+1) "Dauer [Sek]",
       REGEXP_SUBSTR(actual_start_date, '[^ ]+') uhrzeit
FROM user_scheduler_job_run_details
WHERE job_name LIKE (SELECT job_prefix || '%'
                     FROM user_parallel_execute_tasks
                     WHERE task_name = 'update big_tab, chunks by rowid');
=>
JOB_NAME       STATUS         ERROR# Dauer [Sek]    UHRZEIT
-------------- ---------- ---------- -------------- ----------------
TASK$_241_6    SUCCEEDED           0 00:40          17:46:52,578000
TASK$_241_9    SUCCEEDED           0 00:40          17:46:52,781000
TASK$_241_1    SUCCEEDED           0 00:46          17:46:46,781000
TASK$_241_10   SUCCEEDED           0 00:40          17:46:52,843000
TASK$_241_2    SUCCEEDED           0 00:46          17:46:46,890000
TASK$_241_3    SUCCEEDED           0 00:46          17:46:46,953000
TASK$_241_4    SUCCEEDED           0 00:46          17:46:47,109000
TASK$_241_7    SUCCEEDED           0 00:40          17:46:52,656000
TASK$_241_8    SUCCEEDED           0 00:40          17:46:52,718000
TASK$_241_5    SUCCEEDED           0 00:40          17:46:52,578000

Nach der erfolgreichen Ausführung kann man den Task löschen:

BEGIN
   DBMS_PARALLEL_EXECUTE.drop_task('test_task');
END;

FEHLERLOGGING


Was macht man, wenn bei der Prozessierung Fehler auftreten? Wir bauen ein paar Fallen in die Tabelle ein. Die Spalte object_type ist vom Datentyp VARCHAR2(19). Während des Updates wird der Wert mit einem Sternchen konkateniert, also muss ich nur ein paar Einträge verlängern, so dass es während der DML-Aktion kracht:

UPDATE big_tab SET object_type = RPAD(object_type, 19, '+') WHERE MOD(id, 43) = 0;
=> 46511 Zeilen wurden aktualisiert.

In der PL/SQL Packages and Types Reference  findet sich ein Beispiel für die Ausführung der Prozedur incl. Fehlerbehandlung. Statt RUN_TASK wird hier die Prozedur GET_ROWID_CHUNK eingesetzt:

DECLARE
  v_stmt        VARCHAR2(32767);
  v_chunk_id    NUMBER;
  v_start_rowid ROWID;
  v_end_rowid   ROWID;
  v_rows_left   BOOLEAN;
  v_errcnt      NUMBER := 0;
BEGIN
  BEGIN
     DBMS_PARALLEL_EXECUTE.drop_task('update big_tab, chunks by rowid');
  EXCEPTION
     WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Task bereits vorhanden, wird gelöscht');
  END;
  v_stmt := q'[UPDATE big_tab
               SET session_id = sys_context('userenv', 'sessionid'),
                  object_type = object_type||'*',
                      created = sysdate
                 WHERE rowid BETWEEN :start_id AND :end_id]';
-- Task erstellen
   DBMS_PARALLEL_EXECUTE.create_task (
       task_name => 'update big_tab, chunks by rowid');
-- Aufteilung in Chunks
   DBMS_PARALLEL_EXECUTE.create_chunks_by_rowid(
  

Export von Tabellen als CSV-Files mit UTL_File

Bereich:PL/SQL, Version: ab APEX 4.x, Letzte Überarbeitung: 05.08.2023

Keywords:PL/SQL, Standard Packages

Der Export von Tabellen als csv-Files ist immer wieder ein Thema in unseren PL/SQL- und Packages-Kursen, weil die meisten gerne mit Excel arbeiten. Deshalb gebe ich unseren Teilnehmern immer eine einfache Prozedur mit, über die man Inhalte beliebiger Tabellen (solange sie keine LOB-Spalten oder ähnliches enthalten) mittels UTL_FILE als semikolon-separierte Ascii-File exportieren kann. Der Einsatz von UTL_FILE ist nicht der schnellste Weg, aber man kann diese Prozeduren sehr gut in Datenbank-Jobs einbinden oder für andere PL/SQL-Programme  verwenden.

Bei Tom Kyte findet man übrigens eine schöne Übersicht seiner diversen Export-Utilities. Für seine PL/SQL-Funktion dump_csv verwendet er DBMS_SQL

Das Problem ist allerdings: Mit den üblichen Beispieltabellen wie scott.emp oder all_objects klappt das alles natürlich prima, aber wie sieht's denn aus, wenn man "historisch gewachsene" Tabellen mit fast 100 oder mehr Spalten vor sich hat, in denen wegen der Verwendung von CHAR-Datentypen u. U. viel heiße Luft gespeichert ist? Deshalb wollte ich mal ausprobieren, wie man einserseits die kleine Prozedur ausbauen müßte, um solche Problemfälle in den Griff zu kriegen und welche Möglichkeiten es gibt, das Ganze schneller zu machen.

Von Adrian Billington stammt ein sehr lesenswerter Artikel von 2008: PL/SQL-File-IO über verschiedene Ansätze, die einfache Ausgabe über UTL_FILE zu beschleunigen und Performance-Vergleiche zwischen dem Einsatz von UTL_FILE auf der einen und der Kombination von DBMS_LOB und DBMX_XSLPROCESSOR auf der anderen Seite. Seine Codebeispiele für UTL_FILE habe ich leicht abgewandelt, in meine alte Exportprozedur eingebaut und mit verschiedenen Tabellen getestet.


FUNKTIONEN UND PROZEDUREN


Die Erstellung der Spaltenlisten wird in eine Funktion ausgelagert. Die TRIM-Funktion macht beim späteren Export die CHAR- und VARCHAR2-Spalten-Inhalte "schlanker", indem sie Leerzeichen von beiden Seiten löscht.

CREATE OR REPLACE FUNCTION col_list (
      p_tabname   VARCHAR2,
      p_schema    VARCHAR2 DEFAULT user,
      p_trim      NUMBER DEFAULT 0,
      p_delim     VARCHAR2 DEFAULT ',')  RETURN VARCHAR2
AS
    l_col_list VARCHAR2(4000) := ' ';
BEGIN
    FOR rec IN (SELECT column_name,data_type FROM all_tab_columns
                WHERE table_name = UPPER(p_tabname)
                AND owner        = UPPER(p_schema)
                ORDER BY column_id) LOOP
      IF p_trim = 1 AND rec.data_type IN ('CHAR', 'VARCHAR2') THEN
        l_col_list  := l_col_list ||p_delim||'TRIM('||rec.column_name||')';
      ELSE
        l_col_list := l_col_list ||p_delim||rec.column_name;
      END IF;
    END LOOP;
    RETURN NVL(LTRIM(l_col_list, p_delim||' '), 'ungültiger Tabellenname oder fehlende Berechtigung');
END;
/


Diese Prozedur exportiert den Inhalt einer Tabelle als csv-File (mit der Angabe von Datum und Uhrzeit). Das Directory muss natürlich existieren und der Ersteller der Prozedur muss Lese- und Schreibrechte darauf haben.

CREATE OR REPLACE PROCEDURE tab2csv (
   p_directory VARCHAR2,
   p_tabname   VARCHAR2,
   p_schema    VARCHAR2 DEFAULT USER)
IS
    tab_refcur        SYS_REFCURSOR;
    l_header          VARCHAR2(4000);
    l_zeile           VARCHAR2(4000);
    l_col_list        VARCHAR2(4000);
    l_stmt            VARCHAR2(4000);
    l_file            UTL_FILE.FILE_TYPE;
 BEGIN
    l_file := UTL_FILE.FOPEN(
        location  => UPPER(p_directory),
        filename  => p_tabname||'_'||TO_CHAR(sysdate,'yyyy-mm-dd-hh24-mi')||'.csv',
        open_mode => 'w',
     max_linesize => 32767);
  -- Zusammenstellung der Spaltenliste für die Überschriften-Zeile
    l_header := col_list(
            p_tabname => p_tabname,
             p_schema => p_schema,
              p_delim => ';');
  -- Die Überschriften werden in die Datei geschrieben
     UTL_FILE.PUT_LINE(l_file, l_header);
  -- Zusammenstellung der Spaltenliste für den Select
     l_col_list := col_list(
             p_tabname => p_tabname,
              p_schema => p_schema ,
               p_delim => q'[||';'||]', -- alternativ '||'';''||'
                p_trim => 1);
     l_stmt   := 'SELECT '||l_col_list||' FROM '||p_schema||'.'||p_tabname;
  -- Über den Ref Cursor werden die Spalteninhalte jeder Zeile
  -- als Strings aneinandergehängt
     OPEN tab_refcur FOR l_stmt;
     LOOP
  -- und in die Variable eingelesen
       FETCH tab_refcur INTO l_zeile;
       EXIT WHEN tab_refcur%NOTFOUND;
       -- mit der Prozedur PUT_LINE wird Zeile für Zeile geschrieben
     UTL_FILE.PUT_LINE(l_file, l_zeile);
    END LOOP;
    CLOSE tab_refcur;
   UTL_FILE.FCLOSE(l_file);
EXCEPTION
 WHEN OTHERS THEN
   -- hier sollte natürlich eine vernünftige Fehleraufzeichnung passieren
    DBMS_OUTPUT.PUT_LINE(SQLERRM);
    UTL_FILE.FCLOSE(l_file);
   RAISE;
END tab2csv;
/


In der folgenden Prozedur habe ich Adrian Billingtons Idee zur Zwischenspeicherung der Zeilen in einer 32KB-VARCHAR2-Variable verwendet. Erst wenn der Puffer voll ist, wird die Zeile geschrieben. Vor allem für größere Tabellen macht das einen Unterschied.

CREATE OR REPLACE PROCEDURE tab2csv_buffered (
   p_directory VARCHAR2,
   p_tabname   VARCHAR2,
   p_schema    VARCHAR2 DEFAULT USER)
IS
    tab_refcur        SYS_REFCURSOR;
    l_header          VARCHAR2(4000);
    l_zeile           VARCHAR2(4000);
    l_col_list        VARCHAR2(4000);
    l_stmt            VARCHAR2(4000);
    l_file            UTL_FILE.FILE_TYPE;
    l_buffer          VARCHAR2(32767);
 BEGIN
  l_file := UTL_FILE.FOPEN(
        location => UPPER(p_directory),
        filename => p_tabname||'_'||TO_CHAR(sysdate,'yyyy-mm-dd-hh24-mi')||'.csv',
       open_mode => 'w',
    max_linesize => 32767);
    l_header := col_list(
            p_tabname => p_tabname,
              p_schema => p_schema,
              p_delim => ';');
   UTL_FILE.PUT_LINE(l_file, l_header);
    l_col_list := col_list(
             p_tabname => p_tabname,
               p_schema => p_schema ,
               p_delim => q'[||';'||]',
                p_trim => 1);
    l_stmt := 'SELECT '||l_col_list||' FROM '||p_schema||'.'||p_tabname;
    OPEN tab_refcur FOR l_stmt;
    LOOP
       FETCH tab_refcur INTO l_zeile;
       EXIT WHEN tab_refcur%NOTFOUND;
  -- solange der Buffer nicht voll ist, werden weitere Zeilen
  -- getrennt durch Linefeed (chr(10)) eingeladen
       IF LENGTH(l_buffer) + 1 + LENGTH(l_zeile) <= 32767 THEN
         l_buffer := l_buffer || CHR(10) ||l_zeile;
       ELSE
         IF l_buffer IS NOT NULL THEN
  -- der volle Buffer wird in die Datei geschrieben
            UTL_FILE.PUT_LINE(l_file, l_buffer);
         END IF;
  -- der Buffer wird zurückgesetzt
         l_buffer := l_zeile;
       END IF;
    END LOOP;
  -- was nach dem Ende der Schleife noch im Buffer ist
  -- wird hier rausgeschrieben
    UTL_FILE.PUT_LINE(l_file, l_buffer);
    CLOSE tab_refcur;
   UTL_FILE.FCLOSE(l_file);
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE(SQLERRM);
    UTL_FILE.FCLOSE(l_file);
    RAISE;
END tab2csv_fast;
/


Und jetzt Billingtons Turboversion mit Parallel-Antrieb (etwas vereinfacht). Die parallele Ausgabe funktioniert nur mit einer Table Function und ist deshalb etwas komplexer. Zudem ist die Parallelisierung nur bei der Enterprise-Edition möglich.

Statt mit einem Cursor durch die Ergebnismenge zu laufen, wird hier eine nested Table verwendet, die in Portionen von je 100 Zeilen befüllt wird (Dies allein bringt noch keinen Geschwindigkeitsgewinn, da auch bei der Ref Cursor-Version im Hintergrund ein Bulk Collect von je 100 Zeilen durchgeführt wird). Diese 100 Zeilen werden dann über die Puffermethode ins File geschrieben, dann holt sich die PL/SQL Engine die nächsten 100.
Nach dem Schließen des Files wird als Dummy ein Sequenzwert über PIPE ROW ausgegeben, damit die Funktion einen Rückgabewert hat.

-- Nested Table-Objekttyp zum Auffangen der Ergebnismenge
CREATE TYPE num_nt_type AS TABLE OF NUMBER;
/
CREATE SEQUENCE file_seq;
CREATE OR REPLACE FUNCTION parallel_output (
      p_refcur    SYS_REFCURSOR,
      p_directory VARCHAR2,
      p_tabname   VARCHAR2,
      p_schema    VARCHAR2 DEFAULT USER) RETURN num_nt_type PIPELINED
 -- Parallelisierte Abarbeitung aktivieren
   PARALLEL_ENABLE
 -- Oracle soll sich die Anzahl der Slave-Prozesse selber aussuchen
   (PARTITION p_refcur BY ANY)
AS
 -- Nested Table für den zu exportierenden Text
   TYPE string_nt_type IS TABLE OF VARCHAR2(32767);
   string_nt    string_nt_type;
    l_name       VARCHAR2(200);
    l_header     VARCHAR2(4000);
    l_zeile      VARCHAR2(4000);
    l_col_list   VARCHAR2(4000);
    l_stmt       VARCHAR2(4000);
    l_file       UTL_FILE.FILE_TYPE;
    l_buffer     VARCHAR2(32676);
BEGIN
   l_name := p_tabname||'_'||TO_CHAR(sysdate,'yyyy-mm-dd-hh24-mi');
   l_file := UTL_FILE.FOPEN(
        location => UPPER(p_directory),
        filename => l_name||'.csv',
       open_mode => 'w',
    max_linesize => 32767);
    l_header := col_list(
            p_tabname => p_tabname,
              p_schema => p_schema,
              p_delim => ';');
   UTL_FILE.PUT_LINE(l_file, l_header);
   l_name := l_name|| '_' ||file_seq.NEXTVAL|| '.csv';
   l_file := UTL_FILE.FOPEN(
        location => UPPER(p_directory),
        filename => l_name,
       open_mode => 'w',
    max_linesize => 32767);
    LOOP
     FETCH p_refcur BULK COLLECT INTO string_nt LIMIT 100;
     EXIT WHEN string_nt.COUNT = 0;
      FOR i IN 1 .. string_nt.COUNT LOOP
         IF LENGTH(l_buffer) + 1 + LENGTH(string_nt(i)) <= 32676 THEN
            l_buffer := l_buffer ||CHR(10)|| string_nt(i);
         ELSE
            IF l_buffer IS NOT NULL THEN
               UTL_FILE.PUT_LINE(l_file, l_buffer);
            END IF;
            l_buffer := string_nt(i);
         END IF;
      END LOOP;
   END LOOP;
   CLOSE p_refcur;
   UTL_FILE.PUT_LINE(l_file, l_buffer);
   UTL_FILE.FCLOSE(l_file);
   PIPE ROW (file_seq.NEXTVAL);
   RETURN;
 EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE(SQLERRM);
    UTL_FILE.FCLOSE(l_file);<

Oracle Workspace Manager (Package dbms_wm)

Bereich:DBA:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 15.05.2019

Keywords:Workspace Manager, dbms_wm, Tabellenversionierung

Der Workspace Manager dient zur Versionierung von Tabellendaten in verschieden Versionen (Workspaces)
Vorteile

  • Langlaufende Transaktionen können in einem eigenen Workspace laufen, ohne dass sie andere Sessions behindern
  • Unterschiedliche Versionsstände einer Tabelle können unendlich lange gespeichert werden
  • Was wäre, wenn Analysen in beliebiger Anzahl durchgeführt werden, ohne dass die Produktionsdaten dadurch verändert werden?
  • Verfügbar in Standard und Enterprise Edition

Vorbereitungen:

  • Tabellen müssen für den Workspace vorbereitet werden (DBMS_WM.EnableVersioning)
  • Dadurch wird die Tabelle umbenannt in <tabellenname>_LT
  • Dann wird eine View erzeugt mit dem Ursprungsnamen der Tabelle
  • Auf die View wird ein Instead of Trigger gelegt, der die Daten dann wieder in die Tabelle einträgt
  • Der Tablespace für die Tabelle wird nicht gewechselt

Sie können eine Administrationsrolle mit allen Rechten für die Workspacebearbeitung vergeben:

GRANT WM_ADMIN_ROLE TO <user>;


Folgende Einzelrechte können vergeben werden:

  • ACCESS_WORKSPACE
  • CREATE_WORKSPACE
  • MERGE_WORKSPACE
  • REMOVE_WORKSPACE
  • ROLLBACK_WORKSPACE
  • FREEZE_WORKSPACE

Beispiel:

BEGIN
DBMS_WM.GrantWorkspacePriv('ACCESS_ANY_WORKSPACE, MERGE_ANY_WORKSPACE', 'my_workspace_1','SCOTT','NO');
END;
/


Folgendes gilt für die Tabellen:

  • Nur der Eigentümer oder der Inhaber des Rechts WM_ADMIN_ROLE kann die Versionierung aktivieren
  • Die Tabelle muss einen Primärschlüssel besitzen
  • SYS Tabellen können nicht versioniert werden
  • Wenn eine Parent Tabelle versioniert wurde, muss die Child Tabelle es auch sein
  • Aber eine Child Tabelle kann auch ohne Parent Tabelle versioniert werden
  • Foreign Key Constraints dürfen nach Aktivieren der Versionierung nicht mehr nachträglich erzeugt werden

Beispiel:
Tabelle für den Workspace Manager aktivieren:

BEGIN
DBMS_WM.EnableVersioning(Table_Name => 'emp');
END;
/


Zwei Workspaces einrichten:

EXECUTE DBMS_WM.CreateWorkspace('my_workspace_1');


In den ersten Workspace wechseln:

EXECUTE DBMS_WM.GotoWorkspace('my_workspace_1');


Durchführen von Änderungen in der Tabelle emp im Workspace my_workspace_1:

INSERT INTO emp (empno,ename,deptno) VALUES (8000,'Marco',40);
DELETE FROM emp WHERE deptno=10;
UPDATE emp SET sal=sal+1 WHERE deptno=20;


In den Haupt-Workspace wechseln:

EXECUTE DBMS_WM.GotoWorkspace('LIVE');

Dort ist die Tabelle in Ihrem Ursprungszustand zu sehen (ohne die 3 DML Änderungen)

In welchem Workspace sind wir gerade?

SELECT DBMS_WM.GetWorkspace FROM dual;


Die Live Tabelle kann nun auf die Workspace Tabellen-Variante refreshed werden:

BEGIN
DBMS_WM.RefreshWorkspace(
workspace =>'my_workspace_1');
END;
/

Oder die Workspace Variante wird auf Live synchronisiert:

BEGIN
DBMS_WM.MergeWorkspace(
workspace =>'my_workspace_1');
END;
/


Die Tabelle kann wieder aus der Versionsverwaltung herausgenommen werden durch:

EXEC DBMS_WM.DisableVersioning('SCOTT.EMP');

Mit der Option FORCE wird das Kommando auch mit geänderten Workspacedaten durchgeführt, sonst erhält man einen Oracle Fehler:

ORA-20038: cannot disable version a table modified in non-LIVE workspaces

BEGIN
DBMS_WM.DisableVersioning('SCOTT.EMP',force=>TRUE);
END;
/


Zum Löschen eines Workspace verwenden Sie:

BEGIN
DBMS_WM.RemoveWorkspace('my_workspace_1');
END;
/


Weitere Informationen zum Workspace Manager erhalten Sie in unserem PL/SQL II Kurs.
 



Weitere Interessente Artikel zum Thema:



Empfohlene Schulungen zum Thema:


Oracle JSON Date Datentyp Problem

Bereich:PL/SQL:SQL, Version: ab RDBMS 21.1, Letzte Überarbeitung: 14.01.2021

Keywords:Oracle JSON json_serialize json_scalar

Wer schon mal mit dem JSON Date Datentyp gearbeitet hat, ist sich dessen Problemen sicherlich bewusst.
Wir wollen uns im nachfolgenden Atrikel dem Problem mal annehmen:

Nehmen wir mal die in Oracle 21c neue eingeführte Funktion json_scalar und json_serialize

JSON_SCALAR wandelt einen Text/ ein Datum oder eine Anzahl in eine interne BLOB Repräsentanz.

SELECT json_scalar(date '2021-01-01') as datum FROM dual;
DATUM                                                                           
--------------------------------------------
22323032312D30312D30315430303A30303A303022


Wenn wir das zurückwandeln passiert folgendes:

SELECT json_serialize(  (date '2021-01-01')) as datum FROM dual;

DATUM                                                                                                           
---------------------
"2021-01-01T00:00:00"


Da fallen einem gleich zwei Problemzonen ins Auge:
1. "" am Anfang / Ende
2. ein T zwischen Datum und Uhrzeit

Der erste Versuch scheitert deswegen auch:

select to_date(json_serialize(json_scalar(date '2021-01-01')),'YYYY-MM-DD"T"HH24:MI:SS') as scalar_datum
from   dual;

ORA-01841: (Volles) Jahr muss zwischen -4713 und +9999 liegen und darf nicht 0 sein
01841. 00000 -  "(full) year must be between -4713 and +9999, and not be 0"


Das liegt an der etwas eigenartigen Behandlung von Gänsefüßen bei Oracle Strings

select to_date('"01.01.01"') from dual;
ORA-01858: Ein nicht numerisches Zeichen wurde gefunden, während ein numerisches Zeichen erwartet wurde
01858. 00000 -  "a non-numeric character was found where a numeric was expected"

oder auch

select to_date('"01.01.01"','"DD.MM.YY"') from dual;
RA-01861: Literal stimmt nicht mit Formatzeichenfolge überein
01861. 00000 -  "literal does not match format string"

auch das geht schief:

select to_date('"01.01.01"','"""DD.MM.YY"""') from dual;
ORA-01861: Literal stimmt nicht mit Formatzeichenfolge überein
01861. 00000 -  "literal does not match format string"


Deshalb lösen wir das Problem mit zwei Funktionen:
1. Wir ersetzen "" durch nix
2. Wir konvertieren den String mit Hilfe der Funktion to_date und dem Format String 'YYYY-MM-DD"T"HH24:MI:SS'

select
to_date(replace(json_serialize(json_scalar(date '2021-01-01')),'"',''),'YYYY-MM-DD"T"HH24:MI:SS') as scalar_datum
from   dual;

SCALAR_DATUM       
-------------------
01.01.2021 00:00:00


Weitere Tipps erhalten Sie in einem unserer Oracle Kurse...



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


PL/SQL Collection Beispiele

Bereich:PL/SQL, Version: ab RDBMS 12.x:RDBMS 19.3:RDBMS 21.1, Letzte Überarbeitung: 19.03.2020

Keywords:PL/SQL, Collection, Arrays

In unseren Kursen stöhnen die Teilnehmer häufig über Oracles eigene Arrays, die man Collection nennt.
Der Vorwurf lautet:

  • zu kompliziert
  • unnötig

Aber eigentlich sind sie doch ganz schön. Ein paar Beispiele sollen das verdeutlichen:

1. Associative Array vom Typ String wird mit drei Werten gefüllt (und wieder ausgegeben):

SET SERVEROUTOUT ON
DECLARE
TYPE numarray IS TABLE OF VARCHAR2(4000)
INDEX BY BINARY_INTEGER;
l_data  numarray;
BEGIN
    l_data(1) := 'Marco';
    l_data(2) := 'Matthias';
    l_data(3) := 'Andrea';
    for i in 1 .. l_data.count LOOP
     dbms_output.put_line(l_data(i));
    end loop;
END;
/


Nun schreiben wir die Daten aus dem Array in eine Tabelle:

CREATE TABLE t ( txt VARCHAR2(64));

DECLARE
  TYPE numarray IS TABLE OF VARCHAR2(4000)
  INDEX BY BINARY_INTEGER;
  l_data  numarray;
BEGIN
    l_data(1) := 'Marco';
    l_data(2) := 'Matthias';
    l_data(3) := 'Andrea';
    --l_data(4) := 'Andrea'; =>ORA-22160: Element bei Index [3] nicht vorhanden
    forall i in 1 .. l_data.count
      insert into t select l_data(i) from dual
      where l_data(i) is NOT null;
END;
/

SELECT * FROM t;


Beispiel 2: ASCII Tabelle:

DECLARE
  TYPE numarray IS TABLE OF VARCHAR2(2)
  INDEX BY BINARY_INTEGER;
  l_data  numarray;
BEGIN
  FOR i IN 1 .. 255 LOOP
     l_data(i) := chr(i);
  END LOOP;
  FOR i in 1 .. l_data.count LOOP
     dbms_output.put_line(rpad(i,3,' ')||' '||l_data(i));
  END LOOP;
END;
/


Basis des Arrays ist nun ein RECORD:

DECLARE
  TYPE mytyp IS RECORD (
  vorname  VARCHAR2(100),
  nachname VARCHAR2(100));
  TYPE numarray IS TABLE OF mytyp
  INDEX BY BINARY_INTEGER;
  l_data  numarray;
BEGIN
    l_data(1).vorname:='Marco';
    l_data(1).nachname:='Patzwahl';
    l_data(0).vorname:='Hans';
    l_data(0).nachname:='Wesnitzer';
  for i in 0 .. l_data.count-1 LOOP
    dbms_output.put_line('Name='||l_data(i).vorname||' '||l_data(i).nachname);
  END LOOP;
END;
/

DECLARE
  TYPE mytyp IS RECORD (
  vorname  VARCHAR2(100),
  nachname VARCHAR2(100));
  TYPE numarray IS TABLE OF mytyp
  INDEX BY BINARY_INTEGER;
  l_data  numarray;
  l_rtyp  mytyp;
BEGIN
    l_rtyp.vorname:='Peter';
    l_rtyp.nachname:='Kraus';
    l_data(2):=l_rtyp;
  for i in 0 .. l_data.count-1 LOOP
    dbms_output.put_line('Name='||l_data(i).vorname||' '||l_data(i).nachname);
  END LOOP;
END;
/

DECLARE
  TYPE mytyp IS RECORD (
    owner  VARCHAR2(30),
    name  VARCHAR2(30),
    text  VARCHAR2(4000) );
  TYPE numarray IS TABLE OF mytyp
  INDEX BY BINARY_INTEGER;
  l_data  numarray;
  l_rtyp  mytyp;
BEGIN
   dbms_output.put_line('Zeit:'||systimestamp);
   FOR r in (select owner,name,text,rownum as rn from all_source) LOOP
    l_rtyp.owner:=r.owner;
    l_rtyp.name:=r.name;
    l_rtyp.text:=r.text;
    l_data(r.rn):=l_rtyp;
   END LOOP;
    dbms_output.put_line('Anzahl:'||l_data.count);
    dbms_output.put_line('Zeit:'||systimestamp);
END;
/


Basis des Records ist eine Tabellenstruktur

DECLARE
  TYPE emp_type IS TABLE OF scott.emp%ROWTYPE
  INDEX BY BINARY_INTEGER;
  l_data  emp_type;
BEGIN
    l_data(1).empno:=8000;
    l_data(1).ename:='Patzwahl';
    l_data(5).empno:=8001;
    l_data(5).ename:='Huberl';
END;
/


Es kann auch ein VARCHAR2 Wert als Index benutzt werden:

SET SERVEROUTPUT ON
DECLARE
  TYPE country_tab_type IS TABLE OF VARCHAR2(50)
  INDEX BY VARCHAR2(2);
  t_country country_tab_type;
BEGIN
  -- Array fuellen
  t_country('AT') := 'Austria';
  t_country('FR') := 'France';
  t_country('DE') := 'Germany';

  -- Welches Land verbirgt sich hinter dem ISO code "DE"
  DBMS_OUTPUT.PUT_LINE('ISO code "DE" = ' || t_country('DE'));
END;
/


Beispiel Bulk Cursor mit SAVE EXCEPTIONS:

DROP TABLE scott.objects;
DROP TABLE scott.tables;

CREATE TABLE scott.objects AS
  SELECT object_name,owner FROM all_objects
   WHERE owner='SCOTT' and object_type in ('TABLE','INDEX');
CREATE TABLE scott.tables AS
  SELECT table_name,owner FROM all_tables WHERE owner='SCOTT';
ALTER TABLE scott.objects ADD PRIMARY KEY (owner,object_name);

SET SERVEROUTPUT ON
DECLARE
  TYPE object_tab_type IS TABLE OF scott.objects.object_name%TYPE;
  TYPE table_tab_type IS TABLE OF scott.tables.table_name%TYPE;
  myobjects_tab     object_tab_type;
  mytables_tab    table_tab_type;
  error_count    NUMBER;
BEGIN
  SELECT object_name BULK COLLECT INTO myobjects_tab
    FROM scott.objects;
  SELECT table_name BULK COLLECT INTO mytables_tab
    FROM scott.tables;
  FORALL i IN mytables_tab.first .. mytables_tab.last SAVE EXCEPTIONS
    INSERT INTO scott.objects (object_name,owner)
      VALUES (mytables_tab(i),'SCOTT');
  EXCEPTION when others then
    error_count := SQL%BULK_EXCEPTIONS.COUNT;
    for i in 1.. error_count loop
        dbms_output.put_line('** Oracle Fehler:' ||
        sqlerrm(-SQL%BULK_EXCEPTIONS(I).ERROR_CODE)||':'||mytables_tab(SQL%BULK_EXCEPTIONS(i).ERROR_INDEX)||
    '  #  Position:'||SQL%BULK_EXCEPTIONS(i).ERROR_INDEX); 
    end loop;
    COMMIT;
END;
/

 



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


DBMS_OUTPUT Tipps

Bereich:PL/SQL, Version: ab RDBMS 12.x:RDBMS 18.1:RDBMS 18.3:RDBMS 19.1:RDBMS 19.3, Letzte Überarbeitung: 28.07.2020

Keywords:dbms_output, dbms_output.get_lines

Eigentlich hat doch schon ein jeder PL/SQL-Entwickler (und natürlich auch jede Entwicklerin ) sich über das Package dbms_output.put_line geärgert.

Denn:

  • Der Package und Procedure-Name ist viel zu lang:-)
  • Es kann nur sehr wenige Datentypen (Text, Date, Number) ausgeben
  • Wenn man die Ausgabe nicht richtig einstellt, ist sie weg
  • Die Ausgabe bekommt man erst, wenn die Routine zuende ist
  • Wenn die Ausgabe über einen Job (z.B. dbms_scheduler) erfolgen soll, verschwindet sie im Nirvana.

Ok, schauen wir mal, welche Probleme wir lösen können:

CREATE OR REPLACE PROCEDURE P_TEST
  IS PROCEDURE P(P_TEXT)
  IS
    BEGIN DBMS_OUTPUT.PUT_LINE(P_TEXT);
  END;
BEGIN
  P('Hallo');
  P('Ist das nicht schön kurz für ein Oracle Package?');
END;
/


Oder als eigenständige Prozedur:

CREATE OR REPLACE PROCEDURE SCOTT.PROCEDURE
  (TEXT IN VARCHAR2)
  IS
BEGIN
  DBMS_OUTPUT.PUT_LINE(TEXT);
END;
/

BEGIN
  P('Hallo Welt');
END;
/
 


Umleitung von DBMS_OUTPUT zu PIPE: Damit können Sie am Ende des Programms einen Aufruf platzieren, der den DBMS_OUTPUT Puffer ausliest und in eine Pipe schickt. In einer anderen Session kann dann die Ausgabe erfolgen.

CREATE OR REPLACE PROCEDURE TEST_AUSGABE
  -- Rechte notwendig: grant execute on dbms_pipe to scott;
  IS
    PROCEDURE DOP2PIPE
   ( PIPE_NAME IN VARCHAR2 DEFAULT 'DBMS_OUTPUT_PIPE')
  IS
  LINES DBMS_OUTPUT.CHARARR;
  NUM_LINES NUMBER:=1000000;
BEGIN
  DBMS_OUTPUT.GET_LINES(LINES, NUM_LINES);
  FOR I IN 1..NUM_LINES
  LOOP DBMS_PIPE.PACK_MESSAGE (LINES(I));
  END LOOP;
  IF (DBMS_PIPE.SEND_MESSAGE (PIPE_NAME)) <> 0 THEN
    RAISE_APPLICATION_ERROR(-20500,'Fehler beim Senden in Pipe '||PIPE_NAME||' aufgetreten !');
  END IF;
END;
BEGIN
  DBMS_OUTPUT.ENABLE(NULL);
  FOR I IN 1 .. 10 LOOP
  DBMS_OUTPUT.PUT_LINE('Zeile='||I);
  END LOOP;
  DOP2PIPE; -- <=######## Umwandelung von dbms_output in Pipe END;
/

CREATE OR REPLACE PROCEDURE GET_DOUT_FROM_PIPE
  (PIPE_NAME IN VARCHAR2 DEFAULT 'DBMS_OUTPUT_PIPE')
  IS
  V_MESSAGE VARCHAR2(32767);
  V_TIMEOUT NUMBER:=120;
BEGIN
  IF (DBMS_PIPE.RECEIVE_MESSAGE(PIPE_NAME,V_TIMEOUT)) <> 0 THEN
    RAISE_APPLICATION_ERROR(-20501,'Fehler beim Lesen aus Pipe '||PIPE_NAME||' aufgetreten !');
  END IF;
  LOOP
  EXIT WHEN DBMS_PIPE.NEXT_ITEM_TYPE = 0;
  DBMS_PIPE.UNPACK_MESSAGE(V_MESSAGE);
  DBMS_OUTPUT.PUT_LINE(V_MESSAGE);
  END LOOP;
END;
/


Umleitung von dbms_output zu htp

CREATE OR REPLACE PROCEDURE test_ausgabe
IS
    PROCEDURE dop2htp IS
       lines dbms_output.chararr;
       num_lines number:=1000000;
    BEGIN
       dbms_output.get_lines(lines, num_lines);
       FOR i IN 1..num_lines LOOP
          htp.p(lines(i)||'<BR>');
       END LOOP;
    END;
BEGIN
   dbms_output.enable(null);  
   FOR i IN 1 .. 10 LOOP
     dbms_output.put_line('Zeile='||i);
   END LOOP;
dop2htp;   -- <=######## Umwandelung von dbms_output in htp.p Ausgabe
END;
/


DBMS_OUPUT in CLOB speichern

VARIABLE console_clob CLOB
DECLARE
v_clob CLOB;
v_cr VARCHAR2(16):=chr(10); -- Zeilenumbruchzeichen
PROCEDURE dbms_output2clob IS
       lines dbms_output.chararr;
       num_lines number:=1000000;
    BEGIN
     dbms_lob.createtemporary(
    lob_loc =>v_clob,
    cache =>FALSE,
    dur   =>DBMS_LOB.CALL);
       dbms_output.get_lines(lines, num_lines);
       FOR i IN 1..num_lines LOOP
          dbms_lob.writeappend(v_clob,length(lines(i)||v_cr),lines(i)||v_cr);
       END LOOP;
       :console_clob:=v_clob;
    END;
BEGIN
FOR i IN 1 .. 100 LOOP
  dbms_output.put_line(i||' Zeit :'||systimestamp);
END LOOP;
dbms_output2clob;
END;
/

set long 20000000
print console_clob



Weitere Tipps & Tricks erhalten Sie z.B. im PL/SQL, PL/SQL II und PL/SQL Packages Kurs.

 



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


Datumskonvertierungen und Datumsformate in Oracle

Bereich:PL/SQL:SQL, Letzte Überarbeitung: 26.11.2022

Keywords:to_date,to_char, Datum

Wer hat nicht schon mal vor dem Problem einer Datumskonvertierung bei Oracle Datenbanken gestanden?
Da kann sehr viel Zeit verstreichen, bis man eine Lösung gefunden hat. Wir wollen Ihnen hier ein Paar Fallstricke erklären.

Welches Datumsformat wird als Default ausgegeben?

Das hängt vom Parameter nls_date_format ab, der auf Clientseite im Betriebssystem gesetzt wird.

z.B. können Sie unter DOS in einem Fenster angeben:

dos> set NLS_DATE_FORMAT='DD.MM.YYYY HH24:MI:SS'
dos> sqlplus scott/tiger

SELECT sysdate FROM dual;

SYSDATE
-------------------
31.01.2021 11:12:59


Für Linux:

$> export NLS_DATE_FORMAT='DD.MM.YYYY HH24:MI:SS'
$> sqlplus scott/tiger

SELECT sysdate FROM dual;

SYSDATE
-------------------
31.01.2021 11:12:59


Im SQL*Developer kann stattdessen auch unter Extras/Voreinstellungen/Datenbank/NLS das Datumsformat eingestellt werden.

Die Konvertierung in einem SQL-Befehl

Wenn Sie sich in einzelnen Befehlen nicht an den Default halten möchten, können Sie eine Ausgabeformatierung verwenden:

SELECT to_char(sysdate,'DD.MM.YYYY HH24:MI:SS') FROM dual;


Achten Sie darauf, dass die Minuten mit "MI" gekennzeichnet sind. Für Oracle wäre auch folgendes Format OK:

SELECT to_char(sysdate,'Day.Month.Year HH:MM:SS') AS datum FROM dual;

DATUM
--------------------------------------------
Friday   .June     .Twenty Eighteen 11:06:17


Nur leider ist das Format falsch, weil im String "HH:MM:SS" Stunden:Monat:Sekunden ausgegeben werden, und das stimmt nur ganz selten mit der tatsächlichen Uhrzeit überein (eine Minute pro Stunde :-)

Es gibt eine riesige Liste an möglichen Datumskonvertierungen die hier nachgelesen werden können.

Nur ein paar Beispiele für Oracle Datumsformate:

  • DD   Tagnummer (0-31)
  • DAY Tagesname  (Montag)
  • DY Tagesname, kurz (DO)
  • DDD Tagesnummer im Jahr (0-366)
  • MM   Monat (1-12)
  • MONTH   Monatsname ( Mai)
  • YY  Jahreszahl zweistellig
  • YYYY Jahreszahl vierstellig

Datum für Oracle eingeben

Wenn Sie ein Datum in die Oracle Datenbank einpflegen möchten, sollten Sie das idealerweise auch mit einer expliziten Datums-Konvertierung tun.

Beispiel:

INSERT INTO emp (empno,ename,hiredate) VALUES
  (8000, 'MARCO', to_date('30.12.2020','DD.MM.YYYY'));


Hier werden Stunden, Minuten und Sekunden auf 0 gesetzt. Das spart zwar keinen Speicherplatz, erleichtert aber später die Suche nach einem Einstellungstag. Wenn Sie stattdessen schreiben:

INSERT INTO emp (empno,ename,hiredate) VALUES (8000, 'MARCO', sysdate);


Dann wird die aktuelle Uhrzeit im Datum mit gespeichert. Jetzt versuchen Sie mal nach Mitarbeitern zu suchen, die heute eingestellt wurden:

SELECT * FROM emp WHERE hiredate=sysdate; -- FALSCH
SELECT * FROM emp WHERE trunc(hiredate)=trunc(sysdate); -- RICHTIG


Nur sollte es einen normalen Index auf der Spalte hiredate geben, wird er nicht benutzt werden. Deswegen könnte man folgende Variante benutzen:

SELECT * FROM emp
 WHERE hiredate BETWEEN trunc(sysdate)
                    AND trunc(sysdate+INTERVAL '1' HOUR - INTERVAL '1' SECOND);


Hier suchen Sie nach allen Mitarbeitern, die zwischen 00:00.00 und 23:59:59 des aktuellen Tages eingestellt wurden.


Für Freunde des JSON Datentyps: JSON verwendet gerne folgendes Datumsformat: 01.08.2022T12:30:39Z
Man beachte das T in der Mitte und das Z am Ende.
Das können wir wieder in ein Oracle Datumsformat zurückwandeln:

SELECT to_date('01.12.2022T12:30:39Z','DD.MM.YYYY"T"HH24:MI:SS"Z"') FROM dual;



Hat das Ihre Lust auf noch mehr SQL Tipps geweckt? Wir haben bestimmt auch die passende Schulung für Sie!



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:


Debuggen in 12C mit Hilfe des SQL Developers

Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 25.06.2018

Keywords:Oracle Neuerungen, PL/SQL, 12C Release 1

Der SQL Developer als Oracle-proprietäres Tool erfreut sich mittlerweile großer Beliebtheit. Entwicker, die den SQL Developer auch zum Debuggen nutzen, werden bei 12c-Datenbanken jedoch erst einmal eine neue Hürde überwinden müssen. Während für andere Tools, wie TOAD oder PL/SQL Developer, auch in Version 12c die Standard-Grants (DEBUG ANY PROCEDURE, DEBUG CONNECT SESSION) dafür ausreichen, wird für den SQL Developer zusätzlich eine ACL-Freigabe benötigt. Ohne ACL erhalten Sie folgende Fehlermeldung:

Verbindung mit der Datenbank O12c.
PL/SQL wird ausgeführt: ALTER SESSION SET PLSQL_DEBUG=TRUE
PL/SQL wird ausgeführt: CALL DBMS_DEBUG_JDWP.CONNECT_TCP( '10.0.0.21', '50229' )
ORA-24247: Netzwerkzugriff von Access Control-Liste (ACL) abgelehnt
ORA-06512: in "SYS.DBMS_DEBUG_JDWP", Zeile 68
ORA-06512: in Zeile 1
Prozess beendet.
Abmeldung von der Datenbank O12c.

Der Grund liegt darin, dass der Debugger des SQL Developers - im Gegensatz zu den oben genannten Tools - mit dem Package DBMS_DEBUG_JDWP ("Java Debug Wire Protocol") arbeitet. Und ab Version 12c ist dafür eine entsprechende Freigabe erforderlich. Diese wird über das Package DBMS_NETWORK_ACL_ADMIN erteilt, das hier schon einmal erwähnt wurde. Die Art der Freigabe lautet in diesem Fall jdwp:

BEGIN
   DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE( 
      host => '10.0.0.21', 
      ace  => xs$ace_type(
                privilege_list => xs$name_list('jdwp'),   
                principal_name => 'SCOTT',
                principal_type => xs_acl.ptype_db));  
END;
/

Als Host muss selbstverständlich der Rechner angegeben werden, auf dem der SQL Developer läuft. Die Angabe von Ports hat Tücken; am einfachsten ist es, Sie lassen sie ganz weg, wie hier gezeigt. Zwar können Sie im SQL Developer eine Port-Range für den Debugger einstellen, aber wenn Sie beim Aufruf von DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE sowohl lower_port als auch upper_port angeben, wird das mit dieser Fehlermeldung

ORA-24244: Ungültiger Host oder Port für Zuweisung der Access Control-Liste (ACL)
ORA-06512: in "SYS.DBMS_NETWORK_ACL_ADMIN", Zeile 1071

quittiert. Geben Sie NUR den lower_port an, so ist auch nur dieser EINE Port freigegeben. Nach erfolgreicher Freigabe können Sie - an diesem Rechner - den Debugger starten:

Verbindung mit der Datenbank O12c.
PL/SQL wird ausgeführt: ALTER SESSION SET PLSQL_DEBUG=TRUE
PL/SQL wird ausgeführt: CALL DBMS_DEBUG_JDWP.CONNECT_TCP( '10.0.0.21', '50815' )
Debugger hat Verbindung von Datenbank auf Port 50815 akzeptiert.
....

An jedem anderen Rechner bekommen Sie weiterhin die Fehlermeldung, dass die ACL abgelehnt wurde:

PL/SQL wird ausgeführt: CALL DBMS_DEBUG_JDWP.CONNECT_TCP( '10.0.0.190', '57524' )
ORA-24247: Netzwerkzugriff von Access Control-Liste (ACL) abgelehnt

SONSTIGE FUNKTIONEN VON DBMS_DEBUG_JDWP

Das Package DBMS_DEBUG_JDWP enthält übrigens noch zwei weitere, interessante Funktionen, über die sich SID und serial# der eigenen Session - auch ohne Admin-Rechte - ermitteln lassen. Und das nicht erst in 12c:

SELECT dbms_debug_jdwp.current_session_id,
       dbms_debug_jdwp.current_session_serial
  FROM DUAL;

ACEs sind dafür nicht erforderlich, da ja auf keine externe Ressource zugegriffen wird.

Sie möchten wissen, was ist denn eigentlich neu in der Oracle Datenbank Version 12c? Dann besuchen Sie unsere Schulung Neuerungen 12c und stöbern Sie in der Rubrik Oracle Neuerungen zu 12c Release 1 unserer Tipps & Tricks, hier finden Sie weitere kleine Hilfen, die das Leben mit Oracle 12c erleichtern. ;-)



Weitere Interessente Artikel zum Thema:


Empfohlene Schulungen zum Thema:



    Security Scoring

    Bereich:PL/SQL, Version: ab RDBMS 11.x, Letzte Überarbeitung: 05.05.2020

    Keywords:Security, PL/SQL, Standard Packages

    In diesem Tipp zeigen wir eine Möglichkeit zur Bewertung verschiedener potentieller Gefahren in der Oracle Datenbank, welche individualisiert überprüft werden können. Da wären SQL Injection-anfällige Eingaben oder auch die Gefährlichkeit der Rechte bzw. Rollen von Oracle Datenbanknutzern.

    SQL INJECTION
    Um SQL Injection-Angriffe abzuwehren, bietet Oracle das Package dbms_assert welches Eingaben bzw. Strings auf Gültigkeit überprüft. Möchte man allerdings eine Unterscheidung bzw. Einschätzung der potentiellen Gefahr haben, hilft einem dieses Package nicht weiter. Deshalb erstellen wir die Funktion "get_score", die einen zu überprüfenden String entgegennimmt. Zurückgegeben wird ein Scoring-Wert.

    CREATE OR REPLACE FUNCTION get_score (p_text IN VARCHAR2) RETURN NUMBER
    IS
       TYPE wordsArray IS TABLE OF VARCHAR2(64);
       l_words wordsArray := wordsArray(
          'WITH','SELECT','FROM','WHERE','LIKE','IN','UNION','OR','DUAL','CREATE',
          'ALTER','DROP','GATHER','STATS','EXECUTE','GRANT','HOST','TABLE',
          'TABLESPACE','DATABASE','INDEX','TRIGGER','REPLACE','DELETE','FUNCTION',
          'PROCEDURE','BEGIN','RETURN','END','FOR','LOOP','IF','THEN','SYS','DBMS',
          'UTL','DBA','ALL','PRIVS','WWV','v\$','SDO','UTIL','CHECK','SECURITY',
          'MDSYS','JAVA',';','--','(1)=\1','[;].[--]');
       v_pattern  VARCHAR2(32700);
       v_score NUMBER:=0;
    BEGIN
       FOR i IN 1 .. l_words.count LOOP
          v_pattern:=v_pattern||'|'||l_words(i);
       END LOOP;
       v_score:=v_score+REGEXP_COUNT(p_text,LTRIM(v_pattern,'|'),1,'i');
       RETURN v_score;
    END;

    Innerhalb dieser Funktion legen wir uns ein Array mit allen Schlüsselwörter an, die als verdächtig erachtet werden. Da "REGEXP_COUNT" nur ein Schlüsselwort erwartet, konkatenieren wir die Schlüsselwörter jeweils mit einem Pipe "|". Der Parameter "1" legt den ersten Buchstaben als Start fest und "i" beachtet keinen Unterschied zwischen Groß- und Kleinschreibung. Sobald "REGEXP_COUNT" einen Match gefunden hat, wird der Score um eins erhöht.

    Die Scoring-Abfrage auf einen typischen SQL Injection-String sieht folgend aus:

    select GET_SCORE('falscher_nutzer'' OR 1=1 ; DROP TABLE users --') as score
    from dual;
         SCORE
    ----------
             6

    Hinweis: Oracle kann nur einen Befehl dynamisch ausführen, der DROP TABLE würde sowieso nicht funktionieren. Aber manche Hacker gehen von SQL Server oder MYSQL Datenbanken aus, da geht so etwas.

    Folgende Schlüsselwort wurden also abgefangen: "OR", "1=1", "DROP", "TABLE", ";", "--"

    Regex aus dem oberen Beispiel für Schlüsselwörter erklärt:
    "(1)=\1": Wahrheitsvergleich 1=1 der sehr beliebt ist
    "[;].[--]": Typisches Ende einer SQL Injection: ";--" oder auch "; DROP TABLE wichtig -------"

    Da das Thema Reguläre Ausdrücke sehr komplex ist, verweisen wir auf eine gute Stelle im Internet: Opens external link in new windowhttp://www.sqlsnippets.com/en/topic-10764.html

    RECHTE & ROLLEN
    Das oben beschriebene Prinzip bleibt das selbe:
    In ein Array speichern wir potentiell gefährliche Rechte, die wir abfragen möchten. Die Funktion bekommt einen Benutzernamen übergeben, den es zu überprüfen gilt, und zurückgegeben wird ein Scoring-Wert. Es werden nicht nur direkte Rechte abgefragt, sondern auch Rechte die über eine Rolle erteilt wurden.

    CREATE OR REPLACE PACKAGE priv_check IS
       /* Notwendige Rechte:
       grant select on sys.sysauth$ to system;
       grant select on sys.user$ to system;
       grant select on sys.system_privilege_map to system;
       grant select on sys.DBA_ROLE_PRIVS to system;
       */
       FUNCTION get_syspriv_score (p_user IN VARCHAR2) RETURN NUMBER;
    END;
    /
    CREATE OR REPLACE PACKAGE BODY priv_check IS
        FUNCTION get_syspriv_score (p_user IN VARCHAR2) RETURN NUMBER
    IS
        TYPE privArray IS TABLE OF VARCHAR2(64);
        l_privs privArray := privArray(
            'ALTER ANY LIBRARY',
            'ALTER DATABASE',
            'ALTER USER',
            'BECOME USER',
            'CREATE ANY LIBRARY',
            'CREATE EXTERNAL JOB',
            'CREATE LIBRARY',
            'CREATE USER',
            'DROP ANY INDEX',
            'DROP ANY LIBRARY',
            'DROP ANY PROCEDURE',
            'DROP ANY ROLE',
            'DROP ANY SYNONYM',
            'DROP ANY TABLE',
            'DROP TABLESPACE',
            'DROP ANY TRIGGER',
            'DROP ANY VIEW',
            'DROP PUBLIC DATABASE LINK',
            'DROP USER',
            'EXECUTE ANY LIBRARY',
            'EXEMPT ACCESS POLICY',
            'EXEMPT IDENTITY POLICY',
            'EXPORT FULL DATABASE',
            'GRANT ANY PRIVILEGE',
            'GRANT ANY OBJECT PRIVILEGE',
            'IMPORT FULL DATABASE',
            'SYSDBA',
            'SYSOPER',
            'SYSBACKUP'
        );
        v_score NUMBER:=0;
    begin
        -- Direkte Rechte
        FOR c IN (
            select
                u.name username,
                spm.name privilege
            from
                sys.user$ u,
                sys.sysauth$ s,
                sys.system_privilege_map spm
            where
                u.user#=s.grantee# and
                s.privilege#=spm.privilege and
                U.NAME=p_user
        )
        LOOP
            FOR i IN 1 .. l_privs.count LOOP
                IF l_privs(i)= c.privilege THEN
                    v_score:=v_score+1;
                END IF;
            END LOOP;
        END LOOP;
        -- Rechte via Rollen
        FOR c IN (
            SELECT
                u1.name USERNAME,
                U2.NAME ROLENAME,
                SUBSTR(SPM.NAME,1,27) PRIVILEGE
            FROM
                SYS.SYSAUTH$ SA1,
                SYS.SYSAUTH$ SA2,
                SYS.USER$ U1,
                SYS.USER$ U2,
                SYS.SYSTEM_PRIVILEGE_MAP SPM
            WHERE
                SA1.GRANTEE# = U1.USER# AND
                SA1.PRIVILEGE# = U2.USER# AND
                U2.USER# = SA2.GRANTEE# (+) AND
                SA2.PRIVILEGE# = SPM.PRIVILEGE (+) AND
                (U1.NAME IN (
                    SELECT
                        GRANTEE
                    FROM
                        sys.DBA_ROLE_PRIVS connect by prior
                        granted_role=GRANTEE start with GRANTEE IN (
                            SELECT
                                NAME
                            FROM
                                sys.USER$
                            WHERE
                                user# in (
                                    select
                                        privilege#
                                    from
                                        sys.sysauth$ t1,
                                        sys.user$ t2
                                    where
                                        t1.grantee#=t2.user# and
                                        t2.name=p_user
                                )
                        )
                    UNION
                    SELECT
                        GRANTED_ROLE
                    FROM
                        DBA_ROLE_PRIVS connect by prior
                        granted_role=GRANTEE start with GRANTEE IN (
                            SELECT
                                NAME
                            FROM
                                sys.USER$
                            WHERE
                                user# in (
                                    select
                                        privilege#
                                    from
                                        sys.sysauth$ t1,
                                        sys.user$ t2
                                    where
                                        t1.grantee#=t2.user# and
                                        t2.name=p_user
                                )
                        )
                ) OR
                U1.NAME=p_user
                )
        )
        LOOP
            FOR i IN 1 .. l_privs.count LOOP
                IF l_privs(i)= c.privilege THEN
                    v_score:=v_score+1;
                END IF;
            END LOOP;
        END LOOP;
        RETURN v_score;
        EXCEPTION WHEN OTHERS THEN
            RETURN sqlcode;
        END get_syspriv_score;
    END;
    /
    Eine Abfrage des Scores selektiert man wie folgt:
    SELECT
       *
    FROM
       (
       SELECT
          username,
          priv_check.get_syspriv_score(username) AS score
       FROM
          dba_users
       )
    WHERE
       score>0
    ORDER BY
       score DESC;
    USERNAME                         SCORE
    --------------------------- ----------
    SYS                                140
    SYSTEM                              74
    WMSYS                                8
    APEX_050000                          5
    APEX_040200                          5
    DVSYS                                4
    SPATIAL_CSW_ADMIN_USR                2
    SPATIAL_WFS_ADMIN_USR                2
    OLAPSYS                              2
    GSMADMIN_INTERNAL                    2
    SYSBACKUP                            2
    MDSYS                                2
    SYSDG                                1
    XDB                                  1

    Der administrative Benutzer "SYS" besitzt logischerweise den höchsten Scoring-Wert.

    Um überhaupt herauszufinden welche Benutzer direkt oder über eine Rolle solche Rechte besitzen, kann man dies mithilfe von folgendem Statement herausfinden. Der SELECT listet alle betroffenen Benutzer und zeigt diese in hierachischer Struktur an.

    SELECT
        lpad(' ', 2*level) || c "Privilege, Roles

    Wie suche ich in View-Texten?

    Bereich:PL/SQL, Version: ab RDBMS 12.1, Letzte Überarbeitung: 23.05.2022

    Keywords:PL/SQL, SQL

    Manchmal umfassen Applikationen nicht nur viel Quelltext, sondern auch sehr viele Views, in denen u. a. auch Funktionen aus diversen Packages verwendet werden. Wenn nun eine bestimmte Funktion geändert werden soll / muss, so sollte man dringend im Vorfeld klären, welche Stellen davon betroffen sind. So kann man sichergehen, dass die gewünschte Änderung keine unerwünschten Nebenwirkungen hat.

    Dazu muss man aber erst einmal herausfinden, wo die betreffende Funktion überall aufgerufen wird.
    Sofern sich die Signatur der Funktion hinreichend ändert, z. B. weil ein weiterer Parameter dazu kommt, kann man dazu theoretisch in einer Testumgebung einfach die Funktion abändern und schauen, was dadurch INVALID wird und es auch nach Neukompilierung bleibt.

    Wenn sich aber die Signatur NICHT ändert, oder wenn keine vernünftige Testumgebung zur Verfügung steht (sowas soll es geben), dann muss man sich anderweitig auf die Suche begeben.

    Abhängigkeiten helfen hier in aller Regel nicht wirklich weiter: USER_DEPENDENCIES listet nur das Package auf, nicht die einzelne Funktion. Und wenn das Package eine Reihe von Hilfsfunktionen beinhaltet, ist man - fast - genau so schlau wie vorher.

    Solange es sich um Aufrufe innerhalb von PL/SQL handelt, sind die Stellen noch relativ einfach zu finden:

    SELECT *  FROM user_source WHERE LOWER(text) LIKE '%<funktionsname>%';

    Bei Views dagegen wird die Sache schwieriger. Rein intuitiv würde man es ja analog probieren mit:

    SELECT * FROM user_views WHERE LOWER(text) LIKE '%<funktionsname>%';

    Das funktioniert aber nicht, weil text vom Typ LONG ist. Und so wird die Abfrage quittiert mit:

    ORA-00932: Inkonsistente Datentypen: CHAR erwartet, LONG erhalten

    Und jede View einzeln anschauen, kann sehr mühsam werden. Da hilft am besten ein kleiner Umweg über PL/SQL:

    CREATE OR REPLACE FUNCTION read_view_text ( p_viewname IN VARCHAR2)
       RETURN VARCHAR2
    IS
       v_text   VARCHAR2 (32767);
    BEGIN
       SELECT text
         INTO v_text
         FROM user_views
        WHERE UPPER(view_name) = UPPER(p_viewname);
    
       RETURN v_text;
    EXCEPTION
       WHEN OTHERS
       THEN
          RETURN NULL;
    END read_view_text;
    /

    Und dann sucht man mit Hilfe dieser Funktion:

    SELECT * FROM user_views 
     WHERE LOWER(read_view_text(view_name)) LIKE '%<funktionsname>%';

    Das Prinzip ist natürlich auch auf andere LONG-Spalten anwendbar, z. B. query in user_snapshots / user_mviews.

    Ab Version 12.1 hat user_views ein paar neue Spalten erhalten, u. a. auch text_vc vom Typ VARCHAR2(4000). Und damit funktioniert das ganze dann direkt (zumindest für die ersten 4000 Bytes):

    SELECT * FROM user_views WHERE LOWER(text_vc) LIKE '%<funktionsname>%';


    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Suche Datensatz in einem Schema

    Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 12.12.2018

    Keywords:PL/SQL

    Hatten Sie auch schon einmal das Problem, dass Sie einen Datensatz gesucht haben, aber nicht wussten, in welcher Spalte dieser steht? Wir haben noch eines drauf gesetzt und können sogar Werte suchen, von denen man nicht weiß, in welcher Tabelle sie stehen. Diese Fragestellung kann auftreten, wenn man mit einer Applikation arbeitet, deren genaues Datenbank-Layout nicht bekannt ist.

    Es sollte jedoch erwähnt werden, dass hier mehrfach ein Full-Tablescan erzeugt wird, der bei einer großen Tabellenanzahl zu einer SEHR GROSSEN Performance-Belastung führen kann. Besonders die Option ALL wird sehr viel Ressourcen verbrauchen, deswegen sollte man die Procedure nur auf Test-Maschinen absetzen.

    Vorbereitung

    In dieser Procedure werden zwei Data-Dictionary Tabellen (dba_objects und dba_tab_columns) benutzt. Leider sind Rechte an DD-Objekten über Rollen an die Benutzer vergeben, die wiederum in PL/SQL nicht verwendet werden können.

    Drei Möglichkeiten stehen als Problemlösung zur Verfügung:

    • Sie kompilieren die Procedure als Benutzer SYS, denn der hat alle Rechte. :-)
    • Sie vergeben die beiden Rechte DIREKT an den Benutzer, der dann die Procedure kompiliert:

    GRANT SELECT on dba_tab_columns TO system;
    GRANT SELECT on dba_objects TO system;

    • Sie schreiben die Procedure in einen anonymen Block um. Ersetzen Sie den Bereich "CREATE OR REPLACE bis "IS" durch:

    ACCEPT v_schema_name PROMPT "Schema eingeben (ALL für Alle): "
    ACCEPT v_search_string PROMPT "Suchstring eingeben (Bsp: Hallo%): "
    ACCEPT v_table_name "Tabellennamen eingeben (Leer = alle Tabellen): "
    ACCEPT v_type "Datentyp STRING oder NUMBER eingeben: "


    DECLARE
      p_schema_name VARCHAR2(30):=upper('&&v_schema_name');
      p_search_string VARCHAR2(30):='&&v_search_string';
      p_table_name VARCHAR2(30):=upper('&&v_table_name');
      p_type VARCHAR2(30):='&&v_type';
         
    TYPE ref_curs ...

    Folgende Parameter können übergeben werden:

    • p_search_string
      Nimmt den gesuchten String auf. Folgende Möglichkeiten stehen zur Verfügung:
      "STRING", "%STRING" oder "%STRING%"
    • p_schema_name
      Name des Schema, in dem gesucht werden soll. Default ist das aktuelle. Mit der Option ALL werden alle Schematas durchsucht. Ausgeschlossen sind die Schematas von SYS, SYSTEM und allen SYS-nahen Benutzern
    • p_table_name
      Hier kann optional der Tabellenname angegeben werden, dann werden nur deren Spalten durchsucht.
    • p_type
      Wenn es sich um eine String Spalte (char, varchar2,clob) handelt, wo der gesuchte Wert enthalten ist, können Sie hier die Option 'STRING' angeben. Die Option 'NUMBER' durchsucht nummerische Spalten.

    Procedure:

    CREATE OR REPLACE PROCEDURE find_in_tables
       (p_search_string IN VARCHAR2,
        p_schema_name   IN VARCHAR2 DEFAULT NULL,                              
        p_table_name    IN VARCHAR2 DEFAULT NULL,
        p_type IN VARCHAR2 DEFAULT 'STRING')
       uthid current_user

     IS
      TYPE ref_curs_type IS REF CURSOR;
      refc   ref_curs_type;
      sql_str    VARCHAR2(200);
      row_ret    ROWID;
      v_user VARCHAR2(30);

    BEGIN
      /* Wenn Benutzer NULL ist wir aktueller Benutzer verwendet */
      SELECT sys_context('USERENV', 'SESSION_USER') INTO v_user FROM dual;
      v_user := nvl(p_schema_name, v_user);
      dbms_output.put_line
       ('Suche wurde ausgeführt mit: User='||v_user||' Table='||p_table_name);
      FOR rec IN (select do.owner,table_name, column_name, data_type
                    FROM sys.dba_tab_columns dtc, dba_objects do
                   WHERE dtc.table_name = do.object_name
                     and do.object_type = 'TABLE'
                     and dtc.owner = decode(v_user,'ALL',dtc.owner,v_user)
                     and dtc.owner not in ('SYS','SYSTEM','OUTLN','SYSMAN',
                                           'MDSYS','ORDSYS','WMSYS','CTXSYS',
                                           'OLAPSYS')
                     and do.object_name = nvl(p_table_name, do.object_name)) LOOP
        IF upper(p_type) = 'STRING' AND
           (rec.data_type = 'CHAR' OR rec.data_type = 'VARCHAR2' or
            rec.data_type = 'CLOB') THEN
          sql_str := 'SELECT rowid FROM ' || rec.owner || '.' ||
          rec.table_name ||' WHERE ' || rec.column_name || ' like ' ||
          chr(39) || p_search_string || chr(39);
       
          BEGIN
           
            OPEN refc FOR sql_str;
            LOOP
              FETCH refc
                INTO row_ret;
              EXIT WHEN refc%NOTFOUND;
              dbms_output.put_line('Owner='||rec.owner||' Table=' ||
              rec.table_name || ' Col=' ||rec.column_name || ' Rowid=' ||
              row_ret);
           
            END LOOP;
            row_ret := null;
            CLOSE refc;
          EXCEPTION
            WHEN OTHERS THEN
              IF v_user<>'ALL' THEN
               dbms_output.put_line('Fehler in Table=' || rec.table_name);
              END IF;
           
          END;
       
        ELSIF p_type = 'Number' and rec.data_type = 'NUMBER' THEN
          sql_str := 'SELECT rowid FROM ' || p_schema_name || '.' ||
          rec.table_name || ' WHERE ' || rec.column_name || '='
          || p_search_string;
          BEGIN
            OPEN refc FOR sql_str;
            LOOP
              FETCH refc
                INTO row_ret;
              EXIT WHEN refc%NOTFOUND;
              dbms_output.put_line('Owner='||rec.owner||' Table=' ||
              rec.table_name || ' Col=' ||rec.column_name || ' Rowid='
              || row_ret);
           
            END LOOP;
            row_ret := null;
            CLOSE refc;
          EXCEPTION
            WHEN OTHERS THEN
              IF v_user<>'ALL' THEN
              dbms_output.put_line('Fehler in Table=' || rec.table_name);
              END IF;
          END;
       
        END IF;
     
      END LOOP;
    END;
    /

    Beispielaufrufe:

    SQL> EXEC find_in_tables ('PRESI%','SCOTT','EMP');

    Ausgabe: Table=EMP Col=JOB Rowid=AAANOxAAEAAAAAdAAI

    SQL> EXEC find_in_tables ('DALL%','ALL');

    Ausgabe: Table=DEPT Col=LOC Rowid=AAANOvAAEAAAAANAAB



    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Verarbeitung eines dynamischen Selects

    Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 23.12.2020

    Keywords:dynamiches PL/SQL, dbms_sql

    Dynamische Selects zu schreiben ist dank des Ref Cursoren mittlerweile ziemlich einfach geworden. Man muss jedoch wissen, wie viele Spalten man ausliest, und welchen Datentyp sie haben. In der Regel ist das kein Problem, wenn man über die Dynamik nur erreichen will, dass je nach übergebenen Parametern auf unterschiedliche Tabellen zugegriffen wird oder nach unterschiedlichen Spalten sortiert werden soll.

    Was aber tut man, wenn man die Spaltenliste des Select-Befehls nicht kennt?

    In diesem Fall kann man auf das altbewährte und teilweise bereits als veraltet belächelte DBMS_SQL-Package zurückgreifen. Es ermöglicht, über die Prozedur DESCRIBE_COLUMNS die Spaltenliste zu ermitteln. Diese Prozedur liefert die Anzahl der Spalten zurück und eine Beschreibung jeder Spalte in Form einer INDEX BY-Tabelle, die auf einem Record-Datentyp (DBMS_SQL.DESC_REC) beruht. Ausgelesen werden u. a. Name und Datentyp der Spalten.

    Um auch die Anzahl der Spalten komplett dynamisch halten zu können, wurde im unten gezeigten Beispiel mit INDEX BY-Tabellen der wichtigsten Datentypen als Variablen gearbeitet.

    Beispiel:

    CREATE or replace PROCEDURE GETROWS (p_select IN VARCHAR2) AS
       v_desc     DBMS_SQL.DESC_TAB;
       v_id       INTEGER;
       v_count    INTEGER;
       v_ret      INTEGER;
       Type t_ntype IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
       v_col_n    t_ntype;
       Type t_vtype IS TABLE OF VARCHAR2(4000) INDEX BY BINARY_INTEGER;
       v_col_v    t_vtype;
       Type t_dtype IS TABLE OF DATE INDEX BY BINARY_INTEGER;
       v_col_d    t_dtype;
       v_output   VARCHAR2(2000);
    BEGIN
       v_id := DBMS_SQL.OPEN_CURSOR;
       DBMS_SQL.PARSE(v_id, p_select, DBMS_SQL.v7);
       DBMS_SQL.DESCRIBE_COLUMNS(v_id, v_count, v_desc);
       
       FOR i in 1..v_count LOOP
          v_col_n(i) := NULL;
          v_col_v(i) := NULL;
          v_col_d(i) := NULL;
       END LOOP;
       
       FOR i in 1..v_count LOOP
          CASE v_desc(i).col_type
             WHEN DBMS_TYPES.TYPECODE_VARCHAR THEN
                DBMS_SQL.DEFINE_COLUMN(v_id, i, v_col_v(i), 2000);
             WHEN DBMS_TYPES.TYPECODE_NUMBER THEN
                DBMS_SQL.DEFINE_COLUMN(v_id, i, v_col_n(i));
             WHEN DBMS_TYPES.TYPECODE_DATE THEN
                DBMS_SQL.DEFINE_COLUMN(v_id, i, v_col_d(i));
          END CASE;
       END LOOP;
       
       v_ret := DBMS_SQL.EXECUTE(v_id);
       
       LOOP
          v_ret := DBMS_SQL.FETCH_ROWS(v_id);
          EXIT WHEN v_ret IS NULL OR v_ret <=0;
          FOR i IN 1..v_count LOOP
          v_output := 'Spalteninhalt Spalte '||v_desc(i).col_name ||': ';
             CASE v_desc(i).col_type
                WHEN DBMS_TYPES.TYPECODE_VARCHAR THEN
                   DBMS_SQL.COLUMN_VALUE(v_id, i, v_col_v(i));
                   v_output := v_output||v_col_v(i);
                WHEN DBMS_TYPES.TYPECODE_NUMBER THEN
                   DBMS_SQL.COLUMN_VALUE(v_id, i, v_col_n(i));
                   v_output := v_output||v_col_n(i);
                WHEN DBMS_TYPES.TYPECODE_DATE THEN
                   DBMS_SQL.COLUMN_VALUE(v_id, i, v_col_d(i));
                   v_output := v_output||v_col_d(i);
             END CASE;
             DBMS_OUTPUT.PUT_LINE(v_output);
          END LOOP;   END LOOP;
       DBMS_SQL.CLOSE_CURSOR(v_id);
    EXCEPTION
       WHEN CASE_NOT_FOUND THEN
          DBMS_OUTPUT.PUT_LINE('Unbekannter Datentyp einer Spalte');      
          DBMS_SQL.CLOSE_CURSOR(v_id);
    END;
    /

    SET SERVEROUTPUT ON SIZE 1000000
    EXEC GETROWS('SELECT * FROM EMP')


    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Besonderheiten des Datentyps LONG

    Bereich:PL/SQL:SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 18.11.2019

    Keywords:DBA , LONG , PL/SQL

    Das soll sich ändern, wir haben hier nun die wichtigsten Punkte zu Oracle LONG Spalten zusammengefasst:LONG Spalten wurden in Oracle 7.3 eingeführt und sollten die Beschränkung von VARCHAR2 Spalten die nur 4000 Zeichen aufnehmen können, beheben.Deswegen wurden zwei neue Datentypen geschaffen LONG (kann bis 2 GB an Textdaten aufnehmen) und LONG RAW (kann bis zu 2 GB an Binärdaten speichern).

    Leider hat man keine Routinen dazu entwickelt, die mit dieser Speichermenge umgehen konnten. Man hat deshalb mit 2 Speicherbegrenzungen zu kämpfen:

    SQL Funktionen können (wenn überhaupt) nur die ersten 4000 Zeichen einer LONG-Spalte nutzen.
    In PL/SQL kann man die ersten 32760 Zeichen lesen/schreiben. Mit einem Trick (dazu später mehr) lesen wir aber auch darüber hinaus.
    Wo kann man LONG Spalten verwenden?

    •     SELECT auf eine LONG Spalte
    •     In der SET Klausel eines UPDATE Statements
    •     In der VALUES Klausel eines INSERT Statements

    1.) Beschränkungen von LONG Spalten

    •     Objekt Typen dürfen keine LONG Attribute besitzen.
    •     LONG Spalten dürfen nicht in einer WHERE Klausel vorkommen.
    •     LONG Spalten dürfen nur NULL oder NOT NULL Constraints besitzen.
    •     LONG Spalten dürfen keinen Index besitzen (Ausnahme Text-Index).
    •     Reguläre Ausdrücke können nicht auf LONG Spalten verwendet werden.
    •     Funktionen (auch innerhalb eines Package) dürfen keinen LONG Wert zurückgeben (Nur VARCHAR2(4000)).
    •     Bei einem Join über mehrere Datenbanken müssen sich alle LONG Spalten auf einer Datenbankseite befinden.
    •     LONG und LONG RAW Spalten können nicht repliziert werden.
    •     Wenn eine Tabelle LONG und LOB Spalten enthält, dürfen nur jeweils 4000 Zeichen in die Spalten geschrieben werden.
    •     GROUP BY, ORDER BY, CONNECT BY oder DISTINCT dürfen nicht in Verbindung mit LONG Spalten verwendet werden.
    •     Eine Tabelle darf nur eine LONG/LONG RAW Spalte besitzen.
    •     Eine Tabelle mit LONG Spalte kann nicht mittels CREATE TABLE ... AS SELECT <LONG> FROM <table> kopiert werden.
    •     " ALTER TABLE ... MOVE"-Befehl kann nicht verwendet werden, wenn die Tabelle eine LONG Spalte besitzt.
    •     Immerhin werden LONG Spalten wohl auch noch in Oracle 11g unterstützt :-)

    Das schauen wir uns mal genauer an ...

    CREATE TABLE t (
      id   NUMBER,
      text LONG,
      bild LONG RAW);

    FEHLER in Zeile 4:
    ORA-01754: Eine Tabelle darf nur eine einzige Spalte des Typs LONG aufweisen

    Zweiter Versuch funktioniert, da nur eine LONG Spalte verwendet wird:

    CREATE TABLE t (
     id   NUMBER,
     text LONG);

    Das gleiche kann man auch mit einer LONG RAW Spalte machen:

    CREATE TABLE test_LONG_raw (
     id           NUMBER,
     long_col_raw LONG RAW);

     
    2.) Constraints auf LONG-Spalten

        Nur NOT NULL Constraints sind erlaubt
        Die Constraint-Typen P (Primary Key), U (Unique Key), C (Check) können nicht verwendet werden.

    CREATE TABLE t (
     id   NUMBER,
     l    LONG NOT NULL);

    ... alle anderen Constraints sind nicht erlaubt

    CREATE TABLE t (
     id   NUMBER,
     l    LONG PRIMARY KEY);

    ORA-02269:Schlüsselspalte kann nicht vom Datentyp LONG sein
    02269: "key column cannot be of LONG datatype"
    *Cause: Self-evident.
    *Action: Change the datatype of the column, or remove the column from the key.

     
    3.) Indizierung von LONG-Spalten

    Ein normaler Index kann nicht auf eine LONG-Spalte gesetzt werden, aber ein Text-Index schon.

    CREATE INDEX i ON t(text);
    Error: ORA-00997: Unzulässige Verwendung des Datentyps LONG

    Workaround: Text-Index setzen

    CREATE INDEX i ON t(text) INDEXTYPE IS ctxsys.context;
    SELECT * FROM t WHERE CONTAINS( text, 'Muniqsoft GmbH' ) > 0;

     

    4.) LONG Spalte füllen (bis zu 4000 Zeichen)

    INSERT INTO t VALUES (1,lpad('#',4000,'#'));

    LONG RAW Spalte füllen

    INSERT INTO test_LONG_raw (id,LONG_col_raw)
       VALUES(1,utl_raw.cast_to_raw('abcdefghijklmnopqrstuvwxyz'));

     

    5.) Füllung einer LONG-Spalte mit 32760 Bytes in PL/SQL

    DECLARE
       v_l LONG:=lpad('#',32760,'#');
    BEGIN
       INSERT INTO t VALUES (1,v_l);
       COMMIT;
    END;
    /

    Update eines LONG-Datentyps mit bis zu 32760 Bytes

    DECLARE
       v_l LONG;
       CURSOR c IS SELECT text FROM t
                   WHERE id=2 FOR UPDATE;
    BEGIN
       FOR r IN c LOOP
             v_l:=r.text || ' Neuer Text';
             UPDATE t SET text = v_l WHERE CURRENT OF c;
       END LOOP;
       COMMIT;
    END;
    /

     

    6.) Die Funktionen INSTR, SUBSTR und LENGTH..

    ...können nicht auf LONG-Spalten angewendet werden

    SELECT LENGTH(text) FROM t WHERE id=1;

    FEHLER in Zeile 1:
    ORA-00932: Inkonsistente Datentypen: NUMBER erwartet, LONG erhalten

    SELECT SUBSTR(text,1,5) FROM t;

    FEHLER in Zeile 1:
    ORA-00932: Inkonsistente Datentypen: NUMBER erwartet, LONG erhalt

    SELECT INSTR(text,1,'#') FROM t;

    FEHLER in Zeile 1:
    ORA-00932: Inkonsistente Datentypen: NUMBER erwartet, LONG erhalten

    Aber mit einem kleinen Trick bekommen wir das doch noch hin

    Wenn Sie z.B. auf die Spalte view_text aus DBA_VIEWS zugreifen möchten, gehen Sie wie folgt vor:
    1. rufen Sie die Funktion sys_dburigen mit den Parametern: Eigentümer der Tabelle/View, Name der Tabelle/View und Spaltenname auf:

    SELECT sys_dburigen( owner,view_name, text, 'text()').getclob()
    FROM dba_views
    WHERE owner='SCOTT';


    2. verwenden Sie nund ie gewünschte Funktion aus 6. (z.B. substr)

    SELECT substr(sys_dburigen( owner,view_name, text, 'text()').getclob(),1,100)
    FROM dba_views
    WHERE owner='SCOTT';

    Das Ganze geht natürlich mit der Long Spalte:
    TRIGGER_BODY aus Tabelle DBA_TRIGGERS
    SEARCH_CONDITION aus Tabelle DBA_CONSTRAINTS
    u.v. weiteren LONG Spalten
    Achtung: Die 3 Parameter müssen genau eine Zeile zurückgeben, sonst erhält man den Fehler:
    ORA-19003 XML document must have a top level element
    Deswegen funktioniert der TRcik auch leider nicht bei:

    SELECT sys_dburigen( table_owner,partition_name,HIGH_VALUE, 'text()').getxml() FROM all_tab_partitions
    WHERE table_owner='AUDSYS' AND table_name='AUD$UNIFIED' ;


    7.) Trigger auf LONG Spalten

    Ein SQL Statement innerhalb eines Trigger kann Daten in eine LONG oder LONG RAW Spalte einfügen.Die maximale Länge einer Text-Variable (VARCHAR2 oder LONG) in einem Trigger beträgt 32000.:NEW und :OLD dürfen nicht auf LONG oder LONG RAW Spalten gesetzt werden.

    LONG oder LONG RAW Spalten dürfen nur referenziert werden, wenn sie in CHAR oder VARCHAR2 konvertierbar sind.

    CREATE OR REPLACE TRIGGER tr
       BEFORE UPDATE OR INSERT OR DELETE
       ON t
       FOR EACH ROW
    BEGIN
       :new.text:='A';
    END;
    /
    Error: ORA-04093: Referenzen auf Spalten von Typ LONG sind in Triggern nicht zulässig.

     

    8.) Ausgabelänge für Oracle LONG-Spalten in SQL*Plus auf 2 Mio Zeichen setzen (Default= 80)

    Wenn Sie diesen Parameter nicht umsetzen, werden bei einer Ausgabe in SQL*Plus nur die ersten 80 Zeichen einer LONG Spalte ausgegeben.

    SET LONG 200000


    9.) Tabellen mit LONG Spalten kopieren

    Eine Tabelle kann mit einem CREATE TABLE AS SELECT ... nicht kopiert werden, wenn LONG Spalten enthalten sind.

    CREATE TABLE t2 AS SELECT * FROM t;

    FEHLER in Zeile 1:
    ORA-00997: Unzulässige Verwendung des Datentyps LONG

    Workaround mit Konvertierung in einen CLOB Datentypen

    CREATE TABLE t2 AS SELECT to_lob(text) as text FROM t;

    Tabelle wurde erstellt.


    10.) LONG Spalten konvertieren

    Oracle empfiehlt, möglichst zeitnah auf die Datentypen CLOB, BLOB und NCLOB zu konvertieren. Diese stehen ab Version 8.0 zur Verfügung.

    Beispiel: Umwandeln von LONG in CLOB und LONG Raw in BLOB

    ALTER TABLE test_LONG MODIFY (LONG_col CLOB);
    ALTER TABLE test_LONG_raw MODIFY (LONG_col_raw BLOB);

     
    11.) LONG Spalten in PL/SQL

    In PL/SQL wird ein LONG-Datentyp als VARCHAR2(32760) abgebildet. Das heißt folgende beiden Deklarationen sind identisch:

    DECLARE
       v_a LONG;
       v_b VARCHAR2(32760);
    BEGIN
       NULL;
    END;
    /

     
    12.) PL/SQL Package zum Berechnen der LONG Spaltenlänge und zum Auslesen von Daten

    Das nachfolgende kleine PL/SQL Beispiel zeigt, dass man mit einem Spezial-Package (dbms_sql) auch an die Daten einer LONG Spalte herankommt, die hinter der magischen Grenze von 32760 liegen.

    Das Package LLONG bietet Ihnen die folgenden Möglichkeiten:

        Berechnung der Länge eines LONG Feldes. Damit können Sie feststellen, ob Sie die Daten auch in VARCHAR2(4000) bekommen könnten.

        Der Aufruf dazu lautet:

        LLONG.LEN(<rowid>)

        Beispiel:

        SELECT LLONG.LEN(rowid) FROM t;

        Herausschneiden von beliebigen 400 Zeichen in der LONG-Spalte

        Der Aufruf lautet:

        LLONG.SUB_STR(<rowid>, <start_pos>, <end_pos>)

        Der Default für die Startposiion ist 1 und der Default für die Endposition ist 4000.

        Beispiele:

        SELECT LLONG.SUB_STR(rowid) FROM t;
        SELECT LLONG.SUB_STR(rowid, 400000,404000) FROM t;

        Das Suchen innerhalb einer Long Spalte mit Like

        Der Aufruf dazu lautet:

        LLONG.LIKE2(<rowid>,'<such_str>')

        Beispiel:

        SELECT * FROM t
        WHERE LLONG.LIKE2(rowid,'MARCO%') >0

        Das Anhängen von Daten an eine bestimmte Zeile

        Der Aufruf lautet:

        LLONG.APPEND(<rowid>,'<TEXT>')

        Beispiel:

        EXEC LLONG.APPEND('AAAUfXAAEAAAAEAAAC',' Anhänger')

        Das Anzeigen der Position eines Suchstring:

        Der Aufruf dazu lautet:

        LLONG.IN_STR(<rowed>,'<TEXT>',<pos>)

        Beispiel:

        SELECT LLONG.IN_STR(rowid,'MARCO',1) FROM t;

    Und so sieht das komplette Package aus:

    CREATE OR REPLACE PACKAGE LLONG
       AS FUNCTION sub_str(
          p_rowid in rowid,
          p_startpos IN NUMBER DEFAULT 1,
          p_endpos IN NUMBER DEFAULT 4000)
        RETURN CLOB;
       
        FUNCTION len( p_rowid in rowid)
        RETURN VARCHAR2;
       
        PROCEDURE append (
          p_rowid IN ROWID,
          p_value IN VARCHAR2);
         
        FUNCTION like2 (
          p_rowid IN ROWID,
          Like_str In VARCHAR2)
        RETURN NUMBER;
       
        FUNCTION in_str (
        p_rowid IN ROWID,
        search_char In VARCHAR2,
        position IN NUMBER DEFAULT 1)
        RETURN NUMBER;

    END;
    /
    SHOW ERRORS

    CREATE OR REPLACE PACKAGE BODY LLONG
       AS
          l_cursor   integer default dbms_sql.open_cursor;
          l_n        number;
          --l_LONG_val varchar2(4000);
          l_LONG_val CLOB;
          l_LONG_len number;
          l_buflen   number := 4000;
          l_curpos   number := 0;
          v_tab      VARCHAR2(62);
          v_col      VARCHAR2(30);
          offset     integer;
          v_length   integer;
          v_collen   integer;

       FUNCTION get_obj_name (r ROWID) RETURN VARCHAR2
       AS
       BEGIN
       select owner||'.'||object_name INTO v_tab
       from all_objects where object_id=(
       select dbms_rowid.rowid_object(r)
       from dual);
       RETURN v_tab;
       END;
       FUNCTION get_col_name (r ROWID) RETURN VARCHAR2
       AS
       BEGIN
       select column_name INTO v_col
       from all_tab_columns where (owner,table_name) IN  (select
    owner,object_name
       from all_objects where object_id=(
       select dbms_rowid.rowid_object(r)
       from dual)) AND data_type='LONG';
       RETURN v_col;
       END;

       PROCEDURE open_curs (p_rowid IN ROWID)
       AS
       BEGIN
        v_tab:=get_obj_name(p_rowid);
        v_col:=get_col_name(p_rowid);
        l_cursor:=dbms_sql.open_cursor;
        dbms_sql.parse( l_cursor,'select ' || v_col || ' from ' ||    v_tab ||
        ' where rowid = :x',dbms_sql.native );
        dbms_sql.bind_variable( l_cursor, ':x', p_rowid );
        dbms_sql.define_column_LONG(l_cursor, 1);
        l_n := dbms_sql.execute(l_cursor);
       END;

       FUNCTION sub_str(
          p_rowid in rowid,
          p_startpos IN NUMBER DEFAULT 1,
          p_endpos IN NUMBER DEFAULT 4000)
       RETURN CLOB
       AS
    BEGIN
       IF p_endpos-p_startpos > 4000 THEN
          RETURN 'Max Return Length 4000 Bytes !';
       END IF;
       open_curs(p_rowid);
       IF (dbms_sql.fetch_rows(l_cursor)>0) then
          dbms_sql.column_value_LONG(
          l_cursor, 1, p_endpos-p_startpos, p_startpos ,l_LONG_val, l_LONG_len
    );
       END IF;
       dbms_sql.close_cursor(l_cursor);
       return l_LONG_val;
       EXCEPTION WHEN OTHERS THEN RETURN sqlerrm;
    end; --sub_str

      FUNCTION len (p_rowid in rowid)
      RETURN VARCHAR2
      AS
      BEGIN
      open_curs(p_rowid);
    °  IF (dbms_sql.fetch_rows(l_cursor) > 0 ) THEN
          offset := 0;
          v_collen := 0;
      LOOP
          dbms_sql.column_value_LONG(l_cursor,
    1,32767,offset,l_LONG_val, v_length);
          v_collen := v_collen + v_length;
          EXIT WHEN v_length < 32767 OR v_length IS NULL;
          offset := offset + v_length;
      END LOOP;
      END IF;
    dbms_sql.close_cursor(l_cursor);
    return v_collen;
    EXCEPTION WHEN OTHERS THEN RETURN sqlerrm;
    END; --len

    PROCEDURE append (
          p_rowid IN ROWID,
          p_value IN VARCHAR2)
    AS
    v_app  VARCHAR2(32760);
    s      VARCHAR2(1000);
    BEGIN
    open_curs(p_rowid);
    IF (dbms_sql.fetch_rows(l_cursor) > 0 ) THEN
        offset := 0;
        LOOP
           dbms_sql.column_value_LONG(l_cursor,1,32767,offset,l_LONG_val, v_length);
            v_app := v_app || l_LONG_val;
          EXIT WHEN v_length < 32767 OR v_length IS NULL;
                    offset := offset + v_length;
        END LOOP;
      END IF;
    dbms_sql.close_cursor(l_cursor);
     BEGIN
      v_app:=v_app||p_value;
      s:='UPDATE '||get_obj_name(p_rowid)||' SET '||get_col_name(p_rowid)||' =:x WHERE rowid=:y';
      EXECUTE immediate s USING v_app,p_rowid;
      EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20000,'Max Col Length Limit 32767
    eached');
     END;
    END; --append

    FUNCTION like2 (
     p_rowid IN ROWID,
     Like_str In VARCHAR2)
    RETURN NUMBER
    IS
    BEGIN
    open_curs(p_rowid);
    IF (dbms_sql.fetch_rows(l_cursor) > 0 ) THEN
          offset := 0;
      LOOP
          dbms_sql.column_value_LONG(l_cursor,1,32767,offset,l_LONG_val, v_length);
          IF l_LONG_val LIKE like_str THEN
            RETURN 1;
          END IF;
          EXIT WHEN v_length < 32767 OR v_length IS NULL;
          offset := offset + v_length;
      END LOOP;
    END IF;
    dbms_sql.close_cursor(l_cursor);
    RETURN 0;
    END; --like2

    FUNCTION in_str (
        p_rowid IN ROWID,
        search_char IN VARCHAR2,
        position IN NUMBER DEFAULT 1)
        RETURN NUMBER
    IS
    pos NUMBER;
    BEGIN
    open_curs(p_rowid);

    IF (dbms_sql.fetch_rows(l_cursor) > 0 ) THEN
          offset := 0;
       LOOP
          dbms_sql.column_value_LONG(l_cursor,1,32767,offset,l_LONG_val, v_length);
          pos:= instr(l_LONG_val,search_char,mod(position,32767));
          IF pos>0 and pos+offset>position THEN
            RETURN pos+offset;
          END IF;
          EXIT WHEN v_length < 32767 OR v_length IS NULL;
          offset := offset + v_length;
       END LOOP;
    END IF;
    dbms_sql.close_cursor(l_cursor);
    RETURN pos;    
    END; -- instr Function

    END; -- PACKAGE
    /
    SHOW ERROR

    Beispielszenario:

    CREATE TABLE t ( text LONG );

    INSERT INTO t VALUES ( RPAD( '*', 32000, '*' ) );

    BEGIN
      INSERT INTO t VALUES('#');
      FOR i in 1.. 10000 LOOP
      UPDATE T SET text=text||';'||to_char(i);
      END LOOP;
      COMMIT;
    END;
    /

    SELECT LLONG.LEN( rowid) FROM t;

    GETLONG(ROWID)
    ---------------------------------
    4000

    SELECT LLONG.SUB_STR(rowid, 400000, 404000) FROM t;

    Suchen in LONG Data Dictionary Spalten:

    Als Beispiel wird hier in der Tabelle user_constraints Spalte search_condition nach einem Wert gesucht. Das Beispiel ist leich abänderbar, damit auch die Tabellen DBA_VIEWS (Spalte text) oder DBA_TRIGGERS (Spalte trigger_body) mit ihren LONG Spalten ausgelesen werden können.

    CREATE OR REPLACE FUNCTION get_search_condition( p_cons_name IN VARCHAR2 )
    RETURN VARCHAR2
       authid current_user
      IS
        l_search_condition user_constraints.search_condition%type;
      BEGIN
        SELECT search_condition into l_search_condition
          FROM all_constraints
          WHERE constraint_name = p_cons_name;
        RETURN l_search_condition;
    END;
    /

    SELECT constraint_name FROM all_constraints
    WHERE owner='SYSTEM'
    AND get_search_condition(constraint_name) LIKE '%NOT NULL%';

    PS: Die Konvertierung der Funktion kann einige Zeit in Anspruch nehmen!

    CONSTRAINT_NAME
    ------------------------------
    SYS_C002892
    .....

    Zum Vergleich ohne Funktion:

    SELECT constraint_name FROM user_constraints;
        WHERE search_condition LIKE '%NOT NULL%';
       
    where search_condition like '%NOT NULL%'
        *
    FEHLER in Zeile 2:
    ORA-00932: Inkonsistente Datentypen: NUMBER erwartet, LONG erhalten



    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Monatskalender in SQL*Plus

    Bereich:PL/SQL:SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 12.12.2018

    Keywords:PL/SQL , SQL

    Monatskalender in SQL*Plus

    Haben Sie sich auch schon mal gewünscht, dass SQL*Plus einen Monat als schönen Kalender darstellt? Mit der folgenden Funktion können Sie sich den aktuellen Monat (Default) oder einen beliebig anderen Monat anzeigen lassen:

    CREATE OR REPLACE FUNCTION show_cal (month IN VARCHAR2 DEFAULT sysdate)
    RETURN VARCHAR2 IS
       v_week VARCHAR2(4096);
       v_firstday NUMBER;
       v_lastday NUMBER;
    BEGIN
       v_firstday:=to_char(trunc(to_date(month),'MM'),'D');
       v_lastday:=to_number(to_char(last_day(to_date(month)),'DD'));
       v_week:='Monat: '||to_char(to_date(month),'FM Month RRRR')||chr(10);
       v_week:=v_week||'Mon Die Mit Don Fre Sam Son'||chr(10)||
       '-------------------------------------'||chr(10);
       FOR i IN 1.. v_lastday+v_firstday-1 LOOP
          IF i>=v_firstday then
             v_week:=v_week||rpad((i-v_firstday+1),4,' ');
          ELSE
             v_week:=v_week||rpad(chr(32),4,chr(32));
          END IF;
          IF mod(i,7)=0 THEN
             v_week:=v_week||chr(10);
          END IF;
       END LOOP;
       RETURN v_week;
    END;
    /
    show errors
    SELECT show_cal FROM dual;

    Ausgabe:

    SHOW_CAL
    ---------------------------------------------------
    Monat: März 2007 
    Mon    Die    Mit    Don    Fre    Sam    Son
    ---------------------------------------------------
                          1     2      3      4
     5       6     7      8     9     10     11
     12      13    14     15    16    17     18
     19      20    21     22    23    24     25
     26      27    28     29    30    31    

    Da viele grafische Tools (Toad, SQL Developer, ...) mit einem Zeilenumbruch nichts anfangen können, hier eine zweite Alternative mit einer Pipelined Function (nur ab Oracle 10g).

    CREATE OR REPLACE TYPE calender_week_type
    AS OBJECT (woche VARCHAR2(64));
    /
    CREATE OR REPLACE TYPE calender_type as table of calender_week_type;
    /
    CREATE OR REPLACE FUNCTION show_cal (month IN VARCHAR2 DEFAULT sysdate)
    RETURN calender_type PIPELINED IS
       v_week VARCHAR2(4096);
       v_firstday NUMBER;
       v_lastday NUMBER;
    BEGIN
       v_firstday:=to_char(trunc(to_date(month),'MM'),'D');
       v_lastday:=to_number(to_char(last_day(to_date(month)),'DD'));
       PIPE ROW (new calender_week_type('Monat: '||to_char(to_date(month),
                                                      'FM Month RRRR') ));
       PIPE ROW (new calender_week_type('Mon Die Mit Don Fre Sam Son'));
       PIPE ROW (new calender_week_type('--- --- --- --- --- --- ---'));
       FOR i IN 1.. v_lastday+v_firstday-1 LOOP
          IF i>=v_firstday then
             v_week:=v_week||rpad((i-v_firstday+1),4,' ');
          ELSE
             v_week:=v_week||rpad(chr(32),4,chr(32));
          END IF;
          IF mod(i,7)=0 THEN
             PIPE ROW (new calender_week_type(v_week));
             v_week:='';
          END IF;
       END LOOP;
    END;
    /
    show errors
    select * from TABLE(CAST(show_cal AS calender_type));

    Ausgabe:

    Woche
    --------------------------------------------------
    Monat: März 2007 
    Mon    Die    Mit    Don    Fre    Sam    Son
    --------------------------------------------------
                          1      2      3      4
     5      6      7      8      9      10     11
     12     13     14     15     16     17     18
     19     20     21     22     23     24     25
     26     27     28     29     30     31     
    7 Zeilen ausgewählt.



    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Reguläre Ausdrücke in Oracle

    Bereich:APEX:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 05.09.2023

    Keywords:PL/SQL

    Diesmal soll es vor allem um einfache Anwendungsbeispiele für die Funktionen REGEXP_LIKE, REGEXP_SUBSTR und REGEXP_REPLACE gehen.

    Wenn man sich mit der gewöhnungsbedürftigen Syntax (einen Überblick finden Sie am Schluss) erst einmal vertraut gemacht hat, können reguläre Ausdrücke beim Formatieren und Bereinigen von Tabelleninhalten, bei der Formulierung von komplizierten Check-Constraints u.a sehr gute Dienste leisten.

    Ausgangspunkt für die Beispiele ist die folgende Tabelle (s. Anhang), die mit den regexp-Funktionen überprüft, bereinigt und ggf. vor falschen Eingaben geschützt werden sollte:

    NAME         TELEFON             EMAIL                       PLZ     STRASSE
    Maier        +49/012-345678-012  johann.maier@chaos.de       00000   24 a, Langstr.
    meyer        012-345678-013      peter.meyer@chaos.de        a0674   1a,Langstr. G
    Mayerhöfer   012-345678-014      frank.mayerhoefer@chaos.de  00345   10-11, Langstr.
    MEYR         012-345678-015      tomas.meyr@chaos.de         19387   16b, Waldgasse
    Mayer        012-345678016       richard.mayer@chaos.de      11028    22, Richterstr.
    Müller       012-345678-017      martin.mueller@chaos.de     -       235b, Langstr.
    Mair         012-345678-018      wilhelmine.mair@chaos.de    12345    35a, Langstr.
    meir         0049/012-345678-019 sabine.meir@chaos.de         12345  101, Langstr.
    myers        0049-12-345678-029  jeff.myers@chaos.de         03927   10, Schlossallee
    müller       D012-345678-021     kurt.müller2@chaos.de       12098   10 c, Langstr.
    Hintermeier    012-345678-022    hansi.hintermeier@chaos.de   11937  180, Holzweg  
    MEIER        #012-345678023      eva.meier@chaos.de          12346   301, Langstr.
    mAYR         ?49-12-345678-024   klaus.mayr@chaos.de         19207  5d, Gärtnerplatz  
    MILLER       345678025           oscar.miller@chaos.de       1525    10  a, Langstr.

     
    1. Prüfung auf unerwünschte Zeichen mit REGEXP_LIKE

    REGEXP_LIKE vergleicht den Suchstring mit einem Muster und gibt true oder false zurück.

    REGEXP_LIKE(Suchstring, Muster, Match-Parameter)

    Die Angabe des Match-Parameters (siehe Anhang) ist optional.

    Fangen wir mit der PLZ-Spalte an, in der nur 5 Ziffern stehen sollen und nichts anderes (wenn die PLZ mit einer 0 anfängt, darf an der 2. Stelle keine stehen):

    SELECT name, plz, strasse FROM firma
    WHERE NOT REGEXP_LIKE(plz, '^0[1-9][0-9]{3}$|^[1-9][0-9]{4}$');

     

    NAME        PLZ   STRASSE
    Maier       00000 24 a, Langstr.
    meyer       0674  1a,Langstr.
    Mayerhöfer  00345 10-11, Langstr.
    Müller      -     235b, Langstr.
    meir        12345 101, Langstr.
    müller      12098 10 c, Langstr.
    Hintermeier 11937 180, Holzweg
    mAYR        19207 5d, Gärtnerplatz
    MILLER      1525  10 a, Langstr.

    Erläuterungen:

        eckige Klammern schließen eine Auswahl von Zeichen ein, [0-9] steht für die Ziffern von 0 bis 9, (alternative Formulierungen sind \d (ab 10g2) oder [[:digit:]]).
        Die Zahlen in geschweiften Klammern geben die Anzahl der Wiederholungen an, [0-9]{3} steht für 3 Ziffern.
        ^ und $ stehen für Anfang und Ende eines Ausdrucks. ^0 passt auf alle Muster, die mit einer 0 anfangen,[0-9]{3}$ auf alle, die mit 3 Ziffern aufhören.
        Vorsicht: Das Caret-Zeichen ^ hat diese Bedeutung nur, wenn es außerhalb von eckigen Klammern steht.
        Die Postleitzahlen mit einer Null am Anfang passen auf das Muster
        ^0[1-9][0-9]{3}$ (hier darf an der 2. Stelle keine 0 vorkommen), alle anderen auf ^[1-9][0-9]{4}$.
        Die beiden alternativen Muster werden durch das Oder-Zeichen | getrennt.

     
    2. Beseitigung von unerwünschten Zeichen mit REGEXP_REPLACE

    Die Oracle-Funktion REGEXP_REPLACE ersetzt das Muster im Suchstring durch den Ersatzstring.

    REGEXP_REPLACE(Suchstring, Muster, Ersatzstring, Position, Vorkommen, Match-Parameter)

    • Position:         Position im Suchstring, an dem der Mustervergleich anfängt, default ist 1.
    • Vorkommen:  das nte Vorkommen des Musters im Suchstring, default ist 1.

    Die Angabe von Position, Vorkommen und Match-Parameter ist optional.

    Die störenden Leerzeichen und Buchstaben in der PLZ-Spalte kann man mittels REGEXP_REPLACE in einem Rutsch entfernen. (Wenn der Ersatzstring leer bleibt, kann man ihn auch weglassen):

    UPDATE firma SET plz = REGEXP_REPLACE(plz, '[^0-9]','')
    WHERE NOT REGEXP_LIKE(plz, '^0[1-9][0-9]{3}$|^[1-9][0-9]{4}$'); 
    9 Zeilen wurden aktualisiert.

        Ein Caret-Zeichen innerhalb eines Klammerausdrucks kehrt die Zeichenauswahl um,
        [^0-9] steht also für alle Zeichen außer Ziffern, statt [^0-9] kann man auch \D oder [^[:digit:]] schreiben (s. Anhang).

    Kontrolle:

    SELECT name, plz, strasse FROM firma
    WHERE not REGEXP_LIKE(plz, '^0[1-9][0-9]{3}$|^[1-9][0-9]{4}$')OR plz IS NULL;
    NAME       PLZ    STRASSE
    Maier      00000  24 a, Langstr.
    meyer      0674   1a,Langstr.
    Mayerhöfer 00345  10-11, Langstr.
    Müller            235b, Langstr.
    MILLER     1525   10 a, Langstr.

    Diese 5 Felder muss man von Hand korrigieren, bevor man einen Check-Constraint einrichten kann:

    UPDATE firma SET plz = ... WHERE name = ...

     
    3. Verwendung von REGEXP_LIKE in Check-Constraints

    Nach der Korrektur lässt sich durch Hinzufügen eines check-Constraints sicherstellen, dass in Zukunft keine Buchstaben, Sonder- oder Leerzeichen eingegeben werden:

    ALTER TABLE firma
    ADD CONSTRAINT firma_plz_check
    CHECK (REGEXP_LIKE(plz, '^0[1-9][0-9]{3}$|^[1-9][0-9]{4}$'));

    Ähnlich funktioniert die Einrichtung eines Check-Constraints der e-mail-Adressen, z.B.:

    ALTER TABLE firma
    ADD CONSTRAINT firma_mail_check
    CHECK(REGEXP_LIKE(email, '^\w+\.\w+@\w+\.de$'));

        \w (ab 10g2)  steht für alle alphanumerischen Zeichen inklusive "_", + für eine beliebige Anzahl davon. Oracle akzeptiert \w nicht innerhalb der Zeichenlisten in eckigen Klammern.
        Der Punkt muss durch einen Backslash maskiert werden, weil er sonst als beliebiges Zeichen interpretiert würde. (Nur innerhalb von eckigen Klammern muss man die Metazeichen wie Punkt, Stern und Plus etc. nicht maskieren).
        Das @ steht für sich selbst.
        Statt de könnte man auch eine Auswahl angeben: [de|com|org] oder dem User mit \d{2,4} noch freiere Wahl lassen.

    Wenn man auch Bindestriche oder ähnliches in den e-mail-Adressen zulassen will, muss man das Muster anders formulieren (dafür kann man Name und Vorname in einem Klammerausdruck unterbringen), z. B.:

    ^[-._0-9a-zA-Z]+@[-_0-9a-zA-Z]+\.[A-Za-z]{2,4}$'

     
    4. Einfache Extraktion von Teilstrings mit REGEXP_SUBSTR

    REGEXP_SUBSTR gibt einem dem Muster entsprechenden Teilstring des Suchstrings aus.

    Was vor 10g mitunter komplizierte Kombinationen der Funktionen instr, SUBSTR und length erforderte, läßt sich mit REGEXP_SUBSTR in sehr kurzen Ausdrücken bewerkstelligen.

    REGEXP_SUBSTR(Suchstring, Muster, Position, Vorkommen, Match-Parameter)

    • Position:         Position im Suchstring, an dem der Mustervergleich anfängt, default ist 1.
    • Vorkommen:  das nte Vorkommen des Musters im Suchstring, default ist 1.

    Die Angabe von Position, Vorkommen und Match-Parameter ist wiederum optional.

    Die Sekretärin möchte eine Adressenliste der Mitarbeiter als View erstellen, die folgende Informationen enthalten soll: Vorname, Nachname, PLZ, Straße und Hausnummer.

    Die Vornamen kann man aus den e-mail-Adressen extrahieren und mit den Nachnamen verknüpfen:

    SELECT INITCAP(REGEXP_SUBSTR(email, '[^.]+')||' '||name) AS Name FROM firma;

    Aus johann.maier@chaos.de wird somit Johann Maier u.s.w. ...

        [^.] steht für alle Zeichen außer dem Punkt (innerhalb der Klammer steht der Punkt für sich selbst), per default wird der erste Teilstring ausgegeben, der dem Muster entspricht (vorkommen = 1).
        Wollte man den Nachnamen aus der Adresse holen, so müsste man REGEXP_SUBSTR(email, '[^.@]+',1,2) schreiben, (Position = 1, Vorkommen =2)
        REGEXP_SUBSTR(email, '[^.@]+',2) dagegen gibt den Vornamen ab dem 2. Zeichen aus (Position = 2, Vorkommen = default =1).
        Die Oracle-Funktion INITCAP sorgt für Großbuchstaben am Anfang und Kleinschreibung für den Rest.

     
    5. Drehung am Komma mit REGEXP_REPLACE

    Die umgedrehte Reihenfolge von Hausnummer und Strasse im amerikanischen Stil läßt sich durch mittels REGEXP_REPLACE umkehren:

    SELECT LTRIM(REGEXP_REPLACE(strasse, '([^,]+),([^,]+)','\2 \1'))
    AS strasse FROM firma;
    Langstr. 24 a
    Langstr. 1a
    Langstr. 10-11
    ......

        Das Komma teilt den String in 2 Hälften, die runden Klammern dienen der Gruppierung.
        [^,]+ steht für ein bis beliebig viele Zeichen mit Ausnahme des Kommas.
        Durch die negative Formulierung läuft man nicht Gefahr, eine Art von Zeichen zu vergessen, zudem kann man sie für beide Seiten verwenden.
        Positiv formuliert müsste man das Muster z.B. so schreiben: '([- 0-9a-z]+),([a-zA-Z. ]+)'
        \1 und \2 sind sogenannte Rückwärtsreferenzen (back references), die sich auf die Gruppierungen im Suchstring beziehen. Der Ausdruck '\2 \1' zieht aus dem Muster die Gruppen 1 und 2 heraus (ohne das Komma), dreht sie um und fügt zwischen beiden ein Leerzeichen ein.
        Die Oracle-Funktion LTRIM entfernt führende Leerzeichen.

    Jetzt können wir die View erstellen:

    CREATE OR REPLACE VIEW adressen
    AS SELECT
    INITCAP(REGEXP_SUBSTR(email, '[^.]+')||' '||name) AS Name, plz,
    LTRIM(REGEXP_REPLACE(strasse, '([^,]+),([^,]+)','\2 \1')) AS strasse
    FROM firma;
    SELECT * FROM adressen;
    NAME             PLZ   STRASSE
    Johann Maier     12345 Langstr. 24 a
    Peter Meyer      12345 Langstr. 1a
    Frank Mayerhöfer 12345 Langstr. 10-11
    .......

     
    6. Aufteilung von Strings in 2 Spalten mit REGEXP_SUBSTR

    Die folgende View stellt alle Informationen in getrennten Spalten dar:

    CREATE OR REPLACE VIEW adressen
    AS SELECT INITCAP(REGEXP_SUBSTR(email, '[^.]+')) AS Vorname,
              INITCAP(name) as Nachname, plz,
              LTRIM(REGEXP_SUBSTR(strasse, '([^,]+)',1,2)) AS strasse,
              LTRIM(REGEXP_SUBSTR(strasse, '([^,]+)')) AS Nr
    FROM firma;

    Aus einem String wie z.B. '24 a, Langstr.' extrahiert

    REGEXP_SUBSTR(strasse, '([^,]+)',1,2))das 2. Vorkommen des Musters (Strasse), und REGEXP_SUBSTR(strasse, '([^,]+)',1,1)) das 1. Vorkommen (Hausnummer, die beiden Einser können hier auch wegfallen, weil sie der Default sind):

    SELECT * FROM adressen;
    VORNAME NAME       PLZ   STRASSE  NR
    Johann  Maier      12345 Langstr. 24 a
    Peter   Meyer      12345 Langstr. 1a
    Frank   Mayerhöfer 12345 Langstr. 10-11
    ....

     
    7. Sortierung von alphanumerischen Einträgen mit REGEXP_SUBSTR

    Alle Angestellten, die in der Langstrasse wohnen, möchten eine Fahrgemeinschaft bilden. Der Fahrer hätte gern eine nach Hausnummern geordnete Liste der Namen. Wegen der Buchstaben und Leerzeichen etc. funktioniert der direkte Ansatz nicht, auch wenn man die vorangehenden Leerzeichen mit LTRIM eliminiert:

    SELECT name, strasse FROM firma
    WHERE strasse LIKE '%Lang%'
    ORDER BY LTRIM(strasse);
    
    NAME       STRASSE
    meyer      1a,Langstr.
    MILLER     10 a, Langstr.
    müller     10 c, Langstr.
    meir       101, Langstr.
    Mayerhöfer 10-11, Langstr.
    ....

    Eine Kombination dieser alphabetischen Sortierung mit der Sortierung nach dem Zahlenteilstring bringt dagegen das erwünschte Ergebnis:

    SELECT INITCAP(name) Name, LTRIM(REGEXP_SUBSTR(strasse,'[^,]+')) Nr
    FROM firma
    WHERE strasse LIKE '%Lang%'
    ORDER BY TO_NUMBER(REGEXP_SUBSTR(strasse, '[0-9]+')), LTRIM (strasse);

     

    NAME       NR
    Meyer      1a
    Miller     10 a
    Müller     10 c
    Mayerhöfer 10-11
    ....

    REGEXP_SUBSTR(strasse, '[0-9]+') extrahiert die erste zusammenhängende Zahl aus dem String,(z.B. 10-11 ' 10) was die Umwandlung mit TO_NUMBER und damit die richtige Sortierung ermöglicht.

     
    8. Entfernung von doppelten Leerzeichen mit REGEXP_REPLACE

    Die Ausgabe kann man noch etwas verschönern, indem man die doppelten Leerzeichen in der Hausnummern-Spalte entfernt:

    SELECT INITCAP(name) Name,
           LTRIM(REGEXP_REPLACE(REGEXP_SUBSTR(strasse,'[^,]+'),'( ){2,}',
           ' ')) Nr
    FROM firma
    WHERE strasse LIKE '%Lang%'
    ORDER BY TO_NUMBER(REGEXP_SUBSTR(strasse, '[0-9]+')), LTRIM (strasse);
    NAME    NR
    Meyer   1a
    Miller  10 a
    Müller  10 c
    ....

        '( ){2,}' findet Gruppen von mindestens 2 Leerzeichen und ersetzt sie durch eins: ' '. Die runden Klammern dienen nur der Übersichtlichkeit.

     
    9. Formatierung von Ausdrücken mit REGEXP_REPLACE

    Die Telefonnummern sollen von Leer-, Sonderzeichen u. ä. befreit werden und folgendes Format erhalten: (+49-12)345-6789-029. Die zu bereinigenden Einträge findet man mit:

    SELECT telefon FROM firma WHERE REGEXP_LIKE(telefon, '[^-0-9]');
    TELEFON
    +49/012-345678-012
    0049/012-345678-019
    0049-12-345678-029
    D012-345678-021
      012-345678-022
    #012-345678023
    ?49-12-345678-024

    [^-0-9] steht für alles außer Bindestrich und Ziffern. Mit REGEXP_REPLACE kann man so alle Zeichen außer den Ziffern entfernen, indem man den Ersatzstring einfach wegläßt.:

    UPDATE firma SET telefon = REGEXP_REPLACE(telefon, '[^0-9]');
    14 Zeilen wurden aktualisiert

    Für die Formatierung gibt es 2 Möglichkeiten:

    a)   Verwendung der Oracle-SUBSTR-Funktion (weil sich nur die Durchwahl ändert):

    UPDATE firma SET telefon = '(+49-12)345-678-' || SUBSTR(telefon,-3);

    SUBSTR(telefon,-3) startet beim dritten Zeichen von hinten und gibt den Rest aus.

    b)   Verwendung von REGEXP_REPLACE, komplizierter, aber allgemeingültiger:

    UPDATE firma SET telefon = REGEXP_REPLACE(telefon,
              '(0*49)?(0?12)?(\d{3})(\d{3})(\d{3})', '(+49-12)\3-\4-\5');
    Gruppe:      1       2      3      4      5

    Hier werden wieder Backreferences verwendet. Die Gruppen von Zeichen, die man umformatieren will, werden durch runde Klammern gekennzeichnet.

    Die Gruppen 1 (beliebig viele Nullen gefolgt von 49) und 2 (eine oder keine Null gefolgt von 12) die jeweils ein oder kein Mal vorkommen, werden durch (+49-12) ersetzt, die folgenden 3 Gruppen 3 - 5 mit Bindestrich aneinandergehängt.

    Hinweis; Oraclle unterstützt maximal 9 Gruppen (1-9)

    Die Säuberung und Formatierung kann man mit einem geschachtelten REGEXP_REPLACE auch in einem Zug erledigen:

    ROLLBACK;
    UPDATE firma SET telefon =
    REGEXP_REPLACE(REGEXP_REPLACE(telefon,'[^0-9]'),
                   '(0*49)?(0?12)?(\d{3})(\d{3})(\d{3})','(+49-12)\3-\4-\5');
    SELECT telefon FROM FIRMA;
    TELEFON
    (+49-12)345-678-012
    (+49-12)345-678-013
    (+49-12)345-678-014
    (+49-12)345-678-015

     
    10. Suche nach Wörtern mit verschiedener Schreibweise über REGEXP_LIKE:

    Angenommen, die Tabelle hätte 100000 Datensätze und wir wollen alle Angestellten suchen, die Maier oder so ähnlich heißen, unabhängig von Groß- und Kleinschreibung und Schreibweise:

    SELECT name FROM firma WHERE REGEXP_LIKE (name, '^m[ae][yi].?r$','i');

        Hier wird nach einem m am Anfang, gefolgt von a oder e, gefolgt von y oder i gefolgt von 0-1 weiteren Buchstaben und einem r am Schluss gesucht.
        Der Parameter 'i' steht für case insensitive.

    Zusammenfassung der wichtigsten Metazeichen und ihrer Bedeutung:

        Der Punkt steht für ein beliebiges Zeichen (ausser einen Zeilenumbruch).
        eckige Klammern stehen für eine Auswahl von Zeichen,   [g-l] z. B. für g, h, i, j, k oder l
        [0-9A-G] für eine beliebige Ziffer oder einen Großbuchstaben von A bis G .
        Statt die Zeichenauswahl explizit anzugeben, kann man mit den sog. Charakterklassen arbeiten.
        Die wichtigsten sind:
        [:alnum:]   alle alphanumerischen Zeichen, [[:alnum:]] entspricht  also [0-9A-Za-z]
        \w                  alle alphanumerischen Zeichen und der Unterstrich "_"
        [:alpha:]   alle Buchstaben, [[:alpha:]] entspricht [A-Za-z]
        [:digit:] bzw. \d    alle Ziffern, [[:digit:]] entspricht [0-9]  
        [:space:] bzw.  \s    alle Zeichen, die man nicht sieht, Leerzeichen, Tabzeichen, Enter...
        \W, \D und S\   sind die Umkehrungen von  \w, \d und \s
        Die Abkürzungen \d, \w etc. stehen seit Version 10.2 zur Verfügung.
        Das pipe-Symbol | bedeutet "oder", z.B.  [ae|ä], [Otto|Emma], etc.
        Das Dollarzeichen $ verankert das vorausgehende Zeichen am Ende einer Zeile bzw. des Suchstrings,
        'r$' passt z.B. auf 'Müller' oder 'Herr', aber nicht zu 'rau' oder 'Karren'
        Das Caret-Zeichen ^ hat je nachdem, wo es steht, unterschiedliche Bedeutungen
            ein Caret-Zeichen außerhalb eckiger Klammern  verankert das nachfolgende Zeichen am Beginn einer Zeile bzw. des Suchstrings
            ('^z.*' passt auf 'ziel', 'zeichen' etc, aber nicht auf 'platz', 'setzen')
            ein Caret-Zeichen innerhalb eckiger Klammern schließt die nachfolgenden Zeichen aus.
            dementsprechend bedeutet:
            ^[:alnum:]] alle nicht-alphanumerischen Zeichen (Sonderzeichen, Leerzeichen...)
            [^0-9]                 alles ausser Ziffern
        Der Stern *   steht für keine oder beliebig viele Wiederholungen des vorausgehenden Elements (Zeichen oder Gruppe), '.*' passt also auf jeden String
        Das Fragezeichen ?  steht für keine oder eine Wiederholung
        Das Pluszeichen +   steht für eine oder beliebig viele Wiederholungen
        {n}   steht für n Wiederholungen
        {min,max}  steht für min bis max Wiederholungen des Elements ,
        {3,6} z. B für 3 bis 6,
        {3,}  für mindestens 3

    Außerhalb von eckigen Klammern bzw. in Kombinationen mit Charakterklassen muss man den Metazeichen einen Backslash voranstellen, wenn man explizit nach ihnen sucht,

    z.B. \+, \?, \* etc.

     
    Match-Parameter für die Oracle-REGEXP-Funktionen:

        i (case insensitive)
        Groß- und Kleinschreibung wird nicht berücksichtigt
        c (case sensitive)
        Groß- und Kleinschreibung wird berücksichtigt
        n (newline)
        Der Punkt kann in diesem Fall auch für einen Zeilenumbruch stehen.
        m (multiline)
        Die Zeichenkette wird als mehrzeilige Eingabe betrachtet.^und $ können dann auf jede Zeile angewandt werden und nicht nur für Anfang und Ende des Strings.

    Mit dem folgenden SQL-Code können Sie sich die Übungstabelle selbst erstellen:

    DROP TABLE firma;
    CREATE TABLE firma(
    name       VARCHAR2(20),
    telefon    VARCHAR2(20),
    email      VARCHAR2(30),
    plz        VARCHAR2(10),
    strasse    VARCHAR2(50));
    INSERT INTO firma VALUES ('Maier','+49/012-345678-012','johann.maier@chaos.de','00000','24 a,Langstr.');
    INSERT INTO firma VALUES ('meyer','012-345678-013','peter.meyer@chaos.de','a0674','1a,Langstr. ');
    INSERT INTO firma VALUES ('Mayerhöfer','012-345678-014','frank.mayerhoefer@chaos.de','00345','10-11,Langstr.');
    INSERT INTO firma VALUES ('MEYR','012-345678-015','tomas.meyr@chaos.de','19387','16b,Waldgasse');
    INSERT INTO firma VALUES ('Mayer','012-345678016','richard.mayer@chaos.de','11028',' 22,Richterstr.');
    INSERT INTO firma VALUES ('Müller','012-345678-017','martin.mueller@chaos.de','-','235b,Langstr.');
    INSERT INTO firma VALUES ('Mair','012-345678-018','wilhelmine.mair@chaos.de','12345',' 35a,Langstr.');
    INSERT INTO firma VALUES ('meir','0049/012-345678-019','sabine.meir@chaos.de',' 12345','101,Langstr.');
    INSERT INTO firma VALUES ('myers','0049-12-345678-029 ','jeff.myers@chaos.de','03927','10,Schlossallee');
    INSERT INTO firma VALUES ('müller','D012-345678-021','kurt.müller2@chaos.de','12098 ','10 c,Langstr.');
    INSERT INTO firma VALUES ('Hintermeier',' 012-345678-022','hansi.hintermeier@chaos.de',' 11937','180,Holzweg ');
    INSERT INTO firma VALUES ('MEIER','#012-345678023','eva.meier@chaos.de','12346','301,Langstr.');
    INSERT INTO firma VALUES ('mAYR','?49-12-345678-024','klaus.mayr@chaos.de',' 19207','5d,Gärtnerplatz ');


    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Zeilenbasierte Zugriffskontrolle

    Bereich:DBA:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 12.12.2018

    Keywords:DBA , PL/SQL , Security

    Hinweis: In der Standard Edition können Policies nicht erstellt werden!

    Oracle bietet ab Version 8i die Möglichkeit, aufgrund von bestimmten Sessionkriterien den Zugriff auf bestimmte Daten einzuschränken (Application Context). Dadurch können viele statische Views eingespart werden. Für die Einschränkung des Datenzugriffs benötigt man das DBMS_RLS Package.

    Dieses Package implementiert das Feature "Virtual Private Database", das auch unter den Begriffen "Fine Grained Access Control" und "Row Level Security" bekannt ist. Es ist in der Enterprise und Personal Edition verfügbar, nicht aber in der Standard Edition.

    Das DBMS_RLS Package ermöglicht die Verknüpfung von Tabellen oder Views mit Funktionen, die in Abhängigkeit von bestimmten Faktoren (Attribute und deren Attributwerte) nur die Sicht oder die Möglichkeit von Datenänderungen auf Teile des Objekts freigibt. Die Einschränkung wird erzielt, indem die Funktion eine dynamische WHERE Klausel (Prädikat) an den abgesetzten Select- oder DML-Befehl anhängt.

    Die Verknüpfung zwischen der Funktion und der entsprechenden Tabelle erfolgt über eine Policy. Die Einschränkungen auf eine Tabelle können auch mit mehreren Policies gesteuert werden, wobei die einzelnen einschränkenden Bedingungen in der WHERE Klausel mit AND verbunden werden.

    In der Datenbank gibt es einen Standardkontext 'USERENV' aus dem vordefinierte Kontextattribute der derzeitigen Session wie Benutzer, Session_id, Hostname, Servername usw. ermittelt werden können. Diese Attributwerte können zwar ausgelesen, jedoch nicht geändert werden. Die jeweiligen Attributwerte können mit der folgenden Funktion ausgelesen werden:

     
    FUNCTION SYS_CONTEXT ('USERENV', attribute) RETURN VARCHAR2;

     

    Werden darüber hinaus eigene Attribute benötigt, muss ein eigener Kontext erstellt werden.

    Im folgenden Beispiel soll das einschränkende Prädikat anhand des Attributs ‚JOB' im eigenen Kontext ‚emp_restriction' festgelegt werden.

    Anhand des Jobs soll in der Tabelle emp der Zugriff wie folgt gesteuert werden:

    PRESIDENT: Sieht alle Mitarbeiter und kann diese ändern

    MANAGER: Sieht nur alle Mitarbeiter seiner Abteilung und kann nur die Mitarbeiter seiner Abteilung ändern

    ANALYST: Sieht nur seine eigenen Daten und kann nur diese ändern

    • Besitzt der User keinen der oben genannten Jobs, so erhält er keine Daten zum lesen, bzw. ändern

    Der Kontext soll im Schema von Scott erstellt werden.

    Hierzu müssen ihm die erforderlichen Rollen und Rechte erteilt werden:

    Anmeldung als SYS AS SYSDBA

    GRANT CREATE ANY CONTEXT TO SCOTT;
    GRANT EXECUTE on DBMS_RLS TO SCOTT;
    GRANT CREATE PUBLIC SYNONYM TO SCOTT;
     

    Nachdem Scott nun die benötigten Rechte besitzt, sollen alle Mitarbeiter Zugriff auf die Tabelle emp mit Hilfe eines Synonyms erhalten:

    CREATE PUBLIC SYNONYM emp FOR scott.emp;
    GRANT ALL ON scott.emp TO PUBLIC;
     

    Zunächst wird das Kontextobjekt erstellt:

    CREATE CONTEXT emp_restriction USING scott.s_pkg;
     

    Der Kontext verweist auf das Package, welches die Funktionen der Sicherheitsrichtlinien beinhaltet. Das Package braucht noch nicht erstellt zu sein.

    Danach wird das Package mit den benötigten Funktionen erstellt.

    CREATE OR REPLACE PACKAGE scott.s_pkg
    AS
       c_context CONSTANT VARCHAR2(30):= 'emp_restriction';
       c_job_attr CONSTANT VARCHAR2 (3):= 'JOB';
       v_deptno NUMBER (3);
       v_job VARCHAR (20);

       PROCEDURE set_context;

       PROCEDURE show_context;

       FUNCTION job_predicate (p_schema_in IN VARCHAR2, p_name_in IN                            VARCHAR2)
          RETURN VARCHAR2;

    END S_Pkg;
    /

    CREATE OR REPLACE PACKAGE BODY scott.s_pkg
    AS
       PROCEDURE show_context
       IS
       BEGIN
          DBMS_OUTPUT.PUT_LINE('typ = '||SYS_CONTEXT (c_context,                            c_job_attr));
       END;

       PROCEDURE set_context
       IS
       BEGIN
          DBMS_SESSION.SET_CONTEXT(c_context, c_job_attr, v_job);
       END;

       FUNCTION job_predicate (p_schema_in IN VARCHAR2, p_name_in IN                            VARCHAR2)
          RETURN VARCHAR2
       IS
          ma_list VARCHAR2(100):= SYS_CONTEXT (c_context, c_job_attr);
          retval VARCHAR2(4000);
       BEGIN
          CASE ma_list
          WHEN 'ANALYST'
             THEN
                retval := 'ENAME = SYS_CONTEXT                                (''userenv'',''SESSION_USER'')';
          WHEN 'PRESIDENT'
             THEN
                retval := '';
          WHEN 'MANAGER'
             THEN
                retval := 'DEPTNO = ' || v_deptno;
          ELSE
             retval := '1=2';
          END CASE;
          RETURN retval;
       END;
    END S_Pkg;
    /
     

    Im Package befinden sich 2 Prozeduren und eine Funktion:

    • show_context: Mit dieser Prozedur kann der Attributwert des Attributs ‚JOB' in der derzeitigen Session ermittelt werden. Nützlich beim Erstellen und Debuggen.
       
    • set_context: Mit dieser Prozedur wird der Attributwert des Attributs ‚JOB' in der derzeitigen Session gesetzt. Der Aufruf erfolgt in dem noch zu erstellenden LOGON Trigger.
       
    • job_predicate: Diese Funktion generiert anhand vom Attributwert (ma_list) des Attributs 'JOB' im eigens erstellten Kontext 'emp_restriction' das Prädikat (den String der WHERE Klausel) welche an das SELECT- bzw. DML Statement angehängt wird
       
      • ANALYST: Hier wird mit Hilfe vordefinierten Attributs ‚userenv' der derzeitige Sessionuser ermittelt, sodass folgende WHERE Klausel z.B. bei Scott generiert wird:

        WHERE ENAME = 'SCOTT'
         
      • PRESIDENT: Hier wird keine WHERE Klausel generiert, da der President alle Mitarbeiter anschauen und manipulieren darf. Alternativ kann auch 1=1 angegeben werden.
        MANAGER: Hier wird die WHERE Klausel mit der Abteilungsnummer eingeschränkt, in welcher der jeweilige Manager arbeitet. Z.B. BLAKE:

        WHERE DEPTNO = 30
         
      • Alle anderen Jobs bekommen den Zugriff gesperrt mit der WHERE Klausel:

        WHERE 1=2
     

    Die Funktion, welche die Einschränkungen festlegt, benötigt immer 2 VARCHAR2 Parameter, auch wenn diese Parameter in der eigentlichen Funktion nicht benötigt werden. Diese Funktion wird in der Policy mit der Tabelle EMP verknüpft.

    Zunächst jedoch der Zugriff auf das Package für alle User mit Hilfe des Synonyms:

    GRANT EXECUTE ON scott.s_pkg TO PUBLIC;
    CREATE PUBLIC SYNONYM s_pkg FOR scott.s_pkg;
     

    Da das Package mit DEFINER RIGHTS erstellt wurde, benötigen die Anwender keine weiteren Rechte oder Rollen um die Prozeduren des Packages S_Pkg aufzurufen.

    Jetzt kann die Policy erstellt werden, welche die einzuschränkende Tabelle mit der Einschränkfunktion verknüpft:

    BEGIN
    DBMS_RLS.ADD_POLICY(object_schema=>'scott',
          object_name=>'emp',
          policy_name=>'PLC_EMP',
          function_schema=>'SCOTT',
          policy_function=>'s_pkg.job_predicate',
          statement_types=>'SELECT,INSERT,UPDATE,DELETE',
          update_check => FALSE,
          enable => TRUE );
    END;
    /
     

    Wobei:

    object_schema: Schema in dem sich das Objekt (z.B.Tabelle/View) befindet, auf dem die Sicherheitsrichtlinie angewendet werden soll

    object_name: Objekt, auf dem die Sicherheitsrichtlinie angewendet werden soll

    policy_name: Name der Sicherheitsrichtlinie

    function_schema: Schema in dem sich die Funktion befindet, mit dem die Sicherheitsrichtlinie angewendet werden soll

    policy_function: Packagefunktion mit der Sicherheitsrichtlinie ausgeführt wird

    statement_types: Statements, bei denen die Policy Funktion greift. Möglich: SELECT, INSERT, UPDATE, DELETE oder Kombinationen davon

    update_check: TRUE/FALSE. TRUE: Überprüfung der Policy nach INSERT oder UPDATE Statement mit den neuen Werten

    • enable: Policy ist sofort aktiv, wenn sie erstellt wird

    Der sessionabhängige Attributwert des Attributs ‚JOB' wird in LOGON Trigger im SYS Schema festgelegt.

     
     CREATE OR REPLACE TRIGGER set_privacy_after_log_on
       AFTER LOGON ON DATABASE
    DECLARE
     
    BEGIN
       SELECT job, deptno
       INTO s_pkg.v_job, s_pkg.v_deptno
       FROM emp
       WHERE ename = SYS_CONTEXT ('userenv', 'SESSION_USER');

       s_pkg.set_context;

       EXCEPTION
          WHEN OTHERS THEN
             NULL;
    END set_privacy_after_log_on;
    /
     

    In dem Trigger wird ein letzter "freier" Zugriff auf die Tabelle emp ermöglicht, um die Information des Jobs und der Abteilung des Users zu ermitteln. Danach wird der Attributwert gesetzt. Ab dem jetzigen Zeitpunkt hat jeder User in Abhängigkeit von seinem Job nur noch Einblick und Manipulationsmöglichkeiten auf "seine" Daten. Dies ist völlig unabhängig von dem Medium, mit dem sich der Anwender auf die Datenbank anmeldet.

    Test mit folgendem Benutzern:

    CREATE USER king IDENTIFIED BY king;
    CREATE USER blake IDENTIFIED BY blake;
    CREATE USER allen IDENTIFIED BY allen; 

    GRANT CONNECT,RESOURCE TO king;
    GRANT CONNECT,RESOURCE TO blake;
    GRANT CONNECT,RESOURCE TO allen;
     

    Informationen über erstellte Kontexte und Policies im eigenen Schema erhält man über die Data Dictionary Views

    SELECT * FROM all_context;
    SELECT * FROM all_policies;
     

    Informationen über alle erstellte Kontexte und Policies erhält man über die Data Dictionary Views

    SELECT * FROM dba_context;
    SELECT * FROM dba_policies;
     

    Hinweise:

    Für SYS gelten die Policies nicht, er kann die Tabelle uneingeschränkt sehen

    Es können unabhängige Filter auf ein Objekt für SELECT, INSERT, UPDATE, INDEX und DELETE gesetzt werden

    Bei mehreren Filtern auf ein Objekt werden die Filterbedingungen mit AND verknüpft

    Das Recht EXEMPT ACCESS POLICY erlaubt die Policy zu umgehen

    • DIRECT PATH Export umgeht auch Policies

    Löschen, ein- und ausschalten der Policy ist mit folgenden Prozeduren möglich:

    Löschen:

    DBMS_RLS.drop_policy(object_schema, object_name, policy_name);

    Ein-, ausschalten:

    DBMS_RLS.enable_policy(object_schema, object_name, policy_name, TRUE/FALSE);
     

    Wobei:

    object_chemsa: Schema in dem sich das Objekt (z.B.Tabelle/View) befindet, auf dem die Sicherheitsrichtlinie angewendet werden soll

    object_name: Objekt, auf dem die Sicherheitsrichtlinie angewendet werden soll

    policy_name: Name der Sicherheitsrichtlinie

    • TRUE/FALSE: Einschalten/Ausschalten der Policy


    Weitere Interessente Artikel zum Thema:



    Empfohlene Schulungen zum Thema:


    Compound Trigger zur Vermeidung von Mutating Tables in 11g

    Bereich:PL/SQL, Version: ab RDBMS 11.x, Letzte Überarbeitung: 12.12.2018

    Keywords:PL/SQL , 11g

    Es kommt immer wieder vor, dass bei einem DML-Trigger für Überprüfungen oder Protokollierungen Daten aus der zu ändernden Tabelle benötigt werden. Ein normaler Row Trigger scheidet dann in der Regel aus, weil man sonst in das Problem mit den Mutating Tables läuft.

    Ein kleines Beispiel dazu: Stellen Sie sich vor, eine Geschäftsregel besagt, dass niemand innerhalb einer Berufsgruppe mehr als 50% über dem Durchschnittsgehalt verdienen darf. Der einfachste Ansatz dazu sähe folgendermaßen aus:

    CREATE OR REPLACE TRIGGER check_sal
    AFTER UPDATE of sal ON emp
    FOR EACH ROW
    DECLARE
       v_avgsal NUMBER;
    BEGIN
       SELECT avg(sal)
         INTO v_avgsal
         FROM EMP
        WHERE job = :NEW.job;

        IF :NEW.sal > 1.5 * v_avgsal THEN
           RAISE_APPLICATION_ERROR(-20000, 'Zu weit über Durchschnitt');
        END IF;
    END;
    /
     

    Das geht aber leider schief, sobald Sie ein Update ausführen:

    UPDATE emp
       SET sal = 1500
     WHERE empno = 7934
    Error at line 3
    ORA-04091: Tabelle SCOTT.EMP wird gerade geändert, Trigger/Funktion sieht dies möglicherweise nicht
    ORA-06512: in "SCOTT.CHECK_SAL", Zeile 4
    ORA-04088: Fehler bei der Ausführung von Trigger 'SCOTT.CHECK_SAL'
     

    Das Problem mit den Mutating Tables hat zugeschlagen! Ein Workaround dazu könnte so aussehen, dass Sie über einen BEFORE STATEMENT Trigger zuerst den Durchschnitt aller Berufsgruppen einlesen und beispielsweise in einem Package oder in einer Tabelle zwischenspeichern. Der Row Trigger kann dann auf diese Werte zugreifen, ohne die zu ändernde Tabelle lesen zu müssen. Das ist machbar, aber aufwendig.

     

    Ab Version 11g kann die Lösung viel einfacher aussehen: Compound Trigger haben Abschnitte für bisher unterschiedliche Triggertypen und bieten die Möglichkeit, Werte für die Dauer der Durchführung des DML-Befehls zu speichern. Obiges Beispiel könnte mit einem Compound Trigger so aussehen:

    CREATE OR REPLACE TRIGGER check_sal
       FOR UPDATE OF sal ON SCOTT.EMP
    COMPOUND TRIGGER
       TYPE t_number IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
       TYPE t_varchar IS TABLE OF VARCHAR2(30) INDEX BY PLS_INTEGER;
       TYPE t_avgsal IS TABLE OF NUMBER INDEX BY VARCHAR2(30);

       v_avgsal t_number;
       v_job    t_varchar;
       v_avgJob t_avgsal;
       v_index  VARCHAR2(30);

       BEFORE STATEMENT IS
       BEGIN
          SELECT AVG(sal), job
            BULK COLLECT INTO v_avgsal, v_job
            FROM EMP e
           GROUP BY e.job;

          FOR i IN 1..v_avgsal.COUNT
          LOOP
             v_avgJob(v_job(i)) := v_avgsal(i);
          END LOOP;
       END BEFORE STATEMENT;

       AFTER EACH ROW IS
       BEGIN
          IF v_avgJob.EXISTS(:NEW.job)
          THEN
             IF :NEW.sal > 1.5 * v_avgJob(:NEW.job)
             THEN^
                RAISE_APPLICATION_ERROR(-20000,
                                  'Verdienst ist zu weit über Durchschnitt');
             END IF;
          END IF;
       END AFTER EACH ROW;
    END;
    /
     

    Erlaubter Update:

    UPDATE emp
       SET sal = 1500
     WHERE empno = 7934
    1 row updated.

     

    Verbotener Update:

    UPDATE emp
       SET sal = 1800
     WHERE empno = 7934
    ORA-20000: Zu weit über Durchschnitt
    ORA-06512: in "SCOTT.CHECK_SAL", Zeile 29
    ORA-04088: Fehler bei der Ausführung von Trigger 'SCOTT.CHECK_SAL'


    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Access Control Lists mit dbms_network_acl_admin

    Bereich:DBA:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 25.09.2019

    Keywords:PL/SQL , DBA, Firewall, dbms_network_acl_admin

    Viele Applikationen verwenden die Möglichkeiten zur Mailversendung aus Oracle heraus, sei es mit UTL_SMTP, sei es mit UTL_MAIL. Bis Version 10g reichte es, wenn der Eigentümer der Sende-Prozedur das EXECUTE-Recht auf das jeweilige Package hatte. Soll auf 11g migriert werden, so erfordert dies einen zusätzlichen administrativen Aufwand.

    Mit Version 11g wurde nämlich das Sicherheitskonzept dahingehend erweitert, dass Netzwerkzugriffe über Access Control Lists (ACL's) freigegeben werden müssen. Das betrifft neben UTL_SMTP und UTL_MAIL auch die Packages UTL_TCP, UTL_HTTP und UTL_INADDR. Wenn ein entsprechender Aufruf erfolgt, ohne dass der Host bzw. Port explizit freigegeben wurde, so erfolgt nun die Fehlermeldung:

    ORA-24247: Netzwerkzugriff von Zugriffskontrollliste (ACL) abgelehnt


    Darüber hinaus wurden die angegebenen Packages auf Invoker Rights umgestellt, und auch entsprechende Prozeduren, die mit diesen Packages arbeiten, sollten mit AUTHID CURRENT_USER erstellt werden, um ein Unterlaufen der ACL's zu verhindern.

    Am einfachsten kann der DBA ACL's über das Package DBMS_NETWORK_ACL_ADMIN verwalten. Zunächst muss eine ACL erstellt werden, wie in den folgenden Beispielen gezeigt:

    Für SMTP (Email Versand über einen Email Server):

    BEGIN
     DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE (host => '172.30.30.20',
     ace => xs$ace_type(privilege_list => xs$name_list('smtp'),
     principal_name => 'SCOTT',
     principal_type => xs_acl.ptype_db) );
    END;
    /

     

    Oracle REST Verbindungen (ORDS) z.B. für APEX:

    BEGIN
     DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE (host => '172.30.30.20',
     ace => xs$ace_type(privilege_list => xs$name_list('connect'),
     principal_name => 'APEX_190200',
     principal_type => xs_acl.ptype_db) );
    END;
    /

     

    Für LDAP Verbindungen mit einem Microsoft AD (hier via APEX):

    BEGIN
     DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE (host => '172.30.30.20',
     lower_port => 389,
     ace => xs$ace_type(privilege_list => xs$name_list('connect'),
     principal_name => 'APEX_190200',
     principal_type => xs_acl.ptype_db) );
    END;
    /

     

    Für Debugging im SQL*Developer:

    BEGIN
     DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE (host => '172.30.30.20',
     lower_port => null,
     upper_port => null,
     ace => xs$ace_type(privilege_list => xs$name_list('jdwp'),
     principal_name => 'SCOTT',
     principal_type => xs_acl.ptype_db) );
    END;
    /

    Wir hatten häufig Probleme damit die ACL's wieder zu löschen. Es scheint so zu sein, dass wenn auf einem Rechner eine Freigabe auf eine spezielle Portrange und eine Freigabe ohne Portbeschränkung (lower_port und upper_port = NULL) existiert, diese ACL's nicht mehr zu löschen ist.

    Aber wir haben eine Alternative entwickelt:
    Zuerst schauen wir mal nach, welche ACL´s vergeben sind:

    select na.host,na.lower_port,na.upper_port,nap.principal,privilege
            from dba_network_acls na, DBA_NETWORK_ACL_PRIVILEGES nap
            where na.acl=nap.acl
            and host='172.30.30.20';


    Der folgende Block nimmt eine IP Adresse entgegen (Zeile 2: 172.30.30.20) und erstellt ein Skript zum Löschen aller ACL's auf diese IP Adresse.
    Natürlich könnten Sie den Filter abwandeln z.B. alle ACL´s eines Benutzers löschen.
    Sie können auch die ACL's direkt löschen, wenn sie v_debug auf FALSE setzen. Sie sollten sich vorher jedoch erst mal ansehen, was gelöscht werden würde !
    Der Packageaufruf DBMS_NETWORK_ACL_ADMIN.UNASSIGN_ACL ist eigentlich desupported, es gab jedoch für uns keine andere Alternative,
    die wirklich beim Löschen funktioniert hat. Wir können nur keine Gewähr für die Funktionalität übernehmen.

    DECLARE
    v_server VARCHAR2(256):=lower('172.30.30.20');
    v_debug BOOLEAN:=TRUE;
    BEGIN
    FOR c IN (select na.host,na.lower_port,na.upper_port,nap.principal,privilege,na.acl
            from dba_network_acls na, DBA_NETWORK_ACL_PRIVILEGES nap
            where host=v_server ) LOOP

    IF NOT v_debug THEN
        SYS.DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE (
        host        =>c.host,
        lower_port => c.lower_port,
        upper_port => c.upper_port,
        ace          =>xs$ace_type(
            privilege_list => xs$name_list(c.privilege),     
            principal_name => c.principal,
            principal_type => xs_acl.ptype_db),
            remove_empty_acl=>TRUE);
      ELSE      
            dbms_output.put_line(' BEGIN SYS.DBMS_NETWORK_ACL_ADMIN.REMOVE_HOST_ACE (');
            dbms_output.put_line(' host        =>'''||c.host||'''');
            IF c.lower_port IS NOT NULL THEN
               dbms_output.put_line(', lower_port => '||c.lower_port);
            END IF;
             IF c.lower_port IS NOT NULL THEN
                dbms_output.put_line(', upper_port =>'|| c.upper_port);
             END IF;
            dbms_output.put_line(',ace          =>xs$ace_type(
            privilege_list => xs$name_list('''||c.privilege||'''),     
            principal_name =>'''||c.principal||''',
            principal_type => xs_acl.ptype_db),
            remove_empty_acl=>TRUE);
            END;
            /');
            dbms_output.put_line('BEGIN DBMS_NETWORK_ACL_ADMIN.UNASSIGN_ACL (');
           dbms_output.put_line('acl =>'''||c.acl||''',   host=>'''||c.host||'''');
           IF c.lower_port IS NOT NULL THEN
               dbms_output.put_line(', lower_port => '||c.lower_port);
            END IF;
             IF c.lower_port IS NOT NULL THEN
                dbms_output.put_line(', upper_port =>'|| c.upper_port);
             END IF;
            dbms_output.put_line(');');
            dbms_output.put_line('END;');
           dbms_output.put_line('/');
        END IF;
    END LOOP;
    END;
    /


    Für die Version 11g musste noch eine andere Syntax für das Erstellen der ACL's verwendet werden:

    BEGIN
       DBMS_NETWORK_ACL_ADMIN.CREATE_ACL (
          acl          => 'utl_smtp.xml',
          description  => 'Mailversendung',
          principal    => 'SCOTT',
          is_grant     => TRUE,
          privilege    => 'connect');
    END;
    /

    Die ACL wird dann einem Host zugewiesen, wobei auf einen Port bzw. auf einen Bereich eingeschränkt werden kann.

    BEGIN
       DBMS_NETWORK_ACL_ADMIN.ASSIGN_ACL (
          acl          => 'utl_smtp.xml',
          host         => 'mailserver',
          lower_port   => 25);
    END;
    /


    In diesem Beispiel wäre nur der Port 25 auf dem Host mailserver freigegeben. Wird zusätzlich ein upper_port mit angegeben, so ist der Bereich zwischen den beiden Werten freigegeben.
    Einem Host bzw. einem Port-Range kann immer nur eine ACL zugewiesen sein. Bei einer erneuten Zuweisung wird die alte Zuweisung entfernt. Statt Namen können auch IP-Adressen angegeben werden, und es kann mit dem Stern (*) als Wildcard gearbeitet werden, um ganze Bereiche einer Domäne bzw. ganze Subnetze zu verwalten.

    SCOTT kann nun im obigen Beispiel über den Port 25 Mails versenden. Wenn er eine entsprechende Prozedur mit Invoker Rights schreibt und das EXECUTE-Recht an einen User DUMMY vergibt, so braucht dieser zusätzlich die entsprechende Freigabe:

    BEGIN
       DBMS_NETWORK_ACL_ADMIN.ADD_PRIVILEGE (
          acl          => 'utl_smtp.xml',
          principal    => 'DUMMY',
          is_grant     => TRUE,
          privilege    => 'connect');
    END;
    /


    Weitere Interessente Artikel zum Thema:



    Empfohlene Schulungen zum Thema:


    Berechnung des Segmentfüllpegels

    Bereich:DBA:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 14.12.2018

    Keywords:DBA , PL/SQL

    In diesem Tipp des Monats schauen wir uns mal die Speicherbelegung der Tabellen und Indizes an.
    Es gibt zwar seit Version 10g einen Advisor, der einem die Speicherbelegung der Segmente anzeigt; wir wollen hier aber eine Ausgabe in Tabellenform für mehrere Objekte erzeugen.
    Wie heißt es so schön: Es gibt kaum eine Information, die man nicht aus einer Oracle-DB herausholen kann, man muss nur die richtige (Öl)-Quelle anzapfen.
    Dieser Trick beruht auf dem Package DBMS_SPACE, das die Zahl der Blöcke anzeigen kann, die jeweils bis

                           <= 25%,
                           <= 50%,
                           <= 75%
                    oder <=100% gefüllt sind.

    Diese Zahlen werden für jedes Segment zusammengezählt und vom Package zurückgegeben.
    Die Aufgabe meiner Funktion ist nur, das Ergebnis in Tabellenform darzustellen.

    CREATE OR REPLACE TYPE space_seg_type
    AS OBJECT (
    owner       VARCHAR2(30),
    seg_name    VARCHAR2(30),
    o_type      VARCHAR2(30),
    "F<=25%"    NUMBER,
    "F<=50%"    NUMBER,
    "F<=75%"    NUMBER,
    "F<=100%"   NUMBER,
    FULL_BLOCKS NUMBER,
    FREE_BYTES  NUMBER,
    TOTAL_BYTES NUMBER,
    PER_FREE    NUMBER);
    /

     

    CREATE OR REPLACE TYPE space_seg_tab_type as table of space_seg_type;
    /


    Der Benutzer des Package benötigt zwei Rechte DIREKT!

    GRANT SELECT ON sys.dba_segments TO system;
    GRANT ANALYZE ANY TO system;

     

    CREATE OR REPLACE
    FUNCTION get_space_info
    (p_owner                IN VARCHAR2 DEFAULT NULL,
     p_name                 IN VARCHAR2 DEFAULT NULL,
     p_tablespace           IN VARCHAR2 DEFAULT NULL)
    RETURN space_seg_tab_type PIPELINED IS
       v space_seg_type;
       v_unformatted_blocks number;
       v_unformatted_bytes number;
       v_fs1_blocks    number;
       v_fs1_bytes     number;
       v_fs2_blocks    number;
       v_fs2_bytes     number;
       v_fs3_blocks    number;
       v_fs3_bytes     number;
       v_fs4_blocks    number;
       v_fs4_bytes     number;
       v_full_blocks   number;
       v_full_bytes    number;
       v_seg_name VARCHAR2(30);
    BEGIN
       FOR r IN (SELECT owner,segment_name,segment_type,bytes FROM
                 dba_segments
                 WHERE owner=nvl(p_owner,owner)
                 AND segment_name=nvl(p_name,segment_name)
                 AND tablespace_name=nvl(p_tablespace,tablespace_name)
            AND tablespace_name NOT IN ('SYSTEM')
            AND segment_type NOT IN ('LOBINDEX', 'LOBSEGMENT')) LOOP
          BEGIN
            v_seg_name:=r.segment_name;
                -- Neuen Speicherplatz für ein Element im Array reservieren
                -- Package zur Anzeige des Blockfüll-Pegels starten
            dbms_space.space_usage (r.owner, r.segment_name, r.segment_type,
                v_unformatted_blocks, v_unformatted_bytes, v_fs1_blocks,
                v_fs1_bytes, v_fs2_blocks, v_fs2_bytes, v_fs3_blocks,
                v_fs3_bytes, v_fs4_blocks, v_fs4_bytes, v_full_blocks,
                v_full_bytes);
                -- Ausgabezeile vorbereiten
            v:=space_seg_type(r.owner, r.segment_name, r.segment_type,
                v_fs1_blocks, v_fs2_blocks, v_fs3_blocks, v_fs4_blocks,
                v_full_blocks,
                v_fs1_bytes+v_fs2_bytes+v_fs3_bytes+v_fs4_bytes, r.bytes,
                round((v_fs1_bytes+v_fs2_bytes+v_fs3_bytes+v_fs4_bytes)/r.bytes*100,2));
                --Zeile zurückgeben
            PIPE ROW (v) ;
                -- Im Fehlerfall nur Elementname zurückgeben
          EXCEPTION
            WHEN OTHERS THEN
                v:=space_seg_type(substr(sqlerrm,1,29), v_seg_name, null,
                   null, null, null, null, null, null, null, null);
                PIPE ROW (v) ;
          END;
       END LOOP;
       RETURN; -- in 9i erforderlich!
    END;
    /
    show errors


    In 9i ist die RETURN-Klausel am Ende der Funktion zwingend erforderlich, in 10g kann sie auch weggelassen werden.

    Beispiel für alle Objekte aus einem Schema:

    select * from TABLE(get_space_info('SCOTT'));


    Beispiel für ein Objekt aus einem Schema:

    select * from TABLE(get_space_info('SCOTT','EMP'));


    Beispiel für alle Objekte aus einem Tablespace:

    select * from TABLE(get_space_info(null, null, 'USERS'));


    Wenn Sie möchten, legen Sie doch eine View über den Select, dann sieht es richtig professionell aus:

    CREATE OR REPLACE VIEW segment_usage AS
    select * from TABLE(get_space_info('SCOTT'));


    Die Ausgabe könnte dann so aussehen:

    select* from TABLE(get_space_info('SCOTT'));
    OWNER   SEG_NAME O_TYPE F<=
    25%     F<=
    50%     F<=
    75%     F<=
    100%    FULL_
    BLOCKS  FREE_
    BYTES   TOTAL_
    BYTES   PER_
    FREE
    -----  --------------  -----  -  -  -  ---  ---  -------  -------  -----
    SCOTT  TA_AKTION       TABLE  2  1  1  30   210  278528   2097152  13,28
    SCOTT  TA_AKTION_DEL   TABLE  2  3  2  273  90   2293760  3145728  72,92
    SCOTT  TA_FEHLER       TABLE  0  0  0  5    0    40960    65536    62,5
    SCOTT  TA_FIRMA        TABLE  0  1  0  19   160  163840   2097152  7,81
    SCOTT  PK_FIRMA        INDEX  0  4  0  0    9    32768    131072   25
    SCOTT  TA_EM           TABLE  2  6  9  344  323  2957312  6291456  47,01
    SCOTT  PK_KONTAKT      INDEX  0  1  0  0    19   8192     196608   4,17
    SCOTT  TA_KONTAKT_DEL  TABLE  0  5  1  412  62   3424256  4194304  81,64
    SCOTT  TA_PERSON       TABLE  0  1  1  18   130  163840   2097152  7,81
    SCOTT  PK_PERSON       INDEX  0  5  0  0    15   40960    196608   20,83
    SCOTT  TA_PERSON_DEL   TABLE  3  7  6  663  415  5562368  9437184  58,94
    SCOTT  TA_TOUCH        TABLE  0  1  1  5    173  57344    2097152  2,73


    In SQL*Plus kann man die Spalten-Längen noch formatieren.
    Beispiel:

    COL "F<25%" FORMAT 9999

    In der Ausgabe sehen Sie in der letzten Spalte den ungenutzten Platz des Segments in Prozent. Auf Basis dieser Zahl kann man entscheiden, ob eine Reorganisation des Segments nötig ist.

    Zu Risikien und Nebenwirkungen:

    Die Information über den Füllpegel kann nicht von Objekten aus dem SYSTEM-Tablespace oder einem Tablespace ohne SEGMENT SPACE MANAGEMENT AUTO Option erzeugt werden.

    Bei einem großen Schema kann die Ausgabe etwas dauern und einige Ressourcen verbrauchen.

    • Der Eigentümer der Funktion benötigt ein CREATE TYPE Recht und ein Leserecht auf die View DBA_SEGMENTS

    Wenn Sie durch dieses Beispiel Lust auf mehr bekommen haben, besuchen Sie doch einen unserer DB Monitoring oder DB Tuning Kurse. Da machen wir noch ganz andere Sachen :-)

    Wer sich für die Pipelined Row interessiert, ist in unserem PL/SQL gut aufgehoben.

    Viel Spaß beim Segment-Beobachten...



    Weitere Interessente Artikel zum Thema:



    Empfohlene Schulungen zum Thema:


    DBMS_FILE_TRANSFER zum Kopieren von Binär-Dateien

    Bereich:DBA:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 23.12.2020

    Keywords:DBA , PL/SQL , SQL

    Sie wollten schon häufiger Datenbankdateien von einem Server A auf den Server B kopieren und waren aber gerade nicht in der Lage sich lokal mit einem der Server zu verbinden und anschließend den anderen zu mounten? Oder es war Ihnen schlichtweg zu umständlich? Dann haben wir hier vielleicht eine ganz praktische Lösung für Sie.

    Mit Version 10g stellt Oracle ein neues Package zur Verfügung, mit dessen Hilfe sich sehr einfach und komfortabel Binär-Dateien zwischen Datenbank-Servern kopieren lassen. DBMS_FILE_TRANSFER als API unterstützt die folgenden drei Prozeduren:

    • COPY_FILE
    • GET_FILE
    • PUT_FILE


    Nützliches rund um DBMS_FILE_TRANSFER

    Für einen erfolgreichen Kopiervorgang mittels DBMS_FILE_TRANSFER müssen Sie sich folgende Punkte bewusst machen:

    Sowohl die Quell-Datenbank, als auch die Ziel-Datenbank müssen sich in der OPEN-Phase befinden.Der Oracle Benutzer benötigt das Leserecht auf das Quellverzeichnis und das Schreibrecht auf das Zielverzeichnis.Die Größe der zu kopierenden Dateien muss einem Vielfachen von 512 Bytes entsprechen und darf höchstens zwei Terabytes betragen.

    Kopiervorgänge großer Dateien können in der View V$SESSION_LONGOPS überwacht werden.

    Die zu verwendenden Directories für Quell- und Zielverzeichnis müssen bereits existieren, während die Datei im Zielverzeichnis noch nicht bestehen darf.


    COPY_FILE

    Mit der COPY_FILE-Prozedur kopieren Sie eine Binär-Datei innerhalb desselben Servers. Dabei kann sich sowohl das Quellverzeichnis wie auch das Zielverzeichnis entweder im lokalen Dateisystem oder auf einer ASM Disk Group befinden. Soll eine Datei innerhalb des lokalen Dateisystems kopiert werden, kann es sich um eine beliebige Binär-Datei handeln. Stellt eines der Verzeichnisse eine ASM Disk Group dar, können nur Datenbank relevante Dateien kopiert werden, wie z.B. Daten-, Control- oder Logdateien.

    Beispiel:

    Die Datendatei USERS01.DBF soll kopiert werden. Dazu ist es empfehlenswert, den Tablespace OFFLINE oder READ ONLY zu setzen, damit während des Kopiervorgangs keinerlei Änderungen an der Datei vorgenommen werden können.

    CREATE OR REPLACE DIRECTORY db_source_dir AS
           'c:\oracle\product\oradata\o20c';

    CREATE OR REPLACE DIRECTORY db_target_dir AS
           'e:\oracle\oradata\o20c';

    ALTER TABLESPACE users READ ONLY;
    BEGIN

           DBMS_FILE_TRANSFER.COPY_FILE (
             source_directory_object      => 'DB_SOURCE_DIR',
             source_file_name             => 'USERS01.DBF',
             destination_directory_object => 'DB_TARGET_DIR',
             destination_file_name        => 'USERS01.DBF');
         END;
         /

    ALTER TABLESPACE users READ WRITE;


    GET_FILE

    Die GET_FILE-Prozedur ermöglicht es Ihnen, eine Binär-Datei von einem remote Server ins lokale Dateisystem oder auf eine ASM Disk Group zu kopieren. Dazu muss ein Database Link zur remote (Quell-)Datenbank vorhanden sein bzw. erzeugt werden (an den Eintrag in der TNSNAMES.ORA denken, falls Sie nicht mit Easy Connect arbeiten).

    Beispiel:

    Die Exportdatei EXPDAT.DMP soll von einem remote Rechner ins lokale Dateisystem kopiert werden. In der remote Datenbank wird zunächst das Directory für das Quellverzeichnis erzeugt.

    CONNECT system@<remote_db>
    CREATE OR REPLACE DIRECTORY db_source_dir AS
           'c:\oracle\admin\o10g\dpdump';

    CONNECT system@<lokal_db>
    CREATE OR REPLACE DIRECTORY db_target_dir AS 'c:\dpdump';
    CREATE DATABASE LINK s01
           CONNECT TO system IDENTIFIED BY <pwd> USING 's01';

    BEGIN
           DBMS_FILE_TRANSFER.GET_FILE (
             source_directory_object      => 'DB_SOURCE_DIR',
             source_file_name             => 'EXPDAT.DMP',
             source_database              => 'S01',
             destination_directory_object => 'DB_TARGET_DIR',
             destination_file_name        => 'EXPDAT.DMP');
         END;
         /



    PUT_FILE

    Als Gegenstück zu GET_FILE können Sie mit der PUT_FILE-Prozedur eine Binär-Datei vom lokalen Server (aus dem Dateisystem oder aus ASM) zu einem remote Server ins dortige Dateisystem kopieren. Dazu muss wieder ein Database Link zur remote (Ziel-) Datenbank vorhanden sein bzw. erzeugt werden (an den Eintrag in der TNSNAMES.ORA denken, falls Sie nicht mit Easy Connect arbeiten).

    Beispiel:

    Die über ASM verwaltete SPFILE-Datei soll vom lokalen Rechner ins remote Dateisystem kopiert werden. In der remote Datenbank wird – wie gehabt - das Directory für das Zielverzeichnis erzeugt.

    CONNECT system@<remote_db>
    CREATE OR REPLACE DIRECTORY db_target_dir AS
         'c:\oracle\product\21.0.0\db_1\database';

    CONNECT system@<lokal_db>
    CREATE OR REPLACE DIRECTORY db_source_dir AS '+DG1/myasm';
    CREATE DATABASE LINK s01
         CONNECT TO system IDENTIFIED BY <pwd> USING 's01';

    BEGIN
           DBMS_FILE_TRANSFER.PUT_FILE (
             source_directory_object      => 'DB_SOURCE_DIR',
             source_file_name             => 'spfilemyasm.ora',
             destination_directory_object => 'DB_TARGET_DIR',
             destination_file_name        => 'spfile.ora',
             destination_database         => 'S01');
         END;
         /


    Abschlussbemerkung

    Natürlich müssen Sie einiges an Vorarbeit leisten, damit DBMS_FILE_TRANSFER genutzt werden kann. Haben Sie sich aber erst einmal die Directories und Database Links erzeugt sowie die notwendigen Berechtigungen vergeben, ist der Aufruf eine erfreulich unkomplizierte Geschichte. Besonders die Möglichkeit auch ASM Disk Groups sowohl als Quell- wie auch als Zielverzeichnis ansprechen zu können, macht das Package sehr nützlich.

    Bedenken Sie, dass DBMS_FILE_TRANSFER keine Umlaute in Dateinamen unterstützt und nur für Datenbanken, die sich in der OPEN-Phase befinden nutzbar ist.

    Das Package sollte nicht zum Erzeugen von Online Backups genutzt werden, da während des Kopiervorgangs Inkonsistenzen entstehen können. Diese können zwar in Verbindung mit den archivierten Redo-Log-Dateien wieder bereinigt werden, jedoch wird empfohlen, die Dateien vor dem Kopieren OFFLINE oder wenigstens auf READ ONLY zu setzen.



    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    REST Beispiele mit apex_web_service

    Bereich:APEX:PL/SQL:REST, Version: ab RDBMS 19.3:RDBMS 21.1:ORDS 20.3, Letzte Überarbeitung: 06.07.2023

    Keywords:Oracle apex_web_service Beispiele, APEX

    Nachdem das Thema REST immer mehr an Schwung gewinnt, wird es Zeit darüber einen Tipp zu schreiben.
    Wir haben aus eigener Erfahrung lange im Internet gesucht und nur selten etwas gefunden.
    Deswegen haben wir ein paar Beispiele gesammelt:

    Der Einfachheit halber, wird mit nicht mit SSL Verschlüsselung gearbeitet, ansonsten muss noch ein Wallet eingerichtet werden

    1. CLOB Übertragen (upload)

    l_clob:=APEX_WEB_SERVICE.MAKE_REST_REQUEST(      
     p_url=>'http://www.muniqsoft-training.de/ords/my_rest/muso/putCLOB',
     p_http_method        =>'PUT',
     p_transfer_timeout   =>720,
     p_username           =>'rest_user',
     p_password           =>'rest_password'
     p_body               =>text_lob,  --CLOB Datentyp übertragen         
    );


    Auf dem Zielserver dann einen Rest-Service anlegen (Typ PUT):

    BEGIN
     my_proc(:body_text);
    END;


    Die Procedure my_proc könnte dann wie folgt aussehen:

    CREATE OR REPLACE PROCEDURE my_proc (p_clob IN CLOB)
    IS
    ...
    BEGIN
    ...
    END;


    2. BLOB übertragen (upload)

    l_clob:=APEX_WEB_SERVICE.MAKE_REST_REQUEST(      
    p_url=>'http://www.muniqsoft-training.de/ords/my_rest/muso/putBLOB',
     p_http_method      =>'PUT',
     p_transfer_timeout =>720,
     p_username         =>'rest_user',
     p_password         =>'rest_password'
     p_body_blob        =>pdf_lob, --BLOB Datentyp übertragen 
    );


    Auf dem Zielserver dann einen Rest-Service anlegen (Typ PUT):

    BEGIN
    my_proc(:body);
    END;


    Die Procedure my_proc könnte dann wie folgt aussehen:

    CREATE OR REPLACE PROCEDURE my_proc (p_blob IN BLOB)
    IS
    ...
    BEGIN
    ...
    END;


    3. Zwei Parameter übertragen (Upload)

    DECLARE
    l_clob CLOB;
    BEGIN
        apex_web_service.g_request_headers.delete();
         apex_web_service.g_request_headers(1).name  := 'Content-Type';  
         apex_web_service.g_request_headers(1).value := 'application/octet-stream'; --'application/x-www-form-urlencoded';
         apex_web_service.g_request_headers(2).name  := 'PARAMETER_A';  
         apex_web_service.g_request_headers(2).value := '12345';
         apex_web_service.g_request_headers(3).name  := 'PARAMETER_B';  
         apex_web_service.g_request_headers(3).value := to_char(sysdate,'DD.MM.YYYY');
    l_clob:=APEX_WEB_SERVICE.MAKE_REST_REQUEST(
            p_url=>'http://www.muniqsoft-training.de/ords/my_rest/muso/putDATA',
            p_http_method       =>'PUT',
            p_transfer_timeout  =>180,
            p_username          =>'rest_user',
            p_password          =>'rest_passwort',
            p_body              =>my_clob_text);
     if apex_web_service.g_status_code = 200 then
        dbms_output.put_line('Übertragung erfolgreich abgeschlossen.');
     else
        dbms_output.put_line('Fehler aufgetreten: ' || apex_web_service.g_status_code);
     end if;
    END;


    Auf dem Zielserver dann wieder ein REST Service anlegen (TYP: PUT)

    BEGIN
    :status:=my_func(
    p_parameter1=>:PARAMETER_A,
    p_parameter1=>:PARAMETER_A,
    p_clob=>:BODY_TEXT);
    END;


    REST Parameter:

    NameBind-ParameterZugriffm.QuelltypDatentyp
    PARAMETER_APARAMETER_AINHTTP HEADERSTRING
    PARAMETER_BPARAMETER_BINHTTP HEADERSTRING
    STATUSSTATUSOUTRESPONSESTRING


    Die Funktion könnte dann wie folgt aussehen:

    CREATE OR REPLACE FUNCTION my_func(
    p_parameter1 IN VARCHAR2,
    p_parameter2 IN VARCHAR2,
    p_clob IN CLOB) RETURN VARCHAR2
    IS
    BEGIN
    ...
    RETURN 200;
    EXCEPTION WHEN OTHERS THEN
      RETURN 500;
    END;


     



    Weitere Interessente Artikel zum Thema:


    Empfohlene Schulungen zum Thema:


    Import von Bild-Dateien in die Datenbank

    Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 14.12.2018

    Keywords:PL/SQL

    Jeder hat schon mal von den LOB-Datentypen gehört, die für die Speicherung von großen Textdateien bzw. binären Dateien geeignet sind. In der Oracle Datenbankversion 10g können sie Daten bis zu 128 Terabyte pro Feld aufnehmen ((4GB-1) * DB-Blockgröße).
    Das Einfügen von Bildern in Tabellen ist allerdings nicht über einfachen Insert möglich, sondern nur über PL/SQL-Prozeduren und das Package DBMS_LOB.
    Will man ein Bild in der DB nicht nur speichern, sondern auch seine Attribute (Höhe, Breite, Farbtiefe etc.) auslesen oder diverse Manipulationen durchführen wie z.B. Vergrößern, Rotieren etc., bietet sich der Datentyp ORDimage an (den man allerdings nicht in der Oracle Express-Edition verwenden kann, da die Intermedia-Komponente hier fehlt).
    Dieser Monatstipp soll eine kurze Einführung geben, wie man einzelne Bilder bzw. alle Bilder eines externen Verzeichnisses in Datenbanktabellen laden kann.

    Zur Vorbereitung muss man erstmal ein Verzeichnis auf dem Server erstellen und dem Benutzer die nötigen Schreib- und Leserechte erteilen (Das Schreibrecht wird nur für die Erstellung der externen Tabelle benötigt.):

    CREATE OR REPLACE DIRECTORY bilder AS 'C:\temp';
    GRANT READ, WRITE ON DIRECTORY bilder TO scott;

     

    Einzelne Bilder in bestehenden Datensätzen ergänzen

    Als Beispiel dient die beliebte Tabelle scott.emp. Wir hängen zunächst eine Spalte "Bild" mit dem Datentyp BLOB an die Tabelle an:

    CONN scott/tiger
    ALTER TABLE emp add(bild BLOB);

    Dann erzeugen wir eine Prozedur zum Einfügen einzelner Bilder. Übergeben werden der Primärschlüssel des Datensatzes und der Name des Bildes. Die Bilder müssen im Ordner c:\temp liegen und die Bildernamen sollten keine Umlaute enthalten:

    CREATE OR REPLACE PROCEDURE bild_einfuegen (
      p_empno   NUMBER,
      p_bild    VARCHAR2)
    AS
      v_quelle    BFILE;
      v_ziel      BLOB;
    BEGIN
    -- das BLOB-Feld wird erstmal als leerer Blob initialisiert, der in die
    -- Variable v_ziel zurückgeschrieben wird
       UPDATE emp SET bild = empty_blob() WHERE empno = p_empno
       RETURNING bild INTO v_ziel;
    -- über die Funktion bfilename werden der Speicherort und der
    -- Name des Bilds in die Variable v_quelle eingelesen
      v_quelle := bfilename('BILDER', p_bild);
    -- das Bfile wird mit der Prozedur DBMS_LOB.FILEOPEN zum Lesen geöffnet
      DBMS_LOB.FILEOPEN(v_quelle, DBMS_LOB.FILE_READONLY);
    -- über die Prozedur LOADFROMFILE wird das Bfile in das Blob-Feld gelesen
    -- GETLENGTH ermittelt die Zahl der zu importierenden Zeichen
      DBMS_LOB.LOADFROMFILE(v_ziel, v_quelle, DBMS_LOB.GETLENGTH(v_quelle));
    -- Die Ausgabe der Grösse des Bilds dient als Test,
    -- ob der Import geklappt hat
      DBMS_OUTPUT.PUT_LINE('Grösse: '||DBMS_LOB.GETLENGTH(v_ziel));
      DBMS_LOB.FILECLOSE(v_quelle);
      COMMIT;
    END;
    /

     

    -- Testen mit
     exec bild_einfuegen(7839, '0087-Strand.jpg')

     

    Alle Bilder aus einem Verzeichnis in eine Tabelle laden

    DROP TABLE bilder_hawaii;
    CREATE TABLE bilder_hawaii(
    bild_nr   NUMBER,
    bild_name VARCHAR2(100),
    bild   BLOB,
    CONSTRAINT bilder_pk PRIMARY KEY(bild_nr));

    Zuerst schieben wir die Photos in das Bilder-Directory c:\temp und erzeugen dann über die Eingabeaufforderung mit dir /B eine Liste der Filenamen, die in das Bilderverzeichnis zurück kopiert wird:

    c:\temp> dir /B > c:\liste.txt
    c:\temp> copy c:\liste.txt c:\temp\liste.txt


    Diese Liste kann man in das Bilder-Verzeichnis als externe Tabelle einbinden:

    DROP TABLE filenamen;
    CREATE TABLE filenamen (name VARCHAR2(100))
    ORGANIZATION EXTERNAL
    (DEFAULT DIRECTORY bilder
    ACCESS PARAMETERS(RECORDS DELIMITED BY NEWLINE)
    LOCATION('liste.txt'));

     

    SELECT * FROM filenamen;

    NAME
    --------------------------------
    0003-Waikiki.jpg
    0004-Waikiki-Strand.jpg
    0006-Honululu-Tempel.jpg
    0007b-Honululu-Baum.jpg
    0008-Honululu-Blume.jpg
    0009-Honululu-Foster Gardens.jpg
    0012-Honululu-Foster Gardens.jpg
    ....
    667 Zeilen ausgewählt.


    Danach lässt sich der Lade-Vorgang über eine Schleife abwickeln. Im Beispiel wurde zusätzlich eine Geschwindigkeitsmessung eingebaut:

    DROP SEQUENCE bildseq;
    CREATE SEQUENCE bildseq;

     

    CREATE OR REPLACE PROCEDURE bilder_einfuegen
    AS
      CURSOR bildcur IS SELECT name FROM filenamen;
      v_quelle    BFILE;
      v_ziel      BLOB;
      v_start     TIMESTAMP;
      v_ende      TIMESTAMP;
      v_count     NUMBER := 0;
      v_groesse   NUMBER := 0;
    BEGIN
      SELECT SYSTIMESTAMP INTO v_start FROM dual;
    -- Der Cursor liefert die Namen der Bilder
      FOR rec IN bildcur LOOP
         INSERT INTO bilder_hawaii 
         VALUES(bildseq.nextval, rec.name, empty_blob())
         RETURNING bild INTO v_ziel;
         v_quelle := bfilename('BILDER', rec.name);
         DBMS_LOB.FILEOPEN(v_quelle, DBMS_LOB.FILE_READONLY);
         DBMS_LOB.LOADFROMFILE(v_ziel, v_quelle, DBMS_LOB.GETLENGTH(v_quelle));
         DBMS_LOB.FILECLOSE(v_quelle);
         v_count := bildcur%rowcount;
         v_groesse := v_groesse + DBMS_LOB.GETLENGTH(v_ziel);
      END LOOP;
      DBMS_OUTPUT.PUT_LINE
     ('Es wurden '||v_count||' Bilder eingefügt. Gesamtgrösse: '||v_groesse);
      SELECT SYSTIMESTAMP INTO v_ende FROM dual;
      DBMS_OUTPUT.PUT_LINE('Laufzeit: '||
       REGEXP_SUBSTR((v_ende-v_start), '[^ ]+',1,2));
      COMMIT;
    END;
    /

     

    --Testen mit
    exec bilder_einfuegen

    Es wurden 667 Bilder eingefügt. Gesamtgrösse: 413089359
    Laufzeit: 00:01:38.516000000 (bei einer XE-Edition)
    Laufzeit: 00:00:51.406000000 (bei einer 11g-Version auf demselben Rechner)

     

    Einfügen und Bearbeiten von Bildern mit Oracle Intermedia

    Oracle interMedia (ab 11g Oracle Multimedia genannt) ermöglicht neben Speichern und Abrufen auch die Manipulation von Bildern, Audiofiles und Videos in der Datenbank. Diese Komponente wird außer in der XE-Edition per default mitinstalliert.
    Der Datentyp ORDimage hat gegenüber den einfachen Blobs den Vorteil der direkten Integration in die Entwicklungstools von Oracle (z. B. JDeveloper, Oracle Content Management SDK, Oracle Application Server Portal, etc.). Darüber hinaus werden Bildinformationen wie Höhe, Breite, Format, Mime-Typ etc. beim Hochladen automatisch bestimmt und gespeichert.

     

    Einfügen einzelner Bilder

    Zur Nutzung der Intermedia-Optionen muss man das oben beschriebene Prozedere ein bisschen abwandeln. Die Returning-Klausel kann hier nicht verwendet werden, insofern wird die Initialisierung der Bildspalte ausgelagert:

    ALTER TABLE emp DROP COLUMN bild;
    ALTER TABLE emp add(bild ORDSYS.ORDImage);
    UPDATE emp SET bild = ORDSYS.ORDImage.init();
    commit;

     

    CREATE OR REPLACE PROCEDURE bild_einfuegen1 (
      p_empno   NUMBER,
      p_bild    VARCHAR2)
    AS
      v_ziel     ORDSYS.ORDImage;
      v_context  RAW(400) := NULL;
    BEGIN
       SELECT bild INTO v_ziel FROM emp WHERE empno = p_empno FOR UPDATE;
    -- mit der Methode importFrom kann man das Bild hochladen
       v_ziel.importFrom(v_context, 'file', 'BILDER', p_bild);
    -- Alternativ kann man auch erst die Quelle festlegen und dann
    -- das File importieren
    -- v_ziel.setSource('file', 'BILDER', p_bild);
    -- v_ziel.import(v_context);
      UPDATE emp set bild = v_ziel WHERE empno = p_empno;
      COMMIT;
    END;

     

    --Testen mit
    exec bild_einfuegen1 (7521,'0006-Honululu-Tempel.jpg')

     

    Einfügen aller Bilder aus einem Verzeichnis

    DROP TABLE bilder_hawaii;
    CREATE TABLE bilder_hawaii(
    bild_nr   NUMBER,
    bild_name VARCHAR2(100),
    bild   ORDSYS.ORDImage,
    CONSTRAINT bilder_pk PRIMARY KEY(bild_nr));

     

    DROP SEQUENCE bildseq;
    CREATE SEQUENCE bildseq;

     

    CREATE OR REPLACE PROCEDURE bilder_einfuegen1
    AS
      CURSOR bildcur IS SELECT name FROM filenamen;
      v_ziel     ORDSYS.ORDImage;
      v_context  RAW(400);
      v_bildnr   NUMBER;
      v_start    TIMESTAMP;
      v_ende     TIMESTAMP;
      v_count    NUMBER := 0;
      v_groesse  NUMBER := 0;
    BEGIN
      SELECT SYSTIMESTAMP INTO v_start FROM dual;
      FOR rec IN bildcur LOOP
     -- Diesmal wird bei der Initialisierung gleich der Quelltyp,
     -- das Quellverzeichnis und der Name der Datei angegeben.
        INSERT INTO bilder_hawaii
        VALUES(bildseq.nextval, rec.name,ORDSYS.ORDImage.init('file','BILDER',rec.name))
        RETURNING bild_nr INTO v_bildnr;
        SELECT Bild INTO v_ziel FROM Bilder_Hawaii
        WHERE bild_nr = v_bildnr FOR UPDATE;
        v_ziel.importFrom(v_context, 'file', 'BILDER', rec.name);
        UPDATE Bilder_Hawaii SET Bild = v_ziel WHERE bild_nr = v_bildnr;
        v_count := bildcur%rowcount;
        v_groesse := v_groesse + v_ziel.getContentLength;
      END LOOP;
      DBMS_OUTPUT.PUT_LINE
     ('Es wurden '||v_count||' Bilder eingefügt. Gesamtgrösse: '||v_groesse);
      SELECT SYSTIMESTAMP INTO v_ende FROM dual;
      DBMS_OUTPUT.PUT_LINE('Laufzeit: '||
       REGEXP_SUBSTR((v_ende-v_start), '[^ ]+',1,2));
      COMMIT;
    END;

     

    --Testen mit
    exec bild_einfuegen1

    Es wurden 667 Bilder eingefügt. Gesamtgrösse: 413089359
    Laufzeit: 00:01:13.391000000 (bei einer 11g-Version)
    Laufzeit: 00:01:40.342000000 (bei einer 10g-Version)


    Der Prozess dauert zwar ungefähr doppelt solange wie mit der BLOB-Methode, aber dafür werden die Bildeigenschaften gleich mit gespeichert, und man kann sie auch per Select auslesen:

    SELECT b.bild.getContentLength() Grösse,
     b.Bild.getCompressionFormat() Kompression,
     b.Bild.getContentFormat() Farbraum,
     b.Bild.getFileFormat() Dateiformat,
     b.Bild.getHeight() Höhe,
     b.Bild.getWidth() Breite,
     b.Bild.getMetadata() Metadaten
    FROM bilder_hawaii b
    WHERE b.bild_nr = 1;

        GRÖSSE KOMPRESSION FARBRAUM DATEIFORMAT  HÖHE BREITE
    ---------- ------------ --------- ----------- ----- ----------
        534016 JPEG-PROGRESSIVE 24BITRGB JFIF   729  1024


    Alternativ kann man die Metadaten als XML auslesen:

    SELECT b.Bild.getMetadata() Metadaten
    FROM bilder_hawaii b
    WHERE b.bild_nr = 1;

     

    METADATEN
    ----------------------------------------------------
    SYS.XMLTYPE(
    <ordImageAttributes xmlns="http://xmlns.oracle.com/ord/meta/ordimage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.oracle.com/ord/meta/ordimage http://xmlns.oracle.com/ord/meta/ordimage">
    <height>729</height>
    <width>1024</width>
    <contentLength>534016</contentLength>
    <fileFormat>JFIF</fileFormat>
    <contentFormat>24BITRGB</contentFormat>
    <compressionFormat>JPEG-PROGRESSIVE</compressionFormat>
    <mimeType>image/jpeg</mimeType>
    </ordImageAttributes>)

     

    Fazit

    Der Import von Bildern erscheint zwar zunächst ein bisschen umständlich. Wenn man sich aber erst einen Satz geeigneter Prozeduren erstellt hat, steht der sicheren Speicherung großer Bildmengen in der Datenbank nichts mehr im Weg. Die Prozeduren kann man im Fall der BLOBs leicht abgewandelt natürlich auch für den Import anderer binärer Files (wie z. B. PDF- und Word-Dokumente) nutzen.
    Mehr über den Import, Export und die Bearbeitung von LOBs erfahren Sie in unserer Packages-Schulung.



    Weitere Interessente Artikel zum Thema:



    Empfohlene Schulungen zum Thema:



      Undokumentierte Funktion zum Abfragen von NULL Werten

      Bereich:PL/SQL:SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 12.12.2018

      Keywords:PL/SQL , SQL

      Haben wir uns nicht alle schon mal darüber geärgert, dass auf NULL Spalten nur mit der folgenden Klausel abgefragt werden kann:

      SELECT * FROM tab WHERE col IS NULL;
       

      Seit Version 10g gibt es eine undokumentierte Funktion (und damit ist sie

      leider auf Produktionsdatenbanken nur auf eigene Gefahr einsetzbar).

      Die Funktion sys_op_map_nonnull kann zwei NULL Spalten vergleichen.

      Beispiel: Alle Zeilen sollen ausgegeben werden, in denen die Spalte

      mgr=comm= NULL ist:

      select * from scott.emp
      where sys_op_map_nonnull(mgr)=sys_op_map_nonnull(comm)
       

      Oder, alle Zeilen ausgeben, deren Spalte COMM = NULL ist:

      select * from scott.emp
      where sys_op_map_nonnull(comm)=sys_op_map_nonnull(NULL);
       

      Zum Vergleich: Die offizielle Klausel lautet:

      select * from scott.emp
      where comm is NULL;


      Weitere Interessente Artikel zum Thema:



      Empfohlene Schulungen zum Thema:



        Mutating Table Problem umgehen mittels zwei Trigger und einer Collection

        Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 29.06.2018

        Keywords:Mutating Table Problem, Trigger, Collection

        In unseren Schulungen klagen die Teilnehmer häufig über das Mutating Table Problem.
        Man möchte ja nur mal in der Tabelle etwas nachsehen, auf der der Trigger liegt.

        Nur leider quittiert der Trigger einen DML Aufruf mit der folgenden Fehlermeldung:

        ORA-04091: table XXXX is mutating, trigger/function may not see it.

        Lassen Sie uns als Beispiel folgende Problemstellung lösen:

        In der klassischen EMP Tabelle wird eine Spalte hinzugefügt mit dem Namen job_chef. In die soll, wenn die Spalte mgr gefüllt wurde, auch automatisch der Beruf des Vorgesetzten eingetragen werden. (Für alle, die die Tabelle nicht so gut kennen, sei gesagt, dass in der Spalte mgr die Nummer des Vorgesetzten steht)
        Man müsste nun eigentlich nur mal kurz nachsehen, was für einen Beruf der Chef hat, aber da kommt uns das Mutating Table Problem in die Quere.

        Man kann das Problem jetzt über 2 Lösungsansätze angehen.

        Variante 1:

        Wir legen uns erstmal eine Spalte mit Namen job_chef an:

        ALTER TABLE emp ADD (job_chef VARCHAR2(30));

        Dann erstellen wir uns ein Package, dass die komplette! Tabelle in ein Array schreibt. (Vorsicht, bei grossen Tabellen, kann der Hauptspeicher zur neige gehen)

        CREATE OR REPLACE PACKAGE plsql_tab IS
        TYPE tab_type IS TABLE OF scott.emp%ROWTYPE
        INDEX BY BINARY_INTEGER;
        p_tab tab_type;
        PROCEDURE getptab;
        END;
        /
        CREATE OR REPLACE PACKAGE BODY plsql_tab IS
        PROCEDURE getptab  IS
        BEGIN
           SELECT * BULK COLLECT INTO p_tab FROM scott.emp;
        END getptab;
        END plsql_tab;
        /

        Dann brauchen wir einen Statement Trigger, denn der hat das Mutating Table Problem nicht. Der lädt die Daten in eine Collection in den Speicher.

        CREATE OR REPLACE TRIGGER emp_trig
        BEFORE INSERT OR UPDATE ON emp
        DECLARE
        BEGIN
        plsql_tab.getptab;
        END;
        /

        Und nun kommt der eigentliche Trigger ins Spiel. Der braucht jetzt nicht die eigene Tabelle befragen, sondern holt sich die Daten aus dem Speicher.

        CREATE OR REPLACE TRIGGER emp_trg_row
        BEFORE INSERT OR UPDATE ON emp
        FOR EACH ROW
        DECLARE
        BEGIN
        FOR i IN 1 .. plsql_tab.p_tab.count LOOP
            IF plsql_tab.p_tab(i).empno=:new.mgr THEN
                :new.job_chef:=plsql_tab.p_tab(i).job;
            END IF;
        END LOOP;
        END;
        /

         

        Test:
        update emp set mgr=7902 where empno=7369;
        SELECT ename,mgr,job_chef from emp where empno=7369;
        SMITH    7902    ANALYST

         

        Variante 2:

        Das ist jetzt ein bisschen gefährlich, denn wir tricksen das Mutating Table Problem, mit dem Compiler Hinweiß:
        PRAGMA AUTONOMOUS_TRANSACTION aus. Hier wird für den Trigger eine eigene Session geöffnet, die dann das
        Mutating Table Problem nicht hat. Wenn sich aber die Jobbezeichnung des Chefs während des Updates auch ändert, erwischt man u.U. den falschen Wert.

        CREATE OR REPLACE TRIGGER emp_trg
        BEFORE INSERT OR UPDATE ON emp
        FOR EACH ROW
        DECLARE
        PRAGMA AUTONOMOUS_TRANSACTION;
        BEGIN
        SELECT job INTO :new.job_chef
        FROM emp
        where empno=:new.mgr;
        END;
        /

         

        Weitere Tipps und Tricks erhalten Sie z.B. in unserem PL/SQL Kurs oder PL/SQL II Kurs



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Eval Funktion mit Execute Immediate

        Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 30.07.2018

        Keywords:Dynamisches SQL, EXECUTE IMMEDIATE

        In Unix gibt es eine schöne Funktion mit Namen eval. Mit der kann man dynamische Befehle ausführen. So etwas wäre doch auch unter Oracle ganz praktisch. Es gibt ja auch schon drei dynamische Konzepte:

        • EXECUTE IMMEDIATE
        • REF Cursor
        • DBMS_SQL und DBMS_SYS_SQL

        Wir lösen das Ganze mit EXECUTE IMMEDIATE und packen eine kleine Funktion darum.

        CREATE OR REPLACE FUNCTION my_eval (cmd IN VARCHAR2)
        RETURN VARCHAR2
        IS
        PRAGMA AUTONOMOUS_TRANSACTION; -- Damit ist die Funktion eigenständig und beeinflusst niemanden :-)
        v_cmd VARCHAR2(32000);
        BEGIN
        v_cmd:=rtrim(cmd,';'); -- Semmikolon ggf.herausfiltern
        EXECUTE IMMEDIATE v_cmd; -- Befehl ausführen
        RETURN v_cmd||'; --OK'; -- Befehl erfolgreich ausgeführt
        EXCEPTION WHEN OTHERS THEN
         RETURN v_cmd||' --'||sqlerrm;
        END;
        /

        Anwendungsbeispiele:

        REM Löschen aller Tabellen des Benutzers SCOTT die mit Tab beginnen


        SELECT my_eval('drop table scott.'||table_name||';')
        FROM all_tables
        WHERE table_name like 'TAB%';

        oder alle Tabellen im gleichen Tablespace reorgansieren


        SELECT my_eval('alter table scott.'||table_name||' move;')
        FROM dba_tables
        WHERE owner='SCOTT'

         Alle Indizes neu kompileren, die ungültig sind:


        SELECT my_eval('ALTER INDEX '||owner||'."'||index_name||'" REBUILD ONLINE;')
        FROM dba_indexes WHERE status<>'VALID' and PARTITIONED='NO';

        Zu Risikien und Nebenwirkungen:

        Dynamisches PL/SQL hat auch diverse Nachteile:

        1. Es ist langsamer als normales PL/SQL

        2. Es ist anfällig für SQL Injection.  (Aber das wird in einem anderen Tipp besprochen :-) )

        Weitere Tipps erhalten Sie in einem unserer schönen PL/SQL Kurse (PL/SQL I, PL/SQL II, PL/SQL Kompakt, PL/SQL Tuning, PL/SQL Packages)



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Result Cache

        Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 29.06.2018

        Keywords:DBA, Oracle Neuerungen, Oracle Tuning, PL/SQL, Standard Packages, Views, 11g Release

        Der Result Cache ist ein - leider auf die Enterprise Edition beschränktes - Feature, das mit Version 11g eingeführt wurde. Er ist sowohl in SQL für Select-Befehle als auch in PL/SQL für Funktionsaufrufe nutzbar. Die Idee dahinter ist in beiden Fällen, dass das Ergebnis bei der ersten Ausführung gespeichert wird und bei weiteren Aufrufen der Befehl selber nicht mehr ausgeführt, sondern das Ergebnis aus dem Cache abgerufen wird. Um Tom Kyte zu zitieren: "Everyone knows the fastest way to do something is – to not do it".

        Der dafür genutzte Speicherbereich - eben der Result Cache - ist Bestandteil der SGA. Um ihn nutzen zu können, muss nur seine Größe (RESULT_CACHE_MAX_SIZE ) auf einen Wert > 0 eingestellt sein.

        Parameter (serverseitig)

        RESULT_CACHE_MAX_SIZE
        Größe des Result Cache in Bytes.
        Der Default hängt von den Einstellungen für SHARED_POOL_SIZE, SGA_TARGET und MEMORY_TARGET ab

        RESULT_CACHE_MAX_RESULT
        Anteil am Result Cache in Prozent, der maximal für einen einzigen Befehl genutzt werden darf.
        Default: 5


        In einem RAC-Umfeld muss darauf geachtet werden, dass entweder auf ALLEN Instanzen RESULT_CACHE_MAX_SIZE auf einem Wert > 0 steht, oder auf keiner; sonst kann es laut Doku zu falschen Ergebnissen kommen.

        SQL


        In SQL gibt es zwei Wege, den Result Cache zu nutzen, entweder über den (auch auf Session-Ebene einstellbaren) Parameter RESULT_CACHE_MODE oder über den Hint RESULT_CACHE:

        Steht RESULT_CACHE_MODE auf MANUAL (dem Default), dann muss die Verwendung des Result Cache explizit im Select über den Hint RESULT_CACHE angefordert werden. Steht RESULT_CACHE_MODE dagegen auf FORCE, so wird bei JEDEM Select versucht, den Result Cache zu nutzen, es sei denn, das wird explizt durch den Hint NO_RESULT_CACHE unterbunden. Zumindest eine systemweite Einstellung von RESULT_CACHE_MODE=FORCE dürfte eher nicht sinnvoll sein, da der Platz ja begrenzt ist und somit ein ständiger Turnover stattfinden würde.

        Die Verwendung des Result Cache ist auch im Ausführungsplan sichtbar.

        PL/SQL


        Interessant ist der Result Cache vor allem im PL/SQL-Umfeld. Es kommt immer wieder vor, dass man kleine Lookup-Tabellen hat, aus denen häufig ein einzelner Wert - gekapselt in eine Funktion - ausgelesen wird. Oder man verwendet ständig die ein oder andere kleinere Hilfsfunktion (z.B. zur Formatierung oder Berechnung von Werten) für immer wieder die gleichen Werte. Musste man vor Version 11g die Performance steigern, so blieben nur Hilfskonstrukte, wie beispielsweise das Einlesen von Werten in globale Package-Variablen oder -Arrays. Der Nachteil dieses Vorgehens liegt auf der Hand: Package-Variablen behalten ihren Wert nur innerhalb der Session, und spätestens bei Web-Applikationen ist dieser Ansatz daher obsolet.

        In Version 11g kann nun das Ergebnis einer Funktion im Result Cache abgelegt und wieder abgerufen werden, egal, ob es sich um eine Package-Funktion oder um eine Standalone Funktion handelt. Es muss nur in der Funktionsdeklaration angegeben werden (bei Packages sowohl im Header als auch im Body). Und da der Result Cache Bestandteil der SGA ist, nicht der PGA, ist er auch Session-übergreifend nutzbar!

        Beispiel:

        SET SERVEROUTPUT ON
        SET ECHO ON
        CREATE FUNCTION cache_it (
           p_empno  IN  NUMBER
        )
           RETURN VARCHAR2
           RESULT_CACHE
           -- RELIES_ON(SCOTT.emp)  -- nur in Version 11.1 nötig
        IS
           v_name   SCOTT.EMP.ename
        %TYPE;BEGIN
           SELECT ename
             INTO v_name
             FROM SCOTT.emp
            WHERE empno = p_empno;
           RETURN v_name;
        END;
        /
        DECLARE
           v_begin    NUMBER;
           v_end      NUMBER;
           v_ret      VARCHAR2 (100);
           TYPE t IS TABLE OF NUMBER
              INDEX BY PLS_INTEGER;
           v_arr      t;
        BEGIN
           SELECT empno
           BULK COLLECT INTO v_arr
             FROM scott.emp;
           v_begin := DBMS_UTILITY.get_cpu_time;
           FOR i IN 1 .. 1000
           LOOP
              FOR j IN 1 .. v_arr.COUNT
              LOOP
                 v_ret := cache_it (v_arr (j));
              END LOOP;
           END LOOP;
           v_end := DBMS_UTILITY.get_cpu_time;
           DBMS_OUTPUT.put_line ('Zeit: ' || (v_end - v_begin));
        END;
        /
        -- Verwendung des Result Cache (systemweit) deaktivieren
        EXEC DBMS_RESULT_CACHE.bypass(TRUE)
        /
        -- Verwendung des Result Cache (systemweit) wieder aktivieren
        EXEC DBMS_RESULT_CACHE.bypass(FALSE)
        /
        DROP FUNCTION cache_it;
        Ausgabe:
        Zeit: 5   -- mit  Result Cache
        Zeit: 54  -- ohne Result Cache
        Zeit: 5   -- mit  Result Cache

        Das Package DBMS_RESULT_CACHE bietet einige Verwaltungsmöglichkeiten an, wie z.B. Statusabfrage, vorübergehendes Ausschalten (s.o.), explizite Invalidierung oder, wie unten gezeigt, Erzeugung eines Reports zum Speicherverbrauch:

        SET SERVEROUTPUT ON
        EXEC DBMS_RESULT_CACHE.MEMORY_REPORT(detailed => TRUE)

        INVALIDIERUNGEN


        Im obigen Beispiel wird innerhalb der Funktion auf eine Tabelle zugegriffen. Was passiert, wenn diese Tabelle nun von einer anderen Session geändert wird? In diesem Fall werden beim Commit alle davon abhängigen Ergebnisse automatisch invalidiert und bei Bedarf neu ermittelt, so dass die Funktion nie falsche Ergebnisse liefert. Wird die Tabelle innerhalb der EIGENEN Session geändert, so wird für abhängige Funktionen der Result Cache grundsätzlich so lange nicht mehr verwendet, bis die Transaktion beendet ist. Die Lesekonsistenz ist also immer gewährleistet.

        In Version 11.1 musste man mit der Klausel RELIES_ON noch explizit angeben, von welchen Tabellen der Rückgabewert der Funktion abhängig ist. Hat man die Klausel vergessen, so führte das nicht zu einem Kompilierungsfehler, sondern ggf. zu falschen Resultaten. In Version 11.2 werden solche Abhängigkeiten automatisch erkannt.

        EINSCHRÄNKUNGEN


        Natürlich gibt es auch ein paar Einschränkungen bei der Verwendung, die aber normalerweise schon der Compiler nicht zulässt. Dazu gehören

        • OUT-Parameter sind nicht erlaubt (gespeichert werden die Werte für IN-Parameter und Ergebnisse)

         

        • Zulässig als IN-Parameter und Returnwert sind im wesentlichen nur skalare Datentpyen

         

        • Nicht zulässig bei Invoker Rights


        Beispiel für Fehlermeldungen:

        PLS-00999: Implementierungseinschränkung (kann temporär sein) RESULT_CACHE is disallowed on subprograms in Invoker-Rights modules

        Ab Version 12c fällt die letztgenannte Einschränkung weg; dann kann der Result Cache auch bei Invoker Rights genutzt werden.

        Eine genaue Auflistung aller Einschränkungen finden Sie in der PL/SQL Language Reference.

        V$-VIEWS ZUM RESULT CACHE:

         

        • V$RESULT_CACHE_OBJECTS
        • V$RESULT_CACHE_MEMORY
        • V$RESULT_CACHE_DEPENDENCY


        Viele weitere interessante Informationen hierzu erfahren Sie in unseren Schulungen.



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Vergleich von Tabellen in 10G und 11G II

        Bereich:PL/SQL, Version: ab RDBMS 8.x, Letzte Überarbeitung: 11.12.2018

        Keywords:PL/SQL, Standard Packages

        Der Vergleich und die Synchronisation von Tabellen waren schon einmal Gegenstand eines Monatstipps. Damals beschränkte sich der Vergleich jedoch auf bis zu 4 Spalten. Die hier vorgestellten Packages für Oracle ermöglichen den Vergleich aller Spalten.

        Das Package vergleich_11g ist ein Wrapper für das in der Oracle Version 11g eingeführte DBMS_COMPARISON, das den Umgang mit diesem optionsreichen Tool etwas einfacher gestalten soll, das Package Vergleich_10g ist auch in der Oracle-Version 10g lauffähig und darüberhinaus schneller als DBMS_COMPARISON.

        Voraussetzung:

        • Die Tabellen müssen dieselbe Spaltenstruktur und die gleichen Spaltennamen haben
        • Der Primärschlüssel muss für die 10g-Version einspaltig sein (für den Umgang mit mehrspaltigen Primärschlüssel müsste das Package entsprechend erweitert werden)
        • In der 10g-Version braucht der Ersteller des Packages ein direkt verliehenes CREATE TABLE-Recht, in der 11g-Version das EXECUTE-Recht an DBMS_COMPARISON und in beiden Packages natürlich die Select-Rechte an den beteiligten Objekten für den Vergleich bzw. Insert-, Update und Delete-Rechte für die Synchronisation
        • Der Vergleich von Spalten vom Typ CLOB, BLOB, BFILE, LONG, XMLType oder Spalten, die auf benutzerdefinierten Typen basieren, ist nicht möglich. Für den Vergleich von Tabellen, die derartige Spalten enthalten, muss man auf die älteren Versionen aus dem Monatstipp vom Mai 2009 ausweichen und diese Spalten vom Vergleich ausschließen.
        • Ein paralleler Vergleich von mehreren Tabellen ist (noch) nicht möglich, da nicht für jeden Vergleich eine eigene Differenzentabelle erstellt wird.


        Die beiden Packages wurden auf einer 10gR2-Datenbank und einer 11gR2-Datenbank auf Windows7 64bit getestet mit je 2 unterschiedlichen Tabellen im gleichen Schema, in verschiedenen Schemata und verschiedenen Datenbanken getestet.

        Beschreibung der Test-Skripte:

        • user_anlegen.sql erstellt die User local1 und local2 auf der lokalen DB sowie einen User remote auf der remote-DB und die dazugehörigen DB-Links (für letztere müssen vorher die TNSNAMES.ORA-Dateien mit entsprechenden TNS-Alias-Namen ergänzt werden)
        -- user_anlgen.sql auf der lokalen DB ausfuehren
        SET FEEDBACK OFF
        SET TIMING OFF
        SET VERIFY OFF
        SET SERVEROUTPUT ON
        ACCEPT sys_name CHAR PROMPT 'Name eines Users mit sysdba-Rechten angeben:  '
        ACCEPT sys_pwd CHAR PROMPT 'Password angeben:  ' HIDE
        ACCEPT tbs_local_name CHAR PROMPT 'Tablespace fuer den User local1 angeben: '
        conn &&sys_name/&&sys_pwd as sysdba
        SET SERVEROUTPUT ON
        PROMPT User local1 wird gelöscht und neu angelegt
        PROMPT
        BEGIN
          EXECUTE IMMEDIATE 'DROP USER local1 CASCADE';
        EXCEPTION
          WHEN OTHERS THEN NULL;
        END;
        /
        CREATE USER local1 IDENTIFIED BY local1 DEFAULT TABLESPACE &&tbs_local_name QUOTA UNLIMITED ON &tbs_local_name;
        GRANT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK TO local1;
        GRANT EXECUTE ON DBMS_COMPARISON TO local1;
        PROMPT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK und Execute-Recht DBMS_COMPARISON an local1 vergeben
        --Da der Vergleichsname in der ganzen Datenbank unique sein muss, wird er mithilfe einer Sequenz erzeugt.
        PROMPT 'Vergleichssequenz fuer vergleich_11g wird angelegt'
        PROMPT
        DROP SEQUENCE vergleich_seq;
        CREATE SEQUENCE vergleich_seq NOCACHE;
        GRANT SELECT ON vergleich_seq TO public;
        PROMPT Die Differenzen-Tabelle diff_11g im Schema local1 wird angelegt
        PROMPT
        BEGIN
          EXECUTE IMMEDIATE 'DROP TABLE local1.diff_11';
        EXCEPTION
          WHEN OTHERS THEN NULL;
        END;
        /
        CREATE TABLE local1.diff_11g (
          vergleichsname   VARCHAR2(30),
          schema           VARCHAR2(30),
          basistab         VARCHAR2(30),
          zieltab          VARCHAR2(30),
          pk_wert          NUMBER,
          scan_id          NUMBER,
          in_basis         VARCHAR2(10),
          in_vergleich     VARCHAR2(10));
        GRANT SELECT ON local1.diff_11g TO PUBLIC;
        PROMPT User local2 wird gelöscht und neu angelegt
        PROMPT
        BEGIN
          EXECUTE IMMEDIATE 'DROP USER local2 CASCADE';
        EXCEPTION
          WHEN OTHERS THEN NULL;
        END;
        /
        CREATE USER local2 IDENTIFIED BY local2 DEFAULT TABLESPACE &&tbs_local_name QUOTA UNLIMITED ON &&tbs_local_name;
        GRANT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK TO local2;
        GRANT EXECUTE ON DBMS_COMPARISON TO local2;
        PROMPT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK und Execute-Recht an DBMS_COMPARISON an local2 vergeben
        PROMPT
        PROMPT User remote auf der remote-DB wird gelöscht und neu angelegt
        ACCEPT host_name  CHAR PROMPT 'Hostname oder IP-Adresse des Remote-Servers angeben:   '
        ACCEPT remote_sid CHAR PROMPT 'SID des Remote-Servers angeben:    '
        ACCEPT tbs_remote_name CHAR PROMPT 'Tablespace fuer den User remote angeben:    '
        conn &&sys_name/&&sys_pwd@&&host_name/&&remote_sid as sysdba
        SET SERVEROUTPUT ON
        PROMPT
        BEGIN
          EXECUTE IMMEDIATE 'DROP USER remote CASCADE';
        EXCEPTION
          WHEN OTHERS THEN NULL;
        END;
        /
        CREATE USER remote IDENTIFIED BY remote DEFAULT TABLESPACE &&tbs_remote_name QUOTA UNLIMITED ON &&tbs_remote_name;
        GRANT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK TO remote;
        GRANT EXECUTE ON DBMS_COMPARISON TO remote;
        PROMPT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE, CREATE DATABASE LINK und Execute-Recht DBMS_COMPARISON an remote vergeben
        PROMPT Lassen Sie jetzt das Skript tabellen_anlegen.sql laufen
        • im Schema local1 auf dem lokalen Server die Tabellen emp_local1 und emp_local2 sowie big_tab1 und big_tab2 für Vergleiche im selben Schema
        • im Schema local2 auf dem lokalen Server die Tabellen emp_local2 und big_tab2 für Vergleiche zwischen verschiedenen Schemata
        • im Schema remote auf dem remote-Server die Tabellen emp_remote und big_tab2 für Vergleiche zwischen verschiedenen Datenbanken


        VERGLEICH UND SYNCHRONISATION MIT DEM PACKAGE VERGLEICH_10G


        Die Parameter p_hash, p_diff und p_sync der zentralen Prozedur vergleichen ermöglichen verschiedene Durchführungsoptionen, je nachdem, ob man

        • nur wissen will, ob sich die Tabellen unterscheiden, 
        •  die Unterschiede im Detail ermitteln will
        •  die Tabellen synchronisieren will


         Die Ausführungsbeispiele hier beziehen sich nur auf den Vergleich von Tabellen über einen DB-Link:

        -- Vergleich der Hash-Werte
        conn local/local
        set serveroutput on
        BEGIN
          vergleich_10g.vergleichen(
               p_local  => 'local',         -- Name des lokalen Schemas
               p_remote => 'remote',        -- Name des Zielschemas
             p_basistab => 'emp_local',     -- Name der Originaltabelle
              p_zieltab => 'emp_remote',    -- Name der Zieltabelle
                 p_link => 'remote_link',   -- Datenbank-Link zum Remote-Schema
             p_hashonly => TRUE);
        END;
        /
        =>
        Hash-Wert der Basistabelle local1.emp_local1: 245049078305
        Hash-Wert der Zieltabelle remote.emp_remote@remote_link: 239424418269
        Die Tabellen stimmen nicht ueberein
        -- Ermittlung der Unterschiede ohne Synchronisation
        set serveroutput on
        BEGIN
          vergleich_10g.vergleichen(
               p_local => 'local',
              p_remote => 'remote',
            p_basistab => 'emp_local',
             p_zieltab => 'emp_remote',
                p_link => 'remote_link'
                p_diff => true);
        END;
        /
        =>
        Es wurden 97 abweichende Datensätze gefunden und in die Tabelle Diff_10g eingetragen
        SELECT employee_id empno, first_name||' 'last_name name, hire_date, salary, in_basis, in_vergleich
        FROM diff_10g;
        =>
        EMPNO  Name                    HIRE_DATE SALARY    IN_BASIS   IN_VERGLEICH
        499    Hannah Ahrends          13.01.00    3603    fehlt      vorhanden
        166    Sundar Ande             24.03.00    6400    fehlt      vorhanden
        204    Hermann Baer            24.05.94    10500   vorhanden  fehlt
        116    Shelli Baida            10.12.97    3045    vorhanden  fehlt
        116    Shelli Baida            31.12.97    2755    fehlt      vorhanden
        192    Sarah Bell              11.02.96    3800    fehlt      vorhanden
        192    Sarah Bell              21.01.96    4200    vorhanden  fehlt
        303    Centa Berger            07.06.07    6503    vorhanden  fehlt
        151    David Bernstein         24.03.97    9500    vorhanden  fehlt
        407    Wilhelm Busch           01.07.11    3203    fehlt      vorhanden
        148    Gerald Cambrault        22.10.99    10450   fehlt      vorhanden
        148    Gerald Cambrault        01.10.99    11550   vorhanden  fehlt
        403    Philipp der Gute        07.06.07    6503    fehlt      vorhanden
        402    Karl der Kahle          17.08.97    6003    fehlt      vorhanden
        401    Johanna die Wahnsinnige 17.02.08    13003   fehlt      vorhanden
        ....
        -- Ermittlung der Unterschiede mit Synchronisation
        set serveroutput on
        BEGIN
          vergleich_10g.vergleichen(
               p_local => 'local',
              p_remote => 'remote',
            p_basistab => 'emp_local',
             p_zieltab => 'emp_remote',
                p_link => 'remote_link'
                p_sync => true);
        END;
        /
        Es wurden 97 abweichende Datensätze gefunden und in die Tabelle Diff eingetragen
        Beginn der Synchronisation
        Synchronisation abgeschlossen
         

         

        VERGLEICH UND SYNCHRONISATION MIT DEM PACKAGE VERGLEICH_11G


        Das Vergleichspackage für die Version 11g bietet ähnliche Optionen wie vergleich_10g. Für die Befüllung der Differenzentabelle und die Synchronisation gibt es hier aber eigene Prozeduren, da DBMS_COMPARISON Vergleichstemplate erstellt und man die einzelnen Vergleiche über ihren Namen ansprechen kann.


        -- Vergleich der Hash-Werte
        conn local/local
        set serveroutput on
        BEGIN
          vergleich_11g.vergleichen(
                 p_local => 'local1',
              p_basistab => 'emp_local1',
               p_zieltab => 'emp_remote',
                  p_link => 'remote_link'
              p_hashonly => TRUE);
        END;
        /
        Die Objekte stimmen nicht ueberein
        --Ermittlung der Unterschiede
        BEGIN
          vergleich_11g.vergleichen(
               p_local => 'local1',
              p_remote => 'remote',
            p_basistab => 'emp_local1',
             p_zieltab => 'emp_remote'
                p_link => 'remote_link');
        END;
        /
        Vergleichsname: local1_emp_local1_41, aktuelle Scan-Nr: 141
        Es wurden 76 Unterschiede gefunden.
        -- Füllen der Differenzen-Tabelle
        BEGIN
          vergleich_11g.diff_uebersicht (p_vergleichsname => 'local1_emp_local1_41');
        END;
        /
        21 Zeilen haben unterschiedliche Werte
        27 Zeilen fehlen in der Basistabelle
        28 Zeilen fehlen in der zieltabelle
        -- Dieser Select liefert die Pk-Werte der unterschiedlichen Datensätze
        SELECT pk_wert, in_basis "in emp_local vorhanden", in_vergleich "in emp_remote vorhanden"
        FROM diff_11g;
        =>
           PK_WERT in emp_local vorhanden in emp_remote vorhanden
        ---------- ---------------------- -----------------------
               100 ja                     ja
               104 ja                     ja
             &n

        Verzeichnisse Auslesen in PL/SQL

        Bereich:PL/SQL, Version: ab RDBMS 11.2, Letzte Überarbeitung: 11.12.2018

        Keywords:PL/SQL

        Bereits seit der Version 10.1 kann man mit einer (undokumentierten) Funktion den Inhalt von Betriebssystemordnern auslesen. Diese Prozedur wird vom RMAN bei folgendem Befehl verwendet:

        RMAN> CATALOG START WITH c:\temp;

        Hier werden alle Dateien des Ordners c:\temp und auch der Unterordner gelesen und geprüft, ob es sich um eine Oracle-Datei handelt.

        Die folgende PL/SQL-Prozedur liest nur den Inhalt des Ordners c:\temp aus:

        SET SERVEROUTPUT ON
        DECLARE
           ns    VARCHAR2(1024);
           v_dir VARCHAR2(1024):='c:\temp';
        BEGIN
           DBMS_BACKUP_RESTORE.SEARCHFILES(v_dir, ns);
           FOR r IN (SELECT fname_krbmsft as name FROM x$krbmsft) LOOP
              DBMS_OUTPUT.PUT_LINE(r.name);
           END LOOP;
        END;
        /

        Ergibt z. B. die Ausgabe:

        C:\Temp\dozent2_tuning.txt
        C:\Temp\perl_test.pl
        C:\Temp\Muniqsoft_Backup.7z

        Leider darf nur der Benutzer SYS dieses Package ausführen. Wenn auch ein anderer Benutzer in den Genuss dieser Prozedur kommen soll, stehen zwei Varianten zur Verfügung:

        1. VARIANTE: DER BENUTZER BEKOMMT EIN AUSFÜHRUNGSRECHT AN DIESEM PACKAGE.


        Der Nachteil dieser Variante ist, dass dieses Package zwar viele interessante aber auch gefährliche Nutzungsmöglichkeiten bietet.

        GRANT EXECUTE ON DBMS_BACKUP_RESTORE TO SYSTEM;

        Danach muss (im Schema SYS!) eine View auf die fixed table x$krbmsft erstellt werden (Selectrechte an Fixed Tables können nicht direkt vergeben werden):

        CREATE VIEW sys.x$krbmsft_muso_view AS SELECT * FROM sys.x$krbmsft;

        Der User SYSTEM bekommt das Select-Recht auf diese View:

        GRANT SELECT ON sys.x$krbmsft_muso_view TO SYSTEM;

        Jetzt kann man sich als SYSTEM anmelden und schreibt die Prozedur etwas um:

        SET SERVEROUTPUT ON
        DECLARE
            ns   VARCHAR2(1024);
           v_dir VARCHAR2(1024):= 'c:\temp';
        BEGIN
           SYS.DBMS_BACKUP_RESTORE.SEARCHFILES(v_dir, ns);
           FOR r IN (SELECT fname_krbmsft as name FROM sys.x$krbmsft_muso_view) LOOP
             DBMS_OUTPUT.PUT_LINE(r.name);
           END LOOP;
        END;
        /


        2. VARIANTE: WIR SCHREIBEN EINEN WRAPPER UM DEN PACKAGE-AUFRUF, DER UNS NUR ERMÖGLICHT, DIESE EINE PROZEDUR DES PACKAGES ZU NUTZEN


        CREATE OR REPLACE PROCEDURE sys.muso_backup_restore_sfiles(
             v_dir IN OUT VARCHAR2,
             ns       OUT VARCHAR2)
        IS
        BEGIN
           DBMS_BACKUP_RESTORE.SEARCHFILES(v_dir, ns);
        END;
        /

        Dann vergeben wir nur ein Ausführungsrecht an der Prozedur (nicht mehr am kompletten Package!):

        GRANT EXECUTE ON sys.muso_backup_restore_sfiles TO SYSTEM;
        REVOKE EXECUTE ON DBMS_BACKUP_RESTORE FROM SYSTEM;

         

        connect system/sys
        SET SERVEROUTPUT ON
        DECLARE
              ns VARCHAR2(1024);
           v_dir VARCHAR2(1024):='c:\temp';
        BEGIN
           sys.muso_backup_restore_sfiles(v_dir, ns);
           FOR r IN ( SELECT fname_krbmsft as name FROM sys.x$krbmsft_muso_view) LOOP
               DBMS_OUTPUT.PUT_LINE(r.name);
           END LOOP;
        END;
        /


        Diese Prozedur bietet viele Einsatzmöglichkeiten, z. B.:

        • Ein komplettes Verzeichnis (mit Bildern, PDF's oder Word Dokumenten) in BLOB Feldern speichern...
        • Ordner per FTP in die (in jeder Oracle-Version vorhanden) XML-Datenbank hochladen.
        • Prüfen, ob neue Dateien in einem Betriebssystem-Ordner vorhanden sind, und diese mit utl_file oder External Table einlesen.


        Diese und weitere Beispiele lernen Sie u. a. in unserem PL/SQL II oder Reorg und Wartungskurs.
        Besuchen Sie uns doch mal, wir freuen uns auf Sie !



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Dynamische Ausführung von Befehlen

        Bereich:PL/SQL, Version: ab RDBMS 8.x, Letzte Überarbeitung: 29.06.2018

        Keywords:PL/SQL

        Seit Jahren stellt sich mir immer wieder das gleiche Problem:
        Wie kann ich eine Aktion x-Mal in der Datenbank ausführen?

        z.B. Wie kann ich 100 Indizes in einen anderen Tablespace verschieben?
        Bisher war meine Lösung immer:

        SPOOL c:\temp\move_index.sql
        SELECT 'ALTER INDEX '||owner||'.'||index_name||' REBUILD TABLESPACE indx_tbs;'
        FROM dba_indexes
        WHERE index_name like 'PK_%''';  /*da sind ein paar Hochkommata zu viel (evtl. den Underscore noch mit Escape-Zeichen versehen?)*/
        SPOOL OFF
        @c:\temp\move_index.sql

        Dann bekommt man ein paar Fehler durch die Formatierung ausgegeben (kann man durch geschickte SET Kommandos ausblenden), trotzdem funktioniert es.

        Was macht man jedoch, wenn man kein Laufwerk hat, wo man die Spool Datei ablegen darf/kann? Oder wenn kein SQL*Plus zur Verfügung steht? Hier hilft ein kleines simples PL/SQL Skript, dass die Funktionalität nachbaut. Ich habe es eval genannt als Hommage an das Unix Kommando eval(uate).

        CREATE OR REPLACE FUNCTION sys.my_eval (cmd IN VARCHAR2)
        RETURN VARCHAR2
        AUTHID CURRENT_USER
        IS
        PRAGMA AUTONOMOUS_TRANSACTION; /* Damit ist die Funktion eigenständig und beeinflusst niemanden :-) */
        v_cmd VARCHAR2(32000);
        BEGIN
        IF substr(cmd,length(cmd),1)=';' THEN -- Semikolon ggf. herausfiltern
          v_cmd:=substr(cmd,1,length(cmd)-1);
        ELSE
         v_cmd:=cmd;
        END IF;
        EXECUTE IMMEDIATE v_cmd; -- Befehl ausführen
        RETURN v_cmd||'; --OK'; -- Befehl erfolgreich ausgeführt
        EXCEPTION WHEN OTHERS THEN
         RETURN v_cmd||' --'||sqlerrm;
        END;
        /

        Warnung: Die Funktion führt den Befehl sofort aus! Da gibt es keine Rückfrage mehr, wie SIND SIE SICHER?? :-)
        Eigentlich dürfen/sollen Funktionen solche Sachen gar nicht machen :-), aber wir sind ja Advanced User :-)

        Anwendungsbeispiele:

        Löschen aller Tabellen des Benutzers SCOTT, die mit Tab beginnen:

        SELECT my_eval('drop table scott."'||table_name||'"') /* Hinweis der Tabellenname ist in doppelte Hochkommas gesetzt !*/
        FROM dba_tables
        WHERE table_name like 'TAB%';

        Alle Tabellen im gleichen Tablespace reorganisieren:

        SELECT my_eval('alter table scott."'||table_name||'" move') 
        FROM dba_tables
        WHERE owner='SCOTT'

        Alle Indizes neu kompilieren, die ungültig sind:

        SELECT my_eval('ALTER INDEX '||owner||'."'||index_name||'" REBUILD ONLINE')
        FROM dba_indexes WHERE status<>'VALID' and PARTITIONED='NO'

         

        Wie immer gilt:
        Wollen Sie mehr aus Ihrer Datenbank machen? Kommen Sie in eine unserer Schulungen.



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Function Based Indices - Spezialfälle

        Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 11.12.2018

        Keywords:DBA, PL/SQL

        Die häufigsten Einsatzfälle eines Function Based Index dürften sein:

        • eine Suche unabhängig von Groß-/Kleinschreibung zu machen, indem man einen Index auf UPPER(spalte) legt, oder
        • normalerweise nicht indizierte - NULL-Werte für die Indizierung zugänglich zu machen durch NVL(spalte, ...).

        An dieser Stelle sollen zwei besondere Einsatzmöglichkeiten solcher Indices vorgestellt werden.

        ERZWINGUNG SELEKTIVER EINDEUTIGKEIT


        Wir hatten schon öfter in Applikationen folgendes Szenario: In Tabellen soll nicht physikalisch gelöscht werden, sondern nur logisch durch Setzen eines Flags. Gleichzeitig soll ein bestimmtes Merkmal (z.B. ein Name) nur innerhalb der aktiven Einträge eindeutig sein, um anhand dieses - in einer Maske angezeigten - Kriteriums suchen zu können. Ein Beispiel zur Demonstration des Problems:

        CREATE TABLE my_tab(nr NUMBER, NAME VARCHAR2(100), status CHAR(1));
        DECLARE
           v_status   CHAR (1);
           v_name     VARCHAR2 (100);
        BEGIN
           FOR i IN 1 .. 10000
           LOOP
              IF MOD (i, 1000) = 0
              THEN
                 v_status := 'A';
              ELSE
                 v_status := 'I';
              END IF;
              
              v_name := 'DEFAULT_' || TRUNC( i/1000);
              INSERT INTO MY_TAB(nr, NAME, status )
               VALUES (i, v_name, v_status );
           END LOOP;
        END;
        /
        SELECT status, COUNT (DISTINCT NAME) distinct_pro_status,
               COUNT (*) gesamt_pro_status,
               (SELECT COUNT (*) FROM MY_TAB) gesamt
          FROM MY_TAB GROUP BY status;
        S DISTINCT_PRO_STATUS GESAMT_PRO_STATUS     GESAMT
        - ------------------- ----------------- ----------
        A                  10                10      10000
        I                  10              9990      10000

        Ein "normaler" UNIQUE Index hilft hier nicht weiter. Kein Name ist Unique, und auch die Kombination aus Status und Name ist es nicht:

        CREATE UNIQUE INDEX my_tab_idx ON my_tab(NAME, status);
        FEHLER in Zeile 1:
        ORA-01452: CREATE UNIQUE INDEX nicht ausführbar; doppelte Schlüssel gefunden

        Was tun? Ein Ansatz wäre, vor neuen Einträgen zunächst die Anzahl aktiver Datensätze mit diesem Wert zu zählen, und in dem Fall, dass die Anzahl > 0 ist, den Wert abzulehnen. Sinnvoller wäre es aber, wenn die Datenbank selber solche Anforderungen erzwingen würde. Dann können sie in keinem Fall umgangen werden. Hier kann ein Funktionsbasierter Index weiterhelfen. Der Trick dabei ist, dass NULL-Werte nicht indiziert werden:

        CREATE UNIQUE INDEX my_tab_idx ON my_tab(CASE status WHEN 'A' THEN NAME END);
        -- Alternative:
        --CREATE UNIQUE INDEX my_tab_idx ON my_tab(DECODE(status, 'A',NAME));
        Index wurde erstellt.
        INSERT INTO my_tab (nr, NAME, status )
        VALUES (10001, 'DEFAULT_1', 'A' );
        FEHLER in Zeile 1:
        ORA-00001: Unique Constraint (SCOTT.MY_TAB_IDX) verletzt
        INSERT INTO my_tab (nr, NAME, status )
        VALUES (10001, 'DEFAULT_1', 'I' );
        1 Zeile wurde erstellt.

        Ziel erreicht. Die Eindeutigkeit für aktive Datensätze - und nur für aktive Datensätze - wird durch die Datenbank erzwungen.

        INDIZIERUNG EINES BESTIMMTEN WERTES

        Eine andere Fragestellung: Sie haben in Ihrem Workflow eine Tabelle mit zu erledigenden Aufträgen. Es werden laufend neue Einträge mit dem Status 'WAITING' eingetragen. Ein Job arbeitet die Aufträge ab und setzt dabei den Status z.B. zuerst auf 'IN PROGRESS' und am Ende auf 'DONE'. Sie werden sehr bald sehr viele Einträge haben, wobei in der Spalte "Status" nur drei (oder vier: ERROR im Fehlerfall) Werte stehen. Nur ein sehr geringer Prozentsatz davon wird WAITING sein. Aber die sollen effektiv gefunden werden.

        Vielleicht denken Sie jetzt: "Wenige unterschiedliche Werte? Aha - Bitmap Index".  Nun, das wäre das schlechteste, was Sie tun können, da die Tabelle laufend geändert wird.

        Alternative 2 wäre ein normaler B*Tree-Index mit Histogrammen. Das funktionert, solange Sie beim Status nicht mit Bind-Variablen arbeiten und ausgerechnet bei der ersten Ausführung nach einem anderen Status gesucht haben. Allerdings wird der Index unnnötig groß.

        Alternative 3: Sie gehen genauso vor wie im ersten Beispiel beschrieben:

        CREATE TABLE tasks(nr NUMBER, TO_DO VARCHAR2(100), status VARCHAR2(20));
        DECLARE
           v_status   VARCHAR2(20);
           v_task     VARCHAR2 (100);
        BEGIN
           FOR i IN 1 .. 100000
           LOOP
              IF MOD (i, 10000) = 0
              THEN
                 v_status := 'WAITING';
              ELSE
                 v_status := 'DONE';
              END IF;
              v_task := 'Tu was';
              INSERT INTO TASKS(nr, TO_DO, status )
               VALUES (i, v_task, v_status );
           END LOOP;
        END;
        /
        SELECT COUNT (*) gesamt, SUM (DECODE (status, 'WAITING', 1, 0)) wartend  
          FROM tasks;
            GESAMT    WARTEND
        ---------- ----------
            100000         10
        CREATE INDEX status_idx ON tasks(CASE status WHEN 'WAITING' THEN 1 END);
        EXEC dbms_stats.gather_table_stats('SCOTT', 'TASKS', cascade=> true)

        Selbstverständlich können Sie hier auch mit DECODE arbeiten, und es ist letztlich unerheblich, welcher Wert GENAU im Index steht. Es könnte genauso gut 'WAITING' selber sein.

        Hier ist die Zielsetzung eine andere als oben: Der Zugriff soll beschleunigt werden. Entsprechend muss nun beim SELECT-Befehl darauf geachtet werden, dass der Index auch wirklich verwendet wird. Das heisst, die Funktion muss auch in der WHERE-Klausel stehen:

        set autotrace traceonly explain
        SELECT to_do FROM tasks WHERE status = 'WAITING';
        Ausführungsplan
        ----------------------------------------------------------
        Plan hash value: 41374823
        ---------------------------------------------------------------------------
        | Id  | Operation         | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
        ---------------------------------------------------------------------------
        |   0 | SELECT STATEMENT  |       |     1 |    13 |    86   (4)| 00:00:02 |
        |*  1 |  TABLE ACCESS FULL| TASKS |     1 |    13 |    86   (4)| 00:00:02 |
        ---------------------------------------------------------------------------
        --> kein Indexzugriff

         

        SELECT to_do FROM tasks WHERE (CASE status WHEN 'WAITING' then 1 END) = 1;
        Ausführungsplan
        ----------------------------------------------------------
        Plan hash value: 1504895482
        -------------------------------------------------------------------------------
        |Id |Operation                   |Name      |Rows |Bytes |Cost (%CPU)|Time    |
        -------------------------------------------------------------------------------
        |  0|SELECT STATEMENT            |          |  10 |  130 |  11    (0)|00:00:01|
        |  1| TABLE ACCESS BY INDEX ROWID|TASKS     |  10 |  130 |  11    (0)|00:00:01|
        |* 2|  INDEX RANGE SCAN          |STATUS_IDX|  10 |      |   1    (0)|00:00:01|
        -------------------------------------------------------------------------------
        --> Index wird verwendet

         

        Anmerkung: Aus Platzgründen wurden Leerzeichen aus dem Plan entfernt

        Wenn Sie nicht - oder nicht mehr - wissen, wie ein funktionsbasierter Index angelegt wurde, schauen Sie in DBA_IND_EXPRESSIONS nach.

        Funktionsbasierte Indices sind erfreulicherweise seit Version 9.2 auch in der Standard Editon von Oracle enthalten; davor waren sie auf die Enterprise Edition beschränkt.

        Quelle:
        Thomas Kyte, Expert Oracle Database Architecture. 9i and 10g Programming Techniques and Solutions, Apress, 2005



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Tracing mit DBMS_Monitor

        Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 04.07.2018

        Keywords:DBA, PL/SQL, Standard Packages, Tracing

        TRACING einer Session


        In einem früheren Beitrag wurde das Tracing einer Datenbank-Session bereits generell beschrieben, daher soll hier nicht näher auf die Grundlagen eingegangen werden. Neben den dort vorgestellten Methoden gibt es seit Version 10g noch das Package DBMS_MONITOR, das diverse Möglichkeiten zum Tracing bietet.

        Analog zu den bekannten Möglichkeiten bietet auch DBMS_MONITOR eine Prozedur an, um anhand von SID und SERIAL# (aus v$session) für eine bestimmte Session Tracing zu aktivieren: SESSION_TRACE_ENABLE. Die ersten beiden Parameter (session_id und serial_num) identifizieren die Session, für die Tracing aktiviert werden soll. Werden sie weggelassen (bzw. wird NULL für sie übergeben), so ist die eigene Session gemeint. Der dritte (waits) und vierte (binds) Parameter legt fest, ob Informationen über Wartezustände bzw. Bind-Variablen gesammelt werden sollen (TRUE) oder nicht (FALSE).

        -- eigene Session, Waits und Binds:
        EXEC DBMS_MONITOR.SESSION_TRACE_ENABLE(NULL, NULL, TRUE, TRUE)
        -- fremde Session (SID = 128, serial# = 47), nur Waits:
        EXEC DBMS_MONITOR.SESSION_TRACE_ENABLE(128, 47, TRUE, FALSE)

        Ob Tracing eingeschaltet ist, und wenn ja, wofür, ist in v$session in den Spalten sql_trace, sql_trace_waits und sql_trace_binds ersichtlich (manchmal aber erst, wenn diese Session aktiv wurde):

        SELECT sql_trace, sql_trace_waits, sql_trace_binds
        FROM v$session WHERE username = 'SCOTT';
        SQL_TRAC SQL_T SQL_T
        -------- ----- -----
        ENABLED  TRUE  FALSE

        Ausgeschaltet wird diese Art des Tracing mit SESSION_TRACE_DISABLE:

        EXEC DBMS_MONITOR.SESSION_TRACE_DISABLE(NULL, NULL)
        EXEC DBMS_MONITOR.SESSION_TRACE_DISABLE(123, 99)

        So weit, so gut. Aber dafür braucht man doch kein neues Package? Richtig, aber DBMS_MONITOR bietet auch Möglichkeiten, die NICHT an die SID einer Session gebunden sind. Das Package ist vor allem dafür gedacht, 3-Tier-Applikationen zu tracen, bei denen keine durchgehende Session zwischen Endbenutzer und Datenbank besteht.

        TRACING anhand eines Client Identifiers


        Um dieses Feature nutzen zu können, muss ein Client Identifier gesetzt sein. Hier sind die Applikationsentwickler gefordert, einen entsprechenden Aufruf einzubauen, wenn eine Verbindung zu der Datenbank hergestellt wird:

        EXEC DBMS_SESSION.SET_IDENTIFIER('SCOTT')

        Dieser Identifier ist auch sichtbar in v$session. Die Spalte heisst client_identifier.

        Anstelle der SID dient nun dieser Identifier als Identifikationsmerkmal, was mitprotokolliert werden soll. Tracing wird in diesem Fall eingeschaltet mit:

        -- mit waits, ohne Binds:
        EXEC DBMS_MONITOR.CLIENT_ID_TRACE_ENABLE('SCOTT')
        -- mit waits, mit Binds wäre:
        -- EXEC DBMS_MONITOR.CLIENT_ID_TRACE_ENABLE('SCOTT', TRUE, TRUE)

        Diese Art von Tracing verhält sich in einigen Aspekten komplett anders als klassisches Session-Tracing:

        • Es ist sichtbar in DBA_ENABLED_TRACES:

        SELECT primary_id,  waits, binds
          FROM DBA_ENABLED_TRACES
         WHERE trace_type = 'CLIENT_ID';
        PRIMARY_ID WAITS BINDS
        ---------- ----- -----
        USER_1     TRUE  FALSE
        • Es ist nicht an bestimmte Sessions gebunden; daher ist es auch NICHT sichtbar in den entsprechenden Spalten in v$session
        • Es bleibt auch bei einem Neustart der Datenbank aktiv(!). Es sollte daher auf keinen Fall vergessen werden, das Tracing wieder auszuschalten, wenn es nicht mehr nötig ist.


        Ausgeschaltet wird es durch:

        EXEC DBMS_MONITOR.CLIENT_ID_TRACE_DISABLE('SCOTT')

        Die dabei erzeugten Tracefiles können in bekannter Art mit TKPROF ausgewertet werden. Da es hier jedoch zu einem Identifier in der Regel mehrere Tracefiles gibt, ist eine Einzelauswertung mühsam. Vorteilhafter ist es, alle zugehörigen Tracefiles erst zusammenzuführen mit trcsess, einer ebenfalls mit 10g neu eingeführten Utility, und dann das Output-File zu analysieren:

        trcsess output=SCOTT.txt clientid=SCOTT *

        Der "*" bedeutet, es sollen alle Tracefiles einbezogen werden. trcsess unterstützt auch Tracefiles aus klassischem Session-Tracing. Eine Übersicht der Syntax erhalten Sie, wenn Sie trcsess  ohne Parameter aufrufen.

        TRACING anhand von Module und Action


        DBMS_MONITOR bietet als dritte Alternative die Möglichkeit, anhand von Service-Name, Module-Name und Action-Name Tracing einzuschalten. Wird kein Modulname mitgegeben, dann wird standardmäßig für alle Module Tracing aktiviert. Sofern nicht explizit eine bestimmte Instanz angegegben wird, gilt das Tracing für alle Instanzen, die über den angegbenen Service-Namen angesprochen werden. Auch diese Art von Tracing bleibt bei einem Neustart bestehen und muss explizit beendet werden.

        Sowohl Module-Name als auch Action-Name können in einer Session über einen DBMS_APPLICATION_INFO-Aufruf gesetzt werden. Das Package ist dafür gedacht, innerhalb einer Applikation Informationen darüber zu liefern, was gerade passiert, z. B. mit welchem Formular (module) der Anwender gerade welche Aktivität (action) ausführt:

        -- setzt module und action
        EXEC DBMS_APPLICATION_INFO.SET_MODULE ( 'Modul_1', 'testen')
        -- setzt nur action neu
        EXEC DBMS_APPLICATION_INFO.SET_ACTION ('abrechnen')

        Beides ist sowohl in v$session als auch in v$sqlarea sichtbar, die Spalten heissen module und action.

        Wird beim Aufruf nur ein Service-Name mitgegeben, dann ist das Tracing für ALLE Module aktiviert (Default), wird für module_name NULL mitgegeben, dann gilt das Tracing für alle Sessions, bei denen kein module_name gesetzt wurde. Der Aufruf ist streng hierarchisch, das heisst, action_name wird nur im Zusammenhang mit module_name wirksam. Es wird nur protokolliert, wenn alle drei Eigenschaften übereinstimmen.

        BEGIN
           DBMS_MONITOR.SERV_MOD_ACT_TRACE_ENABLE(service_name => 'o18c',
                                                  module_name  => 'Modul_1',
                                                  action_name  => 'testen',
                                                  waits        => FALSE,
                                                  binds        => FALSE);
        END;  
        /
        SELECT trace_type, primary_id, qualifier_id1, qualifier_id2, waits, binds
          FROM DBA_ENABLED_TRACES;
        TRACE_TYPE            PRIMARY_ID QUALIFIER_ID1   QUALIFIER_ID2   WAITS BINDS
        --------------------- ---------- --------------- --------------- ----- -----
        SERVICE_MODULE_ACTION o10g       Modul_1         testen          FALSE FALSE

        Das Tracing wird wieder ausgeschaltet durch die entprechende DISABLE-Methode:

        BEGIN
           DBMS_MONITOR.SERV_MOD_ACT_TRACE_DISABLE(service_name => 'o18c',
                                                  module_name  => 'Modul_1',
                                                  action_name  => 'testen');
        END;  
        /

        Auch hier können Tracefiles über trcsess zusammengefasst werden.  Die entscheidenden Parameter lauten in diesem Fall service, module und action:

        trcsess output=modul.txt service=o18c module=Modul_1 action=testen *


        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Oracle Text I

        Bereich:PL/SQL, Version: ab RDBMS 8.x, Letzte Überarbeitung: 19.03.2020

        Keywords:SQL, PL/SQL, Standard Packages

        Seit der Version 7 bietet Oracle die Möglichkeit der Volltextsuche. Seit Version 9i ist Oracle Text jedoch fester Bestandteil der Datenbank, auch in der Express Edition.
        Das kann man leicht nachprüfen, indem man nach dem User ctxsys sucht:

        conn scott/tiger
        SELECT * FROM all_users WHERE username = 'CTXSYS';
        =>
        USERNAME                          USER_ID CREATED
        ------------------------------ ---------- --------
        CTXSYS                                 25 07.02.20


        Die Basisfunktionalitäten kann man ohne zusätzliche Rechte nutzen, für die Anpassung der Sucheinstellungen braucht man jedoch mindestens das Execute-Recht an dem wichtigsten Package des Schemas ctxsys: ctx_ddl oder die Rolle ctxapp.

        conn / AS sysdba
        GRANT EXECUTE ON ctx_ddl TO scott;
        oder
        GRANT ctxapp TO scott;

         
        WIE FUNKTIONIERT ORACLE TEXT ?
        Die Dokumente werden in einen so genannten Datastore eingelesen. Die zu indizierenden Texte können dabei entweder in CLOB-, VARCHAR2- oder XMLTYPE-Spalten einer Tabelle in der Datenbank liegen (direct_datastore), im Filesystem des Datenbankservers (file_datastore) oder im Inter- bzw. Intranet (url_datastore). Es besteht sogar die Möglichkeit, die Texte über eine selbst definierte Prozedur direkt vor der Indizierung zusammenzustellen (user_datastore).
        Im 2. Schritt werden die Objekte im Bedarfsfall gefiltert. Das ist nur dann nötig, wenn es sich um binäre Files, wie Word-Dokumente oder PDF-Dateien handelt. Text-, HTML- und XML-Dateien müssen nicht gefiltert werden. Oracle erkennt über 150 Formate automatisch.
        Der Sectioner kann HTML- oder XML-Dokumente anhand von Tags (z.B. <H1> ...</H1> in HTML oder <Produktbeschreibung> .... </Produktbeschreibung> in XML) in einzelne Abschnitte aufteilen.
        Der Lexer extrahiert alle relevanten Wörter aus dem Text. Interpunktions- und Sonderzeichen werden entfernt. Bei diesem Schritt kann man u.a. einstellen,
        was als Trennzeichen gewertet bzw. ignoriert werden soll (Leerzeichen, Unterstriche etc.)
        ob Groß- und Kleinschreibung beibehalten werden soll,
        ob zusammengesetzte Worte in ihre Einzelteile zerlegt werden sollen, etc.
        Beim Indizierungsprozeß wird aus den gesammelten Wörtern ein invertierter Index erzeugt. Jedem Wort wird dabei eine Liste seiner Fundstellen zugeordnet.
        Artikel, Konjunktionen, Präpositionen und Hilfsverben etc., bei Oracle Text Stopwörter genannt, werden nicht indiziert.
        Die Einstellungen der sog. wordlist legen fest, welche grammatikalischen Regeln verwendet werden sollen, damit bei der Textsuche auch Beugungsformen des gesuchten Verbs oder Wörter mit ähnlichem Stamm erkannt werden.
        In diesem Monatstipp sollen zunächst die grundlegenden Funktionen der Volltextsuche am Beispiel eines Context-Indexes auf einer Text-Spalte erklärt werden.
        Die Beispiel-Tabelle, eine Sammlung von Sprichwörtern (mit diversen Schreibfehlern und kleinen Abwandlungen) können Sie (Initiates file download) hier als SQL-File herunterladen.

         
        VOLLTEXTSUCHE IN TEXTSPALTEN
        Wir erstellen die Tabelle und erzeugen erstmal einen Index ohne zusätzliche Parameter:

        @ d:\sprichwoerter
        CREATE INDEX sprueche_idx ON sprichwoerter(text)
        INDEXTYPE IS ctxsys.context;

        Die Suche in einem Context-Index wird über das Schlüsselwort CONTAINS durchgeführt:

        SELECT spaltenliste FROM tabelle
        WHERE CONTAINS(index_spalte, '<suchbegriff>')>0;

        Die wichtigsten Suchmöglichkeiten:

        1. EINFACHE SUCHE NACH WÖRTERN Z.B:


        SELECT * FROM sprichwoerter WHERE CONTAINS(text, 'Abend') > 0;
        =>
        NUMMER TEXT
        ------ -------------------------------------------
            50 Es ist noch nicht aller Tage Abend
            89 Man soll den Tag nicht vor dem Abend loben


        2. SUCHE NACH WORT-KOMBINATIONEN ODER -ALTERNATIVEN MIT DEN BOOLESCHEN OPERATOREN "AND" UND "OR":

        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, 'Gold AND Silber') > 0;
        =>
        NUMMER TEXT
        ------ ----------------------------------------
           103 Reden ist Silber, Schweigen ist Gold
        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, 'Abend OR Morgen') > 0;
        =>
        NUMMER TEXT
        ------ ---------------------------------------------------------
            50 Es ist noch nicht aller Tage Abend
            89 Man soll den Tag nicht vor dem Abend loben
            92 Morgen, morgen, nur nicht heute, sagen alle faulen Leute


        3. SUCHE NACH ÄHNLICH GESCHRIEBENEN WÖRTERN:
        Wenn man den Operator "?" vor das gesuchte Wort stellt, kann man auch Wörter mit Rechtschreibfehlern oder Buchstabendrehern finden:

        SELECT * FROM sprichwoerter WHERE CONTAINS(text, '?Länder') > 0;
        =>
        NUMMER TEXT
        ------ ------------------------------
             8 Andere Lender, andere Sitten.


        Dann bekommt man aber leicht unerwünschte  Ergebnisse  - wie hier Messer statt Meister.

        SELECT * FROM sprichwoerter WHERE CONTAINS(text, '?Meister') > 0;
        =>
        NUMMER TEXT
        ------ ------------------------------------------------------------
            45 Einmal findet jeder seinen Meister.
            49 Es ist noch kein Meitser vom Himmel gefallen, aber vom Gerüst.
            90 Messer, Gabel, Scher' und Licht sind für kleine Kinder nicht.
           113 Übung macht den Meister


        Um die Möglichkeiten einzugrenzen, kann man das Schlüsselwort Fuzzy einsetzen, z.B.:

        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, 'FUZZY(Meister, 70, 5, N)') > 0;
        =>
        NUMMER TEXT
        ------ ---------------------------------------------------------
            45 Einmal findet jeder seinen Meister.
            49 Es ist noch kein Meitser vom Himmel gefallen, aber vom Gerüst.
           113 Übung macht den Meister


        Der 1. Parameter bestimmt die Ähnlichkeit (minimal = 1, identisch = 80, default = 60). Daraus resultiert eine Liste ähnlicher Worte (hier Meister, Meitser und Messer). Mit diesen Wörtern wird dann eine oder-Suche durchgeführt.
        Der 2. Parameter bestimmt die maximale Anzahl der Wörter dieser Liste (1- 5000, default = 100).
        Der 3. Parameter wirkt sich nur auf die Relevanzgewichtung (s.u. Score) aus.


        4. SUCHE MIT WILDCARDS: "%"  FÜR KEIN ODER BELIEBIG VIELE ZEICHEN UND "_"  FÜR GENAU 1 ZEICHEN:

         SELECT * FROM sprichwoerter WHERE CONTAINS(text, '_elbst%') > 0;
        =>
        NUMMER TEXT
        ------ -----------------------------------------------------
           109 Selbst ist der Mann!
           110 Selbsterkenntnis ist der erste Weg zur Besserung.

        Eigentlich erwartet man hier auch Ergebnisse, die das Wort "selbst" enthalten. Das wird aber als Stopwort behandelt und nicht indiziert.


        5. SUCHE NACH ÄHNLICH KLINGENDEN WÖRTERN (SOUNDEX-FUNKTION) MIT DEM OPERATOR "!":
        Da die Soundex-Funktion auf englische Phonetik ausgerichtet ist, kann man dabei aber durchaus Überraschungen erleben (wie die zum Hasen mutierte Katze im Beispiel).

        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, '!Kaze') > 0;
        =>
        NUMMER TEXT
        ------ ---------------------------------------------------------------
            19 Da liegt der Hase im Pfeffer.
            28 Die Katze lässt das Mausen nicht.
            87 Lügen haben kurze Beine
           129 Wenn die Katze aus dem Haus ist, tanzen die Mäuse auf dem Tisch.


        6. SUCHE NACH AUSDRÜCKEN, DIE DENSELBEN WORTSTAMM HABEN WIE DAS SUCHWORT ODER MIT DEM SUCHWORT ZUSAMMENGESETZTE WORTE BILDEN, MIT DEM OPERATOR "$":

        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, '$helfen') > 0;
        =>
        NUMMER TEXT
        ------ -------------------------------------------------
            31 Dumm bleibt dumm, da helfen keine Pillen!
            66 Hilf dir selbst, so hilft dir Gott!

        oder

        SELECT * FROM sprichwoerter
        WHERE CONTAINS(text, '$Porzellan') > 0;
        =>
        NUMMER TEXT
        ------ ---------------------------------------------
           120 Vorsicht ist die Mutter der Porzellankiste.

        Enterprise-Edition:


        PRV_OWNER  PRV_PREFERENCE      PRV_ATTRIBUTE             PRV_VALUE
        ---------- ------------------- ------------------------- ----------------------------
        CTXSYS     DEFAULT_WORDLIST    STEMMER                   GERMAN
        CTXSYS     DEFAULT_WORDLIST    FUZZY_MATCH               GERMAN
        CTXSYS     URL_DATASTORE       TIMEOUT                   30
        CTXSYS     URL_DATASTORE       MAXTHREADS                8
        CTXSYS     URL_DATASTORE       URLSIZE                   256
        CTXSYS     URL_DATASTORE       MAXURLS                   256
        CTXSYS     URL_DATASTORE       MAXDOCSIZE                2097152
        CTXSYS     DEFAULT_LEXER       COMPOSITE                 GERMAN
        CTXSYS     DEFAULT_LEXER       MIXED_CASE                YES
        CTXSYS     DEFAULT_LEXER       ALTERNATE_SPELLING        GERMAN
        CTXSYS     DEFAULT_STORAGE     R_TABLE_CLAUSE            lob (data) store AS (cache)
        CTXSYS     DEFAULT_STORAGE     I_INDEX_CLAUSE            compress 2


        Erklärungen:

        STEMMER: legt die Grammatik für Stamm- und Beugungsformen fest. Die Voreinstellung GERMAN führt dazu, dass z.B. die Suche nach $laufen auch Formen wie lief, gelaufen, läuft, etc. liefert.
        FUZZY_MATCH: bestimmt, nach welcher Routine ähnlich geschriebene Wörter gesucht werden (hier sind allerdings nicht allzu viele Unterschiede zwischen GERMAN und GENERIC festzustellen)
        Die Einstellung COMPOSITE = GERMAN im deutschen Lexer ermöglicht die Zerlegung eines Wortes im Index in seine Bestandteile (Neben Porzellankiste wird im Index auch Porzellan und Kiste gespeichert).
        MIXED_CASE = YES bedeutet, dass Groß- und Kleinschreibung unterschieden wird.
        ALTERNATE_SPELLING = GERMAN bewirkt, dass Wörter in alternativen Schreibweisen im Index gespeichert werden, also z.B. Töchter als Toechter und bisschen als bißchen.

        Ein weiterer Unterschied zeigt sich bei den oben erwähnten Stopwörtern, die von Oracle Text nicht indiziert werden, weil sie in jedem Text vorkommen, wie z.B. Artikel, Konjunktionen und Präpositionen. Diese Listen kann man einsehen:

        SELECT * FROM ctx_stoplists WHERE spl_owner = 'CTXSYS';
        =>
        -- 10g und 11g-Enterprise-Edition
        SPL_OWNER  SPL_NAME              SPL_COUNT SPL_TYPE
        ---------- -------------------- ---------- ---------------
        CTXSYS     EMPTY_STOPLIST                0 BASIC_STOPLIST
        CTXSYS     EXTENDED_STOPLIST             0 BASIC_STOPLIST
        CTXSYS     DEFAULT_STOPLIST            235 BASIC_STOPLIST  -- deutsche Stopwörter
        -- Express-Edition
        SPL_OWNER  SPL_NAME              SPL_COUNT SPL_TYPE
        ---------- -------------------- ---------- ---------------------
        CTXSYS     EMPTY_STOPLIST                0 BASIC_STOPLIST
        CTXSYS     DEFAULT_STOPLIST            114 BASIC_STOPLIST  -- englische Stopwörter
        CTXSYS     EXTENDED_STOPLIST             0 BASIC_STOPLIST

        Die darin enthaltenen Wörter erhält man über:

        SELECT spw_word FROM ctx_stopwords WHERE spw_stoplist = 'DEFAULT_STOPLIST';

         

        NACHRÜSTEN DER SPRACHSPEZIFISCHEN EINSTELLUNGEN IN DER EXPRESS-EDITION
        Bei der Installation der Express-Edition wird Oracle Text mit den amerikanischen Einstellungen vorkonfiguriert. Die Konfiguration kann man jedoch leicht über das Skript drdefd.sql im Ordner <Oracle-Home>\ctx\admin\defaults ändern (ein Blick in dieses Skript lohnt sich).
        Wenn die Einstellungen für alle User gelten sollen, nimmt man diese Änderungen am besten direkt im ctxsys-Schema vor. Dies muss man zuerst freischalten:

        CONN / AS sysdba
        ALTER USER ctxsys IDENTIFIED BY text ACCOUNT UNLOCK;
        CONN ctxsys/text


        Die alten Einstellungen werden über Bord geworfen:

        BEGIN
           ctx_ddl.drop_preference('DEFAULT_LEXER');
           ctx_ddl.drop_preference('DEFAULT_WORDLIST');
           ctx_ddl.drop_stoplist('DEFAULT_STOPLIST');
           ctx_ddl.drop_policy('DEFAULT_POLICY_ORACONTAINS');
        END;
        /

        und die neuen eingespielt:

        @ =>\ctx\admin\defaults\drdefd.sql

        Jetzt muss man noch den Index an die neuen Einstellungen anpassen. Das geht entweder über einen Alter Index Befehl:

        conn scott/tiger
        ALTER INDEX sprueche_idx REBUILD PARAMETERS
        ('REPLACE wordlist ctxsys.default_wordlist stoplist ctxsys.default_stoplist lexer ctxsys.default_lexer');

        oder über Löschen und Neuerstellen des Indexes:

        DROP INDEX sprueche_idx;
        CREATE INDEX sprueche_idx ON sprichwoerter (text) INDEXTYPE IS ctxsys.context
        PARAMETERS('wordlist ctxsys.default_wordlist stoplist ctxsys.default_stoplist
        lexer ctxsys.default_lexer');

        Die neuen Einstellungen kann man über die Views CTX_STOPLISTS und CTX_PREFERENCE_VALUES überprüfen.

         

        ANPASSUNGEN DER SPRACH-EINSTELLUNGEN
        Die Einstellungen von wordlist, stoplist und lexer kann man nachträglich noch anpassen. Wenn man z.B. die Case-Sensitivity abschalten will, kann man sich über die Prozedur CTX_DDL.CREATE_PREFERENCE auf der Basis des default-Lexers einen eigenen Lexer erzeugen, dessen Attribute man selber setzen kann. Leider genügt es nicht, hier nur diesen einen Parameter anzupassen, dann gehen nämlich die deutschen Einstellungen für ALTERNATE_SPELLING und COMPOSITE verloren.
        Wenn man einen Text indizieren will, der Wörter mit Sonderzeichen enthält, möchte man nicht, dass diese Sonderzeichen als Trennzeichen gewertet werden. Dies kann man mit dem Attribut PRINTJOINS erreichen. Im Beispiel unten werden Unterstrich, Dollarzeichen, Raute und das Apostroph (das wie üblich mit einem zusätzlichen Apostroph maskiert werden muss)  als Bestandteile von Wörtern festgelegt.
        Zudem kann man über das Attribut NEW_GERMAN_SPELLING bewirken, dass sowohl die alten und die neuen Formen (z.B. rauh / rau, Stengel / Stängel) im Index abgespeichert und bei der Suche gefunden werden.

        BEGIN
        -- Preference löschen, falls schon vorhanden
        -- ctx_ddl.drop_preference('scott_insensitive');
           ctx_ddl.create_preference(
             preference_name => 'scott_insensitive', -- Name der neuen Einstellung
             object_name     => 'basic_lexer');      -- basiert auf dem basic_lexer
           ctx_ddl.set_attribute(
             preference_name => 'scott_insensitive',
             attribute_name  => 'mixed_case',
             attribute_value => 'no');               -- default ist yes
           ctx_ddl.set_attribute('scott_insensitive','composite', 'german');
           ctx_ddl.set_attribute('scott_insensitive','alternate_spelling', 'german');
           ctx_ddl.set_attribute('scott_insensitive','printjoins', '_$#-''');
           ctx_ddl.set_attribute('scott_insensitive','new_german_spelling','yes');
        END;
        /

        Anpassen des Indexes:

        ALTER INDEX sprueche_idx REBUILD PARAMETERS ('REPLACE LEXER scott_insensitive');

        VORSICHT, FALLE!
        Da im neuen Lexer Bindestriche als Bestandteile von Wörtern gewertet werden, sollte die folgende Abfrage eigentlich ein Ergebnis zurückliefern:

        SELECT * FROM sprichwoerter WHERE CONTAINS(text, 'Oracle-Experte') > 0;
        =>
        Es wu

        Online Table Redefinition

        Bereich:DBA:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 12.12.2018

        Keywords:DBA , PL/SQL , Tuning, Online Reorg

        Folgende Operationen zum Reorganisieren von Tabellen sind möglich:

        • Tabelle exportieren
        • Tabelle löschen
        • Tabelle evtl. mit neuen Speicherparametern anlegen
        • Tabelle importieren
        • ALTER TABLE <tab> STORAGE (....) MOVE [ONLINE /* ab 12.2*/] TABLESPACE <tbs>;
          • Hierbei konnte die Tabelle im laufenden Betrieb mit neuen Speicherparametern angelegt werden.
          • Leider war sie während der Reorganisation nicht für Schreiboperationen verfügbar

        Für Besitzer der Enterprise und Express Edition kann eine Tabelle auch online reorganisiert werden, wobei auch Schreibzugriffe während der
        Reorganisation hier möglich sind.

        Mit dem neuen Package DBMS_REDEFINITION können Tabellen mit einer minimalen Sperrzeit online reorganisiert werden.

        Sie können mit diesem Package:

        • die Unterstützung für parallele Abfragen hinzufügen oder entfernen,
        • Partitionierungs-Unterstützung entfernen oder hinzufügen,
        • die Tabelle neu aufbauen, um Fragmentierung zu vermindern,
        • Spalten löschen oder hinzufügen,
        • Tabellen Organisation ändern (IOT zu normal oder umgekehrt),
        • die Speicherparameter einer Tabelle ändern,
        • Löschen/Hinzufügen von Non-Primary Spalten
        • die Tabelle in einen anderen Tablespace (im gleichen Schema) verschieben.

        Einschränkungen beim Reorg:

        • Der doppelte Speicherplatz für die Tabelle muss vorhanden sein
        • Primärschlüsselspalten können nicht verändert werden
        • Die zu ändernden Tabellen müssen einen Primärschlüssel besitzen
        • Die neue Tabelle muss im gleichen Schema liegen
        • Neu hinzugefügte Spalten können erst nach der Redefinition auf NOT NULL gesetzt werden
        • Die Tabelle darf keine LONGs, BFILEs oder User Defined Types enthalten.
        • Cluster Tabellen und Tabellen im Schema SYS und SYSTEM sind ausgeschlossen
        • Tabellen mit Materialized View Logs oder Materialized Views dürfen nicht benutzt werden.

        Vorgehensweise:

        Folgende Änderungen sollen an einer Tabelle emp durchgeführt werden:

        Spalte ENAME soll umbenannt werden in NAME,

        Spalte SAL soll umbenannt werden in VERDIENST und um den Faktor 1,5 erhöht,

        die Spalten MGR, HIREDATE, COMM sollen gelöscht werden.

         

          Tabelle EMP2

                Name               Null?            Typ
            --------------     ---------------  ------------
            EMPNO              NOT NULL         NUMBER(4)
            ENAME                               VARCHAR2(10)
            JOB                                 VARCHAR2(9)
            MGR                                 NUMBER(4)
            HIREDATE                            DATE
            SAL                                 NUMBER(7,2)
            COMM                                NUMBER(7,2)
            DEPTNO                              NUMBER(2)

        Überprüfen, ob EMP zu reorganisieren ist: 

        SQL> exec dbms_redefinition.can_redef_table('SCOTT','EMP');
        PL/SQL-Prozedur wurde erfolgreich abgeschlossen.

        Erstellen Sie eine vorläufige Tabelle mit den gewünschten Änderungen

        SQL> CREATE TABLE scott.vor_emp (
          empno, name, verdienst, deptno)
          AS SELECT empno,ename,sal,deptno FROM scott.emp
          WHERE 1=2;

        Start der Reorganisation:

        SQL> BEGIN
          dbms_redefinition.start_redef_table(
          'SCOTT','EMP','VOR_EMP',
          'EMPNO EMPNO,ENAME NAME,SAL*1.5 VERDIENST');
        END;
        /

        Erstellen Sie den Primärschlüssel auf empno:

        SQL> ALTER TABLE scott.vor_emp ADD
          CONSTRAINT pk_voremp2
          PRIMARY KEY (empno);

        Erstellen Sie einen Fremdschlüssel auf der Spalte deptno der auf die Spalte deptno der Tabelle DEPT verweist:

        SQL> ALTER TABLE scott.vor_emp ADD(
          CONSTRAINT fk_emp
          FOREIGN KEY (deptno)
          REFERENCES scott.dept (deptno));

        Der FK muss ausgeschaltet werden. Dieser wird dann am Ende der Reorganisation automatisch aktiviert.

        SQL> ALTER TABLE scott.vor_emp DISABLE CONSTRAINT fk_emp;

        Wenn nötig, synchronisieren Sie die Tabellen zwischendurch:

        SQL> BEGIN
          dbms_redefinition.sync_interim_table(
        'SCOTT', 'EMP', 'VOR_EMP');
        END;
        /

        PL/SQL-Prozedur wurde erfolgreich abgeschlossen.

        Beenden Sie die Redefinition:

        SQL> BEGIN
         dbms_redefinition.finish_redef_table(
        'SCOTT', 'EMP', 'VOR_EMP');
        END;
        /

        Löschen der alten Tabelle:

        SQL> DROP TABLE vor_emp;

        Der Redefinitions Vorgang kann abgebrochen werden mit:

        exec dbms_redefinition.abort_redef_table('SCOTT', 'EMP', 'EMP');

        Neuerungen ab Version 10.2:

        Bevor die Reorg abgeschlossen ist, können noch diverse Objekte (wie Cosntraints, Trigger, Indizes, u.w.) mit übernommen werden.

        Neuerungen ab Version 11.x:
        Nun können auch abhängige Materialized Views beim Reorg-Prozess mit übernommen werden.

         VARIABLE num_err NUMBER
        SET SERVEROUTPUT ON SIZE 200000
        BEGIN
        DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS(
        uname=>'SCOTT',
        orig_table=>'EMP',
        int_table=>'VOR_EMP',
        copy_indexes=>dbms_redefinition.cons_orig_params,
        copy_triggers=>TRUE,
        copy_constraints=>TRUE,
        copy_privileges=>TRUE,
        copy_statistics=>TRUE,
        copy_mvlog=>TRUE,
        ignore_errors=>FALSE,
        num_errors=>:num_err);
        dbms_output.put_line('Anzahl Fehler bei Übernahme:'||:num_err);
        END;
        /

         

        Weitere Informationen zum Thema Reorganisation erhalten Sie in unseren Tuning und Monitoring Kursen.



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Schutz vor DROP oder TRUNCATE Kommandos

        Bereich:DBA:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 03.07.2018

        Keywords:DROP, TRUNCATE, Schutz

        Heute widmen wir uns dem wichtigen Thema "Schutz vor dem unbeabsichtigten Löschen von wichtigen Datenbank-Objekten".
        Fast jeder hat schon einmal aus Versehen das falsche Objekt gelöscht (ooops ich auch). Wenn es sich dabei um eine wichtige Applikationstabelle handelt,
        ist der Reparaturaufwand oft sehr erheblich. Wenn Sie natürlich FLASHBACK DATABASE (leider nur in der Enterprise Edition erhältlich) zur Verfügung haben, geht es etwas schneller :-)

        Wir erstellen uns eine Liste mit schützenswerten Objekten in einer eigenen Tabelle:

        connect system/sys
        CREATE TABLE prio_objects (
          owner          VARCHAR2(30),
          object_type    VARCHAR2(30),
          object_name    VARCHAR2(30));


        Dann erzeugen wir eine Applikationstabelle, die gegen DROP und TRUNCATE geschützt werden soll:

        CREATE TABLE scott.gehalts_tab as SELECT * FROM dual;


        Diese Tabelle gehalts_tab wird in die Liste der geschützten Objekte aufgenommen:

        INSERT INTO prio_objects (owner,object_type,object_name)
          VALUES('SCOTT','TABLE','GEHALTS_TAB');
        COMMIT;


        Ein DDL Trigger kümmert sich um die Prüfung, welche DROP/TRUNCATE denn abgebrochen werden:

        CREATE OR REPLACE TRIGGER system.check_drop
          BEFORE DROP OR TRUNCATE ON DATABASE
        BEGIN
          FOR rec in (SELECT * FROM system.prio_objects) LOOP
            IF  sys.dictionary_obj_type=rec.object_type AND 
            sys.dictionary_obj_name=rec.object_name THEN 
             IF sys.dictionary_obj_owner=rec.owner  
             OR sys.dictionary_obj_type ='TABLESPACE' THEN
              RAISE_APPLICATION_ERROR(-20002,'Objekt steht auf Prio-Liste und kann nicht gelöscht werden'); -- Absturz
             END IF;
            END IF;
          END LOOP;
        END;
        /


        Wir testen den DROP und siehe da ...

        DROP TABLE scott.gehalts_tab;
        DROP TABLE scott.gehalts_tab
        *
        FEHLER in Zeile 1:
        ORA-00604: Fehler auf rekursiver SQL-Ebene 1
        ORA-20002: Objekt steht auf Prio-Liste und kann nicht gelöscht werden
        ORA-06512: in Zeile 7


        Fall 2: Tablespace gegen Löschen schützen:

        INSERT INTO system.prio_objects (owner,object_type,object_name)
          VALUES ('SYS','TABLESPACE','CRASH_TBS');
        COMMIT;
        DROP TABLESPACE crash_tbs;
        DROP TABLESPACE crash_tbs
        *
        FEHLER in Zeile 1:
        ORA-00604: Fehler auf rekursiver SQL-Ebene 1
        ORA-20002: Objekt steht auf Prio-Liste und kann nicht gelöscht werden
        ORA-06512: in Zeile 6


        Wenn man ein Objekt nun doch löschen oder leeren möchte, nimmt man es einfach von der Liste in der Tabelle:

        DELETE FROM system.prio_objects
          WHERE object_type='TABLESPACE'
            AND object_name='CRASH_TBS';
        COMMIT;
        DROP TABLESPACE crash_tbs;
        TABLESPACE wurde gelöscht.


        Beispiel für eine Tabelle:

        DELETE FROM system.prio_objects
          WHERE object_type='TABLE'
            AND object_name='GEHALTS_TAB';
        COMMIT;
        DROP TABLE scott.gehalts_tab;
        Tabelle wurde gelöscht.

         

        Sie können natürlich auch das ganze umdrehen und alle Objekte Schützen, bis auf die, die dann in der Tabelle stehen. Das vereinfacht die Sache bei vielen Objekten im Schema.
        Sie sehen, auch ein DBA braucht hin und wieder mal PL/SQL-Kenntnisse, die er sich gerne in einem unserer fünf verschiedenen PL/SQL-Kurse aneignen kann. :-)



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Automatische Triggerwiedereinschaltung

        Bereich:PL/SQL, Version: ab RDBMS 10.x, Letzte Überarbeitung: 03.07.2018

        Keywords:Trigger, Reenable

        Häufig möchte man einen Trigger kurz einmal ausschalten, eine Aktion ausführen und ihn dann wieder einschalten.

        Nur dann vergisst man das Einschalten wieder.

        Das muss nicht sein:-)

        Die folgende Routine schaltet einen gewünschten Trigger im aktuellen Schema zwar auf DISABLE, setzt ihn aber nach einer frei wählbaren Zeitspanne (in Minuten) wieder auf ENABLE.

        CREATE OR REPLACE PROCEDURE TRIGGER_SLEEP (
          P_TRIGGER_NAME IN VARCHAR2,
          p_timeout      IN NUMBER default 30) -- in Minuten
          IS
        BEGIN
          EXECUTE IMMEDIATE 'ALTER TRIGGER '||p_trigger_name||' DISABLE';
          DBMS_SCHEDULER.CREATE_JOB(
            JOB_NAME   => 'trigger_reactivate',
            JOB_TYPE   => 'PLSQL_BLOCK',
            JOB_ACTION => q'!BEGIN
                               EXECUTE IMMEDIATE 'ALTER TRIGGER !'||P_TRIGGER_NAME||q'! ENABLE';
                             END;!',
            START_DATE => SYSDATE+ p_timeout/24/60,
            ENABLED    => TRUE);
            COMMIT;
        END;
        /


        Solche und weitere tolle Tricks lernt man in einem unserer PL/SQL Kurse.



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Rechte direkt oder via Rolle

        Bereich:PL/SQL, Version: ab RDBMS 12.x, Letzte Überarbeitung: 19.03.2020

        Keywords:Rechteverwaltung, Rollen unter Oracle

        Wenn Sie in PL/SQL Prozeduren, Funktionen oder Packages schreiben möchten und auf Objekte von anderen Schematas zugreifen, dann müssen Sie die dazugehörigen Rechte direkt bekommen haben und nicht über eine Rolle.

        Nur stellt sich die Frage, welche Rechte habe ich direkt und welche über eine Rolle bekommen?

        Das Problem löst der folgende SELECT:

        SELECT * FROM (
          SELECT 'Direkt' as info, privilege FROM session_privs
          MINUS
          SELECT 'Direkt', privilege FROM role_sys_privs)
        UNION
        SELECT role, privilege FROM role_sys_privs;

        Die Ausgabe könnte dann z.B. so aussehen:

        INFOPRIVILEGE
        DirektADVISOR
        DirektALTER ANY INDEX
        DirektALTER ANY OUTLINE
        DirektALTER DATABASE
        DirektALTER SESSION
        DirektALTER SYSTEM
        DirektALTER TABLESPACE
        DirektALTER USER
        DirektANALYZE ANY
        DirektCREATE ANY DIRECTORY
        DirektCREATE ANY JOB
        DirektCREATE ANY OUTLINE
        DirektCREATE CLUSTER
        DirektCREATE DATABASE LINK
        DirektCREATE DIMENSION
        DirektCREATE EXTERNAL JOB
        DirektCREATE INDEXTYPE
        DirektCREATE JOB
        DirektCREATE MATERIALIZED VIEW
        DirektCREATE OPERATOR
        DirektCREATE PROCEDURE
        DirektCREATE SEQUENCE
        DirektCREATE SESSION
        DirektCREATE SYNONYM
        DirektCREATE TABLE
        DirektCREATE TRIGGER
        DirektCREATE TYPE
        DirektCREATE USER
        DirektCREATE VIEW
        DirektDROP ANY DIRECTORY
        DirektDROP ANY OUTLINE
        DirektGRANT ANY OBJECT PRIVILEGE
        DirektSELECT ANY DICTIONARY
        DirektSELECT ANY TABLE


        Oder für Objektrechte:

        SELECT 'Rolle' AS INFO, role, privilege , owner, table_name
          FROM role_tab_privs
        UNION ALL
        SELECT 'Direkt', grantee, privilege , owner, table_name
          FROM user_tab_privs;


        Das ist nur ein Ausschnitt unserer vielfältigen Beispiele zu Oracle Datenbanken.

        Kommen Sie in einen unserer PL/SQL Kurse und Sie bekommen da eine ganze Menge mehr zu sehen ...



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        Oracle und JSON

        Bereich:PL/SQL, Version: ab RDBMS12.x, Letzte Überarbeitung: 19.05.2022

        Keywords:PL/SQL, SQL, Standard Packages

        Das JSON-Format (Java-Script Object Notation) erfreut sich zunehmender Beliebtheit, da es im Vergleich zu XML wesentlich weniger Overhead hat. Ausserdem ist es einfacher; es kennt im wesentlichen nur Wertepaare und Arrays. Diese können hierarchisch aufgebaut sein und entsprechen der JS-Notation.

        Ein Beispiel:

        {"sessions":
            [
             {"sessionId":"44949505148386823",
             "subject":"JSON in der Praxis",
             "startTime":"2015-11-02T09:30:00.001Z",
             "endTime":"2015-11-02T10:30:00.000Z",
             "attendees": ["Rudi Ratlos","Maria Stuart"],
              "organizer":{"firstName":"Max","lastName":"Mustermann"}
             },
             {"sessionId":"115191805493052423",
             "subject":"Die Zukunft der Hexerei",
             "startTime":"2015-11-11T14:00:00.000Z",
             "endTime":"2015-11-11T15:30:00.000Z",
             "attendees":["Harry Potter","Ron Weasley"],
              "organizer":{"firstName":"Hermine","lastName":"Granger"}
             }
            ]
        }

        Der "Wert" zu "sessions" ist hier ein Array von 2 Objekten. Jedes dieser Objekte hat die skalaren Werte sessionId, subject, startTime und endTime sowie das Array attendees. organizer wiederum stellt ein untergeordnetes Objekt dar. 

        Seit Version 12.1.0.2 unterstützt nun auch Oracle dieses Format nativ mit speziellen Operatoren und Funktionen. In früheren Versionen gibt es zumindest die Möglichkeit, das Package APEX_JSON zu nutzen, das Bestandteil von APEX 5 ist.

        Für die Speicherung in der Datenbank gibt es keinen eigenen Datentyp, je nach Größe bieten sich VARCHAR2 oder CLOB an, aber mit Hilfe des Operators IS JSON kann die Einhaltung des Formats über einen Constraint erzwungen werden:

        CREATE TABLE JSON_TEST
        (ID          NUMBER   GENERATED  ALWAYS AS IDENTITY PRIMARY KEY,
        JSON_DATA    CLOB,
        CONSTRAINT JSON_TEST_CHK CHECK (JSON_DATA IS JSON)
        )
        /
        -- Eintragen der beiden Datensätze aus obigem JSON-Dokument:
        insert into JSON_TEST
        (JSON_DATA)
        values
        ('{"sessionId":"44949505148386823",
           "subject":"JSON in der Praxis",
           "startTime":"2015-11-02T09:30:00.001Z",
           "endTime":"2015-11-02T10:30:00.000Z",
           "attendees": ["Rudi Ratlos","Maria Stuart"],
           "organizer":{"firstName":"Max","lastName":"Mustermann"}
        }')
        /
        insert into JSON_TEST
        (JSON_DATA)
        values
        ('{"sessionId":"115191805493052423",
           "subject":"Die Zukunft der Hexerei",
           "startTime":"2015-11-11T14:00:00.000Z",
           "endTime":"2015-11-11T15:30:00.000Z",
           "attendees":["Harry Potter","Ron Weasley"],
            "organizer":{"firstName":"Hermine","lastName":"Granger"}
           }')
        /
        insert into JSON_TEST
        (JSON_DATA)
        values
        ('"sessionId":"115191805493052423"')
        /
        ORA-02290: CHECK-Constraint (DUMMY.JSON_TEST_CHK) verletzt

        Das Gegenstück zu IS JSON ist IS NOT JSON. Beide Operatoren kann man z.B. auch in der WHERE-Klausel nutzen. Das macht natürlich nur dann Sinn, wenn eine Spalte auch normale Strings enthalten kann. (Auf bestimmte Pfade kann man übrigens über json_exists abprüfen.)

        JSON_VALUE UND JSON_QUERY
        Um JSON auszuwerten, muss man in aller Regel mit Pfadangaben arbeiten, ähnlich wie bei XML. Für skalare Werte verwendet man JSON_VALUE, für nicht skalare JSON_QUERY. Dabei durchwandert man die Hierarchie über Punktnotation. Ausgangspunkt ist immer das JSON-Dokument selber, das durch $ gekennzeichnet wird. Standardmäßig wird der Wert als String zurückgegeben; über RETURNIN kann man aber NUMBER als Datentyp angeben:

        SELECT JSON_value (JSON_DATA, '$.subject') subject FROM JSON_TEST
        /
        SUBJECT                       
        ------------------------------
        JSON in der Praxis            
        Die Zukunft der Hexerei       
        2 rows selected.
        SELECT JSON_value (JSON_DATA, '$.sessionId' RETURNING NUMBER) sessionId FROM JSON_TEST
        /
                    SESSIONID
        ---------------------
            44949505148386823
           115191805493052423
           
        SELECT JSON_QUERY (JSON_DATA, '$.attendees') attendees 
        FROM JSON_TEST
        /
        ATTENDEES                               
        ----------------------------------------
        ["Rudi Ratlos","Maria Stuart"]          
        ["Harry Potter","Ron Weasley"]   

        Das Standardverhalten von JSON ist sehr fehlertolerant; deshalb kann ein leerer Wert auch bedeuten, dass die Abfrage fehlerhaft ist. Will man bei falschen Pfadangaben z. B. einen Fehler zurückbekommen, muss man das explizit  mit ERROR ON ERROR anfordern (der Default ist NULL ON ERROR):

        SELECT JSON_value (JSON_DATA, '$.attendees') attendees FROM JSON_TEST
        /
        ATTENDEES                                                                       
        ---------------------------------------------------------------------------
                                                                                        
                                                                                        
        2 rows selected.
        SELECT JSON_value (JSON_DATA, '$.attendees' ERROR ON ERROR) attendees 
        FROM JSON_TEST
        /
        Error at line 1
        ORA-40456: JSON_VALUE wurde als nicht-skalarer Wert ausgewertet

        Eine Alternative zu JSON_VALUE und JSON_QUERY ist noch die Punktnotation: 

        SELECT t.json_data.subject , t.json_data.attendees
          FROM JSON_TEST t
        /
        SUBJECT                        ATTENDEES                               
        ------------------------------ ----------------------------------------
        JSON in der Praxis             ["Rudi Ratlos","Maria Stuart"]     
        Die Zukunft der Hexerei        ["Harry Potter","Ron Weasley"]            

        Die funktioniert aber nur, wenn die Tabelle wie oben mit einem Check-Constraint angelegt wurde, der JSON erzwingt. Wenn nicht, bekommt man die Fehlermeldung:

        ORA-00904: "T"."JSON_DATA"."ATTENDEES": ungültiger Bezeichner

        JSON_TABLE
        JSON_TABLE ist für die Verarbeitung von JSON die umfassendste und interessanteste Funktion. Analog zu XMLTABLE für XML ermöglicht JSON_TABLE die relationale Darstellung der Inhalte:

        SELECT sessionid, thema, vorname, nachname
          FROM JSON_TEST , 
              json_table(json_data, '$'
          columns (sessionid     NUMBER       path '$.sessionId',
                   thema         VARCHAR2(30) path '$.subject', 
                   vorname       VARCHAR2(10) path '$.organizer.firstName',
                   nachname      VARCHAR2(10) path '$.organizer.lastName'
                   )
             )      
        /
                 SESSIONID THEMA                     VORNAME    NACHNAME  
        ------------------ ------------------------- ---------- ----------
         44949505148386823 JSON in der Praxis        Max        Mustermann
        115191805493052423 Die Zukunft der Hexerei   Hermine    Granger   

        Übergeben werden dabei

        • Das JSON-Dokument (bzw. die Spalte mit dem Inhalt)
        • Der Ausgangspunkt der Pfadangaben in der columns-Klausel
        • Die Liste der zu extrahierenden Werte über die columns-Klausel

        Auch auf Inhalte eines Arrays kann zugegriffen werden, indem man den Index mitgibt. Die Zählung beginnt bei 0. Ein nicht vorhandener Index führt auch hier standardmäßig nicht zu einem Fehler:

        SELECT thema, teilnehmer_1, teilnehmer_2
         FROM JSON_TEST , 
              json_table(json_data, '$'
          columns (thema         VARCHAR2(30) path '$.subject', 
                   teilnehmer_1  VARCHAR2(30) path '$.attendees[0]',
                   teilnehmer_2  VARCHAR2(30) path '$.attendees[1]'
                   )
             )      
        /
        THEMA                    TEILNEHMER_1    TEILNEHMER_2   
        ------------------------ --------------- ---------------
        JSON in der Praxis       Rudi Ratlos     Maria Stuart   
        Die Zukunft der Hexerei  Harry Potter    Ron Weasley    
        SELECT thema, teilnehmer_1, teilnehmer_3
          FROM JSON_TEST , 
               json_table(json_data, '$'
          columns (thema         VARCHAR2(30) path '$.subject', 
                   teilnehmer_1  VARCHAR2(30) path '$.attendees[0]',
                   teilnehmer_3  VARCHAR2(30) path '$.attendees[2]'
                   )
             )    
        /
        THEMA                     TEILNEHMER_1    TEILNEHMER_3   
        ------------------------- --------------- ---------------
        JSON in der Praxis        Rudi Ratlos                    
        Die Zukunft der Hexerei   Harry Potter                   

        Auch hier kann nach der Pfadangabe ergänzt werden ERROR ON ERROR. Dann wird die zweite Abfrage mit einem Fehler quittiert.

        JSON IN PL/SQL
        Wesentlich interessanter als in Tabellen ist JSON jedoch als Austauschformat, z. B. bei Webservices (application/json). Auch hier kann man sehr gut mit JSON_TABLE arbeiten.

        Angenommen, Sie rufen einen entsprechenden Webservice über PL/SQL auf, der die Daten nicht in Form einzelner Datensätze liefert sondern im folgenden Format und Sie wollen die Datensätze in normale relationale Tabellen eintragen:

        {"sessions":
          [
           {"sessionId":"44949505148386823",
             "subject":"JSON in der Praxis",
             "startTime":"2015-11-02T09:30:00.001Z",
             "endTime":"2015-11-02T10:30:00.000Z",
                    "attendees":
                    [
                    {"firstName":"Rudi","lastName":"Ratlos","email":"rudi.ratlos@firma.de"},
                    {"firstName":"Maria","lastName":"Stuart","email":"maria.stuart@firma.de'"}
        ],
        "organizer":"firstName":"Max","lastName":"Mustermann"}
           },
           {"sessionId":"115191805493052423",
             "subject":"JSON in der Praxis",
             "startTime":"2015-11-02T09:30:00.001Z",
             "endTime":"2015-11-02T10:30:00.000Z",
                    "attendees":
                    [
                    {"firstName":"Harry","lastName":"Potter","email":"harry.potter@hogwarts.uk
                    {"firstName":"Ron","lastName":"Weasley","email":"ron.easley@ogwarts.k"}
                    ],
             "organizer":{"firstName":"Hermine","lastName":"Granger"}
           }
           ....
          ]
        }

        Zunächst einmal können Sie den http-Response in eine VARCHAR2- oder CLOB-Variable einlesen (normalerweise werden mehr als zwei Datensätze übertragen), die dann Ihr JSON-Dokument darstellt. Dann brauchen Sie nur noch den richtigen Ausgangspunkt für den Pfad, um zunächst einmal die Daten der Sessions auszulesen. Da Sie alle Sessions haben wollen, ist das $.sessions[*] . Der Stern (*) bedeutet "alle".

        Und natürlich wollen Sie auch die Teilnehmerdaten auslesen. Hier kommt dann nested path

        Oracle Dokumentationsübersicht von Version 11.2 bis Oracle 21c

        Bereich:DBA:Monitoring:PL/SQL:SQL, Version: ab RDBMS 11.2:RDBMS 12.x:RDBMS 18.1:RDBMS 18.3:RDBMS 19.1:RDBMS 19.3:RDBMS 20.1, Letzte Überarbeitung: 05.07.2023

        Keywords:Oracle Doku, Oracle Doc

        Haben Sie auch schon einmal verzweifelt nach der richtigen Oracle Dokumentation Ihrer Oracle Datenbank gesucht.
        Leider hat Oracle im Gegensatz zu Postgres keinen einheitlichen Doc Server, sondern legt die Dokumenation immer auf einen neuen Server...
        Die folgende Matrix soll Ihnen die Suche nach der richtigen Version für Ihre Oracle Datenbank helfen

         

         11.212.112.218c19c21c23c
        Oracle HauptseiteDokuDokuDokuDokuDokuDokuDoku
        SQL DocDokuDokuDokuDokuDokuDokuDoku
        PL/SQL DocDokuDokuDokuDokuDokuDokuDoku
        PL/SQL PackagesDokuDokuDokuDokuDokuDokuDoku
        Oracle Datenbank AdministrationDokuDokuDokuDokuDokuDokuDoku
        Oracle Upgrade GuideDokuDokuDokuDokuDokuDokuDoku
        Oracle ReferenceDokuDokuDokuDokuDokuDokuDoku
        Oracle ConceptsDokuDokuDokuDokuDokuDokuDoku
        Oracle Database UtilitiesDokuDokuDokuDokuDokuDokuDoku
        Oracle Backup & RecoveryDokuoracleDokuDokuDokuDokuDoku
        Oracle Doc Server


        Und für alle Oracle APEX Fans:

        APEX Release18.219.119.220.120.221.222.122.223.123.2
        Oracle APEX HauptseiteDokuDokuDokuDokuDokuDokuDokuDokuDoku 
        APEX Install GuideDokuDokuDokuDokuDokuDokuDokuDokuDoku 
        APEX App Builder User GuideDokuDokuDokuDokuDokuDokuDokuDokuDoku 
        APEX API ReferenceDokuDokuDokuDokuDokuDokuDokuDokuDoku 


        Nun kommen Sie bei Ihrer Doku-Suche schneller zum Ziel. Wer die Dokumentation nicht so gerne liest, kann natürlich auch gerne in eine von unseren Schulungen kommen.



        Weitere Interessente Artikel zum Thema:


        Empfohlene Schulungen zum Thema:


        Visual Studio Code für Oracle SQL und PL/SQL

        Bereich:PL/SQL:SQL, Version: ab RDBMS 8.x, Letzte Überarbeitung: 14.06.2019

        Keywords:MS Visual Studio für Oracle

        In den letzten Jahren wird von vielen Entwicklern verstärkt das Tool Visual Studio Code bei der Entwicklung verwendet.
        Wir haben das Tool in Verbindung mit Oracle SQL und PL/SQL mal näher angesehen und beschreiben in diesem Tipp die Einrichtung

        Netterweise gab es zu dem Thema schon einen Blog von Morten Braten, der aber noch die Version 0.1.0 verwendet, aktuelle Version ist 2.0.0 (Stand Juni 2019). Wir haben ein paar Sachen ergänzt und korrigiert, damit es unter der aktuellen Visual Studio Code Version läuft

        Sie können Visual Studio Code hier herunterladen.
        Zusätzlich sollten Sie folgende Extensions installieren:

        • Language PL/SQL von xyz
        • Notepad++ keymap
        • SQL Server (mssql) (falls Sie mal für den SQL Server tätig werden möchten)

        VisualStudioCode

        Extensions werden installiert durch Klick auf das zweite Icon von unten, links im Hauptfenster (Screenshot (1)). Danach im Bereich "Search..." den gesuchten Namen eintragen, Suche starten und bei der gewünschten Extension auf "Install" klicken.
        Danach legen Sie bitte einen Ordner im Betriebssystem an (z.B. c:\temp\VC)
        Gehen Sie nun auf File/Open Folder und wählen Sie den Ordner aus. Der Inhalt des Ordners wird Ihnen links (Screenshot (2)) angezeigt.
        Wenn Sie auf eine Datei (links) klicken, wird der Inhalt im Fenster (Screenshot (3)) angezeigt.
        Nun müssen wir einige Konfigdateien anlegen, die Visual Studo Code sagen, was zu tun ist.
         

        Datei 1: _run_sqlplus.bat

        REM Setup sqlplus Umgebung für Unicode
        REM Dieses Skript setzt Voreinstellungen für Unicode (Multibyte Zeichen)
        set NLS_LANG=.AL32UTF8
        chcp 65001
        cls

        REM Fehler werden mittels problemMatcher angezeigt
        echo exit | echo @_show_errors.sql | sqlplus -s %1 %2
        color a0


        Datei 2: _show_errors.sql

        set pagesize 9999
        set linesize 9999
        set heading off
        set trim on

        HOST color c0

        select lower(attribute) -- error oder warning
          || ' '
          || line || '/' || position -- Zeile und Spalte
          || ' '
          || lower(name) -- Dateiname
          || case -- Datei-extension
            when type = 'PACKAGE' then '.pks'
            when type = 'PACKAGE BODY' then '.pkb'
            else '.sql'
          end
          || ' '
          || replace(text, chr(10), ' ') -- Zeilenbrüche entfernen
          as user_errors
        from user_errors
        where attribute in ('ERROR', 'WARNING')
        order by type, name, line, position;

         
          und die wichtigste, Datei 3: task.json im Unterordner .vscode

        {
            "version": "2.0.0",

            // sqlplus startet über Batch-Datei
            "windows": {
                "command": "./_run_sqlplus.bat"
            },
            "osx": {
                "command": "./_run_sqlplus.sh"
            },
            "linux": {
                "command": "./_run_sqlplus.sh"
            },

            "presentation": {
                "reveal": "always", // never,silent,always
                "panel": "shared"  // new,shared,dedicated
            },

            //         $1= Datenbank Connect String mit Benutzer/PWD@TNS-String   $2=Dateiname
            "args": ["scott/tiger@172.30.30.30:1521/xe.muniqsoft-training.de", "@${file}"],

            // Wenn Kompilierungsfehler auftreten, werden diese im "Problems" Reiter (unten Links angezeigt)
            "problemMatcher": {
                "owner": "plsql",
                "fileLocation": ["relative", "${fileDirname}"],
                "pattern": [
                  {
                    "regexp": "(.*) (\\d*)\/(\\d*) (.*?) (.*)",
                    "severity": 1,
                    "line": 2,
                    "column": 3,
                    "file": 4,
                    "message": 5,
                    "loop": true
                }
              ]
            }
        }


        Passen Sie bitte in dieser Datei in der Zeile "args" ihre Daten bzgl. Benutzername/Passwort und Datenbank Daten an.

        Nun können wir loslegen:
        Wir öffnen den Ordner (File/Open Folder), bei uns war das der Ordner c:\temp\VC
        In diesen Ordner legen Sie eine bereits existierende SQL bzw. PL/SQL Datei (in unserem Screenshot(2)) z.B: die Datei vc_test.sql
        Wenn Sie eine neue Datei erstellen möchten, klicken Sie mit der rechten Maustaste in den Dateibereich, Menue: New File. Geben Sie einen Namen an, dann wird die Datei im Ordner angelegt.
        Klicken Sie also auf die gewünschte Datei, die dann im rechten Fenster angezeigt wird.
        Ändern Sie ggf noch etwas ab und speichern Sie Datei (z.B. durch STRG-S)
        Drücken Sie nun STRG-Shift-B (für Build) und ein kleines Fenster öffnet sich oben.
        Wählen Sie dort ./_run_sqlplus.bat aus und die Datei wird in Oracle SQL*Plus ausgeführt.
        Die Ausgabe wird im Reiter "TERMINAL" unten ausgegeben (Screenshot(4)). Sollte in der Select Liste dort nicht _run_sqlplus stehen, dann bitte ändern.
        Mit dem Mülleimer Symbol (Screenshot(5)) kann das Fenster wieder geleert werden.

        Mit Klick auf das Dateiensymbol (Screenshot(6)) werden die aktuellen Dateien im Ordner angezeigt, ein nochmaliger Klick öffnet den Code als Vollbild.

        Wer es etwas kürzer mag (aber unflexibler), kann als tasks.json Datei auch die folgende verwenden:

        {
            "version": "2.0.0",
            "tasks": [
                {
                    "taskName": "build",
                    "type": "shell",
                    "command": "sqlplus -s scott/tiger@172.30.30.30:1521/xe.muniqsoft-training.de @${file}",
                },     
            ]
        }


        Dann kann man sich auch die Dateien _run_sqlplus.bat und _show_errors.sql sparen hat dann aber

        • keine schönen Fehlermeldungen in einem eigenen Reiter (man sollte dann unter jeden PL/SQL im Skript "show errors" schreiben)
        • keine Umstellung auf ein Unicode Terminal
        • keine Möglichkeit einfache Anpassungen für Windows/Unix/Max durchzuführen.


        Weitere tolle Tipps erfahren Sie in einem unserer vielen Entwicklungskurse...

         



        Weitere Interessente Artikel zum Thema:



        Empfohlene Schulungen zum Thema:


        LISTAGG Alternative mit CLOB Datentyp (und damit 128TB Maximallänge)

        Bereich:PL/SQL:SQL, Version: ab RDBMS 10.x:RDBMS 12.x:RDBMS 19.3:RDBMS 21.1, Letzte Überarbeitung: 11.01.2021

        Keywords:listagg clob

        Wer kennt es nicht, immer wenn man mal listagg benötigt, reciht der Speicherplatz nicht, weil listagg maximal 32767 Bytes zurückgeben kann.

        Mit einem kleinen Trick klappt es auch mit CLOB als Rückggaabetyp. Dazu verwenden wir die XML Funktion XMLELEMENT.
        Keine Angst, sie müssen kein XML dazu beherschen:-)

        Wir haben das ganze in eine WITH Funktion gekappselt, für den Fall, dass Sie kein CREATE PROCEDURE RECHT haben.
        Die Funktion hat bis zu 3 Parameter,
        column_name ist der Name der Spalte, die zusammengefasst werden soll
        table_name ist der Name der Tabelle, in der die Spalte enthalten ist
        where_cond ist Optional und beinhaltet eine Filterklausel Default 1=1 also alles)
        order_by Optional kann auch ein Sortierreihenfolge bei der Ausgabe definiert werden (Default: Sortiert nach der Ausgabespalte)
        delimiter ist auch Optional und gibt das Trennzeichen zwischen der Ausgabe der Spaltenwerte an (Default ',' )

        WITH
        FUNCTION listagg_clob (
        column_name IN VARCHAR2,
        table_name  IN VARCHAR2,
        where_cond  IN VARCHAR2 DEFAULT NULL,
        order_by    IN VARCHAR2 DEFAULT NULL,
        delimiter   IN VARCHAR2 DEFAULT ',')
        RETURN CLOB
        IS
        ret_clob CLOB;
        BEGIN
        EXECUTE IMMEDIATE q'!select replace(replace(XmlAgg(
                          XmlElement("a", !' || column_name ||')
                          order by ' ||nvl(order_by,'1') ||
                          q'!)
                          .getClobVal(),
                      '<a>', ''),
                    '</a>','!'|| delimiter ||q'!') as aggname
           from !' || table_name || q'!
          where  !' || nvl(where_cond,' 1=1') INTO ret_clob;
        RETURN ret_clob;
        END;
        SELECT listagg_clob('table_name','all_tables') FROM dual;



        Wenn Sie das Ganze als eigenständige Funktion anlegen möchten:

        CREATE OR REPLACE FUNCTION listagg_clob (
        column_name IN VARCHAR2,
        table_name  IN VARCHAR2,
        where_cond  IN VARCHAR2 DEFAULT NULL,
        order_by    IN VARCHAR2 DEFAULT NULL,
        delimiter   IN VARCHAR2 DEFAULT ',')
        RETURN CLOB
        IS
        ret_clob CLOB;
        BEGIN
        EXECUTE IMMEDIATE q'!select replace(replace(XmlAgg(
                          XmlElement("a", !' || column_name ||')
                          order by ' ||nvl(order_by,'1') ||
                          q'!)
                          .getClobVal(),
                      '<a>', ''),
                    '</a>','!'|| delimiter ||q'!') as aggname
           from !' || table_name || q'!
          where  !' || nvl(where_cond,' 1=1') INTO ret_clob;
        RETURN ret_clob;
        END;
        /
         


        Testcases:

        SELECT listagg_clob('table_name','all_tables')
        FROM dual;
        SELECT listagg_clob('table_name','all_tables',order_by=>'table_name desc nulls last')
        FROM dual;

        SELECT length(listagg_clob(q'!owner||'.'||table_name!','all_tables', where_cond=>q'!table_name<>'MARCO'!'))
        FROM dual;
        =>60962

        SELECT listagg_clob('table_name','all_tables',order_by=>'table_name desc nulls last', delimiter=>':')
        FROM dual;
         


        Damit können Sie nun (fast) unbegrenzt die Werte einer Spalte zusammenfassen und weiterverarbeiten.



        Weitere Interessente Artikel zum Thema:


        Empfohlene Schulungen zum Thema:


        Oracle APEX Variablen im Oracle Trigger auslesen

        Bereich:APEX:PL/SQL, Version: ab APEX 18.1, Letzte Überarbeitung: 12.01.2021

        Keywords:Oracle Apex Trigger Variablen

        Wenn man in seiner Oracle Datenbank Trigger zu Audit-Zwecken verwendet, ist man erstaunt, das als Apex Benutzer nicht der Anmeldebenutzer sondern ANONYMOUS oder APEX_PUBLIC_USER erscheint.

        Das Problem kann man natürlich mit ein paar PL/SQL Objekten lösen.:-)
         

        GRANT CREATE ANY CONTEXT TO scott;
        DROP CONTEXT APEX_ENV;


        Etwas unbekannt ist das Oracle Objekt Context, mit dem die Funktion sys_context erweitert werden kann.
        Die Funktion sys_context besteht aus zwei Parametern:
        Parameter 1: userenv
        Parameter 2: ca 60 Stück u.a. OS_OSER, SESSION_USER, ...
        Wir können die Funktionen nun erweitern, indem wir den ersten Parameter ändern auf apex_env
         

        CREATE OR REPLACE CONTEXT apex_env
        USING apex_env
        /


        Die Context Funktion benötigt zum Auffruf immer ein Package, das wir hiermit anlegen:

        CREATE OR REPLACE PACKAGE apex_env
        IS
         PROCEDURE set_context(
         p_variable IN VARCHAR2,
         p_value    IN VARCHAR2);
        END;
        /

        CREATE OR REPLACE PACKAGE BODY apex_env IS
          PROCEDURE set_context(
            p_variable IN VARCHAR2,
            p_value    IN VARCHAR2) IS
          BEGIN
            dbms_session.set_context('apex_env',p_variable, p_value);
          END set_context;
        END apex_env;
        /

         

        CREATE PUBLIC SYNONYM apex_env for apex_env;
        GRANT EXECUTE ON my_login_package TO scott;


        Führen wir einen Testcase durch und setzen den Context manuell:

        BEGIN
        apex_env.set_context('APP_USER','Marco');
        apex_env.set_context('APP_PAGE_ID',123);
        apex_env.set_context('APP_ID',100);
        apex_env.set_context('APP_SESSION',123454678);
        END;
        /


        Nun können wir den Context wieder auslesen:
         

        SELECT
        sys_context('apex_env','APP_USER') as APP_USER,
        sys_context('apex_env','APP_PAGE_ID') as APP_PAGE_ID,
        sys_context('apex_env','APP_ID') as APP_ID,
        sys_context('apex_env','APP_SESSION') as APP_SESSION
        FROM dual;


        Jetzt erstellen wir uns eine Audit-Tabelle, in der stehen soll, welcher APEX Benutzer auf welcher Seite, in welcher APP mit welcher Session ID
        die Änderung durchgeführt hat.

        CREATE TABLE emp_audit (
        empno        INT,
        app_user     VARCHAR2(30),
        app_page_id  INT,
        app_id       INT,
        app_session  INT);


        und der passende Trigger dazu:

        CREATE OR REPLACE TRIGGER EMP_TRG
        BEFORE DELETE OR INSERT OR UPDATE ON EMP
        FOR EACH ROW
        BEGIN
          INSERT INTO emp_audit (empno,app_user,app_page_id,app_id,app_session)
          VALUES (nvl(:new.empno,:old.empno),
            sys_context('apex_env','APP_USER'),
            sys_context('apex_env','APP_PAGE_ID'),
            sys_context('apex_env','APP_ID'),
            sys_context('apex_env','APP_SESSION'));
        END;
        /


        Jetzt brauchen wir nur noch den kleinen Testblock in unsere APEX-App einbauen:
        Gehen Sie dazu auf: Edit Application Properties / Security und dort auf Database Session.
        Im Bereich "Initialization PL/SQL Code" geben Sie dort ein:

        BEGIN
        apex_env.set_context('APP_USER',:APP_USER);
        apex_env.set_context('APP_PAGE_ID',:APP_PAGE_ID);
        apex_env.set_context('APP_ID',:APP_ID);
        apex_env.set_context('APP_SESSION',:APP_SESSION);
        END;
        /


        Weitere Tipps und Tricks erfahren Sie in einem unserer APEX oder PL/SQL Kurse (auch als Video-Streaming)



        Weitere Interessente Artikel zum Thema:


        Empfohlene Schulungen zum Thema:


        JSON Daten extrahieren

        Bereich:APEX:PL/SQL, Version: ab RDBMS 18.3, Letzte Überarbeitung: 19.05.2022

        Keywords:JSON, Oracle

        In unserem heutigen Beitrag wollen wir uns mit dem Thema beschäftigen, wie man aus einem JSON String wieder die Daten extrahieren kann.
        Diesen Anwendungsfall hatte ich die letzten 24 Monat sehr häufig, deswegen wurde es mal wieder Zeit sich mit dem Thema JSON in Oracle zu beschäftigen.

        Wir fangen mit einer kleinen Test-Tabelle basiernd auf den Emp und Dept Daten des Benutzers SCOTT an:
        Die Besonderheit / Schwierigkeit besteht darin, dass die Mitarbeiter in einem Array eingetragen werden (["CLARK","KING","MILLER"]
         

        CREATE TABLE dept_json   
        ( json_text CLOB,   
            CONSTRAINT check_json CHECK ( json_text IS JSON ) );

        INSERT INTO dept_json   
        VALUES ('{ "DEPT" : {
                               "DEPTNO" : 10,
                               "DNAME" : "ACCOUNTING",   
                               "LOC" : "NEW YORK",  
                               "EMPS"  : ["CLARK","KING","MILLER"] }}');

        INSERT INTO dept_json   
        VALUES ('{"DEPT" : {
                               "DEPTNO" : 20,
                               "DNAME" : "RESEARCH",   
                               "LOC" : "DALLAS",  
                               "EMPS"  : ["SMITH","JONES","SCOTT","ADAMS","FORD"] }}');


        INSERT INTO dept_json   
        VALUES ('{"DEPT" : {
                               "DEPTNO" : 30,
                               "DNAME" : "SALES",   
                               "LOC" : "CHICAGO",  
                               "EMPS"  : ["ALLEN","BLAKE","JAMES","MARTIN","TURNER","WARD"] }}');


        Als Bonustrack legen wir einen Oracle Text-Index auf die JSON Spalte:


        CREATE INDEX dept_json_index   
          ON dept_json(json_text)   
          INDEXTYPE IS CTXSYS.CONTEXT   
          PARAMETERS ('SECTION GROUP CTXSYS.JSON_SECTION_GROUP SYNC (ON COMMIT)');


        Wir können dann mit der Funktion JSON_TEXTCONTAINS im JSON String suchen.

        SELECT json_text   
          FROM dept_json   
         WHERE JSON_TEXTCONTAINS(json_text, '$', 'KING');


        Das Ergebnis sieht dann so aus:


        { "DEPT" : {
                               "DEPTNO" : 10,
                               "DNAME" : "ACCOUNTING",   
                               "LOC" : "NEW YORK",  
                               "EMPS"  : ["CLARK","KING","MILLER"] }}


        Wenn man die Daten der JSON Tabelle wieder extrahieren (und weiterverarbeiten) möchte:


        SELECT deptno, dname, loc, linenum, arrayval
        from dept_json,json_table(json_text, '$.DEPT[*]'
        columns ( arraynum for ordinality, DEPTNO,DNAME,LOC,
            nested path '$.EMPS[*]'
                columns ( linenum for ordinality,nested path '$[*]'
                         columns( arrayval varchar2 path '$')
          ) ));


        Das Ergebnis sieht dann so aus:
         

        DEPTNODNAMELOCLINENUMARRAYVAL
        10ACCOUNTINGNEW YORK1CLARK
        10ACCOUNTINGNEW YORK2KING
        10ACCOUNTINGNEW YORK3MILLER
        20RESEARCHDALLAS1SMITH
        20RESEARCHDALLAS2JONES
        20RESEARCHDALLAS3SCOTT
        20RESEARCHDALLAS4ADAMS
        20RESEARCHDALLAS5FORD
        30SALESCHICAGO1ALLEN
        30SALESCHICAGO2BLAKE
        30SALESCHICAGO3JAMES
        30SALESCHICAGO4MARTIN
        30SALESCHICAGO5TURNER
        30SALESCHICAGO6WARD


        Wenn man mal einen unbekannten JSON String auswerten möchte stellt Oracle dafür auch ein Package zur Verfügung:

        SELECT json_dataguide(json_text, DBMS_JSON.FORMAT_HIERARCHICAL, DBMS_JSON.PRETTY)
        FROM dept_json;


        Die Ausgabe sieht dann so aus:

        {
          "type" : "object",
          "o:length" : 128,
          "properties" :
          {
            "DEPT" :
            {
              "type" : "object",
              "o:length" : 128,
              "o:preferred_column_name" : "DEPT",
              "properties" :
              {
                "LOC" :
                {
                  "type" : "string",
                  "o:length" : 8,
                  "o:preferred_column_name" : "LOC"
                },
                "EMPS" :
                {
                  "type" : "array",
                  "o:length" : 64,
                  "o:preferred_column_name" : "EMPS",
                  "items" :
                  {
                    "type" : "string",
                    "o:length" : 8,
                    "o:preferred_column_name" : "scalar_string"
                  }
                },
                "DNAME" :
                {
                  "type" : "string",
                  "o:length" : 16,
                  "o:preferred_column_name" : "DNAME"
                },
                "DEPTNO" :
                {
                  "type" : "number",
                  "o:length" : 2,
                  "o:preferred_column_name" : "DEPTNO"
                }
              }
            }
          }

        }

        Und nun viel Spaß mit der Auswertung von Daten aus JSON-Ausdrücken. Bei uns wird das Thema ausführlich behandelt in den Kursen Oracle PL/SQL II, Oracle REST und Oracle Dev New Features 12c - 21c.



        Weitere Interessente Artikel zum Thema:


        Empfohlene Schulungen zum Thema:


        JSON Umwandlung in realtionales Format mit JSON_TABLE Beispiele

        Bereich:APEX:PL/SQL, Version: ab RDBMS 19.3, Letzte Überarbeitung: 22.07.2022

        Keywords:JSON, Array

        In vielen unserer Kurse ist JSON ein Thema (z.B. im APEX oder ORDS oder PL/SQL II Kurs). Da die Syntax gewöhnungsbedürftig ist, haben wir mal ein paar Beispiele generiert um die syntax besser zu verstehen.
        Ausgangslage ist ein JSON String, der in ein relationales Modell überführt werden soll.

        Übersicht der Beispiele:
        Beispiel 1, mit 3 Werte
        Beispiel 2, Verschiedene Datentypen in JSON
        Beispiel 3, mit Geschachtelten Arrays
        Beispiel 4, mit 3 Zeilen mit 2 Feldern
        Beispiel 5, mit 3 Zeilen mit 2 Feldern und einem Root Eintrag (var)
        Beispiel 6, mit 2 Feldern und 3 Zeilen
        Beispiel 7, mit Sub-Sub Feldern
        Beispiel 8, nur einen Wert extrahieren
        Beispiel 9, Wieviele Elemente hat das Array?
        Beispiel 10, Sub-Arrays (Darstellungsvariante 1)
        Beispiel 11, Sub-Arrays (Darstellungsvariante 2)   
        Beispiel 12, Tabelle die von Oracle mit Auto-Rest zurückkommt zerlegen
        Beispiel 13: JSON String gefiltert

        Beispiel 1, mit 3 Werte

        WITH j_son as (SELECT '
        {"a":1,
         "b":2,
         "c":3
        }' as txt from dual)
        SELECT a,b,c
        FROM j_son,  
             JSON_TABLE(j_son.txt, '$'
             COLUMNS ( a,b,c )
                  ) jt;


        Ergebnis:
         

        ABC
           
        123



        Beispiel 2, Verschiedene Datentypen in JSON:

        WITH j_son as (SELECT '
        {"Zahl":1, "Text": "aBc", "Bool":TRUE,
        "Datum1":"2022-07-15", "Datum2":"2019-07-15T14:11:27",
        "Datum3":"2022-07-15T14:11:27Z", "Datum4":"2022-07-15T14:11:27+00:00"
        }' as txt from dual)
        SELECT Zahl,Text,Bool,Datum1,Datum2,Datum3,Datum4
        FROM j_son,  JSON_TABLE(j_son.txt, '$' COLUMNS (
               Zahl NUMBER PATH '$."Zahl"',
               Text VARCHAR2(2000) PATH '$."Text"',
               Bool VARCHAR2(10) PATH '$."Bool"',
               Datum1 DATE PATH '$."Datum1"',
               Datum2 DATE PATH '$."Datum2"',
               Datum3 DATE PATH '$."Datum3"',
               Datum4 DATE PATH '$."Datum4"'
                  )
             ) jt;


        Ergebnis:

        ZahlTextBoolDatum1Datum2Datum3Datum4
               
        1aBctrue15.07.2022 00:00:0015.07.2022 00:00:0015.07.2022 00:00:0015.07.2022 00:00:00

        Hinweis: Die Stunden / Minuten / Sekunden verschwinden hier. Alternative: Datum als Text parsen und mittels to_date in Datum umwandeln.

        Beispiel 3, mit Geschachtelten Arrays

        WITH j_son as (SELECT '
        {"a":"a1",
         "b": [
                {"c": "c1",
                 "d": [1,2]}
              ]
        }' as txt from dual)
        SELECT j_son.txt, "a","c","d"
        FROM j_son,  
             JSON_TABLE(j_son.txt, '$' COLUMNS (
               "a" VARCHAR2(5) PATH '$.a',
               NESTED PATH '$.b[*]' COLUMNS (
                  "c" VARCHAR2(5) PATH '$.c',
                  NESTED PATH '$.d[*]' COLUMNS (
                     "d" NUMBER PATH '$[0]'
                  )
               )
             )) jt;

            
        Ergebnis:

        a1cd
           
        a1c11
        a1c12



        Beispiel 4, mit 3 Zeilen mit 2 Feldern

        WITH j_son as (SELECT '
        [{"A":1,"B":2},
        { "A":3,"B":4},
        { "A":5,"B":6}]' txt from dual)
        SELECT A,B
        FROM j_son,json_table(j_son.txt,'$[*]'
        columns (A,B));

         Ergebnis

        AB
        12
        34
        56



        Beispiel 5, mit 3 Zeilen mit 2 Feldern und einem Root Eintrag (var)

        WITH j_son as (select '
        {var:[{"A":1,"B":2},
        { "A":3,"B":4},
        { "A":5,"B":6}]}' txt from dual)
        select A,B
        FROM j_son,json_table(j_son.txt,'$.var[*]' columns (A,B));

          Ergebnis

        AB
        12
        34
        56



        Beispiel 6, mit 2 Feldern und 3 Zeilen

        WITH j_son as (select '[
        {"Vorname":"Marco","Nachname":"Patzwahl"},
        {"Vorname":"Hansi","Nachname":"Wurst"},
        {"Vorname":"Karin","Nachname":"Sorglos"}
        ]' txt from dual)
        select vorname,nachname
        FROM j_son,json_table(j_son.txt,'$[*]'
        columns (Vorname,Nachname));

          Ergebnis

        VornameNachname
          
        MarcoPatzwahl
        HansiWurst
        KarinSorglos



        Beispiel 7, mit Sub-Sub Feldern

        SELECT id, var1, sub_id, sub_val
        FROM JSON_TABLE (
                to_clob('{ id: 123, var1: "abc",
                subvalues : [
                    { id: 1, value: "a", },
                    { id: 2, value: "b" } ]} '),
                '$' COLUMNS (
                    id NUMBER PATH '$.id',
                    var1 VARCHAR PATH '$.var1',
                    NESTED PATH '$.subvalues[*]'
                    COLUMNS ( sub_id NUMBER PATH '$.id',
                    sub_val VARCHAR2(4000) PATH '$.value'
                    ) ) );


        oder

        WITH j_son as (select '{ id: 123, var1: "abc",
                subvalues : [
                    { id: 1, value: "a", },
                    { id: 2, value: "b" }
                    ]}' as txt from dual)
        SELECT id, var1, sub_id, sub_val
        FROM j_son,JSON_TABLE (j_son.txt,
                '$' COLUMNS (
                    id NUMBER PATH '$.id',
                    var1 VARCHAR PATH '$.var1',
                    NESTED PATH '$.subvalues[*]'
                    COLUMNS ( sub_id NUMBER PATH '$.id',
                    sub_val VARCHAR2(4000) PATH '$.value'
                    ) ) );

          Ergebnis

        IDVar2SUB_IDSUB_VAL
        123abc1a
        123abc2b
            



        Beispiel 8, nur einen Wert extrahieren

        SELECT json_value ('{"emp":{ "name":"Marco","city":"Bern" } } ','$.emp.name') as name
        FROM dual;


        Beispiel 9, Wieviele Elemente hat das Array?

        SELECT json_value('[1, 2, 3, 4 ]','$.size()') FROM dual;

        =>4
        oder

        SELECT json_query('[1,[2,2,2],3]','$[*].size()' WITH ARRAY WRAPPER)  FROM dual;

        =>[1,3,1]

        Beispiel 10: Sub-Arrays (Darstellungsvariante 1)

        WITH j_son as (select '
        {var:[{"A":[1,2],"B":[3,4]},
        { "A":[5,6],"B":[7,8]},
        ]}' txt from dual)
        select A,B
        FROM j_son,json_table(j_son.txt,
        '$.var' columns (
            nested path '$.A[*]' columns (A number path '$[0]'),
            nested path '$.B[*]' columns (B number path '$[0]') ));

            
        Ergebnis:
         

        AB
          
        1 
        2 
         3
         4
        5 
        6 
         7
         8


           
        Beispiel 11: Sub-Arrays (Darstellungsvariante 2   

        WITH j_son as (select '
        {var:[{"A":[1,2],"B":[3,4]},
        { "A":[5,6],"B":[7,8]},
        ]}' txt from dual)
        select val
        FROM j_son,json_table(j_son.txt,
        '$.var' columns (
            nested path '$.*[*]' columns (val number path '$[0]' )));

            

        VAL
         
        1
        2
        3
        4
        5
        6
        7
        8


        Beispiel 12, Tabelle die von Oracle mit Auto-Rest zurückkommt zerlegen

        WITH j_son as (SELECT '
        {"items":[
        {"deptno":10,"dname":"ACCOUNTING","loc":"NEW YORK",
        "links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/10"}]},
        {"deptno":20,"dname":"RESEARCH","loc":"DALLAS",
        "links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/20"}]},
        {"deptno":30,"dname":"SALES","loc":"CHICAGO",
        "links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/30"}]},
        {"deptno":40,"dname":"OPERATIONS","loc":"BOSTON",
        "links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/40"}]}],
        "hasMore":false,"limit":25,"offset":0,"count":4,"links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/"},
        {"rel":"edit","href":"http://127.0.0.1:8080/ords/scott/dept/"},
        {"rel":"describedby","href":"http://127.0.0.1:8080/ords/scott/metadata-catalog/dept/"},
        {"rel":"first","href":"http://127.0.0.1:8080/ords/scott/dept/"}]}' as txt from dual)
        SELECT deptno,dname,loc,href
        FROM  
             j_son,JSON_TABLE(j_son.txt, '$.items[*]'
             COLUMNS ( deptno,dname,loc,
             nested path '$.links[*]' columns (href),
             limit,offset,count)
                  ) jt;

        Ergebnis:

        DEPTNODNAMELOCHREF
            
        10ACCOUNTINGNEW YORKhttp://127.0.0.1:8080/ords/scott/dept/10
        20RESEARCHDALAShttp://127.0.0.1:8080/ords/scott/dept/20
        30SALESCHICAGOhttp://127.0.0.1:8080/ords/scott/dept/30
        40OPERATIONSBOSTONhttp://127.0.0.1:8080/ords/scott/dept/40

        oder ausführlicher (aber nur für eine Ergebnis-Zeile zur Vereinfachung):


        WITH j_son as (SELECT '
        {"items":[
        {"deptno":10,"dname":"ACCOUNTING","loc":"NEW YORK",
        "links":[{"rel":"self","href":"http://127.0.0.1:8080/ords/scott/dept/40"}]}],
        "hasMore":false,"limit":25,"offset":0,"count":4,"links":[{"rel":"self",
        "href":"http://127.0.0.1:8080/ords/scott/dept1/"},
        {"rel":"edit","href":"http://127.0.0.1:8080/ords/scott/dept2/"},
        {"rel":"describedby","href":"http://127.0.0.1:8080/ords/scott/metadata-catalog/dept/"},
        {"rel":"first","href":"http://127.0.0.1:8080/ords/scott/dept3/"}]}' as txt from dual)
        SELECT deptno,dname,loc,href,hasMore,offset,count,href2,href3
        FROM  j_son,
             JSON_TABLE(j_son.txt, '$.items[*]'
             COLUMNS ( deptno,dname,loc,
             nested path '$.links[*]' columns (href)) ),
             JSON_TABLE(j_son.txt, '$'
             COLUMNS ( hasMore,limit,offset,count,
             nested path '$.links[0]' columns (href2 varchar2 path '$.href')) ),
             JSON_TABLE(j_son.txt, '$'
             COLUMNS (
             nested path '$.links[1]' columns (href3 varchar2 path '$.href')) )jt;
        DEPTNODNAMELOCHREFHASMOREOFFSETCOUNTHREF2HFREF3
                 
        40OPERATIONSBOSTONhttp://127.0.0.1:8080/ords/scott/dept/40false04http://127.0.0.1:8080/ords/scott/dept1/http://127.0.0.1:8080/ords/scott/dept2/

        Beispiel 13: JSON String gefiltert (finde die Werte für B, wenn a=4 , 7, 10 oder 13 ist)

        WITH j_son AS (
        select '{
          "items": [
            {
              "sub": [
                {
                  "a": 1,
                  "b": 2,
                  "c": 3
                },
                {
           &nb