Sonntag, 10. September 2017

12.2.: Mehr Performance für Wildcard Abfragen mit Reverse Token Index

Wildcard Abfragen stellen in allen Textsuchmaschinen gewisse Herausforderungen dar. Unterscheiden kann man dabei im Prinzip folgende Abfragen mit Wildcards
  1. zu Beginn oder von links wie zum Beispiel %frage für Abfrage, Anfrage etc.
  2. in der Mitte wie z.B. bei An%l für Anteil usw.
  3. am Ende oder von rechts wie zum Beispiel An% für Anfrage, Angst, Anteil etc.
Für die führende Wildcard Verwendung (linke Seite) gibt es bei Oracle Text schon seit jeher den sogenannten Substring Index, der zusätzlich zur $I Tabelle eine spezielle $P Hilfstabelle anlegt. In dieser Tabelle werden dann die möglichen Endungen abgelegt. Bei einer Suchabfrage wird dann zuerst auf die $P Tabelle zugegriffen. Ist die $I Tabelle allerdings groß, wächst auch die $P Tabelle an und die Gesamtzeit für die Abfrage könnte damit steigen und somit die Gesamtperformance der Abfrage sinken. Übrigens Informationen zu $P bzw. zu allen DR$ Tabellen findet sich im folgenden Blogspost mit dem Titel Welche DR$-Tabellen gibt es und wozu sind sie gut?.

In 12.2 hat man sich nun dieser Problematik angenommen und eine weitere Struktur, einen neuen Index, mit Namen $V auf die Token Tabelle $I angelegt. Der $X Index kann nämlich bei Abfragen wie %XXX nicht verwendet werden. $V Index hingegen ist ein Function Based Index, der mit der Funktion REVERSE auf die Tokens angelegt wird - also die Tokens in umgekehrter Reihenfolge (REVERSE) indiziert. Damit können nun langandauernde Abfragen mit führenden Wildcards beschleunigt werden. Wie legt man nun diesen neuen Index an und wie kann man die Verwendung monitoren? Wie alle Strukturen für den Präfix und Substring Index wird hierfür die Wordlist vorwendet. Starten wir im ersten Schritt mit einem Präfix und Substring Index wie er vor 12.2 existiert. Unsere Testtabelle mit Namen TEXT_1 besteht aus 261988 Einträgen. Indiziert wird die Spalte ORT, die verschiedene Ortsnamen speichert.
SQL> select count(*) from text_1;

  COUNT(*)
----------
    261988
SQL> select ort from text_1 where rownum<10;

ORT
--------------------------------------------------------------------------------
Madrid
Aachen
Frankfurt
Leipzig
Frankfurt/M
Bonn
Frankfurt
London
Oroville


SQL> begin
  2  ctx_ddl.create_preference('mywordlist', 'BASIC_WORDLIST');
  3  ctx_ddl.set_attribute('mywordlist','PREFIX_INDEX','TRUE');
  4  ctx_ddl.set_attribute('mywordlist','PREFIX_MIN_LENGTH', '1');
  5  ctx_ddl.set_attribute('mywordlist','PREFIX_MAX_LENGTH', '20');
  6  ctx_ddl.set_attribute('mywordlist','SUBSTRING_INDEX', 'YES');
  7  end;
  8  /

PL/SQL procedure successfully completed.

SQL> CREATE INDEX text_ind ON text_1(ort) 
     INDEXTYPE IS ctxsys.context PARAMETERS ('wordlist mywordlist');

Index created.
Nun überprüfen wir die Strukturen, wir können sehen dass es unter anderem eine Hilfstabelle $P gibt.
SQL>select object_name, object_type from user_objects where object_name like 'DR$%';

OBJECT_NAME          OBJECT_TYPE
-------------------- -----------------------
DR$TEXT_IND$I        TABLE
DR$TEXT_IND$K        TABLE
DR$TEXT_IND$N        TABLE
DR$TEXT_IND$P        TABLE
DR$TEXT_IND$R        TABLE
DR$TEXT_IND$RC       INDEX
DR$TEXT_IND$U        TABLE
DR$TEXT_IND$X        INDEX

8 rows selected.
Um den neuen Index $V anzulegen, müssen wir die Wordlist um das Attribut REVERSE_INDEX erweitern und danach ein Index Rebuild durchführen.
SQL> execute ctx_ddl.set_attribute('mywordlist','REVERSE_INDEX', 'TRUE');

PL/SQL procedure successfully completed.

SQL> alter index text_ind rebuild parameters ('replace wordlist mywordlist');

Index altered.
Nun überprüfen wir erneut die Strukturen wie folgt.
SQL> select object_name, object_type from user_objects where object_name like 'DR$%';

OBJECT_NAME          OBJECT_TYPE
-------------------- -----------------------
DR$TEXT_IND$I        TABLE
DR$TEXT_IND$K        TABLE
DR$TEXT_IND$N        TABLE
DR$TEXT_IND$P        TABLE
DR$TEXT_IND$R        TABLE
DR$TEXT_IND$RC       INDEX
DR$TEXT_IND$U        TABLE
DR$TEXT_IND$V        INDEX
DR$TEXT_IND$X        INDEX

9 rows selected.
Betrachten wir uns auch etwas näher diesen neuen $V Index.
SQL> select index_name, table_name, index_type from user_indexes where index_name like 'DR$%';

INDEX_NAME           TABLE_NAME                INDEX_TYPE
-------------------- ------------------------- -------------------------
DR$TEXT_IND$RC       DR$TEXT_IND$R             NORMAL
DR$TEXT_IND$V        DR$TEXT_IND$I             FUNCTION-BASED NORMAL
DR$TEXT_IND$X        DR$TEXT_IND$I             NORMAL

SQL> select table_name, COLUMN_EXPRESSION  from user_ind_expressions where index_name='DR$TEXT_IND$V';

TABLE_NAME                COLUMN_EXPRESSION
------------------------- ------------------------------
DR$TEXT_IND$I             REVERSE("TOKEN_TEXT")
Wie sieht es nun mit der Verwendung aus? Betrachtet man den Ausführungsplan, wird man keine Änderung erkennen. Die interne Verwendung des $V Index ist nicht über den Ausführungsplan nachzuvollziehen.
SQL> select count(*) from text_1 where contains(ort,'%rg')>0;

  COUNT(*)
----------
     16634

SQL> select * from table(dbms_xplan. display_cursor());

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
SQL_ID  cwns7m7vaprfc, child number 0
-------------------------------------
select count(*) from text_1 where contains(ort,'%rg')>0

Plan hash value: 2904455991

-----------------------------------------------------------------------------
| Id  | Operation        | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT |          |       |       |  2684 (100)|          |
|   1 |  SORT AGGREGATE  |          |     1 |    19 |            |          |
|*  2 |   DOMAIN INDEX   | TEXT_IND | 14137 |   262K|  2684   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("CTXSYS"."CONTAINS"("ORT",'%rg')>0)


19 rows selected.
Auch hier hilft die Nutzung von Trace Events. Neu für $V Zugriffe sind dabei die Events mit Nummer 37, 38 und 39. Eine Beschreibung der Events findet sich hier.
Setzen wir diese Events in unserer Umgebung und führen wieder Abfragen mit %rg aus.
-- Time spent in executing the $V cursor
SQL> execute CTX_OUTPUT.ADD_TRACE(37);
-- Time spent in fetching rows from $V
SQL> execute CTX_OUTPUT.ADD_TRACE(38);
--Number of rows with $V fetched metadata
SQL> execute CTX_OUTPUT.ADD_TRACE(39);

SQL> select * from ctx_trace_values;

    TRC_ID  TRC_VALUE
---------- ----------
        37          0
        38          0
        39          0
Zu Beginn sind die Spalten TRC_VALUE mit 0 initialisiert. Nach der ersten Ausführung sollte sich das nun ändern.
SQL> select count(distinct(ort)) from text_1 where contains(ort,'%rg')>0;

COUNT(DISTINCT(ORT))
--------------------
                1461

SQL> select * from ctx_trace_values;

    TRC_ID  TRC_VALUE
---------- ----------
        37        558
        38       5594
        39        650
Wie man erkennen kann, ist die V$ Indexstruktur verwendet worden: 650 Zeilen sind offensichtlich über die V$ Metadaten abgerufen worden.

Donnerstag, 8. Juni 2017

Suche in JSON Dokumenten in 12.2: wie geht das?

Seit 12c (12.1.0.2) sind auch JSON Zugriffe in der Datenbank möglich. Die Verwendung ist ganz einfach: Man definiert eine Datenbankspalte mit einem beliebigen Datentyp für Textstrings (wie zum Beispiel VARCHAR2 oder CLOB). Mit der Bedingung IS JSON kann zusätzlich der Inhalt validiert werden - auf Wohlgeformtheit oder auf die Art der Syntaxverwendung (STRICT oder LAX). Die Zugriffe erfolgen dann mit Standardmitteln und neu eingeführten SQL/JSON Funktionen. Starten wir mit einem einfachen Beispiel. Wir laden uns die Datei PurchaseOrders.dmp mit JSON Dokumenten von Github und stellen diese via EXTERNAL TABLE Syntax zur Verfügung.
create table json_contents (json_document CLOB)
organization external 
(type oracle_loader default directory json_dir
 access parameters
    (records delimited by 0x'0A'
     fields (json_document CHAR(5000)))
     location ('PurchaseOrders.dmp')) reject limit unlimited;
Im vorher definierten logischen Directory JSON_DIR liegt dabei PurchaseOrders.dmp. Danach legen wir eine relationale Tabelle an, die mithilfe der Bedingung IS JSON die Dokumente auf Gültigkeit validiert. Danach laden wir die Dokumente in die Tabelle JSON_TAB.
create table json_tab 
  (id            number generated as identity, 
   json_document clob constraint ensure_json CHECK (json_document IS JSON));
insert into json_tab (json_document) 
      select json_document from json_contents;
Commit;
Soweit so gut. Wie sieht es jetzt aber mit der Suche aus? In 12.2 ist dazu eine neue Datenstruktur eingeführt worden. Ein einfaches Beispiel demonstriert die Verwendung.
create search index JSON_TAB_GUIDE 
 on JSON_TAB (JSON_DOCUMENT) 
 for json PARAMETERS ('DATAGUIDE ON SYNC (ON COMMIT)')
Was bewirkt diese Syntax? Wichtig für Oracle Text User ist die Information, dass damit eine neue Art von Text Index angelegt wird, der beim Commit synchronisiert wird - der neue JSON Search Index. Zusätzlich wird ein sogenannter Data Guide angelegt. Mit diesem wird die Struktur des JSON Dokuments im Data Dictionary hinterlegt. Diese Strukturen können manuell ausgelesen werden, zur automatischen View Erzeugung beitragen oder automatisch virtuelle Spalten anlegen bzw. löschen. Mehr dazu findet sich übrigens auch im Blockeintrag "JSON in 12.2: JSON Generierung, neues Data Guide Konzept, Objekttypen". Alle Syntaxformen und eine Erklärung dazu finden sich im Text Reference Guide. Aber listen wir einfach einmal die erzeugten Objekte auf. Einige davon kommen Ihnen sicher bekannt vor. Die Tabelle DR$JSON_TAB_GUIDE$I gibt beispielsweise die gespeicherten Tokens aus. Eine Erklärung aller Komponenten würde hier allerdings zu weit führen.
SQL> select object_name, object_type from user_objects where object_name like 'DR$%'

OBJECT_NAME                    OBJECT_TYPE
------------------------------ -----------------------
DR$JSON_TAB_GUIDE$DG           TABLE
DR$JSON_TAB_GUIDE$DGSQ         SEQUENCE
DR$JSON_TAB_GUIDE$G            TABLE
DR$JSON_TAB_GUIDE$H            INDEX
DR$JSON_TAB_GUIDE$I            TABLE
DR$JSON_TAB_GUIDE$K            TABLE
DR$JSON_TAB_GUIDE$KI           INDEX
DR$JSON_TAB_GUIDE$N            TABLE
DR$JSON_TAB_GUIDE$NI           INDEX
DR$JSON_TAB_GUIDE$R            TABLE
DR$JSON_TAB_GUIDE$RC           INDEX
DR$JSON_TAB_GUIDE$SN           TABLE
DR$JSON_TAB_GUIDE$SNI          INDEX
DR$JSON_TAB_GUIDE$ST           TABLE
DR$JSON_TAB_GUIDE$STI          INDEX
DR$JSON_TAB_GUIDE$U            TABLE
DR$JSON_TAB_GUIDE$UI           INDEX
DR$JSON_TAB_GUIDE$X            INDEX
Alle bekannten CTX Views wie wie CTX_USER_INDEXES, CTX_USER_INDEX_VALUES usw. können nun zur Hilfe genommen werden um den Index näher zu beleuchten. Interessant sind in erster Linie aber die Abfrage Möglichkeiten. Im Unterschied zu den "normalen" Textabfragen wird dazu nicht der Operator CONTAINS verwendet sondern die neue Bedingung JSON_TEXTCONTAINS, die im SQL Reference Guide beschrieben ist. In der Kurzfassung sieht die Syntax folgendermassen aus:
JSON_TEXTCONTAINS( column, JSON_basic_path_expression, string )
Dabei gilt für den Suchstring folgendes:
A character string. The condition searches for the character string in all of the string and numeric property values in the matched JSON object, including array values. The string must exist as a separate word in the property value. For example, if you search for 'beth', then a match will be found for string property value "beth smith", but not for "elizabeth smith". If you search for '10', then a match will be found for numeric property value 10 or string property value "10 main street", but a match will not be found for numeric property value 110 or string property value "102 main street".

