Posts mit dem Label 11g werden angezeigt. Alle Posts anzeigen
Posts mit dem Label 11g werden angezeigt. Alle Posts anzeigen

2013-03-25

ViewObject Query mit IN Where Clause

Anwendungsfall: Wir möchten zur Laufzeit die Abfrage an eine Datenbanktabelle durch eine Where-Clause IN Bedingung einschränken.

Beispiel: SELECT * FROM EMPLOYEES WHERE EMPLOYEE_ID IN (:dynamischeListe)

Problemstellung: Deklarativ bietet ADF nicht die Möglichkeit eine IN Abfrage innnerhalb eines ViewCriterias zu erstellen. Daher müssen wir an dieser Stelle ein ViewCriteria programmatisch erstellen.

Lösungsmöglichkeit:

Schritt 1 - Wir erstellen an dem gewünschten ViewObject eine JavaMethode, welche die IN Einschränkung durchführt.



(Die Methode kann natürlich auch auf andere Datentypen erweitert bzw. generell verallgemeinert werden.)

Schritt 2 - Wir stellen die Methode dem Client zur verfügung.
 

Schritt 3 - Test (hier mittels Test-JavaKlasse)

Was das folgende (gewünschte Ergebnis liefert)

Zum Testen: BC-ProgrammaticAdditionOfViewCriteriaINClause-11.1.1.6

2013-02-08

Fehler beim Start des integrierten WebLogic Servers vom JDeveloper unter Windows 7

Ich hatte bereits in einem vorherigen Post eine Möglichkeit beschrieben, wie vorgegangen werden kann, falls sich der lokale WebLogic Server unter Windows 7 nicht mehr vom JDeveloper aus starten lässt. Die ursprüngliche Lösung hatte den Nachteil, dass der Zwischenschritt über das Deployment der .ear Datei nötig war.

Eine weitere Lösungsmöglichkeit stellt das setzen einer neuen JDeveloper Home Variable dar. Dies kann ganz einfach über zwei Wege gemacht werden.

  1.  Setzen der Systemvariable JDEV_USER_DIR:
    Start -> Rechtsklick Computer -> Eigenschaften
    ErweiterteSystemeinstellungen im linken Bereich
     Umgebungsvariablen...
     Im Bereich Benutzervariablen -> Neu...

    Wert der Variablen entspricht dem Pfad zum gewünschten Verzeichnis (um den Fehler zu umgehen sollte ein Pfad ohne Leerzeichen und Sonderzeichen gewählt werden)


  2. Starten des JDeveloper Mittels Kommandozeilenparameter:

    Rechtsklick auf Verknüpfung -> Eigenschaften

    Einfügen des Parameters -J -Dide.user.dir="Pfad zum gewünschten Verzeichnis"

Der Vorteil der unteren Variante ist sicherlich, dass die Variable nicht für alle installierten JDeveloper Versionen allgemeingültig ist, sondern nur eine Instanz abdeckt, während erstere Variante eine Variable schreibt, die im Standardfall von allen JDeveloper Versionen ausgelesen wird. Will man dies unterdrücken, kann in der JDeveloper boot Datei ($JDEV_HOME/jdev/bin/jdev.boot) der Wert entsprechend geändert werden.

In dem nun gesetzten User Dir wird eine neue Instanz des integrierten WLS installiert, welcher sich nun auch wieder über den JDeveloper starten lassen sollte.

2013-02-05

Data Pump Export mit Flashback-Zeit über Enterprise Manager

Im Database Control einer 11.2.0.2 Datenbank habe ich versucht einen lesekonsistenten Data Pump Export auf eine Flashback-Zeit als Job zu konfigurieren. Dabei bin ich auf zwei Probleme getroffen:

  1. Bei der Auswahl der Flashback-Zeit kann keine volle Stunde gewählt werden, da als Minuten 5 bis 60 in Fünferschritten angeboten werden. 00 Minuten fehlen und 60 Minuten ist ja Unsinn bei einer Zeitangabe. Siehe auch folgenden Bildschirmabzug:
    OEM-DP-Job-Fehler
  2. Wenn eine sinnvolle Flashback-Zeit (z.B. 11.03.2013 15:05 Uhr) angegeben ist, dann führt das Weiterleiten des Jobs zum Fehler ORA-39001, ORA-39150, ORA-08186:
     OEM-DP-Job-Fehler2

Den ersten Fehler kann ich in anderen Installationen (10gR2, 11gR1 und 11gR2) nicht nachvollziehen. Das kann also eventuell an einer ungünstigen Kombination der NLS Parameter liegen.