Probieren wir eine erste Abfrage und überprüfen wir das Ganze mit dem Ausführungsplan.
SQL> select distinct(json_value(json_document, '$.CostCenter'))from json_tab
where json_textcontains(json_document, '$.CostCenter', 'A40');  2

(JSON_VALUE(JSON_DOCUMENT,'$.COSTCENTER'))
----------------------------------------------------------------------------------------------------
A40

SQL> select * from table (dbms_xplan.display_cursor());

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
SQL_ID  8su4ht9v5ks1a, child number 0
-------------------------------------
select distinct(json_value(json_document, '$.CostCenter'))from json_tab
where json_textcontains(json_document, '$.CostCenter', 'A40')

Plan hash value: 333039129

-----------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |       |       |     8 (100)|          |
|   1 |  HASH UNIQUE                 |                |     1 |  1997 |     8  (13)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| JSON_TAB       |     5 |  9985 |     7   (0)| 00:00:01 |
|*  3 |    DOMAIN INDEX              | JSON_TAB_GUIDE |       |       |     4   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("CTXSYS"."CONTAINS"("JSON_TAB"."JSON_DOCUMENT",'A40 INPATH
              (/CostCenter)')>0)
Offensichtlich wird der Index verwendet. Aber auch typische Oracle Text Abfrage Operatoren wie NEAR, FUZZY, $ können verwendet werden. Folgende Beispiele lassen sich dazu einfach ausprobieren.
... json_textcontains(json_document,'$','Sporting near green') ...
... json_textcontains(json_document,'$','Sporting near green') ...
... json_textcontains(json_document,'$.ShippingInstructions','fuzzy(fransesco)') ...
... json_textcontains(json_document, '$.LineItems', '$tie') ...
Dabei verwendet auch der neue Operator JSON_EXISTS, den neuen JSON Search Index, wie folgendes Beispiel zeigt.
SQL>select distinct(json_value(json_document, '$.ShippingInstructions.Address.zipCode')) from json_tab
    where json_exists(json_document,'$.ShippingInstructions.Address.zipCode');

(JSON_VALUE(JSON_DOCUMENT,'$.SHIPPINGINSTRUCTIONS.ADDRESS.ZIPCODE'))
----------------------------------------------------------------------------------------------------
26192
98199
99236

SQL>  select * from table (dbms_xplan.display_cursor());

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
SQL_ID  brjtjcwgqz2rc, child number 0
-------------------------------------
select distinct(json_value(json_document,
'$.ShippingInstructions.Address.zipCode')) from json_tab where
json_exists(json_document,'$.ShippingInstructions.Address.zipCode')

Plan hash value: 333039129

-----------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |       |       |     8 (100)|          |
|   1 |  HASH UNIQUE                 |                |     1 |  1997 |     8  (13)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| JSON_TAB       |     5 |  9985 |     7   (0)| 00:00:01 |
|*  3 |    DOMAIN INDEX              | JSON_TAB_GUIDE |       |       |     4   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access("CTXSYS"."CONTAINS"("JSON_TAB"."JSON_DOCUMENT",'HASPATH(/ShippingInstruct
              ions/Address/zipCode)')>0)

Lust auf mehr JSON in der Oracle Datenbank? Weitere Informationen in deutscher Sprache finden Sie auch unter:

Donnerstag, 20. April 2017

Oracle Text in 12c Release 2

Seit März diesen Jahres gibt es ein neues White Paper zum Thema Oracle Database 12c Release 2. Hier kann man einen ersten kurzen Eindruck über einige neue Text Features im Release 2 erhalten. Schlagworte sind dabei: Sentiment Analyse, Reverse Index und Erweiterungen beim NEAR Operator. Zusätzlich sind - wie immer übrigens - alle Änderungen im neuen Release im Text Application Developer's Guide unter "Changes in Oracle Text 12c Release 2 (12.2.0.1)" zu finden.

Dienstag, 18. April 2017

REL7: Package Missing in 12.1.0.2

Seit längerer Zeit haben wir nichts mehr auf unserem Text Blog gepostet. Nun nehme ich eine plattform spezifische Problematik zum Anlass mit neuen Einträgen zu starten. Worum geht es? Es geht um das Package compat-libstdc++-33-3.2.3, das auf Red Hat Enterprise Linux Version 7 nicht mehr automatisch installiert ist. Text Funktionalität, die ctxhx nutzt - also die Oracle Filter Funktionalität - ist davon in 12.1.0.2 betroffen.

Gut beschrieben ist das Ganze in der Note mit Doc ID 2254198.1: Missing or Ignored package compat-libstdc++-33-3.2.3 causes Text Issues in 12.1.0.2 (Doc ID 2254198.1). Hier findet man auch den Tipp, einfach ein Test mit ctxhx durchzuführen, um festzustellen, ob die Library in der eigenen Umgebung vorhanden ist.

Freitag, 29. Januar 2016

Oracle TEXT Schulungen von ORDIX in Wiesbaden

Die Firma ORDIX bietet im Jahr 2016 an mehreren Terminen kostenpflichtige Oracle TEXT Seminare in Wiesbaden an. Für diejenigen, die für 3 Tage lang nochmals richtig tief einsteigen, ist dies sicherlich eine interessante Gelegenheit.

Dienstag, 8. September 2015

Transaktionen und CTX_DDL: COMMIT oder nicht COMMIT?

Heute geht es um ein kleines, aber dennoch nützliches Feature in Oracle TEXT, wenn es um das Erstellen von Preferences geht. Die verschiedenen Prozeduren in CTX_DDL setzen alle ein implizites COMMIT ab, und verhalten sich damit wie normale SQL DDL Kommandos. Das folgende Codebeispiel zeigt das - obwohl ein ROLLBACK erfolgte, ist die neue Preference noch sichtbar - sie wurde vorher schon festgeschrieben.
SQL> select pre_name, pre_class from ctx_user_preferences;

Es wurden keine Zeilen ausgewählt

SQL> exec ctx_ddl.create_preference('MYLEXER', 'BASIC_LEXER');

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> rollback;

Transaktion mit ROLLBACK rückgängig gemacht.

SQL> select pre_name, pre_class from ctx_user_preferences;

PRE_NAME                       PRE_CLASS
------------------------------ ------------------------------
MYLEXER                        LEXER

1 Zeile wurde ausgewählt.
In den meisten Fällen ist dies sicherlich auch das gewünschte Verhalten; aber es geht auch anders. Letztlich werden durch die verschiedenen CTX_DDL-Aufrufe doch nur Zeilen in interne Tabellen eingefügt - es wäre also schon interessant, wenn man kein implizites COMMIT machen würde - dann könnte man auch mehrere CTX_DDL-Aufrufe per Rollback rückgängig machen. Und das geht so.
begin
  CTX_DDL.PREFERENCE_IMPLICIT_COMMIT := FALSE;
end;
/

PL/SQL-Prozedur erfolgreich abgeschlossen.
Wenn man die Kommandos von oben nun nochmals laufen lässt, sieht das Bild so aus.
SQL> select pre_name, pre_class from ctx_user_preferences;

Es wurden keine Zeilen ausgewählt

SQL> exec ctx_ddl.create_preference('MYLEXER', 'BASIC_LEXER');

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> rollback;

Transaktion mit ROLLBACK rückgängig gemacht.

SQL> select pre_name, pre_class from ctx_user_preferences;

Es wurden keine Zeilen ausgewählt
Kombiniert man dies in einem SQL-Skript mit einem WHENEVER SQLERROR EXIT oder WHENEVER SQLERROR ROLLBACK, so muss man sich nicht mehr mit dem Problem "halb" angelegter Index-Preferences herumschlagen. Mehr dazu findet Ihr in der Oracle TEXT Dokumentation.

Montag, 1. Juni 2015

12c Feature: $I Tabelle mit neuer Storage Preference BIG_IO

Wir haben in den letzten Blogeinträgen schon Einiges zum Thema Oracle Database 12c veröffentlicht. Auch in diesem Blog wollen wir uns wieder einem neuen Oracle Text 12c Feature widmen. Es geht dabei um neue Möglichkeiten innerhalb der Text Indexstruktur, Änderungen an dem Default Speicherverhalten vorzunehmen, um unter Umständen bei Indexzugriffen weniger I/Os durchführen zu müssen. Gemeint ist damit die $I Tabelle und die Spalte TOKEN_INFO.

Welche Informationen speichert die Spalte TOKEN_INFO? Zur Erinnerung: Jedes Wort (besser Token) wird über die DOCID (Dokumenten ID des Dokuments, das das Token enthält) und die entsprechenden Wortpositionen in diesem Dokument gefunden. Beide Informationen werden in der Spalte TOKEN_INFO in binärer Form gespeichert. Standardmässig ist diese Spalte vom Datentyp BLOB und kann Informationen bis zu 4000 Bytes (dies ist eine interne Begrenzung) speichern. Müssen viele Informationen (mehr als 4000 Bytes) in der TOKEN_INFO Spalte gespeichert werden, werden neue Zeilen hinzugefügt. Die Idee in 12c ist nun, weniger Zeilen für große TOKEN_INFO Einträge speichern zu müssen. Aber schauen wir uns zuerst das Standrrdverhalten an einem einfachen Beispiel an.

SQL> create table my_table( id number primary key, text varchar2(2000) );
Table created.

SQL> create index my_index on my_table( text ) indextype is ctxsys.context;
Index created.

SQL> desc DR$MY_INDEX$I
 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 TOKEN_TEXT                                NOT NULL VARCHAR2(64)
 TOKEN_TYPE                                NOT NULL NUMBER(10)
 TOKEN_FIRST                               NOT NULL NUMBER(10)
 TOKEN_LAST                                NOT NULL NUMBER(10)
 TOKEN_COUNT                               NOT NULL NUMBER(10)
 TOKEN_INFO                                         BLOB

Überprüft man die genaue Definition der Tabelle DR$MY_INDEX$I, kann man feststellen, dass sich die Defaultspeicherung von LOBs in 12c geändert hat und nun statt der Basicfile eine Securefile Speicherung vorliegt. Für diejenigen, die es genau wissen wollen: Dies liegt an dem geänderten Parameterwert PREFERRED für den Initialisierungsparameter DB_SECUREFILE.
SQL> set long 10000 pagesize 100  

SQL> execute DBMS_METADATA.SET_TRANSFORM_PARAM(TRANSFORM_HANDLE=>-
     DBMS_METADATA.SESSION_TRANSFORM, name=>'STORAGE', value=>false);
SQL> SELECT DBMS_METADATA.GET_DDL(object_type=>'TABLE', name=>'DR$MY_INDEX$I') AS ausgabe 
     FROM dual;

AUSGABE
--------------------------------------------------------------------------------
  CREATE TABLE "SCOTT"."DR$MY_INDEX$I"
   (    "TOKEN_TEXT" VARCHAR2(64) NOT NULL ENABLE,
        "TOKEN_TYPE" NUMBER(10,0) NOT NULL ENABLE,
        "TOKEN_FIRST" NUMBER(10,0) NOT NULL ENABLE,
        "TOKEN_LAST" NUMBER(10,0) NOT NULL ENABLE,
        "TOKEN_COUNT" NUMBER(10,0) NOT NULL ENABLE,
        "TOKEN_INFO" BLOB
   ) SEGMENT CREATION IMMEDIATE
  PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255
 NOCOMPRESS LOGGING
  TABLESPACE "USERS"
 LOB ("TOKEN_INFO") STORE AS SECUREFILE (
  TABLESPACE "USERS" ENABLE STORAGE IN ROW CHUNK 8192
  NOCACHE LOGGING  NOCOMPRESS  KEEP_DUPLICATES )
  MONITORING
Dies ändert allerdings noch nichts an der internen Begrenzung auf 4000 Bytes. Ein einfaches Beispiel demonstriert das Standardverhalten - auch in 12c. Dazu legen wir eine einfache Tabelle mit einer Spalte an und speichern 5000 Mal den Eintrag HELLO ab.
SQL> drop table my_table purge;
Table dropped
SQL> create table my_table (text varchar2(80));
Table created
SQL> begin
  for i in 1 .. 5000 loop
    insert into my_table values ('hello');
    commit;
  end loop;
end;
/
SQL> create index my_index on my_table (text) indextype is ctxsys.context;
Index created
Selektieren wir nun die $I Tabelle, stellen wir fest, dass 5 Zeilen für das Token HELLO verwendet wurden - mit einer Länge von 2499 bzw. 3501 Bytes.

SQL> column token_text format a15
SQL> select token_text, length(token_info) from dr$my_index$i;
 
TOKEN_TEXT LENGTH(TOKEN_INFO)
--------------- ------------------
HELLO         2499
HELLO         3501
HELLO         3501
HELLO         3501
HELLO         3501

Nun wenden wir das neue Feature BIG_IO an, dass diese Grenze aufheben soll. Dazu ist die neue Option BIG_IO als Attribute in der Preference BASIC_STORAGE auf TRUE zu setzen.
SQL> begin 
     ctx_ddl.create_preference( 'my_storage', 'BASIC_STORAGE' );
     ctx_ddl.set_attribute ( 'my_storage', 'BIG_IO', 'true' );
     end;
/ 
-- entweder Neuanlegen des Index oder mit ALTER INDEX
SQL> alter index my_index rebuild parameters ('replace storage my_storage');
Index altered.
Wenn wir nun die Token Information überprüfen, finden wir nur noch einen einzigen (!) Eintrag vor - allerdings der Länge 15023.
SQL> select token_text, length(token_info) from dr$my_index$i;

TOKEN_TEXT LENGTH(TOKEN_INFO)
--------------- ------------------
HELLO        15023


BIG_IO sorgt also dafür, dass wir unter Umständen weniger Einträge pro Token in der $I Tabelle speichern. Dies kann dann dabei helfen, die Zugriffe auf große Indexfragmente zu reduzieren.

Mittwoch, 25. März 2015

JSON (ab 12.1.0.2) mit Oracle TEXT indizieren

Mit dem Patchset 12.1.0.2 wurde die JSON-Unterstützung in der Oracle-Datenbank eingeführt; auf dem Blog SQL und PL/SQL in Oracle ist dazu auch ein Posting erschienen. Gemeinsam mit dem SQL/JSON-Funktionen wurde auch eine JSON-Unterstützung in Oracle TEXT eingeführt. Um die soll es jetzt gehen: Angenommen, wir haben eine Tabelle JSON_TAB, welche die JSON-Dokumente als CLOB in der Spalte JSON enthält ...
SQL> desc json_tab
 Name                                      Null?    Typ
 ----------------------------------------- -------- ----------------------------
 FILENAME                                           VARCHAR2(200)
 JSON                                               CLOB


SQL> select json from json_tab where rownum = 1;

JSON
--------------------------------------------------------------------------------
{"PurchaseOrder":{"$":{"xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance","
xsi:noNamespaceSchemaLocation":"http:/localhost:9021/public/XMLDEMO/purchaseOrde
r.xsd"},"Reference":["ADAMS-2001112712104128PST"],"Actions":[{"Action":[{"User":
["SCOTT"]}]}],"Reject":[""],"Requestor":["Julie P. Adams"],"User":["ADAMS"],"Cos
tCenter":["R20"],"ShippingInstructions":[{"name":["Julie P. Adams"],"address":["
300 Oracle Parkway\r\nRedwood Shores\r\nCA\r\n94065\r\nUSA"],"telephone":["650 5
06 7300"]}],"SpecialInstructions":["Hand Carry"],"LineItems":[{"LineItem":[{"$":
{"ItemNumber":"1"},"Description":["The Life of Brian"],"Part":[{"$":{"Id":"71551
5010320","UnitPrice":"39.95","Quantity":"2"}}]},{"$":{"ItemNumber":"2"},"Descrip
tion":["Hamlet"],"Part":[{"$":{"Id":"037429128428","UnitPrice":"29.95","Quantity
Wie man mit den SQL-Funktionen JSON_VALUE, JSON_QUERY oder JSON_TABLE in diese JSON-Dokumente "hineingreifen" kann, könnt Ihr in oben erwähntem Blog-Posting nachlesen - jetzt wollen wir aber die Volltextsuche ermöglichen - dazu erzeugen wir, wie immer, einen Oracle TEXT-Index und verwenden dazu die neue Section Group CTXSYS.JSON_SECTION_GROUP - JSON-Dokumente werden also ganz ähnlich wie XML-Dokumente indiziert.
create index ft_jsontable on json_tab (json) 
indextype is ctxsys.context 
parameters ('section group ctxsys.json_section_group');
Nach dem Indizieren werfen wir einen Blick in die Token-Tabelle ($I). JSON-Attributnamen werden (wie auch XML-Tags) als Token-Type 7, Daten als Token-Type 0 indiziert.
SQL> select token_text from DR$FT_JSONTABLE$I where token_type = 7 and rownum <= 100;

TOKEN_TEXT
----------------------------------------------------------------
Action
Actions
CostCenter
Description
Id
ItemNumber
:

SQL> select token_text from DR$FT_JSONTABLE$I where token_type = 0 and rownum <= 100;

TOKEN_TEXT
----------------------------------------------------------------
WALKABOUT
WARD
WASHINGTON
WATCHED
WAVE
WESTMINSTER
WHISPERS
WILD
:
Demzufolge kann man die JSON-Dokumente nun, wie auch XML-Dokumente, mit WITHIN abfragen ... und natürlich gehen auch Oracle Text-Funktionen wie die Fuzzy-Suche. Achtet bei euren WITHIN-Abfragen aber darauf, dass die Namen der JSON-Attribute auch für Oracle TEXT Case-Sensitiv sind. Die Daten, also der Token-Type 0 werden dagegen (normalerweise) Case-Insensitiv indiziert.
SQL> select json_value(json, '$.PurchaseOrder.Reference[0]') 
  2  from json_tab 
  3  where contains(json, '?Washingtn within (Description)') > 0;

JSON_VALUE(JSON,'$.PURCHASEORDER.REFERENCE[0]')
--------------------------------------------------------------------------------
BLAKE-20021009123336231PDT
ALLEN-2002100912333742PDT
ALLEN-20021009123337553PDT
FORD-20021009123337463PDT
:

18 Zeilen ausgewählt.

SQL> select json_value(json, '$.PurchaseOrder.Reference[0]') 
  2  from json_tab 
  3  where contains(json, '?Washingtn within (UnitPrice)') > 0;

Keine Zeilen ausgewählt.

SQL> select json_value(json, '$.PurchaseOrder.Reference[0]') 
  2  from json_tab 
  3  where contains(json, '?Washingtn within (DESCRIPTION)') > 0;

Keine Zeilen ausgewählt.
Mit JSON_TEXTCONTAINS wurde noch ein zusätzlicher Query-Operator geschaffen; anstelle von CONTAINS mit WITHIN kann also auch eine Abfrage mit JSON_TEXTCONTAINS ausgeführt werden - letzterer erlaubt die Verwendung der gleichen JSON-Path-Expressions, wie sie auch in den SQL/JSON-Funktionen JSON_VALUE, JSON_QUERY und JSON_TABLE verwendet werden.
SQL> select json_value(json, '$.PurchaseOrder.Reference[0]') 
  2  from json_tab 
  3  where json_textcontains(json, $.PurchaseOrder.LineItems.LineItem.Description', 'Washington');

JSON_VALUE(JSON,'$.PURCHASEORDER.REFERENCE[0]')
--------------------------------------------------------------------------------
BLAKE-20021009123336231PDT
ALLEN-2002100912333742PDT
ALLEN-20021009123337553PDT
FORD-20021009123337463PDT
FORD-20021009123337653PDT
:

18 Zeilen ausgewählt.
Allerdings unterstützt JSON_TEXTCONTAINS nicht die Oracle TEXT Abfragesyntax; das bedeutet, dass man zwar standardisierte JSON-Pfadausdrücke verwenden kann, nicht jedoch Features wie Stemming, Fuzzy-Suche, UND/ODER-Kombinationen, Progressive Relaxation und vieles mehr. Insofern bleibt der "klassische" CONTAINS-Operator auch für JSON-Dokumente nach wie vor interessant. Viel Spaß beim Ausprobieren.

Donnerstag, 5. Februar 2015

Schnellere TEXT-Abfragen mit SNIPPET in Oracle 12c: Forward Index / Save Copy

Das Indizieren binärer Dokumente, wie PDF- oder Office-Formate, ist mit Oracle TEXT bekanntlich überhaupt kein Problem. Einfach in den Index-Parametern den "FILTER" aktivieren und los geht's.
create index idx_ppt_folien on ppt_folien(FOLIENSATZ)
indextype is ctxsys.context
parameters('filter ctxsys.auto_filter');
Und danach kann man auch schon abfragen ...
select dateiname from ppt_folien where contains (FOLIENSATZ, '"Oracle TEXT"') > 0;

DATEINAME
--------------------------------------------------------------------------------
XMLDB_US.ppt
11gDWHFeatures_us.ppt
11g_dwh_us.ppt
TextMining_11g_200912_ccz.ppt
SecureFiles_odd_201002_us_cc.ppt
OracleText-ccz-201410.pptx
01_BigDataWorkshop_BigData_Overview_032012_ccz.pptx
:

38 Zeilen ausgewählt.

Abgelaufen: 00:00:00.99
Achtet auf die Ausführungszeit - die ist hier völlig OK. Nun reicht der Dateiname zur Anzeige natürlich nicht aus; man hätte schon gerne den Textausschnitt, in dem das Suchwort gefunden wurde - auch das ist für Oracle TEXT kein Problem - dazu gibt es die Funktion CTX_DOC.SNIPPET; die neue Query sieht also so aus.
select 
  dateiname, 
  ctx_doc.snippet('IDX_PPT_FOLIEN',rowid,'"Oracle TEXT"','**','**') as snippet 
from ppt_folien where contains (FOLIENSATZ, '"Oracle TEXT"') > 0;

DATEINAME                      SNIPPET
------------------------------ --------------------------------------------------------------------
XMLDB_US.ppt                   **Oracle TEXT**-Index (Alle Speicherungsformen) ¦ Volltextrecherche
11gDWHFeatures_us.ppt          Funktionstests mit neuen **Oracle Text** Indizes (CDI)
TextMining_11g_200912_ccz.ppt  in einer Tabelle abgelegt ¦ **Oracle TEXT**-Abfragesyntax
:

38 Zeilen ausgewählt.

Abgelaufen: 00:00:16.10
Das funktioniert sehr gut, aber die Performance lässt doch sehr zu wünschen übrig; es sind nun 16 Sekunden (!) - vorher war es eine. Der Grund ist schnell gefunden: um das Snippet generieren zu können, muss Oracle TEXT das Binärdokument nochmals abrufen, nochmals filtern und dann die Snippets berechnen. Dabei geht viel Zeit verloren. Bislang wurde spätestens an dieser Stelle empfohlen, die Dokumente einfach in gefilterter Form in eine eigene Tabelle abzulegen und diese dann zu indizieren.
Ab Oracle 12.1 bietet Oracle TEXT ein Feature an, welches uns die Arbeit dafür abnimmt: Der Forward Index und das Save Copy-Feature. Das Speichern des gefilterten Textes übernimmt dann Oracle TEXT für uns; die Verwaltung der eigenen Tabelle müssen wir nicht mehr selbst übernehmen. Zusätzlich speichert Oracle TEXT im Forward Index weitere Informationen über die konkreten Wortpositionen, was das Berechnen der Snippets nochmal beschleunigt. Und das ganze geht so.
  • 1. Neue Storage Preference erzeugen und die Attribute SAVE_COPY und FORWARD_INDEX setzen:
    begin
      ctx_ddl.create_preference('MY_STORAGE','BASIC_STORAGE');
      ctx_ddl.set_attribute('MY_STORAGE','forward_index', 'TRUE');
      ctx_ddl.set_attribute('MY_STORAGE','save_copy', 'PLAINTEXT');
      ctx_ddl.set_attribute('MY_STORAGE','save_copy_max_size', '0');
    end;
    
    Wenn bereits eine Storage Preference existiert, nimmt man natürlich diese. Das Attribut SAVE_COPY wird entweder mit dem Wert NONE, PLAINTEXT oder HTML versehen. PLAINTEXT reicht für das Erzeugen der Snippets aus; möchte man auch HTML-Vorschauversionen bereitstellen (Preview-Funktion), so sollte man HTML wählen. Das Attribut SAVE_COPY_MAX_SIZE legt eine Obergrenze für die Größe des gefilterten Dokumentes fest.
  • 2. Index neu erstellen - mit der neuen Storage Preference
    drop index idx_ppt_folien;
    
    Index wurde gelöscht.
    
    create index idx_ppt_folien on ppt_folien(FOLIENSATZ)
    indextype is ctxsys.context
    parameters('filter ctxsys.auto_filter storage MY_STORAGE');
    
    Index wurde erstellt.
    
Und fertig. Setzt man die Abfrage mit dem Snippet nochmal ab, so merkt man, dass diese nun wesentlich schneller ist ...
select 
  dateiname, 
  ctx_doc.snippet('IDX_PPT_FOLIEN',rowid,'"Oracle TEXT"','**','**') as snippet 
from ppt_folien where contains (FOLIENSATZ, '"Oracle TEXT"') > 0;

DATEINAME                      SNIPPET
------------------------------ --------------------------------------------------------------------
XMLDB_US.ppt                   **Oracle TEXT**-Index (Alle Speicherungsformen) ¦ Volltextrecherche
11gDWHFeatures_us.ppt          Funktionstests mit neuen **Oracle Text** Indizes (CDI)
TextMining_11g_200912_ccz.ppt  in einer Tabelle abgelegt ¦ **Oracle TEXT**-Abfragesyntax
:

38 Zeilen ausgewählt.

Abgelaufen: 00:00:00.95
Auf der anderen Seite verbraucht das Speichern des Snippets natürlich Platz - allerdings hält dieser sich, wie man hier sehen kann, in Grenzen. Die Tabelle, welche die Plaintext-Versionen enthält, ist die $D-Tabelle (hier rot markiert); der Forward Index ist in der $O-Tabelle abgelegt (blau markiert). Die verschiedenen Tabellentypen sind hier beschrieben. Es werden (in diesem Fall) etwa 8MB zusätzlich verbraucht - die indizierten PPT-Dokumente sind etwa 750MB.
Natürlich hängt dieses Verhältnis stark von den indizierten Dokumenten ab - Folien enthalten viele Bilder und wenig Text; bei anderen Dokumenttypen ist das anders. Generell ist das neue "Forward Index / Save Copy" Feature jedoch eine gute Variante, um die Performance von "Snippet-Queries" zu verbessern.
select t.table_name, s.segment_name, sum(s.bytes) 
from user_tables t, user_lobs l, user_segments s
where  l.table_name = t.table_name and ( l.segment_name = s.segment_name or t.table_name = s.segment_name)
  and (t.table_name like 'DR$IDX_PPT_FOLIEN$%' or t.table_name = 'PPT_FOLIEN')
group by t.table_name, s.segment_name
order by 1

TABLE_NAME                SEGMENT_NAME                SUM(S.BYTES)
------------------------- ------------------------- --------------
DR$IDX_PPT_FOLIEN$D       SYS_LOB0000135517C00003$$      6.488.064
DR$IDX_PPT_FOLIEN$D       SYS_LOB0000135517C00002$$        131.072
DR$IDX_PPT_FOLIEN$D       DR$IDX_PPT_FOLIEN$D              131.072
DR$IDX_PPT_FOLIEN$I       SYS_LOB0000135503C00006$$      1.245.184
DR$IDX_PPT_FOLIEN$I       DR$IDX_PPT_FOLIEN$I            3.145.728
DR$IDX_PPT_FOLIEN$O       SYS_LOB0000135514C00007$$      1.245.184
DR$IDX_PPT_FOLIEN$O       DR$IDX_PPT_FOLIEN$O              524.288
DR$IDX_PPT_FOLIEN$R       DR$IDX_PPT_FOLIEN$R               65.536
DR$IDX_PPT_FOLIEN$R       SYS_LOB0000135508C00002$$        131.072
PPT_FOLIEN                PPT_FOLIEN                       131.072
PPT_FOLIEN                SYS_LOB0000135467C00003$$    785.580.032

9 Zeilen ausgewählt.
Viel Spaß damit.

Donnerstag, 6. November 2014

Performance beim Erzeugen eines Oracle Text Index

Häufig erhalten wir die Frage, ob und wie sich die Performance beim Erzeugen eines Oracle Text Index beeinflussen lässt. Die Frage taucht in der Regel dann auf, wenn dem Anwender das Indexanlegen zu lange dauert. Eine Überprüfung der Oracle Text Memory Einstellung ist eine erste einfache Massnahme bei dieser Fragestellung. Setzt man nämlich einen Oracle Text Index ohne zusätzliche Memory Parameter Einstellung ab, wird die Default Einstellung, die häufig zu klein ist, genommen.

Wie kann man nun die Default Einstellungen einsehen? Eine einfache Abfrage auf die Tabelle CTX_PARAMETERS zeigt die Einstellungen für den Oracle Text Index.
col PAR_NAME format a40
col PAR_VALUE format a30
select * from ctx_parameters where PAR_NAME like '%MEMORY%';

PAR_NAME                                 PAR_VALUE
---------------------------------------- ------------------------------
DEFAULT_INDEX_MEMORY                     67108864
MAX_INDEX_MEMORY                         274877906944
Die Defaulteinstellung DEFAULT_INDEX_MEMORY liegt also bei 64 MB. Das bedeutet, beim CREATE INDEX werden normalerweise 64 MB verwendet. Der zweite Parameter gibt die maximale Einstellung an, die beim Indexanlegen überhaupt genutzt werden können. Hier - in einer 12.1.0.2 Umgebung - ist der Wert des Parameters 256 GB.

Es gibt nun zwei Möglichkeiten, die Memoryeinstellung zu ändern. Entweder während der Laufzeit über das CREATE INDEX Statement oder über die Änderung der Gesamteinstellung mit dem Package CTX_ADM. Folgendes Beispiel zeigt die Nutzung beim Indexanlegen.
create index my_idx on customers(CUST_STREET_ADDRESS) 
indextype is ctxsys.context parameters ('memory 100M');
Folgendes Beispiel zeigt die Verwendung von CTX_ADM.
begin
ctxsys.ctx_adm.set_parameter('MAX_INDEX_MEMORY','100G');
end;
/

begin
ctxsys.ctx_adm.set_parameter('DEFAULT_INDEX_MEMORY','512M');
end;
/
Ziel sollte sein, das Index Memory so hoch wie möglich zu setzen - allerdings unter Vermeidung von Paging. Einerseits kann mit dieser Massnahme das Indexanlegen beschleunigt werden, andererseits wird dabei auch die Fragmentierung des Index geringer.

Wichtiger Hinweis: Die maximale Einstellung für MAX_INDEX_MEMORY hat sich übrigens von 11g nach 12c verändert. In 11g lag der Wert noch bei 2 GB, in 12c ist dieser Wert auf 256 GB erhöht worden. Dies wird übrigens auch in den Defaulteinstellung sichtbar (siehe obige Abfrage auf CTX_PARAMETERS). Beachten Sie auch: 256 GB ist eine theoretische Grenze, die tatsächliche Grenze von MAX_INDEX_MEMORY könnte je nach Umgebung niedriger ausfallen.

Dienstag, 23. September 2014

Ergebniscache für TEXT-Abfragen: Oracle12c TEXT Query Filter Cache

Heute geht es um den mit Oracle12c neu eingeführten Query Filter Cache. Das ist ein Ergebnis-Cache, speziell für Oracle TEXT Abfragen. Ergebnisse von Oracle TEXT-Abfragen werden - ganz ähnlich zum "normalen" SQL Result Cache - in einem separaten Hauptspeicherbereich abgelegt bei Bedarf wiederverwendet. Kann ein Ergebnis aus dem Cache geholt werden, muss die eigentliche Oracle TEXT Abfrage nicht mehr ausgeführt werden; die Antwortzeit sollte dann wesentlich besser sein.
Gut geeignet ist der Query Filter Cache zum nachträglichen Filtern von Abfrageergebnissen. Dabei kann es um das Anwenden von Zugriffsregeln gehen (Sicherheit); aber auch Anwendungen, die die nachträgliche An- und Abwahl von Dokumentkategorien erlauben, können so durch einen Ergebniscache unterstützt werden. Denn jede An- oder Abwahl einer Kategorie (bspw. durch Setzen einer Checkbox in der Anwendung) führt ja zu einer erneuten Ausführung der Query. Nutzt man den Query Filter Cache, so müssen die Ergebnisse nur noch aus dem Cache geholt und nachgefiltert werden. Das folgende Beispiel stellt den Query Filter Cache vor - los geht's wie immer mit dem Erstellen einer Tabelle.
create table texttabelle(
  id          number(10),
  ressort     varchar2(20),
  dokument    clob
)
/

insert into texttabelle values ( 1, 'Politik',    'A-Partei gewinnt Wahl in Hansestadt');
insert into texttabelle values ( 2, 'Panorama',   'Terror in Nahost: Kriminalität steigt immer weiter an');
insert into texttabelle values ( 3, 'Wirtschaft', 'Wirtschaft: Erneuter Gewinnzuwachs in diesem Jahr');
insert into texttabelle values ( 4, 'Sport',      'Olympia rückt näher: Der Fackellauf ist in vollem Gange');
insert into texttabelle values ( 5, 'Politik',    'Wer wird US-Präsident? Obama und Clinton machen Wahlkampf');
insert into texttabelle values ( 6, 'Politik',    'Papst bestürzt über jüngsten Skandal!');
insert into texttabelle values ( 7, 'USA',        'Wahlkampf in den USA: Clinton und Obama LIVE zu sehen');
insert into texttabelle values ( 8, 'Wirtschaft', 'Software-Kenntnisse werden immer wichtiger');
insert into texttabelle values ( 9, 'Wirtschaft', 'Umfrage:  Alle wollen mehr Geld!');
insert into texttabelle values (10, 'Panorama',   'Der Papst liest seine erste Messe in den USA!');

commit
/
In der Spalte DOKUMENT soll gesucht werden, anhand der Spalte RESSORT soll dann nachträglich gefiltert werden. Um den Query Filter Cache nutzen zu können, muss eine explizite Storage Preference erstellt und im Index genutzt werden. Dabei wird die Größe des Cache mit dem Attribut QUERY_FILTER_CACHE_SIZE in Bytes angegegen. Dieser Speicherbereich wird dann in der SGA allokiert - die Größe sollte also mit Bedacht - und nach Absprache mit dem DBA bestimmt werden.
begin
 ctx_ddl.create_preference('my_idx_storage', 'basic_storage');
 ctx_ddl.set_attribute('my_idx_storage', 'query_filter_cache_size', '10M');
end;
/  
 

create index idx_text on texttabelle (dokument)
indextype is ctxsys.context
filter by ressort
parameters ('storage my_idx_storage sync (on commit)')
/
Die Data Dictionary View CTX_FILTER_CACHE_STATISTICS gibt Informationen über den Cache - Zu Beginn stehen alle Einträge natürlich auf Null.
SQL> select * from ctx_filter_cache_statistics

FCS_INDEX_OWNER FCS_INDEX_NAME FCS_PARTITION_NAME   FCS_SIZE FCS_ENTRIES FCS_REQUESTS   FCS_HITS
--------------- -------------- ------------------ ---------- ----------- ------------ ----------
TEXT            IDX_TEXT                                   0           0            0          0
Nun geht es daran, den neuen Cache zu nutzen. Das geschieht zunächst, ganz normal, mit Hilfe des Oracle TEXT SQL-Operators CONTAINS. In diesem wird nun aber die spezielle Abfragefunktion CTXFILTERCACHE verwendet - und zwar wie folgt.
select * from texttabelle 
where contains(dokument, 'ctxfiltercache((Wirtschaft), true, true)') > 0;

        ID RESSORT              DOKUMENT
---------- -------------------- ---------------------------------------------------------------------------
         3 Wirtschaft           Wirtschaft: Erneuter Gewinnzuwachs in diesem Jahr
Der neue Cache wird also mit dem Schlüsselwort CTXFILTERCACHE explizit angesprochen. Der erste Parameter (Wirtschaft) ist die Text-Query, die anderen beiden bestimmen das Verhalten des Cache näher. Der zweite Parameter (true) legt fest, ob der SCORE-Wert eines Ergebnisses ebenfalls im Cache abgelegt werden soll. Standardmäßig steht dieser Parameter auf FALSE: Ein aus dem Cache geholtes Ergebnis hat dann einen Score von 100. Wird der Parameter, wie oben, auf TRUE gesetzt, werden die SCORE-Werte ebenfalls in den Cache gelegt und wiederverwendet. Der dritte Parameter bestimmt, ob nur die "TOP-N"-Ergebnisse in den Cache gelegt werden sollen - wieviele das sind, bestimmt die Datenbank automatisch; eine manuelle Festlegung ist nicht möglich. Um TOP-N auf TRUE zu setzen, muss auch der zweite SCORE-Parameter auf TRUE gesetzt sein. Die Dokumentation enthält nähere Details.
Nach der ersten Ausführung der Query kann man anhand der Statistik-View schon erkennen, dass mit dem Cache gearbeitet wurde.
SQL> select * from ctx_filter_cache_statistics

FCS_INDEX_OWNER FCS_INDEX_NAME FCS_PARTITION_NAME   FCS_SIZE FCS_ENTRIES FCS_REQUESTS   FCS_HITS
--------------- -------------- ------------------ ---------- ----------- ------------ ----------
TEXT            IDX_TEXT                               13096           1            1          0
Anhand von FCS_ENTRIES sieht man, dass nun eine Abfrage mit dem Cache gearbeitet hat. Es erfolgte eine Anfrage an den Cache (FCS_REQUESTS), aber da die Abfrage zum ersten Mal abgesetzt wurde, ergab sich noch kein Hit (FCS_HITS). Anders sieht es aus, wenn man die Abfrage nochmals absetzt: Nun ergeben sich zwei Anfragen und ein Cache-Hit. Die Abfrage wurde also aus dem Cache bedient.
SQL> select * from ctx_filter_cache_statistics

FCS_INDEX_OWNER FCS_INDEX_NAME FCS_PARTITION_NAME   FCS_SIZE FCS_ENTRIES FCS_REQUESTS   FCS_HITS
--------------- -------------- ------------------ ---------- ----------- ------------ ----------
TEXT            IDX_TEXT                               13096           1            2          1
Nun zu einem der eigentlichen Anwendungsfälle. Wir suchen in der oberen Tabelle nach Obama.
SQL> select * from texttabelle where contains(dokument,  'ctxfiltercache((Obama), true, true)') > 0 ;

        ID RESSORT              DOKUMENT
---------- -------------------- ----------------------------------------------------------------------------
         5 Politik              Wer wird US-PrSsident? Obama und Clinton machen Wahlkampf
         7 USA                  Wahlkampf in den USA: Clinton und Obama LIVE zu sehen

2 Zeilen ausgewählt.

SQL> select * from ctx_filter_cache_statistics;

FCS_INDEX_OWNER FCS_INDEX_NAME FCS_PARTITION_NAME   FCS_SIZE FCS_ENTRIES FCS_REQUESTS   FCS_HITS
--------------- -------------- ------------------ ---------- ----------- ------------ ----------
TEXT            IDX_TEXT                               13096           1            1          0

1 Zeile wurde ausgewählt.
Diese Abfrage liefert Ergebnisse aus zwei Ressorts. Wieder haben wir einen Request an den Cache und (noch) keinen Hit. Nun nehmen wir aber an, dass der Anwender in der Benutzeroberfläche in der Checkbox für die Ressorts klickt - und zwar wählt er das Ressort "USA" ab. Das führt zu einer neuen Query, bei der ein SDATA-Ausdruck anstelle einer AND-Verknüpfung auf SQL-Ebene verwendet wird. Dies dient der Vermeidung von Mixed Queries - was bei der Arbeit mit Oracle TEXT generell zu empfehlen ist (siehe Blog Posting Abfrage-Optimierung mit Composite Domain Index).
select * from texttabelle where contains(
  dokument,  
  'ctxfiltercache((Obama), true, true) and sdata(ressort = ''Politik'')'
) > 0;

        ID RESSORT              DOKUMENT
---------- -------------------- --------------------------------------------------------------------------------
         5 Politik              Wer wird US-PrSsident? Obama und Clinton machen Wahlkampf

1 Zeile wurde ausgewählt.

SQL> select * from ctx_filter_cache_statistics;

FCS_INDEX_OWNER FCS_INDEX_NAME FCS_PARTITION_NAME   FCS_SIZE FCS_ENTRIES FCS_REQUESTS   FCS_HITS
--------------- -------------- ------------------ ---------- ----------- ------------ ----------
TEXT            IDX_TEXT                               13096           1            2          1

1 Zeile wurde ausgewählt.
Obgleich die Gesamt-Abfrage eine andere war, wurde der Cache angefragt und genutzt. Der Query Filter Cache ist also eine gute Unterstützung für das nachträgliche Filtern von Oracle TEXT Abfrageergebnissen, wie es in vielen Anwendungen mittlerweile Standard ist.
Der Filter Cache muss in der Anwendung jedoch explizit angesprochen werden; eine automatische Nutzung für existierende Anwendungen ist nicht möglich - dazu sind die möglichen Seiteneffekte, speziell beim Score, einfach zu wesentlich. Solange also CTXFILTERCACHE nicht innerhalb CONTAINS verwendet wird, wird auch kein Cache genutzt. Als Entwickler muss man eine explizite Entscheidung für die Nutzung des Cache treffen und dies die CONTAINS-Abfragen entsprechend kodieren.

Freitag, 18. Juli 2014

FILE_DATASTORE und Security oder: Was ist die FILE_ACCESS_ROLE?

Dass Oracle TEXT einen FILE_DATASTORE anbietet, mit dem man Dateien im lokalen Dateisystem des Datenbankservers indizieren kann, ist den meisten bekannt. Dazu braucht es (wie immer) eine Tabelle, in der sich nun aber keine Dokumente, sondern Dokumentpfade befinden, wie folgendes Beispiel zeigt.
create table index_files (
  id number(10), 
  path varchar2(500)
)
/

insert into index_files (id, path) values (1, '/home/oracle/twp_apex_best_practices.pdf')
/

commit
/
Wenn nun ein Textindex auf die Spalte PATH erzeugt wird, ist es klar, dass nicht die Pfade selbst, sondern die Dateien, die sich hinter dem Pfad befinden, indiziert werden sollen. Das wird mit der Datastore-Preference FILE_DATASTORE bestimmt. Ein erster Versuch wird allerdings mit einer Fehlermeldung quittiert.
create index ft_files on index_files (path)
indextype is ctxsys.context
parameters (
'filter ctxsys.auto_filter 
 datastore ctxsys.file_datastore
')
/

create index ft_files on index_files (path)
*
FEHLER in Zeile 1:
ORA-29855: Fehler bei Ausführung der Routine ODCIINDEXCREATE
ORA-20000: Oracle Text error:
DRG-10758: index owner does not have the privilege to use file or URL datastore
ORA-06512: in "CTXSYS.DRUE", Zeile 160
ORA-06512: in "CTXSYS.TEXTINDEXMETHODS", Zeile 366
Diejenigen, die Oracle TEXT schon länger kennen, dürften sich nun wundern; denn "früher" wäre dieses CREATE INDEX-Kommando fehlerfrei durchgelaufen. Nun fehlt ein Privileg - und das ist auch gut so - denn über den Umweg des Textindex ließen sich ja ansonsten große Teile des Filesystems auf dem Datenbankserver auslesen. Also wurde hier ein zusätzlicher Schutz eingeführt. Der Eigentümer des Oracle-Text-Index braucht eine zusätzliche Rolle. Der Name dieser Rolle ist vom Oracle TEXT Development allerdings flexibel gehalten worden (und genau das führt oft zu Konfusion). Im Oracle TEXT Dictionary wird eine Datenbankrolle als sog. FILE_ACCESS_ROLE registriert. Diese Rolle muss der Eigentümer des Textindex dann zugewiesen bekommen.
  • Also legen wir zunächst eine Rolle an, welche die FILE_ACCESS_ROLE sein soll. Das geschieht als DBA.
    SYS@[sccloud033:1521/orcl]
    SQL> create role text_dateizugriff not identified;
    
    Rolle wurde erstellt.
    
  • Dann legen wir diese Rolle (hier also TEXT_DATEIZUGRIFF) als die FILE_ACCESS_ROLE für Oracle TEXT fest.
    SYS@[sccloud033:1521/orcl]
    SQL> begin
      2    ctxsys.ctx_adm.0set_parameter ('FILE_ACCESS_ROLE', 'TEXT_DATEIZUGRIFF');
      3  end;
      4  /
    
    PL/SQL-Prozedur erfolgreich abgeschlossen.
    
  • Schließlich wird die Rolle TEXT_DATEIZUGRIFF dem Datenbankschema zugewiesen, welches den Textindex erzeugen möchte, also SCOTT oder wie hier: TESTIT.
    SYS@[sccloud033:1521/orcl]
    SQL> grant text_dateizugriff to testit;
    
    Benutzerzugriff (Grant) wurde erteilt.
    
Versucht der User TESTIT nun nochmals, den Index mit CREATE INDEX zu bauen, so wird er nun erfolgreich sein.
SQL> create index ft_files on index_files (path)
  2  indextype is ctxsys.context
  3  parameters (
  4  'filter ctxsys.auto_filter
  5   datastore ctxsys.file_datastore
  6  ')
  7  /

Index wurde erstellt.

SQL> select * from index_files where contains(path, 'application express') > 0
/

        ID PATH
---------- --------------------------------------------------
         1 /home/oracle/twp_apex_best_practices.pdf
Also alles ganz einfach. Zusammengefasst kann man sagen:
  • Möchte man mit Oracle TEXT den FILE_DATASTORE verwenden, um Inhalte des lokalen Dateisystems zu indizieren, so braucht der Datenbankuser eine zusätzliche Rolle.
  • Diese Rolle heißt nicht FILE_ACCESS_ROLE - vielmehr muss man (als DBA) eine eigene Rolle (mit eigenem Namen) erzeugen, diese als FILE_ACCESS_ROLE im Oracle TEXT Dictionary registrieren und dem Datenbankschema zuweisen.
Das ist auch in der Oracle TEXT Dokumentation - im Abschnitt FILE_DATASTORE and Security beschrieben.

Montag, 12. Mai 2014

11.2.0.4 und Oracle Text - das sollte man wissen!

Nach einem Upgrade auf 11.2.0.4 können nach einem DELETE auf Tabellen mit Oracle Text Index Fehler wie folgt eintreten:
ERROR at line 1: 
ORA-00604: error occurred at recursive SQL level 1 
ORA-06550: line 1, column 7: 
PLS-00306: wrong number or types of arguments in call to 'SYNCRN' 
ORA-06550: line 1, column 7: 

PL/SQL: Statement ignored

Die genutzte Tabelle und die zugehörigen Text Indizes sind dabei vor dem Upgrade angelegt worden.

Was ist passiert?
Die Analyse des zugehörigen Bug 17501296 gibt als Ursache das Fehlen des Primary Keys der $R Tabelle an.

Was kann man tun?
Eine ausführliche Antwort hierzu liefert My Oracle Support Note 1608029.1 (Oracle Text Release 11.2.0.4.0 mandatory Patches). Hier wird das notwendige Vorgehen beschrieben. Kurz zusammengefasst handelt es sich dabei im Wesentlichen um 2 Dinge: 
  1. Die Ausführung von Patch 17501296 korrigiert das fehlerhafte Upgrade und Downgrade Script, so dass nachfolgende Upgrades erfolgreich sind. 
  2. Ein zusätzliches SQL Skript korrgiert die schon existierenden und migrierten Tabellen mit Text Indizes, falls die Constraints für die $R Tabellen nicht existieren.
Und fertig!

Dienstag, 8. April 2014

Oracle12c: Verändertes Verhalten bei Datastore Triggern

Falls Sie einen Multicolumn Datastore oder einen User Datastore nutzen, kann folgender Blogeintrag ganz nützlich sein. Es geht um das veränderte Verhalten bei Verwendung von Datenbank Trigger und was man tun kann, um das alte Verhalten wiederherzustellen. Als Erinnerung bzw. als Grundlage nehmen wir den Blog Mehrere Tabellenspalten indizieren: MULTICOLUMN_DATASTORE.
Hier kurz noch einmal der Code zur Erinnerung:

-- Anlegen der Tabelle
drop table  produktions_hinweise purge;
create table produktions_hinweise(
  fall_id               number(10),
  kommentar_werk        varchar2(4000),
  kommentar_controlling varchar2(4000),
  hinweise_produktion   clob);

-- Einfügen eines Satzes
insert into produktions_hinweise 
(fall_id, kommentar_werk, kommentar_controlling, hinweise_produktion) 
values 
(1,'Auslastung','Abschreibungen', 'Spezialmaschine');
Um einen Multicolumn Datastore nutzen zu können, muss zunächst eine sogenannte Preference erzeugt werden; hier werden die Spalten, welche gemeinsam indiziert werden sollen, konfiguriert.

begin
  ctx_ddl.create_preference(
    preference_name => 'kommentare_store'
   ,object_name     => 'MULTI_COLUMN_DATASTORE'
  );
  ctx_ddl.set_attribute(
    preference_name => 'kommentare_store'
   ,attribute_name  => 'columns'
   ,attribute_value => 'KOMMENTAR_WERK, KOMMENTAR_CONTROLLING, HINWEISE_PRODUKTION'
  );
end;
/

Man kann nun eine der drei Dokumentspalten zur Indizierung nehmen; aus Gründen der Übersichtlichkeit empfiehlt es sich jedoch, eine eigene "Dummy"-Spalte zu erzeugen.

alter table produktions_hinweise add (indexspalte char(1));
Nun Erzeugen wir einen Index - die Preferences geben den Datastore, die Section Group und das Synchronisieren nach dem COMMIT an.

create index idx_kommentare on produktions_hinweise (indexspalte)
indextype is ctxsys.context parameters 
('datastore kommentare_store 
  section group CTXSYS.AUTO_SECTION_GROUP
  SYNC (ON COMMIT)');
Wir haben nun einen Index auf einer spezifischen Spalte; der Index wird allerdings nur verändert, wenn diese Spalte sich ändert. Damit der Index aber immer beim UPDATE auf einer anderen Spalte (hier beispielsweise KOMMENTAR_WERK) verändert wird, konnte man vor 12c einen BEFORE UPDATE Trigger verwenden wie folgendes Beispiel zeigt:

create or replace trigger trg_produktions_hinweise
before update on produktions_hinweise
for each row
begin
  :new.indexspalte := :new.indexspalte;
end;
/
Ändert man nun irgendeine Spalte mit anschliessendem COMMIT (in unserem Beispiel KOMMENTAR_WERK), wird der Index synchronisiert. Dieses Verhalten hat sich in 12c geändert, nachdem Bug 14155722 gefixed wurde! Oracle Text hat nämlich implizit folgende Funktion verwendet - Zitat aus dem Bug:
"If an enabled "BEFORE UPDATE" row trigger references a column A but that column A is not updated by the trigger, only referenced, logminer reports SQL_REDO as updating the column A to itself when some other column B is updated. This issue has been reported in the Bug 7720026: SQL_REDO SHOWS UPDATES OF COLUMNS NOT IN UPDATE STATEMENT"
Trigger prüfen nun als Fix zu Bug 14155722 den "before" und "after" Zustand aller referenzierten Spaltenwerte. Falls die Spalte sich NICHT verändert hat, dann findet auch KEIN Index Update statt. Dies bedeutet, dass unser Trigger kein Index Update initiiert, wenn eine andere Spalte verändert wird.

Was kann man tun? Laut Roger Fords (Productmanager von Oracle Text) Blogeintrag kann man in der Session oder im ganzen System ein Fix Control verwenden.
alter session set "_fix_control"='14155722:OFF';
-- oder
alter system set "_fix_control"='14155722:OFF';
Was ist noch einmal ein Fix Control (Bezeichnung:_FIX_CONTROL)? "_FIX_CONTROL" ist ein spezieller "hidden" Parameter - eingeführt mit 10.2.0.2 - mit dem man Bug Fixes ein- und ausschalten kann. Der Default (ON oder OFF) für einen Fix wird meist über den Wert von OPTIMIZER_FEATURES_ENABLE bestimmt. Ansehen kann man sich die Einstellungen übrigens mit V$SYSTEM_FIX_CONTROL oder über V$SESSION_FIX_CONTROL. Mehr dazu findet sich auch in My Oracle Support Note 567171.1.

Zum Schluss treten wir natürlich noch den Beweis in 12c an :) ...
-- Fix Control setzen 
alter session set "_fix_control"='14155722:OFF';
--Überprüfung, VALUE 0 bedeutet OFF
SQL> select * from V$SESSION_FIX_CONTROL where bugno=14155722;

SESSION_ID      BUGNO      VALUE
---------- ---------- ----------
SQL_FEATURE
----------------------------------------------------------------
DESCRIPTION
----------------------------------------------------------------
OPTIMIZER_FEATURE_ENABLE       EVENT IS_DEFAULT     CON_ID
------------------------- ---------- ---------- ----------
        67   14155722          0
QKSFM_CBO_14098180
check whether outbinds were modified in before row UPDATE trigge
8.0.0                              0          0          0
-- ein Update
update produktions_hinweise set kommentar_werk='TTT';
commit; 
-- die Abfrage
select fall_id from produktions_hinweise
where contains(
  indexspalte, 
  '(Spezialmaschine WITHIN (HINWEISE_PRODUKTION)) and 
   (Abschreibungen WITHIN (KOMMENTAR_CONTROLLING)) and 
   (TTT WITHIN (KOMMENTAR_WERK))'
)>0
/
   FALL_ID
----------
         1

Und das alte Verhalten ist wiederhergestellt.

Montag, 17. Februar 2014

Welche DR$-Tabellen gibt es und wozu sind sie gut?

Den meisten Anwendern fällt recht schnell auf, dass sich nach dem Erstellen eines Oracle TEXT-Index einige zusätzliche Tabellen im Datenbankschema befinden.
SQL> create index FT_PO on PURCHASEORDER_TAB (XML_DOCUMENT)
  2  indextype is ctxsys.context
  3  /

Index wurde erstellt.

SQL> select * from tab;

TNAME                          TABTYPE  CLUSTERID
------------------------------ ------- ----------
PURCHASEORDER_TAB              TABLE
DR$FT_PO$N                     TABLE
DR$FT_PO$R                     TABLE
DR$FT_PO$K                     TABLE
DR$FT_PO$I                     TABLE
:                              :
Diese Tabellen enthalten den eigentlichen Textindex - meist sind es vier Tabellen, wie wir noch sehen werden, können es aber durchaus mehr werden. Das Namensschema ist immer gleich.
  • Es beginnt mit dem Präfix DR
  • Bei nichtpartitionierten Indizes folgt ein "$", bei partitionierten Indizes ein "#"
  • Dann folgt der Name des Volltextindex
  • Bei einem partitionierten Index schließt sich eine "Partitions-ID" direkt an den Indexnamen an
  • Es folgt wieder ein "$"
  • Abschließend folgt ein Suffix, welches die genaue Aufgabe der Tabelle bezeichnet - im folgenden werden die einzelnen Tabellentypen näher erläutert
Ein Oracle TEXT Index besteht aber immer aus wenigstens vier Tabellen - in Oracle12c können es aber bis zu neun werden.

$I: Token-Tabelle

 ----------------------------------------- -------- ------------------
 TOKEN_TEXT                                NOT NULL VARCHAR2(64)
 TOKEN_TYPE                                NOT NULL NUMBER(10)
 TOKEN_FIRST                               NOT NULL NUMBER(10)
 TOKEN_LAST                                NOT NULL NUMBER(10)
 TOKEN_COUNT                               NOT NULL NUMBER(10)
 TOKEN_INFO                                         BLOB
 ----------------------------------------- -------- ------------------
Die Token-Tabelle ist die eigentliche Indextabelle. Sie speichert die Tokens, also die einzelnen Wörter, die sich durch die Zerlegung des Fließtexts ergeben haben, in der Spalte TOKEN_TEXT ab. Die Spalte TOKEN_INFO enthält (in binärer Form) die Informationen, in welchen Dokumenten, an welchen Stellen das jeweilige Token vorkommt. Die übrigen Spalten sind Hilfsspalten, damit das Interpretieren des BLOB effizienter wird.
Wird eine Volltextabfrage ausgeführt, so schaut Oracle TEXT zuerst in diese Tabelle - anhand der Suchbegriffe werden die BLOBs aus TOKEN_INFO ausgelesen und weiter verarbeitet.

$R: Mapping von DOCID auf ROWID

 ----------------------------------------- -------- ------------
 ROW_NO                                    NOT NULL NUMBER(3)
 DATA                                               BLOB
 ----------------------------------------- -------- ------------
Oracle Text arbeitet intern nicht mit ROWIDs als Zeiger auf die Tabellenzeilen, sondern mit DOCIDs. Bei der Indizierung erhält die erste Zeile die DOCID 1 und dann wird weitergezählt. Der Grund dafür ist, dass ein Volltextindex eine invertierte Liste ist - im Gegensatz zu einem "normalen" Index zeigen mehrere Einträge auf eine Tabellenzeile. Die Verwendung von ROWIDs würde zu extrem großen Indizes führen, so dass man die sparsameren DOCIDs verwendet. Die bereits erwähnte Spalte TOKEN_INFO in der $I-Tabelle liefert also DOCIDs zurück. Damit Oracle TEXT aber Tabellenzeilen zurückliefern kann, braucht es eine Mapping-Tabelle - anhand der $R-Tabelle kann nun zu jeder gefundenen DOCID die ROWID herausgesucht werden. Die ROWID zeigt dann schließlich auf die gewünschte Tabellenzeile. Normalerweise ist ein Oracle Text Index so aufgesetzt, dass diese Tabelle immer im Hauptspeicher residiert, also zur Abfragezeit keine I/O-Last generiert.

$K: Mapping von ROWID auf DOCID

 ----------------------------------------- -------- ----------------------------
 DOCID                                              NUMBER(38)
 TEXTKEY                                   NOT NULL ROWID
 ----------------------------------------- -------- ----------------------------
Die $K-Tabelle ist das "Gegenstück" zur $R-Tabelle - anhand einer gegebenen ROWID findet sie die für den Textindex relevante DOCID heraus. Für Abfragen ist diese Tabelle weniger wichtig; benötigt wird sie aber bei DML-Operationen auf bereits indizierte Zeilen - wird beispielsweise eine Zeile mit SQL DELETE gelöscht, dann braucht Oracle TEXT die DOCID, um das Löschen auch im Textindex zu vermerken.

$N: Negativliste - enthält gelöschte Dokumente

 ----------------------------------------- -------- ----------------------------
 NLT_DOCID                                 NOT NULL NUMBER(38)
 NLT_MARK                                  NOT NULL CHAR(1)
 ----------------------------------------- -------- ----------------------------
Bekanntlich arbeitet Oracle TEXT asynchron (der Parameter TRANSACTIONAL bleibt hier außer Acht). Eine SQL INSERT-Anweisung bewirkt, dass die ROWID der Tabellenzeile in die PENDING-Tabelle eingetragen und damit "zur Indizierung vorgesehen" wird (die tatsächliche Indizierung erfolgt durch eine SYNC-Operation). Eine SQL DELETE Anweisung bewirkt, dass die Datenbank die DOCID ermittelt ($K-Tabelle) und diese dann in die Negativliste einträgt. Bei Abfragen ermittelt Oracle TEXT die Treffermenge ganz normal und filtert dann die Einträge der Negativliste heraus. Ein SQL UPDATE wird wie ein DELETE, gefolgt von einem INSERT, behandelt. Das hat den interessanten Effekt, dass neu eingefügte Dokumente erst nach dem SYNC sichtbar werden, gelöschte dagegen sofort verschwinden und mit SQL UPDATE veränderte Einträge ebenfalls bis zum nächsten SYNC "unsichtbar" werden.

$P: Hilfstabelle für den Substring-Index

 ----------------------------------------- -------- ----------------------------
 PAT_PART1                                 NOT NULL VARCHAR2(61)
 PAT_PART2                                 NOT NULL VARCHAR2(64)
 ----------------------------------------- -------- ----------------------------
Der Substring-Index muss mit Hife der Wordlist-Preference explizit eingeschaltet werden und unterstützt Abfragen mit einem Wildcard auf der linken Seite. Sucht man bspw. nach dem Text %DATENBANK, so ist klar, dass die $I-Tabelle (Spalte TOKEN_TEXT) prinzipiell komplett geparst werden muss - anstelle des Wildcard können ja beliebige Zeichen stehen. Bei sehr großen Indizes kann das ein Problem sein - schließlich wird auch die $I-Tabelle nun sehr groß. Der Substring-Index legt die $P-Tabelle als Hilfstabelle an - das Token ORACLEDATENBANK würde in dieser Tabelle wie folgt abgelegt:
PAT_PART1            PAT_PART2
-------------------- --------------------
ORACLEDATENB         ANK
ORACLEDATEN          BANK
ORACLEDATE           NBANK
ORACLEDAT            ENBANK
:                    :
ORACLE               DATENBANK
:
Ist diese Tabelle vorhanden, so werden die Suchbegriffe mit einer Wildcard links zuerst anhand dieser Tabelle über die Spalte PAT_PART2 aufgelöst - mit der sich ergebenden Tokenliste wird danach normal verfahren. Die Größe der $P-Tabelle liegt typischerweise zwischen 10 und 20 Prozent der Größe des Textindex.

$S: Hilfstabelle für strukturierte Elemente (SDATA-Sections)

 ----------------------------------------- -------- ----------------------------
 SDATA_ID                                  NOT NULL NUMBER
 SDATA_LAST                                NOT NULL NUMBER
 SDATA_DATA                                         RAW(2000)
 ----------------------------------------- -------- ----------------------------
In der $S-Tabelle werden strukturierte Bestandteile des Index gespeichert - dies ist seit Oracle11g mit den Composite Domain Indexes möglich. Oracle TEXT erlaubt auf diesen SDATA-Section zusätzlich auch Suchen wie <, >, BETWEEN und andere. SDATA-Sections haben einen Datentypen - unterstützt ist neben VARCHAR2 auch NUMBER oder DATE, was für Abfragen wichtig ist (intern speichert die Tabelle die Daten im RAW-Format ab). SDATA-Sections können entweder durch die Klauseln FILTER BY und ORDER BY im CREATE INDEX-Kommando oder durch die Erstellung einer Section Group mit expliziten SDATA Sections deklariert werden.

$G: Automatic Near Realtime Index (Oracle12c)

 ----------------------------------------- -------- ----------------------------
 TOKEN_TEXT                                NOT NULL VARCHAR2(64)
 TOKEN_TYPE                                NOT NULL NUMBER(10)
 TOKEN_FIRST                               NOT NULL NUMBER(10)
 TOKEN_LAST                                NOT NULL NUMBER(10)
 TOKEN_COUNT                               NOT NULL NUMBER(10)
 TOKEN_INFO                                         BLOB
 ----------------------------------------- -------- ----------------------------
Die $G-Tabelle wurde mit dem neuen Feature Automatic Near Realtime Indexing in Oracle12c eingeführt; sie sieht genauso aus wie die Token-Tabelle ($I-Tabelle). Das Feature wurde bereits in einem Blog Posting näher beschrieben.

$E: Hilfstabelle für XML-Namespaces (Oracle12c)

 ----------------------------------------- -------- ----------------------------
 ID                                        NOT NULL VARCHAR2(12)
 NAMESPACE                                          VARCHAR2(4000)
 LNAME                                     NOT NULL VARCHAR2(256)
 NS                                                 VARCHAR2(100)
 ----------------------------------------- -------- ----------------------------
Ein weiteres neues Feature in Oracle12c ist die Unterstützung von XQuery Fulltext, also der Volltextsuche in XML-Dokumenten mit vollständiger Unterstützung des XML-Datenmodells. Die $E-Tabelle speichert dabei XML-Zusatzinformationen ab, die im "normalen" Volltextindex keinen Platz finden - hier sind vor allem die XML-Namespaces zu nennen, die denn auch vor Oracle12c nicht von Oracle TEXT unterstützt wurden. Das Feature wurde ebenfalls bereits in einem Blog-Posting näher beschrieben.

$D: Hilfstabelle für das Save-Copy Feature (Oracle12c)

 ----------------------------------------- -------- ----------------------------
 DOCID                                     NOT NULL NUMBER(10)
 ATTRIBUTES                                         BLOB
 DOC                                                BLOB
 CONFIG                                             VARCHAR2(2000)
 FLAG                                      NOT NULL NUMBER(5)
Auch das Save Copy Feature wurde in Oracle12c eingeführt. Ist es aktiviert, so speichert Oracle TEXT die Plaintext- oder gefilterte Version eines Binärformates wie PDF, DOC, PPT oder anderen in der $D-Tabelle ab. Zum Generieren von SNIPPETs, MARKUPs oder HIGHLIGHT-Dokumenten können dann die in dieser Tabelle abgelegten Kopien verwendet werden - früher musste das Originaldokument hierfür nochmals gefiltert werden, was sich natürlich negativ auf die Performance auswirkte ...

Montag, 2. Dezember 2013

Oracle12c: Unterstützung für XML-Namespaces in Oracle TEXT

Heute geht es nochmals um Oracle12c: Oracle TEXT hat ein hochinteressantes neues Feature dazubekommen: Und zwar die Unterstützung für XQuery Full Text. Damit bietet Oracle TEXT nun wirklich eine vollständige Unterstützung für XML. Aber Moment mal: Konnte Oracle TEXT nicht schon immer mit XML umgehen? Im Prinzip ja - aber einige XML-Besonderheiten wurden von Oracle TEXT nicht unterstützt - zum Beispiel XML Namespaces. Dazu ein kleines Beispiel: Zunächst füllen wir eine Tabelle mit ein paar XML-Dokumenten:
create table tab_xml (id number, docs xmltype)
/

/*
 * Das Dokument enthält ein Tag "tag" aus dem Namespace "http://mynamespaces.com/ns1"
 */
insert into tab_xml values (
  1, 
  '<ns1:dokument xmlns:ns1="http://mynamespaces.com/ns1" 
                 xmlns:ns2="http://mynamespaces.com/ns2">
    <ns1:tag att="attr1">Das ist ein Text</ns1:tag>
   </ns1:dokument>'
)
/

/*
 * Das Dokument enthält ein Tag "tag" aus dem Namespace "http://mynamespaces.com/ns2"
 * Also ein anderer Namespace als Dokument 1
 */
insert into tab_xml values (
  2, 
  '<ns1:dokument xmlns:ns1="http://mynamespaces.com/ns1" 
                 xmlns:ns2="http://mynamespaces.com/ns2">
    <ns2:tag att="attr1">Das ist ein Text</ns2:tag>
   </ns1:dokument>'
)
/

/*
 * Das Dokument enthält ein Tag "tag" aus dem Namespace "http://mynamespaces.com/ns2"
 * Also gleicher Namespace wie Dokument 2 - ABER: anderer Präfix!
 */
insert into tab_xml values (
  3, 
  '<ns_1:dokument xmlns:ns_1="http://mynamespaces.com/ns1" 
                  xmlns:ns_2="http://mynamespaces.com/ns2">
    <ns_2:tag att="attr1">Das ist ein Text</ns_2:tag>
   </ns_1:dokument>'
)
/

commit
/
Das Besondere an diesen Dokumenten sind die XML Namespaces. Die XML-Tags enthalten ein sogenanntes Namespace-Präfix. Das ist aber noch nicht der Namespace selbst - denn oben im ersten Tag wird dieser Präfix mit dem Attribut xmlns auf den tatsächlichen Namespace abgebildet. Das erste Dokument definiert also zwei Namespaces: http://mynamespaces.com/ns1 und http://mynamespaces.com/ns2. Das XML-Tag names tag trägt den Präfix ns1, gehört also zum Namespace http://mynamespaces.com/ns1. Im zweiten Dokument werden die gleichen Namespaxes und auch die gleichen Namespace-Präfixe verwendet - nur gehört das Tag tag hier zum anderen Namespace http://mynamespaces.com/ns2.
Im dritten Dokument kommt es nun: Auch hier werden die gleichen Namespaces verwendet, aber mit anderen Präfixen. Das XML Tag tag trägt nun den Präfix ns_2 - dieser ist aber auf den Namespace http://mynamespaces.com/ns2 gemappt - es ist also tatsächlich das gleiche Tag wie in Dokument 2.
In XML kann der gleiche Namespace tatsächlich mit unterschiedlichen Präfixen angesprochen werden; wenn man wissen möchte, ob zwei XML Tags identisch sind, reicht es also nicht aus, nur auf die Präfixe zu schauen; man muss nachsehen, auf welche Namespaces diese Präfixe gemappt wurden. Und genau das tut Oracle TEXT bis einschließlich Oracle11g nicht. Legen wir zunächst einen "klassischen" Oracle TEXT-Index mit "XML-Unterstützung" an.
create index ft_tabxml on tab_xml (docs)
indextype is ctxsys.context
parameters ('section group ctxsys.path_section_group')
/
Ein mit der PATH_SECTION_GROUP erzeugter Volltextindex eröffnet die Möglichkeit, mit den CONTAINS-Operatoren INPATH und HASPATH zu arbeiten. Diese unterstützen XML auch ganz wunderbar; man kann ganz ähnlich zu XPath arbeiten und auch Attribute werden unterstützt. Nur ... mit den Eigenheiten der XML-Namespaces kann die PATH_SECTION_GROUP in Oracle11g nicht umgehen ...
select id from tab_xml
where contains(docs, 'text INPATH(/ns1:dokument/ns2:tag)') > 0
/

        ID
----------
         2

1 Zeile wurde ausgewählt.
Es gibt von der Syntax her keine Möglichkeit, einen Namespace "richtig" anzugeben. Man kann nur den Namespace-Präfix angeben (und der wird so behandelt, als wäre er Teil des Tag-Namens). Aber Oracle TEXT kann in 11g nicht nachsehen, auf welchen Namespace er gemappt wurde und ob der gleiche Namespace noch von anderen Präfixen verwendet wird. Dokument 3 verwendet einen anderen Präfix, daher wird es nicht gefunden - es müsste aber gefunden werden, denn es ist das gleiche Tag. Doch Oracle12c schafft Abhilfe - zunächst muss der Index etwas anders erzeugt werden ...
begin
  ctx_ddl.create_section_group('my_sg_xquery', 'PATH_SECTION_GROUP');
  ctx_ddl.set_sec_grp_attr('my_sg_xquery', 'xml_enable', 'true');
end;
/
sho err

create index ft_tabxml on tab_xml (docs)
indextype is ctxsys.context
parameters ('section group my_sg_xquery')
/
Vor dem CREATE INDEX muss eine eigene Section Group vom Typ PATH_SECTION_GROUP erstellt werden. Das muss sein, denn bei dieser muss das Attribut XML_AWARE auf TRUE gestellt sein. Danach kann der Index normal erzeugt werden.
Nun liegt ein Textindex vor, der auch XML-Namespaces vollständig unterstützt. Nicht erweitert wurde dagegen die Syntax des CONTAINS-Operators - und das hat einen guten Grund: Denn für die Volltextsuche in XML-Dokumenten gibt es inzwischen einen Standard - XQuery Full Text - und diesen sollte man auch verwenden. Es machte also keinen Sinn, mit einer Erweiterung für CONTAINS neue, proprietäre Syntax zu schaffen, man entschied sich, auf den Standard zu setzen. Und der sieht so aus:
SELECT id
FROM tab_xml
WHERE XMLExists('declare namespace ns="http://mynamespaces.com/ns2";
                 //ns:tag[. contains text "text"]'
                  PASSING docs)
/
Achtung: Wenn Ihr mit SQL*Plus arbeitet, kann euch das Semikolon in der dritten Zeile in die Query kommen - SQL*Plus betrachtet die Abfrage als dort zu Ende. Das kann man mit einem set sqlterminator # umstellen. Diese Abfrage findet nun alles, was gefunden werden soll ...
        ID
----------
         2
         3

2 Zeilen ausgewählt.
Schauen wir nun mal in die Indexstrukturen hinein. Wie immer bei Oracle TEXT, ist der Index tatsächlich in den DR$-Tabellen abgelegt.
SQL> select tname from tab where tname like 'DR$%'
  2  /

TNAME
-------------------------------------------------------
DR$FT_TABXML$R
DR$FT_TABXML$N
DR$FT_TABXML$K
DR$FT_TABXML$I
DR$FT_TABXML$E
DR$FT_TABXML$D

6 Zeilen ausgewählt.
Die XML-Unterstützung ist nun in den $D- und $E-Tabellen enthalten. Die $E-Tabelle kann man lesen: Wie die folgende Abfrage zeigt, speichert Oracle TEXT dort alle XML-Tags mitsamt Ihren Namespaces ab.
SQL> select * from DR$FT_TABXML$E
/

ID           NAMESPACE                    LNAME        NS
------------ ---------------------------- ------------ ----------------------------
_-_1         http://mynamespaces.com/ns1  dokument     http://mynamespaces.com/ns1
_-_2         http://mynamespaces.com/ns1  tag          http://mynamespaces.com/ns1
_-_3         http://mynamespaces.com/ns2  tag          http://mynamespaces.com/ns2

3 Zeilen ausgewählt.
Die $D-Tabelle gehört eigentlich zum Save Copy Feature von Oracle TEXT, welches hier gar nicht explizit angesprochen wurde. In dieser Tabelle speichert Oracle TEXT, aktiviert man das Feature, gefilterte Versionen von Binärdokumenten ab, um Snippet- oder Markup-Operationen schneller durchführen zu können (auch dazu gibt es noch ein Blog Posting). Ein "XML-Aware" Textindex nutzt diese Tabellen aber ebenfalls für seine interne Verarbeitung. Die Spalte DOC der $D-Tabelle enthält eine nochmals aufbereitete Variante des XML-Dokumentes.
Zusammenfassend kann man also sagen, dass Oracle TEXT in der Version 12c eine umfangreiche und wirklich komplette Volltextsuche für XML anbietet - mit der Unterstützung für XQuery Full Text. Weitere Informationen zum Thema findet Ihr im XML DB Developers' Guide: Indexing XML Data for Full-Text Queries. Viel Spaß beim Ausprobieren.

Dienstag, 8. Oktober 2013

Oracle Text in Oracle 12c: Automatic Near Real-Time Index

Oracle Database 12c steht schon seit einiger Zeit zur Verfügung und wie schon angesprochen, wollen wir im Rahmen unserer Blogpostings die neuen Features nach und nach beschreiben. Dieses Mal geht es um das sogenannte Feature "Automatic Near Real-Time Indexing". Dabei geht es um die typische Anforderung den Index möglich aktuell zu halten - sogar bei hoher Änderungsrate. Hoher Aktualitätsanspruch bedeutet allerdings häufiges Synchronisieren und führt natürlich zu hoher Fragmentierung bzw. zu vermehrtem und länger andauernden OPTIMIZE Operationen.

12c löst dieses Dilemma mit dem neuen Konzept des "two-level" Index bzw. auch unter dem Feature Name "near real time index" bekannt. Dies erlaubt einen kleinen, fragmentierten Stage Index, der alle aktuellen Veränderungen enthält, vorzuhalten, ohne Änderungen an dem großen Index vornehmen zu müssen. Die Idee dabei ist, dass der Stage Index klein genug ist, um in die SGA zu passen. Die Daten können dann nach und nach vom Stage Index zum Hauptindex verlagert werden. Dies geschieht mit einem neuen MERGE Modus für Indizes.

Folgendes Beispiel zeigt wie das Feature genutzt werden kann. Zuerst stellen wir eine leere Tabelle zur Verfügung.
drop table texttabelle
/
drop sequence s
/
create sequence s
/
create table texttabelle(
  id          number(10) default s.nextval,
  dokument varchar2(1000))
/
Realisiert wird das Feature dann mit der zusätzlichen Storage Option STAGE_ITAB.
exec ctx_ddl.drop_preference ('my_storage');
exec ctx_ddl.create_preference('my_storage', 'BASIC_STORAGE');
exec ctx_ddl.set_attribute ('my_storage', 'STAGE_ITAB', 'true');
Nun erzeugen wir den Index.
create index my_index on texttabelle (dokument) 
indextype is ctxsys.context
parameters( 'storage my_storage sync (on commit)');
Überprüfen wir die erzeugten Tabellenobjekte, stellen wir fest, dass zusätzlich eine neue Tabelle mit Namen DR$MY_INDEX$G generiert wurde, die die gleiche Struktur wir die $I Tabelle besitzt.
SQL> select * from tab
  2  /

TNAME                          TABTYPE  CLUSTERID
------------------------------ ------- ----------
DR$MY_INDEX$G                  TABLE
DR$MY_INDEX$N                  TABLE
DR$MY_INDEX$R                  TABLE
DR$MY_INDEX$K                  TABLE
TEXTTABELLE                    TABLE
DR$MY_INDEX$I                  TABLE

6 rows selected.
Darüberhinaus werden weitere Objekte wie zum Beispiel ein $H B-tree Index auf der $G Tabelle erzeugt - dieser erfüllt die gleiche Aufgabe wie der $X Index auf der $I Tabelle.

Nun fügen wir einfach zwei Zeilen ein und schließen jeweils mit COMMIT ab.
insert into texttabelle (dokument) values 
         ('A-Partei gewinnt Wahl in Hansestadt');
commit;
insert into texttabelle (dokument) values 
         ('Terror in Nahost: Kriminalität steigt immer weiter an'); 
commit;
Überprüfen wir die Inhalte der beiden Tabellen, stellen wir fest, dass die $I Tabelle leer ist, wohingegen die $G Tabelle befüllt wurde. Neue Inhalte werden also nicht mehr direkt in der $I Tabelle geschrieben, sondern in der $G Tabelle mitgeführt.
SQL> select token_text from DR$MY_INDEX$I;
no rows selected

SQL> select token_text from DR$MY_INDEX$G;

TOKEN_TEXT
----------------------------------------------------------------
GEWINNT
HANSESTADT
IMMER
KRIMINALIT?T
NAHOST
PARTEI
STEIGT
TERROR
WAHL
WEITER

10 rows selected.
Um diese Tabelle in der SGA zu halten, kann man sich entweder auf das normale Caching Verhalten verlassen oder aber - falls konfiguriert - den KEEP Pool der Datenbank nutzen. Dazu kann man die folgenden Storage Attribute verwenden.
exec ctx_ddl.set_attribute ('my_storage', 'G_TABLE_CLAUSE', 
                                                  'storage (buffer_pool keep)');
exec ctx_ddl.set_attribute ('my_storage', 'G_INDEX_CLAUSE', 
                                                 'storage (buffer_pool keep)');
Möchte man nun die Daten in den Hauptindex manuell integrieren, kann man dazu das folgende OPTIMIZE Kommando verwenden.
execute ctx_ddl.optimize_index(idx_name=>'MY_INDEX', optlevel=>'MERGE');
Damit werden die Einträge aus $G in optimierter Form in die $I Tabelle überführt und gleichzeitig aus der $G Tabelle gelöscht.

Optimal wäre es nun, wenn dieser Aufgabe von Oracle automatisch durchgeführt werden könnte. Die neue Prozedur ADD_AUTO_OPTIMIZE erfüllt diese Aufgabe.
exec ctx_ddl.add_auto_optimize( 'my_index' )
PL/SQL procedure successfully completed.
Nach der Ausführung wird der Index zur automatischen Optimierung registriert, wie wir in der folgenden View sehen können.
SQL> select * from ctx_user_auto_optimize_indexes;

AOI_INDEX_NAME                 AOI_PARTITION_NAME
------------------------------ ------------------------------
MY_INDEX
Ausgeführt wird diese Aktion von einem DBMS_SCHEDULER Job, der automatisch im Hintergrund angelegt und ausgeführt wird.
SQL> select job_name, program_name, schedule_type, last_start_date 
     from dba_scheduler_jobs 
     where owner='CTXSYS';

JOB_NAME             PROGRAM_NAME         SCHEDULE_TYP
-------------------- -------------------- ------------
LAST_START_DATE
---------------------------------------------------------------------------
DR$BGOPTJOB          DR$BGOPTPRG          IMMEDIATE
08-OCT-13 04.42.13.977418 PM EUROPE/VIENNA
Übrigens lässt sich dieses Konzept des "two-level" Index - auch nachträglich mit einem ALTER INDEX REBUILD Kommando zu einem bestehenden Index hinzufügen.
alter index my_index rebuild parameters('replace storage my_storage');

Donnerstag, 29. August 2013

DBMS_PCLXUTIL zur Erzeugung von lokalen Indizes

Um die Performance beim Aufbau eines Index zu erhöhen, können Oracle Text Indizes mit dem Schlüsselwort PARALLEL aufgebaut werden. Geht es um den parallelen Aufbau eines lokalen partitionierten Index, kann allerdings auch das altbewährte Package DBMS_PCLXUTIL eine Alternative darstellen. Wer eine Beschreibung des Package sucht, wird im PL/SQL Packages Guide fündig; die Verwendung im Oracle Text Umfeld ist etwas versteckt im Oracle Text Reference Guide nachzuschlagen. Da es in letzter Zeit vermehrt Anfragen dazu gab, haben wir uns entschlossen, die Verwendung an einem Beispiel zu demonstrieren.
Zuerst zur Definition des Package: Das Package DBMS_PCLXUTIL sieht folgendermassen aus:
desc dbms_pclxutil
PROCEDURE BUILD_PART_INDEX
 Argument Name                  Type                    In/Out Default?
 ------------------------------ ----------------------- ------ --------
 JOBS_PER_BATCH                 NUMBER                  IN     DEFAULT
 PROCS_PER_JOB                  NUMBER                  IN     DEFAULT
 TAB_NAME                       VARCHAR2                IN     DEFAULT
 IDX_NAME                       VARCHAR2                IN     DEFAULT
 FORCE_OPT                      BOOLEAN                 IN     DEFAULT
Die Verwendung von TAB_NAME und IDX_NAME muß sicherlich nicht erklärt werden. Anzumerken ist höchstens, dass man nur als Eigentürmer der Tabellen und des Index das Package verwenden kann; eine Benennung über eine zusätzliche Schemabezeichnung ist nicht möglich. Die beiden Parameter JOBS_PER_BATCH und PROCS_PER_JOB bestimmen dabei im Unterschied zu anderen Methoden die Parallelisierung auf zwei (!) Ebenen. JOBS_PER_BATCH ist für die sogenannte Inter Parallelität zuständig, die mit DBMS_JOB Prozessen realisiert wird, und PROCS_PER_JOB für die sogenannte Intra Parallelität, die mit parallelen Prozessen ausgeführt wird.

Genauer bedeutet dies:
JOBS_PER_BATCH steht für die Anzahl der Job Prozesse, die gleichzeitig arbeiten. Hier sollte gelten: Das Minimum ist 1, das Maximum stellt die Anzahl der Partitionen dar.

PROCS_PER_JOB bestimmt die Anzahl der parallelen Query Prozesse pro Job. Auch hier stellt die Zahl 1 das Minimum dar.

FORCE_OPT kann den Wert TRUE oder FALSE haben. FALSE führt nur ein REBUILD für UNUSABLE Indizes aus, TRUE hingegen für alle Partitionen.

Bevor wir starten, sollte zuerst die Einstellung der Job Prozesse geprüft werden. Der Initialisierungsparameter JOB_QUEUE_PROCESSES gibt die maximale Anzahl der Job Prozesse an.
show parameter job

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
job_queue_processes                  integer     1000
Wichtig zu wissen ist, dass die Prozedur BUILD_PART_INDEX davon ausgeht, dass die Data Dictionary Informationen zum Index schon existieren. Falls nicht erhält man folgende Fehlermeldung:
*
ERROR at line 1:
ORA-20001: Specified local index name 'PROD_DESC_IDX' does not exist
ORA-06512: at "SYS.DBMS_PCLXUTIL", line 301
ORA-06512: at line 1
Vorab ist also folgendes Kommando sinnvoll:
CREATE INDEX prod_desc_idx ON product_part (prod_desc) INDEXTYPE IS ctxsys.context LOCAL UNUSABLE;
Ab 11g Release 2 wird für UNUSABLE Index Partitionen übrigens kein Speicherplatz mehr verbraucht, so dass die Information nicht in USER_SEGMENTS verzeichnet ist. Die Data Dictionary Informationen sind aber wie immer in USER_INDEXES gespeichert.
SELECT * FROM user_segments WHERE segment_name='PROD_DESC_IDX';

no rows selected

SELECT index_name, index_type FROM user_indexes WHERE index_name LIKE 'PROD_DESC%'

INDEX_NAME           INDEX_TYPE
-------------------- ---------------------------
PROD_DESC_IDX        DOMAIN
Um eine Anwendung zu demonstrieren, erzeugen wir einen lokalen Text Index auf die Spalte PROD_DESC der partitionierten Tabelle PRODUCTS_PART mit den 4 Partitionen P_10, P_100, P_1000 und PARTMAXVALUE.
EXECUTE dbms_pclxutil.build_part_index
(JOBS_PER_BATCH => 4,
 PROCS_PER_JOB  => 1,
 TAB_NAME       => 'PRODUCT_PART',
 IDX_NAME       => 'PROD_DESC_IDX',
 FORCE_OPT      => TRUE); 
Parallel dazu lassen sich die Job Prozesse überwachen - es werden 4 Jobs gestartet jeweils mit Parallelität 1.
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE        NEXT_DATE        FAILURES
---------- ---------------- ---------------- ----------
WHAT
--------------------------------------------------------------------------------
        42                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_100" parallel (degree 1)');

        43                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_1000" parallel (degree 1)');

        44                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "PARTMAXVALUE" parallel (degree 1)');

        41                  27.08.2013 17:41
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_10" parallel (degree 1)');
Am Schluss sollte natürlich auch die erfolgreiche Indexerstellung überprüft werden.
SELECT err_timestamp, err_text FROM ctx_user_index_errors
ORDER BY err_timestamp DESC;

no rows selected
Möglich wäre allerdings auch folgendes Kommando mit insgesamt 2 Jobs - dabei jeweils 2 parallele Prozessen pro Job.
EXECUTE dbms_pclxutil.build_part_index
(JOBS_PER_BATCH => 2,
 PROCS_PER_JOB  => 2,
 TAB_NAME       => 'PRODUCT_PART',
 IDX_NAME       => 'PROD_DESC_IDX',
 FORCE_OPT      => TRUE);
Prüft man die Jobs, stellt man folgende Aufrufe fest.
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE NEXT_DATE FAILURES
---------- --------- --------- ----------
WHAT
--------------------------------------------------------------------------------
       221 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "PARTMAXVALUE" parallel (degree 2)');

       222 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_10" parallel (degree 2)');
Dann nach einer gewissen Zeit ...
SELECT job, this_date, next_date, failures, what FROM dba_jobs;

       JOB THIS_DATE NEXT_DATE FAILURES
---------- --------- --------- ----------
WHAT
--------------------------------------------------------------------------------
       241 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_100" parallel (degree 2)');

       242 28-AUG-13 28-AUG-13
dbms_utility.exec_ddl_statement('alter index "SH"."PROD_DESC_IDX" rebuild partit
ion "P_1000" parallel (degree 2)');
Er laufen also immer zwei Jobs zu einer Zeit. Prüft man gleichzeitig die Parallelität über v$px_session und V$session, erkennt man, dass wirklich 2 Prozesse pro Job arbeiten.
Username     QC/Slave   Slave Set  SID    QC SID Requested DOP Actual DOP
------------ ---------- ---------- ------ ------ ------------- ----------
SH           QC                    34     34
 - p001      (Slave)    1          43     34                 2          2
 - p000      (Slave)    1          87     34                 2          2
SH           QC                    94     94
 - p003      (Slave)    1          95     94                 2          2
 - p002      (Slave)    1          98     94                 2          2
Wo liegt nun der Unterschied zwischen den beiden Läufen? Beim zweiten Lauf arbeiten mehrere -hier 2 - Prozesse pro Job gleichzeitig. Unter Umständen sind dadurch die Index Partitionen schneller erstellt, was bei großen Indizes mit langen Laufzeiten sicherlich wünschenswert ist. Allerdings ist folgendes zu beachten: Da wir mit mehreren Prozessen pro Job arbeiten, kann es zu einer höheren Fragmentierung der einzelnen Index Partitionen kommen. Hier wäre dann ein anschliessendes CTX_DDL.OPTIMIZE_INDEX nötig.

Zum Schluss vielleicht noch ein wichtiger Hinweis: Vergessen Sie bei all diesen Operationen die Memory Einstellung nicht. Die Überprüfung kann zum Beispiel über CTX_PARAMETERS erfolgen.
SELECT * FROM ctx_parameters WHERE par_name LIKE '%MEM%';

PAR_NAME                       PAR_VALUE
------------------------------ -----------------------------------
DEFAULT_INDEX_MEMORY           67108864
MAX_INDEX_MEMORY               1073741824
Da DBMS_PCLXUTIL ein Package zur allgemeinen Verwendung ist, sind keine Text spezifischen Einstellungen über den Package Aufruf möglich - also auch keine Memory Einstellungen. Möchte man eine spezielle Memory Einstellung verwenden, kann beispielsweise die Prozedur CTXSYS.CTX_ADM.SET_PARAMETER verwendet werden.

Beliebte Postings