Den zweiten Fehler habe ich auch in anderen Installationen, so auch beim Database Control einer 10.2.0.3 Datenbank als auch im Grid Control 11g bei einer 11.1.0.7 und einer 11.2.0.1 Datenbank. Das scheint also ein Bug zu sein.
Der eigentliche Fehler ORA-08186 resultiert aus dem generierten Data Pump Script, speziell aus der folgenden Zeile:
  dbms_datapump.set_parameter(handle => h1, name => 'FLASHBACK_TIME', value => '11-03-13 15.05');
Es wird implizite und keine explizite Typkonvertierung durchgeführt. Das Format der Datum/Zeitangabe, endspricht nicht dem Default. Dieser kann ermittelt werden mit dem folgenden Select (siehe MOS Note ID 464132.1):
  SELECT * FROM V$NLS_PARAMETERS where parameter = 'NLS_TIMESTAMP_FORMAT';
In meinem Fall ist das Ergebnis:
  PARAMETER VALUE
  NLS_TIMESTAMP_FORMAT DD.MM.RR HH24:MI:SSXFF

Das Format entspricht der deutschen Territory Einstellung. Diese wird aber weder im Database Control noch im Grid Control berücksichtigt.

Wenn man den Data Pump Export mit Flashback-Zeitpunkt über den Enterprise Manager machen möchte, bleibt also nichts anderes übrig als einen manuellen SQL-Skript Job anzulegen und darin dann entweder den Zeitpunkt im zum Default korrekten Format anzugeben oder aber eine explizite Typkonvertierung durchzuführen, z.B.:
  dbms_datapump.set_parameter(handle => h1, name => 'FLASHBACK_TIME', value => '11.03.13 15:00:00');

Ich hoffe diese Information hilft anderen bei der Problemanalyse.

2012-12-13

Oracle Text Composite Domain Index

Bei Nutzung der Oracle Text Volltextsuche kann es oft vorkommen, dass in der WHERE-Bedingung eines SQL-Query sowohl gegen die indizierte Textspalte (mit CONTAINS), als auch gegen eine oder mehrere normale relationale Spalten gefiltert wird, man also eine gemischte Abfrage oder auch „Mixed Query“ hat. Eine solche Abfrage ist womöglich nicht optimal, da sie zu Performanceproblemen führen kann. Was ist in einem solchen Fall zu tun?

Seit Oracle 11g kann man unter Verwendung von z.B. Multi-Column-Datastores, die zusätzlich die relationalen Spalten enthalten, sowie passenden SDATA-Sections die strukturellen Informationen mit in den Volltextindex holen. Die relationalen Abfragen werden dann mit dem SDATA-Operator innerhalb des CONTAINS ausgeführt.

Diese Vorgehensweise hatte bei einem Kunden nicht zum erwünschten Erfolg geführt, was mich zu einer anderen Maßnahme greifen liess:

Ebenfalls seit Oracle 11g gibt es noch eine weitere Möglichkeit, mit
„Mixed Queries umzugehen: Den Composite-Domain-Index (CDI). Hierbei kann man sich das Anlegen des Multi-Column-Datastore und der SDATA-Sections sparen. Man gibt einfach beim Anlegen des Volltext-Indexes die entsprechenden Where-Filter-Spalten als FILTER BY Spalten und/oder die entsprechenden Sortier-Spalten als ORDER BY Spalten an. An der Struktur, die der Index anlegt kann man erkennen, dass hier im Hintergrund eine SDATA-ähnliche Technik arbeitet.

CREATE INDEX mein_index ON meine_tabelle(meine_textspalte)
   INDEXTYPE IS ctxsys.context
   FILTER BY suchspalte1, suchspalte2, ...
   ORDER BY sortierspalte1, sortierspalte2, ...

Dies brachte eine deutliche Performance-Verbesserung gegenüber der „manuellen“ SDATA-Anwendung. Ein weiterer Vorteil: Man muss die SQL-Queries nicht umstellen, die relationalen Abfragen werden bei Verwendung eines CDI wie gehabt verwendet und müssen nicht mit in den CONTAINS-Ausdruck.

2012-10-23

Download älterer Versionen der Oracle Software

Seit Mitte 2012 können ältere Versionen der Oracle Software nicht mehr über das Oracle Technology Network (OTN) und auch nicht ohne weiteres aus der Oracle Software Delivery Cloud herunter geladen werden. Das betrifft alle Produkte.

Die Downloadlinks älterer Versionen sind aus dem OTN spurlos verschwunden.

Beim Enterprise Manager Download im OTN hatte ich vor einigen Wochen noch einen Hinweis gesehen, wie man an den Downloadlink einer alten Version herankommt. Dieser Hinweis ist aber nun auch nicht mehr vorhanden und bei anderen Produkten stand er nicht. Im OTN findet sich also kein Hinweis ob und wie man eine alte Version herunter laden kann.

In einem anderen Blog habe ich den Hinweis gefunden, dass man mit einem bekannten Link auf eine alte Software Version im OTN diese noch herunter laden kann. Das geht nun jedoch auch nicht mehr. Die alten Softwarestände scheinen aus dem OTN komplett entfernt zu sein.

Benötigt man ältere Softwarestände der Oracle Produkte, so muss man über einen gültigen Support Vertrag mit Oracle verfügen. Die Medien oder Downloads kann man dann über My Oracle Support (MOS) anfordern (siehe FAQ: Downloading 9i, 10g, and 11g database software media [ID 1476075.1]). Dazu erstellt man einen “Contact Us” bzw. “non-technical” Service Request. Details, wie ein solcher Service Request (SR) erstellt wird finden sich in folgender MOS Note:

Wünscht man im SR kein “physical software media shipment” dann erhält man als Ergebnis bzw. Lösung im SR einen Link auf die Oracle Software Delivery Cloud mit einem eigenen Token. Über diesen kann man die Software dann herunterladen. Bei komplexeren Produkten, wie z.B. Enterprise Manager Grid Control, für die mehrere Downloads erforderlich sind (z.B. Grid Control, WebLogic Server, 32bit jdk for 64bit Java, Grid Control Agent), kann man diese in einem gemeinsamen SR anfordern und kann sie dann auch unter einem Link herunter laden.

Wichtig: Der angeforderte Downloadlink ist nur eine begrenzte Zeit (7 Tage) verfügbar!

Danach bekommt man folgende Meldung:
error

2012-08-24

PL/SQL Performancebremsen mit dem hierarchischen Profiler finden

Ein paar scheinbar unbedeutende Anpassungen in einer PL/SQL Prozedur, in einer weiteren nur ein neues, im „Trockentest“ rasend schnelles SQL Query hinzugefügt, noch eben ein kleiner Testdurchlauf auf dem Entwicklungssystem: Funktioniert, ist auch schnell genug. Also können die Erweiterungen in den Ladeprozess des Datawarehouse-Testsystems eingespielt werden. Was aber tun, wenn der veränderte Code auf dem Testsystem mit einem deutlich größeren Datenvolumen nun eben nicht mehr schnell genug ist und den vorher performanten Ladelauf ausbremst?

Anstatt jetzt sofort alle Code-Änderungen in Frage zu stellen, gibt es eine recht einfache Möglichkeit, zumindest das PL/SQL-Unterprogramm, dass für die Performanceverschlechterung verantwortlich ist, oder sogar die Zeile, in der ein langsamer SQL-Befehl steht, zu identifizieren: Der hierarchische Profiler im DBMS_HPROF-Package, das seit der Version 11g Release 1 in der Oracle-Datenbank enthalten ist! Dieser Profiler speichert das dynamische Ausführungsprofil von PL/SQL-Programmen und deren Unterprogrammen und lässt so die Analyse von z.B. Anzahl der Aufrufe und in einzelnen Unterprogrammen verbrauchter Zeit zu. Dieser Blogeintrag soll einen kleinen Einblick in die Möglichkeiten des Profilers geben.

Um das DBMS_HPROF-Package zu nutzen, müssen zunächst einige Voraussetzungen mit DBA-Rechten geschaffen werden:

1. Ein Oracle-Directory zum Ablegen einer Tracedatei durch DBMS_HPROF anlegen. Der im Beispiel verwendete Name profiler ist dabei beliebig gewählt.

   CREATE OR REPLACE DIRECTORY profiler
      AS '/home/oracle/profiler';

2. Lese- und Schreibrechte auf dem Verzeichnis vergeben, im Beispiel an den Benutzer dwh_target.

   GRANT READ, WRITE ON DIRECTORY profiler TO dwh_target;

3. Zu guter Letzt muss noch das Privileg zum Ausführen des DBMS_HPROF-Package vergeben werden.

   GRANT EXECUTE ON DBMS_HPROF TO dwh_target;

Das Profiling wird, z.B. in einem anonymen PL/SQL-Block, mit DBMS_HPROF.START_PROFILING (unter Angabe von Ziel-Directory und Ziel-Datei) gestartet und mit DBMS_HPROF.STOP_PROFILING wieder angehalten. Im Beispiel wird die Prozedur fehlerpruefung aus dem Package dq_check_pkg aufgerufen. Diese Prozedur ruft, wie später zu sehen sein wird, weitere Unterprogramme auf.

   BEGIN
      -- Profiling starten unter Angabe von Ziel-Directory und -Datei
      DBMS_HPROF.START_PROFILING('PROFILER', 'profiler.log');
  
      -- Aufruf der zu untersuchenden PL/SQL-Unterprogrammstruktur    
      dq_check_pkg.fehlerpruefung;
  
      -- Profiling stoppen
      DBMS_HPROF.STOP_PROFILING;
   END;

Die dabei entstandene Datei kann man sich mit einem Texteditor anschauen.
P#V PLSHPROF Internal Version 1.0
P#! PL/SQL Timer Started
P#C PLSQL."DWH_TARGET"."DQ_CHECK_PKG"::11."__pkg_init"
P#X 5
P#R
P#C PLSQL."DWH_TARGET"."DQ_CHECK_PKG"::11."FEHLERPRUEFUNG"#980980e97e42f8ec #2130
P#X 26
P#C SQL."DWH_TARGET"."DQ_CHECK_PKG"::11."__static_sql_exec_line2147" #2147
P#X 89
P#R
P#X 14
P#C SQL."DWH_TARGET"."DQ_CHECK_PKG"::11."__static_sql_exec_line2153" #2153
P#X 31496559
P#R
P#X 22
P#C SQL."DWH_TARGET"."DQ_CHECK_PKG"::11."__static_sql_exec_line2157" #2157
P#X 9645
P#R
P#X 6
P#C PLSQL."DWH_TARGET"."DQ_CHECK_PKG"::11."FEHLERPRUEFUNG.C_SENDUNGEN"#980980e97e42f8ec #2140
P#X 22
P#C SQL."DWH_TARGET"."DQ_CHECK_PKG"::11."__static_sql_exec_line2141" #2141
P#X 937
P#R
P#X 2
.
.
.
P#R
P#C PLSQL."SYS"."DBMS_HPROF"::11."STOP_PROFILING"#980980e97e42f8ec #59
P#R
P#! PL/SQL Timer Stopped

Das Zeilenprefix beschreibt dabei den Inhalt der jeweiligen Zeile:
P#V: Startzeile mit Versionsinformation
P#C: Unterprogrammaufruf
P#R: Rückkehr / Rückgabe vom Unterprogramm
P#X: Zwischen vorherigem und folgendem Ereignis verstrichene Zeit (Mikrosekunden)
P#!: Kommentarzeile

Eine Aufrufzeile ist wie folgt aufgebaut:

zeile

Die Analyse der Ausführung mag für kleine Programmstrukturen noch anhand dieser Tracedatei möglich sein, je größer die Datei jedoch wird, desto schwieriger wird es auch, grundsätzliche Informationen auf einen Blick zu erkennen. In einem solchen Fall hilft wieder DBMS_HPROF weiter, denn das Package hält eine weitere nützliche Funktion bereit: DBMS_HPROF.ANALYZE. Mit dieser lassen sich die Ergebnisse aus der Tracedatei aufbereitet in Datenbanktabellen abspeichern.

Bevor die Analyse beginnen kann, muss noch die entsprechende Tabellenstruktur angelegt werden. Ein Script, das diese Aufgabe übernimmt, liegt unterhalb des Datenbank-Home-Verzeichnisses und kann z.B. mit SQL*Plus aufgerufen werden:

   >sqlplus user/passwort@connect_id @$ORACLE_HOME/rdbms/admin/dbmshptab.sql

Dann wird der Inhalt der Tracedatei analysiert, z.B. mit folgenden Code:

   DECLARE
      runid NUMBER;
   BEGIN
      runid := DBMS_HPROF.ANALYZE(LOCATION => 'PROFILER',        
                                  FILENAME => 'profiler.log');
      DBMS_OUTPUT.PUT_LINE('runid = ' || runid);
   END;

Mit der RUNID, die zurückgegeben wird, lassen sich in den Tabellen die zum jeweiligen Analysedurchlauf gehörenden Datensätze identifizieren. Die Ergebnisse sind in den folgenden drei Tabellen zu finden:

DBMSHP_RUNS: Allgemeine Informationen über den Analysedurchlauf

runs

RUNID:             ID des Analysedurchlaufs
RUN_TIMESTAMP:     Zeitstempel des Analysedurchlaufs
TOTAL_ELAPSED_TIME:Dauer des Analysedurchlaufs in Mikrosekunden
RUN_COMMENT:       Kommentar, den man dem ANALYZE optional übergeben kann

DBMSHP_FUNCTION_INFO: Informationen über Unterprogramme

function

RUNID:                ID des Analysedurchlaufs
SYMBOLID:             Eindeutige ID für dieses Unterprogramm in diesem Analysedurchlauf
OWNER:                Besitzer des Moduls, in dem das Unterprogramm definiert ist
MODULE:               Modul, in dem das Unterprogramm definiert ist
TYPE:                 Typ des Moduls, in dem das Unterprogramm definiert ist
FUNCTION:             Name des Unterprogramms
LINE#:                Zeilennummer, an der das Unterprogramm im Modul definiert ist
HASH:                 MD5 Hash für die Signatur des Unterprogramms
NAMESPACE:            Namespace des Unterprogramms (z.B. SQL oder PLSQL)
SUBTREE_ELAPSED_TIME: Im Unterprogramm und dessen Unterprogrammen verbrauchte Zeit
FUNCTION_ELAPSED_TIME:Im Unterprogramm verbrauchte Zeit
CALLS:                Anzahl der Aufrufe dieses Unterprogramms

DBMSHP_PARENT_CHILD_INFO: Informationen über Eltern-Kind-Aufrufe

parentchild

RUNID:                ID des Analysedurchlaufs
PARENTSYMID:          Eltern-SYMBOLID (DBMSHP_FUNCTION_INFO)
CHILDSYMID:           Kind-SYMBOLID (DBMS_FUNCTION_INFO)
SUBTREE_ELAPSED_TIME: Im Unterprogramm und dessen Unterprogrammen beim Aufruf durch dieses Eltern-Element verbrauchte Zeit
FUNCTION_ELAPSED_TIME:Im Unterprogramm beim Aufruf durch dieses Eltern-Element verbrauchte Zeit
CALLS:                Anzahl der Aufrufe dieses Unterprogramms durch dieses Eltern-Element

Verknüpft man nun die Tabellen DBMSHP_PARENT_CHILD_INFO und DBMSHP_FUNCTION_INFO in einem Query miteinander, kann man über entsprechende Filterbedingungen einzelne Unterprogrammaufrufstrukturen gezielt analysieren und erhält Ergebnisse, mit denen imperformante Unterprogramme und sogar Zeilen mit imperformantem SQL-Code identifiziert werden können:

   SELECT pfi.module PARENT_MODULE,
          pfi.function PARENT_FUNCTION,
          pfi.namespace PARENT_NAMESPACE,
          cfi.module CHILD_MODULE,
          cfi.function CHILD_FUNCTION,
          cfi.namespace CHILD_NAMESPACE,
          pci.subtree_elapsed_time/1000000 SUBTREE_TIME_S,
          pci.function_elapsed_time/1000000 FUNCTION_TIME_S,
          pci.calls CALLS
     FROM dbmshp_parent_child_info pci,
          dbmshp_function_info pfi,
          dbmshp_function_info cfi
    WHERE pci.parentsymid = pfi.symbolid
      AND pci.childsymid = cfi.symbolid
      AND pci.runid = 1
    START WITH pfi.function = 'FEHLERPRUEFUNG'
          CONNECT BY PRIOR pci.childsymid = pci.parentsymid;

analyse


Beschäftigt man sich etwas mit den Möglichkeiten und Informationen, so lassen sich auch komplexere PL/SQL-Aufrufstrukturen relativ einfach analysieren.

2012-07-20

PL/SQL - Eine Liste von Tokens aus einem Text mit Hilfe von Regular Expressions erstellen

Problem:

Ein Text soll in PL/SQL nach Auftreten von Ausdrücken durchsucht werden. Als Ergebnis sollen die Treffer als Collection zurückgeben werden.

Lösung:

Der hier vorgestellte Lösungsansatz basiert auf der Verwendung von Regular Expressions. Wir nutzen eine Nested Table zum Sammeln der gefundenen Token und nutzen diesen Typ auch als Rückgabe unserer Splitfunktion.
Mit der Standard-PL/SQL-Funktion regexp_substr lassen sich Bereiche aus einem String herauslesen:
REGEXP_SUBSTR(
   source_string,
   pattern [,
   position [,
   occurrence [,
   match_parameter ]]]
)
source_string:       Zu untersuchender Text
pattern:                 regular expression
position:                Position in source_string, ab der gesucht werden soll
occurence:            Nr. des Tokens, welches als Ergebnis zurückgeben
                            werden soll

match_parameter:  kann eine beliebige Kombination der folgenden
                             Angaben sein:

                                   i: Groß-/Kleinschreibung wird ignoriert
                                  c: Groß-/Kleinschreibung wird beachtet
                                  n: Punkt (.) im Pattern gibt an, dass neue
                                      Zeilen ebenfalls beachtet werden sollen

                                  m: ^ und $ markieren den Anfang bzw.
                                      das Ende einer Zeile bei Multi-Line Strings
Des Weiteren wird die Funktion regexp_instr verwendet, die die Position (Anfang oder Ende) eines Teilstrings ermittelt:
REGEXP_INSTR(
   source_string,
   pattern [,
   position [,
   occurrence [,
   return_option[,
   match_parameter ]]]]
)
source_string:       Zu untersuchender Text
pattern:                regular expression
position:               Position in source_string, ab der gesucht werden soll
occurence:           Nr. des Tokens, welches als Ergebnis zurückgeben
                           werden soll

return_option:      0, wenn die erste Position eines gefundenen Tokens
                               zurückgegeben werden soll;

                           1, wenn die Position nach dem gefundenen Token
                               ausgegeben werden soll

match_parameter: kann eine beliebige Kombination der folgenden
                            Angaben sein:

                                   i: Groß-/Kleinschreibung wird ignoriert
                                  c: Groß-/Kleinschreibung wird beachtet 
                                  n: Punkt (.) im Pattern gibt an, dass neue
                                      Zeilen ebenfalls beachtet werden sollen

                                  m: ^ und $ markieren den Anfang bzw. das
                                      Ende einer Zeile bei Multi-Line Strings
Im Folgenden werden hier zwei Lösungswege vorgestellt. In der ersten Lösung (reg_split(p_string, p_pattern)) wird der String über ein CONNECT-BY-SELECT-Statement mehrfach untersucht. Dabei wird die LEVEL-Variable dazu genutzt den Occurence-Parameter zu füllen.
Die zweite Lösung (reg_split_rec(p_string, p_pattern, p_pos)) verwendet einen rekursiven Ansatz, bei dem der nächste verfügbare Startpunkt an die nächste Rekursionsebene übergeben wird.
IMPLEMENTIERUNG ALS ANONYMER PL/SQL-BLOCK:
==========================================

declare
   type nt_tab_vc2 is table of varchar2(4000);
   v_return nt_tab_vc2;

   function reg_split(p_string in varchar2, 
                      p_pattern in varchar2)
   return nt_tab_vc2 is
      v_ret nt_tab_vc2;
   begin
      v_ret := nt_tab_vc2();
      select
         regexp_substr(p_string, p_pattern, 1, level) token
      bulk collect into
         v_ret
      from
         dual
      where
         regexp_substr(p_string, 
                       p_pattern, 
                       1,
                       level,
                       'i') is not null
      connect by
         regexp_instr(p_string, 
                      p_pattern, 
                      1, 
                      level) > 0;
      return v_ret;
   end;

   function reg_split_rec(p_string in varchar2, 
                          p_pattern in varchar2,
                          p_pos in number default 1)
   return nt_tab_vc2 is
      v_ret nt_tab_vc2;
      v_token varchar2(4000);
      v_pos_next number;
   begin
      v_token := regexp_substr(p_string, 
                               p_pattern, 
                               p_pos, 
                               1);
      if v_token is not null then
         --pattern is found
         v_pos_next := regexp_instr(p_string, 
                                    p_pattern, 
                                    p_pos,
                                    1,
                                    1);

         v_ret := reg_split(p_string,
                           
p_pattern,
                           
v_pos_next);
         v_ret.extend(1);
         v_ret(v_ret.last) := v_token;
         return v_ret;
      else
         --pattern is not found
         return nt_tab_vc2();
      end if;
  end;

begin
   v_return := reg_split(
                    'Hello @world@! The weather is @great@ today.', 
                    '(\@)([a-z-]\w+)(\@)');
   if v_return.count > 0 then
      for i in v_return.first .. v_return.last
      loop
         dbms_output.put_line(lpad(i, 
                                   round(log(10,v_return.count))+1)||' '||v_return(i));
      end loop;
   end if;
end;

BEISPIEL-AUSGABE:
=================
1 @world@
2 @great@