Beiträge

Oracle Statspack verbessern: Historische Pläne – Teil 2

Grundlegendes: Ändern des Statspack Code

English version here : Improving Statspack: Add Support for Plan Stability

Natürlich sollte man den Statspack Code nicht leichtfertig ändern. Das wird Niemand wollen. Die hier besprochene Änderung erlaubt es, schnell und ohne großes Nachdenken einen Plan aus der Vergangenheit wieder her zu stellen.

Für manche Applikationen und Datenkonstellationen ist diese Möglichkeit wirklich wichtig.
In diesem Fall sollte man es sich gründlich überlegen, ob man die hier beschriebene Veränderung nicht doch machen will.

Schließlich hat man den Source Code des Statspack. Man sollte die originale Codeversion behalten und kann daher jederzeit den Originalcode schnell wiederherstellen. Die zusätzliche optionale Spalte stört da nicht.

Ausgangslage

Bei meinem Webinar „The good plan and the bad Plan” für die DOAG ging es darum, gute Pläne aus der Vergangenheit wieder zu aktivieren.

Kann man die AWR Daten lesen, so kann man beispielsweise aus den DBA_HIST Daten mittels dbms_xplan.display_awr ein Outline erzeugen. Dieses Outline kann man dann in aktuelle Pläne einpflanzen.

Ich wurde damals gefragt, ob eine solche Methode auch auf einer Oracle Standard Edition Datenbank funktioniert.

Mit dem SQL_PATCH [2] gibt es eine legale Möglichkeit, ein Outline in einen SQL Befehl einzupflanzen.

Die Frage ist nur, wie man auf einer Oracle Standard Edition zu einem Outline eines alten Planes kommt.
Gibt es mit Statspack eine Funktion ähnlich dem dbms_xplan.display_awr?

Uwe Küchler, alias Oraculix hat eine sehr kreative Lösung parat [1].

Man verwendet einfach dbms_xplan.display im Zusammenhang mit einer Suchfunktion:

select * from table(dbms_xplan.display(
  table_name   => 'perfstat.stats$sql_plan',
  statement_id => null,
  format       => 'ALL -predicate -note',
  filter_preds => 'plan_hash_value = '|| &&phv
);

Man muss dazu noch eine Spalte in der Tabelle stats$sql_plan ergänzen, die Einzelheiten finden Sie im Oraculix Blog.

Was passiert nun, wenn man jetzt einfach im Format noch +OUTLINE angibt?
Dann müsste man doch das Outline bekommen. Da könnte man dann über einen SQL_PATCH in ein Statement schreiben und voila, hat man den alten Plan zurück.

Oder etwa nicht?

Nein, die Format Anweisung wird scheinbar einfach ignoriert.
Der Grund hierfür ist, dass in den Statspack Plänen einige Spalten fehlen. Konkret geht es hier um die Spalte OTHER_XML, die nicht mitgespeichert wird.

Die Spalte other_xml

In dieser Spalte findet sich unter anderen das Outline. Wer also die sehr wichtige Funktion der historischen Pläne haben will, darf nicht davor zurückscheuen, den Statspack code zu verändern. Wenn Oracle schon die der MOS Note 2182680.1 vorschlägt das Statspack zu verändern, weshalb sollte wir nicht ein wenig weiter gehen?

Wir beginnen, in dem wir die Spalte hinzufügen.

ALTER TABLE perfstat.stats$sql_plan ADD timestamp INVISIBLE AS (cast(NULL AS DATE));
ALTER TABLE perfstat.stats$sql_plan ADD OTHER_XML CLOB;

Danach müssen wir den Code anpassen. Analog zum Vorgehen in MOS Note 2182680.1 habe ich zuerst eine Kopie des aktuellen Codes gemacht.
Wieder ist es das Skript spcpkg, welches angepasst werden muss.

Zu ändern ist das Insert Statement beginnend mit:

insert into stats$sql_plan
                 ( plan_hash_value
                 , id
                 , operation
                 , options
                 , object_node
                 , object#

Hier muss die Spalte Other_XML ergänzt werden. Dabei gibt es jedoch eine Schwierigkeit.

Im Select Teil des Inserts steht eine Maximum Funktion, z.B. so:

                 , max(sp.operation)
                 , max(sp.options)
                 , max(sp.object_node)
                 , max(sp.object#)
                 , max(sp.object_owner)
                 , max(sp.object_name)
                 , max(sp.object_alias)
                 , max(sp.object_type)

Die Maximum Funktion ist ziemlich sicher nur dafür da, um duplizierte Sätze zu vermeiden.

Also schreiben wir doch einfach: Max(other_xml), oder?

Prompt kommt ORA-00932. Der Grund dafür ist, dass CLOB nicht sortierbar sind, daher kann auch kein Maximum berechnet werden.

ANY_VALUE

Natürlich gäbe es noch andere Möglichkeiten, um die Werteliste eindeutig zu machen.
Beispielsweise analytische Funktionen wie row_number ().

Ich wolle jedoch nicht so viel am Original Code verändern.

Mir fiel eine Idee aus dem Oracle Technologie Network ein, bei der es darum ging, für genau solche Probleme eine neue Art von Gruppenfunktion ein zu führen, genannt ANY_VALUE [3.].

Die Idee wird anscheinend mit Version 20c umgesetzt. Zu schade. Mit Version 19 wäre besser.

Als ehemaliger Oracle Mitarbeiter weiß ich jedoch, dass manchmal die Vorgängerversionen undokumentiert schon Änderungen der Folgeversion enthalten.

Und in der Tat: any_value gibt es schon.
Leider wirft auch diese Implementation völlig unnötig ORA-00932.

Ich habe Chris Saxon darauf aufmerksam gemacht und hoffe das Oracle die Implementierung noch ändert.
Nun, wenn Oracle nicht liefert muss ich das selbst machen.

Ich habe mir über das User-Defined Aggregate Functions Interface die Funktion selbst definiert und any_lob genannt.

Den Code finden sie am Ende dieses Blogs. Hier erst einmal der vollständige Insert Befehl. Die alle vom Standard abweichenden Änderungen sind mit Rot gekennzeichnet. Die Hints habe ich im vorigen Blog zum Thema Statspack beschrieben.

            insert into stats$sql_plan
                 ( plan_hash_value
                 , id
                 , operation
                 , options
                 , object_node
                 , object#
                 , object_owner
                 , object_name
                 , object_alias
                 , object_type
                 , optimizer
                 , parent_id
                 , depth
                 , position
                 , search_columns
                 , cost
                 , cardinality
                 , bytes
                 , other_tag
                 , partition_start
                 , partition_stop
                 , partition_id
                 , other
                 , other_xml                 
                 , distribution
                 , cpu_cost
                 , io_cost
                 , temp_space
                 , access_predicates
                 , filter_predicates
                 , projection
                 , time
                 , qblock_name
                 , remarks
                 , snap_id
                 )
            select /*+ leading(spu@np ssp@sq  s sp) */
                   new_plan.plan_hash_value
                 , sp.id
                 , max(sp.operation)
                 , max(sp.options)
                 , max(sp.object_node)
                 , max(sp.object#)
                 , max(sp.object_owner)
                 , max(sp.object_name)
                 , max(sp.object_alias)
                 , max(sp.object_type)
                 , max(sp.optimizer)
                 , max(sp.parent_id)
                 , max(sp.depth)
                 , max(sp.position)
                 , max(sp.search_columns)
                 , max(sp.cost)
                 , max(sp.cardinality)
                 , max(sp.bytes)
                 , max(sp.other_tag)
                 , max(sp.partition_start)
                 , max(sp.partition_stop)
                 , max(sp.partition_id)
                 , max(sp.other)
                 , any_lob(sp.other_xml)
                 , max(sp.distribution)
                 , max(sp.cpu_cost)
                 , max(sp.io_cost)
                 , max(sp.temp_space)
                 , 0 -- should be max(sp.access_predicates) (2254299)
                 , 0 -- should be max(sp.filter_predicates)
                 , max(sp.projection)
                 , max(sp.time)
                 , max(sp.qblock_name)
                 , max(sp.remarks)
                 , max(new_plan.snap_id)
              from (select /*+ QB_NAME(NP)  */  
                           spu.plan_hash_value
                         , spu.hash_value    hash_value
                         , spu.address       address
                         , spu.text_subset   text_subset
                         , spu.snap_id       snap_id
                      from stats$sql_plan_usage spu
                     where spu.snap_id         = l_snap_id
                       and spu.dbid            = p_dbid
                       and spu.instance_number = p_instance_number
                       and not exists (select /*+ QB_NAME(SQ)  */ *
                                         from stats$sql_plan ssp
                                        where ssp.plan_hash_value 
                                            = spu.plan_hash_value
                                      )
                   )          new_plan
                 , v$sql      s      -- join reqd to filter already known plans
                 , v$sql_plan sp
             where s.address         = new_plan.address
               and s.plan_hash_value = new_plan.plan_hash_value
               and s.hash_value      = new_plan.hash_value
               and sp.hash_value     = new_plan.hash_value
               and sp.address        = new_plan.address
               and sp.hash_value     = s.hash_value
               and sp.address        = s.address
               and sp.child_number   = s.child_number
             group by 
                   new_plan.plan_hash_value, sp.id
             order by
                   new_plan.plan_hash_value, sp.id; -- deadlock avoidance

Zusammenfassung

Der Code scheint recht gut zu funktionieren.

Nur eine Sache war etwas ärgerlich: Immer, wenn ich Syntaxfehler im Insert hatte, musste ich auch die Funktion any_lob neu kompilieren.

Hinweis: Man muss den Snap Level auf 6 oder höher schalten, damit das Statspack Pläne sammelt.  Z.B.:

EXECUTE statspack.snap(i_snap_level => 7);

Create or replace type any_lob_type as object
(
  v_clob CLOB,

  static function ODCIAggregateInitialize
    ( sctx in out any_lob_type )
    return number ,

  member function ODCIAggregateIterate
    ( self  in out any_lob_type ,
      value in     CLOB
    ) return number ,

  member function ODCIAggregateTerminate
    ( self        in  any_lob_type,
      returnvalue out CLOB,
      flags in number
    ) return number ,

  member function ODCIAggregateMerge
    ( self in out any_lob_type,
      ctx2 in     any_lob_type
    ) return number
);
/

create or replace type body any_lob_type
is
  static function ODCIAggregateInitialize
  ( sctx in out any_lob_type )
  return number
  is
  begin
    sctx := any_lob_type( null ) ;
    return ODCIConst.Success ;
  end;

  member function ODCIAggregateIterate
  ( self  in out any_lob_type ,
    value in     CLOB
  ) return number
  is
  begin
    self.v_clob := value ;
    return ODCIConst.Success;
  end;

  member function ODCIAggregateTerminate
  ( self        in  any_lob_type ,
    returnvalue out CLOB ,
    flags       in  number
  ) return number
  is
  begin
    returnvalue := self.v_clob;
    return ODCIConst.Success;
  end;

  member function ODCIAggregateMerge
  ( self in out any_lob_type ,
    ctx2 in     any_lob_type
  ) return number
  is
  begin
      return ODCIConst.Success;
  end;
end;
/

create or replace function any_lob
  ( input CLOB )
  return CLOB
  deterministic
  parallel_enable
  aggregate using any_lob_type
;
grant execute on any_lob to public;
create public sysnonym for sys.any_lob;

Quellen

 

Oracle Statspack verbessern: Schnellere Snapshots – Teil1

Oracle Statspack verbessern: Schnellere Snapshots – Teil1

Ausgangslage: Langsamer Statspack Snapshot

Auf verschiedenen Standard Edition Datenbanken sieht man immer wieder den Statspack Snapshot lange laufen.

Dies ist für mich als Consultant sehr unangenehm.
Oft muss ich den Kunden sagen, dass das Statspack auf einer Standard Edition Datenbank unverzichtbar ist.

Und dann sieht mein Kunde, wie der Statspack Snapshot minutenlang einen der kostbaren, limitierten Cores blockiert.

Ich beschloss, etwas dagegen zu unternehmen.
Bei näherer Betrachtung erkennt man zwei Statements, die die Datenbank belasten.

In erster Linie ist dies:

INSERT INTO stats$sql_plan

Aber auch dieser Befehl läuft lange:

INSERT INTO stats$seg_stat

Grundsätzliches: Hints im Statspack

Man hört oft, dass man Hints so wenig wie möglich machen soll und lieber andere Mittel verwenden soll, um den Optimizer zu steuern.

Gute Statistiken zum Beispiel.
Ich bin auch dieser Meinung.
Allerdings gibt es auch Ausnahmen.

Bei Tools wie Statspack kann man sich nicht darauf verlassen, dass die Statistiken immer aktuell sind.

Es werden auch interne Tabellen X$ verwendet. Nicht jeder DBA macht fixed table stats und selbst wenn, schwanken diese Statistiken oft stark.

Ein Tool wie Statspack muss immer zuverlässig laufen.
Glücklicherweise ist bei den zwei Statspack Queries die wir betrachten werden, ziemlich klar wie der Plan auszusehen hat.

Hints stellen also kein großes Risiko dar. Jedoch habe ich dennoch so wenig wie möglich festgelegt.

Statement : INSERT INTO stats$sql_plan

Das Problem wird in der MOS Note 2182680.1 behandelt.

In dieser Note wird vorgeschlagen eine alternative Implementation des Statspack Package (SCPPKG.SQL) herunter zu laden und zu implementieren.

Diese Implementation enthält einen geänderten Hint, den ich im folgenden Beispiel mit Rot hervorgehoben habe.

Vom ganzen Insert Statement zeige ich nur den SELECT Teil, weil der Eintrag sonst zu lange wird.
Auch vom Select Teil habe ich die Liste der Spalten gekürzt, damit der Code Teil übersichtlich bleibt.

SELECT /*+ no_merge(new_plan) leading(new_plan s sp) use_nl(s) use_nl(sp) */
            new_plan.plan_hash_value,
            sp.id,
            MAX(sp.operation),
            MAX(sp.options),  
            . . .
            MAX(new_plan.snap_id)
        FROM
            (
                SELECT /*+ index(spu) */
                    spu.plan_hash_value,
                    spu.hash_value    hash_value,
                    spu.address       address,
                    spu.text_subset   text_subset,
                    spu.snap_id       snap_id
                FROM
                    stats$sql_plan_usage spu
                WHERE
                    spu.snap_id = :b3
                    AND spu.dbid = :b2
                    AND spu.instance_number = :b1
                    AND NOT EXISTS (
                        SELECT /*+ nl_aj */
                            *
                        FROM
                            stats$sql_plan ssp
                        WHERE
                            ssp.plan_hash_value = spu.plan_hash_value
                    )

Wie unschwer zu erkennen ist, befinden sie relevanten Suchkriterien auf der Tabelle stats$sql_plan_usage mit dem Alias spu.

Es ist also wichtig, dass im Execution Plan mit dem Lesen dieser Tabelle begonnen wird.

Der Hint Leading(new_plan .. ist also folgerichtig.

Das Problem besteht jedoch darin, dass new_plan keine Tabelle, sondern der Name einer Unterfrage ist.
Obwohl Version 19c den Hint als korrekt meldet, wird er in tieferen Versionen oft nicht akzeptiert.

Der Alias für den Hint wäre eigentlich spu, jedoch ist dieser in der Hauptabfrage nicht zugänglich, weil die Tabelle stats$sql_plan_usage in einer Unterabfrage angesprochen wird. (Achtung: wenn ein Alias vorhanden ist, muss der Alias im Hint angegeben werden und nicht der Tabelle Name.)

Diese Unterabfrage bildet einen eigenen Queryblock und dessen Inhalt ist von der Hauptabfrage nicht direkt referenzierbar.

In der Tat zeigt der Plan des Insert Befehles, dass der Leading Hint ignoriert wird.

Hier ein Beispiel von einem unserer Kunden:

Operation | Name | A-Time 
-------------------------------------------------------------------------------
INSERT STATEMENT | |00:05:07.72
 LOAD TABLE CONVENTIONAL | STATS$SQL_PLAN |00:05:07.72
  SORT GROUP BY | |00:05:07.72
   NESTED LOOPS | |00:05:07.71
    NESTED LOOPS | |00:05:07.71
     FIXED TABLE FULL | X$KGLCURSOR_CHILD |00:00:02.30
     VIEW PUSHED PREDICATE | |00:05:05.33
      NESTED LOOPS ANTI | |00:05:05.23
       TABLE ACCESS BY INDEX ROWID BATCHED| STATS$SQL_PLAN_USAGE |00:03:42.53
        INDEX RANGE SCAN | STATS$SQL_PLAN_USAGE_PK|00:02:22.49
       INDEX RANGE SCAN | STATS$SQL_PLAN_PK |00:01:03.19
   FIXED TABLE FIXED INDEX | X$KQLFXPL (ind:3) |00:00:00.01
-------------------------------------------------------------------------------

Wie man sieht, benötigt der Insert 5 Minuten und 8 Sekunden.

Queryblöcke

Um eine Queryblock in der Hauptabfrage referenzieren zu können, muss man dem untergeordneten Queryblock mit dem QB_NAME hint einen Namen geben.

Dann kann man die Tabellen des untergeordneten Queryblocks mittels „tabellenalias@queryblock“ ansprechen.
Dies ist ein dokumentiertes Vorgehen und sollte in allen Versionen stabil funktionieren.

In unserem Beispiel sieht das so aus:

SELECT /*+ leading(spu@np ssp@sq  s sp) */
            new_plan.plan_hash_value,
            sp.id,
... 
            MAX(new_plan.snap_id)
        FROM
            (
                SELECT /*+ QB_NAME(NP)  */  
                    spu.plan_hash_value,
                    spu.hash_value    hash_value,
                    spu.address       address,
                    spu.text_subset   text_subset,
                    spu.snap_id       snap_id
                FROM
                    stats$sql_plan_usage spu
                WHERE
                    spu.snap_id = :b3
                    AND spu.dbid = :b2
                    AND spu.instance_number = :b1
                            AND NOT EXISTS (
                        SELECT /*+ QB_NAME(SQ)  */
                            *
                        FROM
                            stats$sql_plan ssp
                        WHERE
                            ssp.plan_hash_value = spu.plan_hash_value
                    )
            ) new_plan,

Diesmal hält der Optimzer sich an die Hints, das Ergebnis sieht wie folgt aus:

--------------------------------------------------------------------------------
Operation                                | Name                     |   A-Time  
--------------------------------------------------------------------------------
INSERT STATEMENT                         |                          |00:00:00.05
 LOAD TABLE CONVENTIONAL                 | STATS$SQL_PLAN           |00:00:00.05
  SORT GROUP BY                          |                          |00:00:00.05
   NESTED LOOPS                          |                          |00:00:00.05
    NESTED LOOPS                         |                          |00:00:00.04
     HASH JOIN ANTI                      |                          |00:00:00.04
      TABLE ACCESS BY INDEX ROWID BATCHED| STATS$SQL_PLAN_USAGE     |00:00:00.01
       INDEX RANGE SCAN                  | STATS$SQL_PLAN_USAGE_PK  |00:00:00.01
      INDEX FAST FULL SCAN               | STATS$SQL_PLAN_PK        |00:00:00.01
     FIXED TABLE FIXED INDEX             | X$KGLCURSOR_CHILD (ind:1)|00:00:00.01
    FIXED TABLE FIXED INDEX              | X$KQLFXPL (ind:3)        |00:00:00.01
--------------------------------------------------------------------------------

Also 5 hundertstel Sekunden statt 5 Minuten.

Das ist über 6000x schneller und kann sich sehen lassen.

Nachdem ich die Hints gefunden hatte, habe ich eine Sicherheitskopie des Scriptes SCPPKG.SQL angelegt und den neuen Hint in das Package kopiert.
Durch den Aufruf des Scriptes wurde das neue Package dann installiert.

Statement: INSERT INTO stats$seg_stat

Wie sieht nun das zweite Insert aus?

Diesmal gibt es keine MOS Note.

Auch hier zeige ich wieder den sql code, mit den hervorgehobenen Hints:

SELECT /*+  ordered use_nl(s1.gv$segstat.X$KSOLSFTS) */
            :b3,
            :b2,
...
            SUM(decode(s1.statistic_name, 'row lock waits', value, 0))
        FROM
            v$segstat s1
        WHERE
            ( s1.dataobj#,
              s1.obj#,
              s1.ts# ) IN (
                SELECT /*+ unnest */
                    s2.dataobj#,
                    s2.obj#,
                    s2.ts#
                FROM
                    v$segstat s2
                WHERE
                    s2.obj# > 0
                    AND s2.obj# < 4254950912
                    AND ( decode(s2.statistic_name, 'logical reads', s2.value, 0) > :b10
                          OR decode(s2.statistic_name, 'physical reads', s2.value, 0) > :b9
                          OR decode(s2.statistic_name, 'buffer busy waits', s2.value, 0) > :b8
                          OR decode(s2.statistic_name, 'row lock waits', s2.value, 0) > :b7
                          OR decode(s2.statistic_name, 'ITL waits', s2.value, 0) > :b6
                          OR decode(s2.statistic_name, 'gc cr blocks received', s2.value, 0) > :b5
                          OR decode(s2.statistic_name, 'gc current blocks received', s2.value, 0) > :b4 )
            )
        GROUP BY
            s1.ts#,
            s1.obj#,
            s1.dataobj#
;

Wieder sieht man hier ein sehr ungewöhnliches Hint Format.

Wieder meldet aber die Report Funktion in 19c keinen Fehler und in unseren Test hat der Optimizer den Hint befolgt.

Jedoch bin ich diesmal mit der Wirkung des Hints nicht einverstanden.

Sehen wir uns dazu Laufzeitstatitiken an:

------------------------------------------------------------------------------
Id  | Operation                | Name          | Starts | A-Rows |   A-Time   
------------------------------------------------------------------------------
  0 | INSERT STATEMENT         |               |      1 |      0 |00:01:16.83 
  1 |  LOAD TABLE CONVENTIONAL | STATS$SEG_STAT|      1 |      0 |00:01:16.83 
  2 |   HASH GROUP BY          |               |      1 |    746 |00:01:16.83 
  3 |    NESTED LOOPS          |               |      1 |  19396 |00:01:16.81 
  4 |     VIEW                 | VW_NSO_1      |      1 |    747 |00:00:00.19 
  5 |      SORT UNIQUE         |               |      1 |    747 |00:00:00.19 
  6 |       FIXED TABLE FULL   | X$KSOLSFTS    |      1 |    814 |00:00:00.19 
  7 |     FIXED TABLE FULL     | X$KSOLSFTS    |    747 |  19396 |00:01:16.62 
------------------------------------------------------------------------------

Immerhin läuft auch dieser Befehl noch über eine Minute.

Die Zeit fällt fast zu 100% in der Zeile 7 an und zwar deshalb, weil die Zeile 747 Mal wiederholt wird.

Ein Hash join wäre hier wesentlich Laufzeit stabiler.
Der Nested Loop join hat Vorteile bei kleinen Datenmengen.

Allerdings spielt es ohnehin keine Rolle, welchen Join man nimmt, bei kleinen Datenmengen sind alle schnell.

Ich habe also den use_nl hint gegen einen use_hash hint getauscht.

Das ist das Resultat:

-------------------------------------------------------------------------------
Id  | Operation                | Name           | Starts | A-Rows |   A-Time   
-------------------------------------------------------------------------------
  0 | INSERT STATEMENT         |                |      1 |      0 |00:00:00.39 
  1 |  LOAD TABLE CONVENTIONAL | STATS$SEG_STAT |      1 |      0 |00:00:00.39 
  2 |   HASH GROUP BY          |                |      1 |    748 |00:00:00.39 
  3 |    HASH JOIN             |                |      1 |  19448 |00:00:00.38 
  4 |     VIEW                 | VW_NSO_1       |      1 |    748 |00:00:00.19 
  5 |      SORT UNIQUE         |                |      1 |    748 |00:00:00.19 
  6 |       FIXED TABLE FULL   | X$KSOLSFTS     |      1 |    815 |00:00:00.19 
  7 |     FIXED TABLE FULL     | X$KSOLSFTS     |      1 |    390K|00:00:00.13 
-------------------------------------------------------------------------------

Insgesamt läuft der Snapshot jetzt in 6 Sekunden durch.

Long Parse aufgrund von Skew Detection in Hybrid Hash Distribution

Für einen detaillierten Überblick wie Skew Detection/Skew Handling in Hybrid Hash Distributionen funktioniert und welche Ziele es verfolgt, empfehle ich den sehr guten Blogeintrag von Randolf Geist zu lesen: https://oracle-randolf.blogspot.com/2014/05/12c-hybrid-hash-distribution-with-skew.html

Im Blog von Herrn Geist werden unter anderem sieben Vorbedingungen genannt, die nötig sind, damit dieses Feature wirksam wird.

Zumindest in Oracle 12.2 konnte ich auf Basis meiner Untersuchung eine weitere Vorbedingung finden:
Dynamic Sampling muss ebenfalls aktiviert sein (d.h auf einem Level > 0 konfiguriert sein; der Default ist 2).

Die rekursive Query die während der Optimierungsphase (=Hard Parse) ausgeführt wird, um einen Skew zu erkennen sieht nun auch ein wenig anders aus.

Zum Großteil wurden hier weitere Hints hinzugefügt.
Auf Oracle 12.2 hat sie nun folgende Form:

--kkopqSkewInfo: Query:SELECT /* DS_SKEW */ /*+ RESULT_CACHE no_parallel dynamic_sampling(0) no_sql_tune no_monitoring */ * FROM (SELECT SYS_OP_COMBINED_HASH("ID"), COUNT(*) CNT, TO_CHAR("ID") FROM "ASC_SKEW_PART" SAMPLE(99.900000) SEED(1) GROUP BY "ID" ORDER BY CNT DESC, "ID") WHERE ROWNUM <= 2;

Was hier sofort auffällt ist die Größe des gewählten Samples (99.9%).

Nach ein paar Tests mit unterschiedlichen Tabellengrößen, kam ich zum Schluss, dass das Sample so gewählt wird, um ca. 5.500 Rows aus der Tabelle zu selektieren, um auf dieser Datenbasis die Skew Erkennung durchzuführen.

Die Beschränkung auf 5.500 Rows wurde in älteren Releases schon vorgenommen, wenn Histogramme im Zuge der Statistikberechnung erzeugt wurden. Für Tabellen die weniger als 5.500 Rows haben, wird das Sample mit 99,9% gewählt.

Sehen wir uns nun an unter welchen Umständen das zu sehr langen Parse Zeiten führen kann:

--#######################################
--# Small lookup table                  #
--#######################################
drop table asc_pids;
create table asc_pids
as
select 45e6 + rownum id
  from dual
 connect by level <= 13;
 
--#######################################
--# Big, partitioned fact table         #
--#######################################
--5 mio records in each partition from P1 to P9
--only 4 records in partition P10
drop table asc_skew_part;
create table asc_skew_part
nologging
partition by range(id)
(
   partition P1 values less than (5e6),
   partition P2 values less than (10e6),
   partition P3 values less than (15e6),
   partition P4 values less than (20e6),
   partition P5 values less than (25e6),
   partition P6 values less than (30e6),
   partition P7 values less than (35e6),
   partition P8 values less than (40e6),
   partition P9 values less than (45e6),
   partition P10 values less than (50e6)
)
as
with a as
(
    select rownum id
      from dual
     connect by level <= 1e5
)
select /*+ parallel(4) */rownum id, 
       lpad('*', 255, '*') padding
  from a, a
 where rownum <= 45e6
union all
select 45e6 + mod(rownum, 2) + 1 id,
       lpad('*', 255, '*') padding
  from dual
 connect by level <= 4;
 
--#######################################
--# histograms on id on fact table      #
--####################################### 
exec dbms_stats.gather_table_stats(user, 'asc_skew_part', method_opt=>'for all columns size 1 for columns id size 255', granularity=>'ALL');


--#######################################
--# clear RC                            #
--#######################################
exec dbms_result_cache.flush();

Ich habe eine große, partitionierte Faktentabelle erstellt. Darin befinden sich jeweils 5 Millionen Rows in den Partitionen P1 bis P9 und nur 4 Rows in der letzten Partition P10 (2 Rows pro Value, das reicht um die notwendige Vorbedingung für die Skew Erkennung zu erfüllen).

Nachdem ich Histogramme auf der Faktentabelle erzeugt und den Result Cache (aufgrund des Hints in der rekursiven Query) geflushed habe, joine ich diese Tabelle nun zur kleinen Lookup-Tabelle.
Dabei schränke ich in der where Klausel aber so ein, dass nur Datensätze der kleinen Partition P10 selektiert werden.

--#######################################
--# note the long parsing time          #
--#######################################
explain plan for
select /*+ leading(b a) use_hash(a) parallel(8) pq_distribute(a HASH HASH)*/*
  from asc_skew_part a, asc_pids b
 where a.id = b.id
   and a.id between 45000000 and 50000000;

Die rekursive Query die im 10053 Event/Optimizer Trace ersichtlich ist, ist exakt jene die ich schon zu Beginn des Posts gezeigt habe:

--kkopqSkewInfo: Query:SELECT /* DS_SKEW */ /*+ RESULT_CACHE no_parallel dynamic_sampling(0) no_sql_tune no_monitoring */ * FROM (SELECT SYS_OP_COMBINED_HASH("ID"), COUNT(*) CNT, TO_CHAR("ID") FROM "ASC_SKEW_PART" SAMPLE(99.900000) SEED(1) GROUP BY "ID" ORDER BY CNT DESC, "ID") WHERE ROWNUM <= 2;

Hier wurde während des Parsens ein 99,9% Sample über alle Partitionen in einer Tabelle mit 45 Millionen Rows gelesen, eine Hash Funktion auf der Join Spalte angewandt und aggregiert.

Es sieht so aus als wäre die Größe des Samples nur von der kleinen Partition P10 (4 Rows) abgeleitet worden, die ich mit meiner where Klausel treffe.

Angewandt wurde das Sampling dann aber über die gesamte Tabelle, eine where Klausel um auf die entsprechende Partition zu filtern gibt es in der rekursiven Query nicht.

Im konkreten Fall ist das auf einer Produktions-DB mit Milliarden von Rows und hunderten Partitionen aufgetreten, woraufhin das bl0ße Parsen über Stunden gedauert hat.

Update: 

Offensichtlich wird die Anzahl der Zeilen, die der Optimizer nach Anwendung der Filter in der where Klausel schätzt, als Basis für die Größe des Samples herangezogen.

Wenn wir eine Tabelle mit 1 Mrd. Rows hätten, dabei ein Wert besonders oft vorkommen würde (≥30% defaultmäßig) und die anderen eindeutig wären, würde ein Filter auf einen der eindeutigen Werte zu einem Sample von 99,9% in der rekursiven Query führen (da der CBO 1 Row nach dem Filter schätzen würde; 1 ≤ 5,500 deswegen → 99,9%).

Natürlich müssen alle anderen Vorbedingungen (Histogramme, Hash Join, etc.) natürlich weiterhin erfüllt sein.

Um eine Zeile aus einer Tabelle von 1 Mrd. Rows zu lesen, möchte man es wahrscheinlich vermeiden 99,9% aller Rows während des Parsens zu lesen und zu aggregieren 😉

Beispiel:

create table asc_skew
nologging
as
with generator as
(
   select *
     from dual
    connect by level <= 1e4
)
select (case when mod(rownum, 2) = 0 then -1 else rownum end)            
       id, 
       rpad('*', 255, '*') padding
  from generator, generator
 where rownum <= 50e6;

create table asc_pids
as
select rownum id
  from dual
 connect by level <= 13;

exec dbms_stats.gather_table_stats(user, 'asc_skew', method_opt=>'for all columns size 1 for columns id size 255', granularity=>'ALL');

--this will parse very long
explain plan for
select /*+ leading(b a) use_hash(a) parallel(8) pq_distribute(a HASH HASH)  */*
  from asc_skew a, asc_pids b
 where a.id = b.id
   and a.id = 42;

Fazit:

Meiner Meinung nach sollte die Auswahl des Samples angepasst werden auf die Größe des Segments vor der Anwendung der where Klausel.

Weiters sollte auf partitionierte Tabellen besonders Rücksicht genommen werden.

Eventuell wäre es auch sinnvoll bei 99,9% komplett auf die sample Klausel zu verzichten, da diese z.B auch Smart Scans auf Exadata verhindert.

 

Oracle Text Troubleshooting

Oracle Text Index – Concept and troubleshooting its related issues

With Oracle text indexes (or Domain index), we can index text documents and search it based on contents using text patterns with specialized text query operators.

Oracle Text index is different from the traditional B-Tree or Bitmap indexes. They have several components communicates internally.

In a query application, the table must contain the text or pointers to where the text is stored. Text is usually a collection of documents but can also be small text.

Oracle Text index differs from the traditional B-Tree or Bitmap. In an Oracle Text index, the text data is not directly indexed rather, the text data is split into a set of tokens (these splits stored in database internal tables) and tokens are indexed.

Oracle Text Index objects

Oracle Text index has four tables: $I, $K, $N and $R tables.

The $I table contains the data which is being indexed, all the tokens (words) generated from the text document is stored in this table. The tokens in this table are indexed by a B-Tree index with name format DR${index_name}$X.

The $K table maps the internal DOCID values to external ROWID values (fetching a DOCID when we know the ROWID value) .

The $R table maps the ROWID values to DOCID values, (fetching a ROWID when we know the DOCID value). The entries from this table are indexed by a B-Tree index with name format DRC${index_name}$R.

The $N table contains a list of deleted DOCID values, which are cleaned up by the index optimization process.

Oracle Text Health Check

 Oracle Text Status and Version:

  1. A: Status of all CTXSYS objects status :
SELECT * FROM dba_objects
WHERE status !='VALID' AND OWNER = 'CTXSYS' 
ORDER BY object_type,     object_name;

B: The query for health check of the index.
The idx_docid_count is Number of documents indexed. The number of idx_docid_count should be the same or close to the number of rows of base table. The domidx_status  is domain index status.

SELECT c.idx_owner,c.idx_name,c.idx_text_name,c.idx_type,
c.idx_docid_count, i.status,i.domidx_status
FROM ctxsys.ctx_indexes c, dba_indexes i
WHERE c.idx_owner = ‘OWNER’
AND c.idx_name = ‘INDEX_NAME' and c.idx_name=i.index_name
ORDER BY 2,3;

C: Compilation errors of invalid Text-related objects:

SELECT owner, name, type, line, position, text
  FROM dba_errors
 WHERE owner = 'CTXSYS'
      OR (owner = 'SYS' AND (name like 'CTX_%' or name like 'DRI%'))
  ORDER BY owner, name, sequence;

  SELECT * FROM ctxsys.ctx_index_errors
   ORDER BY err_timestamp DESC, err_index_owner, err_index_name;

D: Extract the DDL of the existing index :

SELECT CTX_REPORT.CREATE_INDEX_SCRIPT('SCHEMA.INDEX_NAME') FROM DUAL;
  1. Validating the Index integrity:

A: Validate $K against the base table(there should be no rows selected for a valid INDEX) :

select  *
from dr$INDEX_NAME$k k
where not exists (select 1
from TABLE_NAME t
where k.textkey = t.rowid);

The keys on $K  table should be match with the base table rowids.

B: Validate $R against $K(there should be no rows selected for a valid INDEX) :

select  *
from table(ctx_diag.decode_r('dr$INDEX_NAME$R')) r
where not exists (select 1
from dr$INDEX_NAME$k k
where r.textkey = k.textkey);

C: validate $R (find duplicates) (there should be no rows selected for a valid INDEX) :

column docids for a40

select  textkey, listagg(docid, ', ') within group (order by docid) docids
from table(ctx_diag.decode_r('dr$INDEX_NAME$R'))
group by textkey
having count(*) > 1;

The above queries for validating the TEXT index should have no return values therefore the index would be consistence.

Types of Oracle Text Indexes

CONTEXT

Use this index to build a text retrieval application when your text consists of large coherent documents.

You can index documents of different formats such as MS Word, HTML or plain text.

You can customize the index in a variety of ways.

This index type requires CTX_DDL.SYNC_INDEX after DML on base table.

Note! Transactional CONTEXT Indexes: The new TRANSACTIONAL parameter to CREATE INDEX and ALTER INDEX enables changes to a base table to be immediately queryable.

CTXCAT

Use this index type for better mixed query performance.

Typically, with this index type, you index small documents or text fragments.
Other columns in the base table, such as item names, prices, and descriptions can be included in the index to improve mixed query performance.

This index is larger and takes longer to build than a CONTEXT index.

The size of a CTXCAT index is related to the total amount of text to be indexed, the number of indexes in the index set, and the number of columns indexed.
Consider your queries and your resources before adding indexes to the index set.

This index type is transactional, automatically updating itself after DML to base table.

No CTX_DDL.SYNC_INDEX is necessary.

CTXRULE

Use CTXRULE index to build a document classification or routing application.

This index is created on a table of queries, where the queries define the classification or routing criteria. 

ALTER INDEX Sync Methods

 MANUAL:

No automatic synchronization. This is the default. You must manually synchronize the index with CTX_DDL.SYNC_INDEX.

EVERY:

Automatically synchronize the index at a regular interval specified by the value of interval-string.

ON COMMIT:

Synchronize the index immediately after a commit.   

TRANSACTIONAL:

Specify that documents can be searched immediately after they are inserted or updated.

If a text index is created with TRANSACTIONAL enabled, then, in addition to processing the synchronized rowids already in the index, the CONTAINS operator will process unsynchronized rowids as well.

To turn on TRANSACTIONAL index property:

ALTER INDEX myidx REBUILD PARAMETERS('replace metadata transactional');
                To turn off TRANSACTIONAL index property:
ALTER INDEX myidx REBUILD PARAMETERS('replace metadata nontransactional');

Oracle Text Index preferences

DATASTORE:

  1.  DIRECT_DATASTORE: Indicates that the data is stored internally in text columns of a database table.
  2. MULTI_COLUMN_DATASTORE: Indicates that the data is stored in text table in more than one column. Columns are concatenated (joined) to create a virtual document and each concatenated row is indexed as a single document
  3. DETAIL_DATASTORE: Indicates that the data is stored internally in a text column.
  4. NESTED_DATASTORE: Indicates that the data is stored in nested tables
  5. FILE_DATASTORE: Indicates that the data is stored in Operating System files. This type of data source is supported only for CONTEXT index.
  6. URL_DATASTORE: Indicates that the data is stored over Internet.
  7. USER_DATASTORE: Indicates that the documents would be synthesized at index time by a user defined stored procedure

FILTER:

In this phase the text stream can be converted to format that is recognized by the Oracle text processing engine. 

SECTIONER:

 The task of the sectioner is to divide the incoming text stream into multiple sections based on the internal document structures (HTML or XML).

 LEXER:

This property determines the language associated with the incoming document.

 How to SYNC_INDEX after DML on base table

The following script is useful to synchronize the index with the base table when using context type of text index:

export ORACLE_SID=SID_NAME
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:$ORACLE_HOME/ctx/lib:$ORACLE_HOME/lib32:/usr/lib
sqlplus "/as sysdba" << EOF
exec ctx_ddl.sync_index(idx_name =>'SCHEMA.INXEX’);
exit;
EOF
OAUTH2 Authentifizierung für ORDS REST Services

OAUTH2 Authentifizierung für ORDS REST Services

Oracle bietet über den ORDS (Oracle REST Data Services) die Möglichkeit REST Services in der Datenbank zu erstellen.

Um diese auch entsprechend zu schützen kann man den Zugriff darauf mittels einer OAUTH2 Authentifizierung einschränken. Die Authentifizierung und Autorisierung arbeiten mit Benutzern, Rollen und Berechtigungen (Privileges).

Der Aufbau ist dabei wie folgt:

Authentifizierung und Autorisierung

Abbildung 1: Rollen

Das bedeutet, ein Benutzer kann mehrere Rollen besitzen, welche ihm wiederum jeweils mehrere Berechtigungen zuweisen. Eine Berechtigung wiederum gilt für 1 oder mehrere Module (=Services) und darin eingebettete Ressourcen. Die Einschränkung auf Ressourcen wird allerdings nicht über eine Verknüpfung festgelegt, sondern einen Textfilter, welcher mit Regular Expressions arbeitet.

Die einzelnen Elemente können folgendermaßen angelegt werden:

Rolle

  • Per PL/SQL mit einem Aufruf der Prozedur create_role im Package ords.
  • Über das entsprechende REST Service Interface in APEX

Abbildung 1: Rollen

Berechtigung

  • Per PL/SQL mit einem Aufruf der Prozedur create_privilege im Package ords. Hier wird gibt es zwei Aufrufe, entweder wird nur 1 Rolle übergeben oder ein Array an Rollen.
  • Über das entsprechende REST Service Interface in APEX

Abbildung 2: Berechtigungen

Abbildung 3: Berechtigungen Detail

Berechtigungen Details

Zuweisungen der Berechtigung zum Modul werden mittels der Prozedur set_module_privilege im Package ords erstellt oder wie in Abbildung 3 zu sehen über das REST Service Interface in APEX.

Zuweisungen der Berechtigung zur jeweiligen Ressource werden mittels der Prozedur create_privilege_mapping im Package ords erstellt oder wie in Abbildung 3 zu sehen über das REST Service Interface in APEX.

 

ACHTUNG!!!

Es scheint hier einen Bug zu geben wenn ein Universal-Model verwendet wird – sprich „/*“.

Dieser wird dann nicht modulmäßig eingeschränkt und wirkt sich dann auch auf alle APEX Applikationen aus, welche dann potentiell nicht mehr aufgerufen werden können.

Benutzer

Benutzer können AUSSCHLIESSLICH per PL/SQL mittels der Prozedur create_client im Package oauth erzeugt werden!

Die Rolle muss dann ebenfalls per PL/SQL zugewiesen werden mittels der Funktion grant_client_role im Package oauth.

Die Tatsache, dass die Benutzer nicht über das Interface angelegt werden können, muss unbedingt berücksichtig werden.

Einsatz der angelegten Objekte

Um das Service nun zu nutzen muss eine Authentifizierung durchgeführt werden. Beim Anlegen des OAUTH Clients wird ein Benutzername übergeben. Mit diesem können dann die benötigten Benutzerdaten aus der Tabelle user_ords_clients geholt werden z.B. mit folgendem Statement:

SELECT client_id, client_secret FROM user_ords_clients WHERE name = 'NAME_DES_USERS';

Die client_id und das client_secret werden dann wiederum benötigt um den OAUTH Zugriffstoken zu erhalten. Dazu wird standardmäßig ein eigenes Webservice bereitgestellt, welches mit einer speziellen URL erreicht werden kann. Die URL setzt sich aus dem Basispfad zusammen und wird am Ende mit oauth/token ergänzt – z.B. https://www.myserver.at/ords/apex/oauth/token

Dieser URL werden client_id und client_secret als HTTP-Authentifizierung mitgegeben (User:Passwort), im Payload wird „grant_type=client_credentials“ mitgegeben. Als Antwort erhält man ein JSON mit folgenden Attributen:

  • access_token: der Token selbst
  • token_type: ist immer „bearer“ – muss mit angegeben werden mit dem Token
  • expires_in: Gültigkeitsdauer in Sekunden – standardmäßig 1 Stunde (dieser Wert kann nicht individuell je Service angepasst werden, nur global für alle Services dieser ORDS Instanz)

Beim Aufruf der eigentlichen Services welches durch die OAUTH Authentifizierung geschützt wird, wird dann im Header des Requests folgendes mitgegeben:

„Authorization: Bearer [OAUTH Token]“

OAUTH Token ist der vorhin mittels Webservice generierte Token. Falls der Token nicht mehr gilt (sprich die Gültigkeit abgelaufen ist oder der Token schlicht falsch ist) – gibt das Service einen HTTP 403 Forbidden Fehler zurück.

Datenbank Performance Problem und Lösung

Performance Problem: Eine parallele Abfrage stellt ihre Arbeit ein

Kürzlich traf ich bei einem Kunden auf einen ungewöhnlichen Bug der Oracle Version 12.2.
Es handelt sich um eine parallele Abfrage, die scheinbar einfach anhält und ihre Arbeit einstellt. 🙁

Bei näherer Betrachtung erkennt man eine interne Verklemmung zwischen den Koorditinatorprozess und einem der Parallelprozesse. Das Ganze ähnelt einem Dead Lock!

Problem Analyse

Beide Prozesse warten auf „table queue“ Kommunikation.

Der Query Coordinator wartet mit „PX Deq: Execute Reply“ und der blockiernde Parallel Process wartet mit „PX Deq: Table Q Normal“.

Der Rest der Parallelprozesse warten mit dem event „PX Deq: Execution Msg“.

Damit es zum besagten Problem kommt, muß auch eine analytic_function beteiligt sein.

Im Kern geht es darum, wie Oracle den Window Sort parallelisiert, der mit einer analytic function zwangsweise verbunden ist.

In früheren Oracle Versionen war dieser Sort of weniger effizient als ein regulärer Sort und daher entsprechend langsamer.

In dem sehr gute Post von Phythian’s Christo Kutrovsky wird das Thema im Detail beschrieben: Oracle parallel query hints reference – part 5: PQ_DISTRIBUTE_WINDOW

Problem Lösung

Für unsere Zwecke genügt es zunächst festzuhalten, dass es drei Methoden gibt, wie ein Window Sort parallelisiert werden kann.

Methode 3 ist die bisher verwendete Methode, Methode 1 und 2 sind neu in Version 12. Wenn Methode 2 verwendet wird, kann es zum oben beschriebenen Bug kommen.

Mein Kollege Andreas Schlögl hat einen Testcase erstellt und gezeigt, dass man mittels des neuen  PQ_DISTRIBUTE_WINDOW Hint den Bug umgehen kann, in dem man auf Methode 1 umstellt.

Den Code des Testcases finden Sie hier. Viel Spass beim ausprobieren!

rem ##################################
rem # Objects                        #
rem ##################################

alter session set optimizer_adaptive_plans = false;
alter system flush shared_pool;

drop table asc_dmy1;
drop table asc_dmy3;

create table asc_dmy1
parallel 8
as 
select 'AAA' f001
  from xmltable('1 to 300');
  
--note: this table has no parallel degree
create table asc_dmy3
as
select 'AAA' f001, 1 acc206
  from dual;

rem #############################################
rem # SORT then distribute by HASH (Bug)        #
rem #############################################  
/*
   leads to a HASH JOIN in Line 7, which imo must be a HASH JOIN BUFFERED (due to 2 active PX SENDs at 9 and 13) 
   This SQL hangs and never finishes 
   
   https://oracle-randolf.blogspot.com/2012/12/hash-join-buffered.html
   "At most one data distribution can be active at the same time"
   
   "Since it doesn't seem to be supported to have two PX SEND operations active at the same time, 
    some artificial blocking operation needs to be introduced, in this case the HASH JOIN BUFFERED, 
	that first consumes the second row source completely before starting the actual probe phase"
*/
select /*+ pq_distribute_window(@"SEL$1" 2) */
       max(v.acc206) over (partition by v.f001) max_bew
  from asc_dmy3 v,
       asc_dmy1 e
 where e.f001 = v.f001
   and v.f001 = e.f001;  

/*   
-----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name     | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |    TQ  |IN-OUT| PQ Distrib |
-----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |          |      1 |   419 |     6  (17)| 00:00:01 |        |      |            |
|   1 |  PX COORDINATOR              |          |        |       |            |          |        |      |            |
|   2 |   PX SEND QC (RANDOM)        | :TQ10003 |      1 |   419 |     6  (17)| 00:00:01 |  Q1,03 | P->S | QC (RAND)  |
|   3 |    WINDOW CONSOLIDATOR BUFFER|          |      1 |   419 |     6  (17)| 00:00:01 |  Q1,03 | PCWP |            |
|   4 |     PX RECEIVE               |          |      1 |   419 |     6  (17)| 00:00:01 |  Q1,03 | PCWP |            |
|   5 |      PX SEND HASH            | :TQ10002 |      1 |   419 |     6  (17)| 00:00:01 |  Q1,02 | P->P | HASH       |
|   6 |       WINDOW SORT            |          |      1 |   419 |     6  (17)| 00:00:01 |  Q1,02 | PCWP |            |
|*  7 |        HASH JOIN             |          |      1 |   419 |     5   (0)| 00:00:01 |  Q1,02 | PCWP |            |
|   8 |         PX RECEIVE           |          |      1 |   415 |     3   (0)| 00:00:01 |  Q1,02 | PCWP |            |
|   9 |          PX SEND HASH        | :TQ10000 |      1 |   415 |     3   (0)| 00:00:01 |  Q1,00 | S->P | HASH       |
|  10 |           PX SELECTOR        |          |        |       |            |          |  Q1,00 | SCWC |            |
|  11 |            TABLE ACCESS FULL | ASC_DMY3 |      1 |   415 |     3   (0)| 00:00:01 |  Q1,00 | SCWP |            |
|  12 |         PX RECEIVE           |          |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,02 | PCWP |            |
|  13 |          PX SEND HASH        | :TQ10001 |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | P->P | HASH       |
|  14 |           PX BLOCK ITERATOR  |          |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | PCWC |            |
|  15 |            TABLE ACCESS FULL | ASC_DMY1 |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | PCWP |            |
-----------------------------------------------------------------------------------------------------------------------   
*/

rem #############################################
rem # distribute by HASH then SORT  (Success)   #
rem #############################################  
/*
   leads to a HASH JOIN *BUFFERED* in Line 6, which is inevitably necessary imo
   This SQL finishes immediately
*/ 
select /*+ pq_distribute_window(@"SEL$1" 1) */
       max(v.acc206) over (partition by v.f001) max_bew
  from asc_dmy3 v,
       asc_dmy1 e
 where e.f001 = v.f001
   and v.f001 = e.f001;    

/*
------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name     | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |    TQ  |IN-OUT| PQ Distrib |  OMem |  1Mem |  O/1/M   |
------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |          |        |       |     6 (100)|          |        |      |            |       |       |          |
|   1 |  PX COORDINATOR            |          |        |       |            |          |        |      |            | 73728 | 73728 |          |
|   2 |   PX SEND QC (RANDOM)      | :TQ10003 |      1 |   419 |     6  (17)| 00:00:01 |  Q1,03 | P->S | QC (RAND)  |       |       |          |
|   3 |    WINDOW SORT             |          |      1 |   419 |     6  (17)| 00:00:01 |  Q1,03 | PCWP |            | 20480 | 20480 |     8/0/0|
|   4 |     PX RECEIVE             |          |      1 |   419 |     5   (0)| 00:00:01 |  Q1,03 | PCWP |            |       |       |          |
|   5 |      PX SEND HASH          | :TQ10002 |      1 |   419 |     5   (0)| 00:00:01 |  Q1,02 | P->P | HASH       |       |       |          |
|*  6 |       HASH JOIN BUFFERED   |          |      1 |   419 |     5   (0)| 00:00:01 |  Q1,02 | PCWP |            |  3400K|  3091K|     8/0/0| 
|   7 |        PX RECEIVE          |          |      1 |   415 |     3   (0)| 00:00:01 |  Q1,02 | PCWP |            |       |       |          |
|   8 |         PX SEND HASH       | :TQ10000 |      1 |   415 |     3   (0)| 00:00:01 |  Q1,00 | S->P | HASH       |       |       |          |
|   9 |          PX SELECTOR       |          |        |       |            |          |  Q1,00 | SCWC |            |       |       |          |
|  10 |           TABLE ACCESS FULL| ASC_DMY3 |      1 |   415 |     3   (0)| 00:00:01 |  Q1,00 | SCWP |            |       |       |          |
|  11 |        PX RECEIVE          |          |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,02 | PCWP |            |       |       |          |
|  12 |         PX SEND HASH       | :TQ10001 |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | P->P | HASH       |       |       |          |
|  13 |          PX BLOCK ITERATOR |          |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | PCWC |            |       |       |          |
|* 14 |           TABLE ACCESS FULL| ASC_DMY1 |    300 |  1200 |     2   (0)| 00:00:01 |  Q1,01 | PCWP |            |       |       |          |
------------------------------------------------------------------------------------------------------------------------------------------------
*/

DBConcepts Business Breakfast 2019 – Jetzt anmelden

Es ist uns wieder gelungen, sehr interessante und kurzweilige Vorträge mit folgenden Titeln für unser Business Breakfast zu bündeln:

  • 24 – 7 – 365 Verfügbarkeit für Unternehmen mit Veeam
  • Das Sherlock Holmes SQL Mystery
  • Wann lohnt sich der Einsatz von Postgres/EnterpriseDB?
  • Datenbank Evangelist challenges Autonomous Database
  • So messen Sie die Java Nutzung in Ihrem Unternehmen

Termine & Locations:
02. April, Dornbirn
03. April, Innsbruck
04. April, Salzburg
09. April, Linz
10. April, Graz
11. April, Wien

Bitte melden Sie sich so bald wie möglich an, da nur eine sehr begrenzte Anzahl an Plätzen zur Verfügung steht und bereits nach der „Save the Date“ Aussendung viele Anmeldungen erfolgt sind.

Alle weiteren Details sowie die Excerpts zu den Vorträgen und die Anmeldung finden Sie auf der Webseite des Events.

 

Oracle Apex 19.1 Early Adopter

Oracle APEX 19.1 – Early Adopter

Eine gute Nachricht für alle APEX-Fans, die ihre Software aktuell halten wollen und/oder auf neue Features warten:
Seit kurzem gibt es die Early Adopter von APEX 19.1. Unter 19.1 Early Adopter – Features gibt es erste Informationen zu den kommenden Inhalten, wie etwa:

  • REST Enabled Forms (Anzeigen und Bearbeiten von Daten aus Remote-Quellen),
  • Upgrades von jQuery und JET Charts,
  • Deklarative Erweiterungen des Interactive Grid oder auch
  • ein dunkles Layout für die APEX-Entwicklungsumgebung.

Aktuell gibt es noch keinen Download, sodass man sich die Änderungen momentan in einem Workspace bei Oracle anschauen muss. Und auch die verfügbaren Informationen sind noch etwas spärlich – werden aber üblicherweise sukzessive erweitert.

Select * from dual in postgresql

Select * from DUAL in Postgres

For the English Version of this post  >> click here

Wann immer ein simpler Wert per SQL gesetzt oder abgefragt werden soll, wird die Pseudotabelle DUAL dafür herangezogen.

Einer der populärsten Einsatzzwecke dafür war bis Oracle Version 10 die Nutzung von Sequences in PL/SQL.
Sequences mussten in diesen Oracle Versionen immer über eine SQL-Query abgefragt werden, der folgende Code funktionierte:

select my_seq.nextval from dual;

Der folgende Code funktionierte allerdings nicht:

v_myvar := my_seq.nextval;

Die DUAL Tabelle ist eine tatsächlich existierende physische Tabelle in der Datenbank welche im SYS-Schema liegt.

Sie verfügt über eine Spalte mit der Bezeichnung „DUMMY“ welche den Datentyp VARCHAR2(1) hat.
Die Zeile hat eine Zeile mit dem Wert ‚X‘, das kann mit folgendem Statment abgefragt werden:

select * from dual;

Theoretisch kann diese Tabelle auch verändert werden.
Natürlich rät Oracle streng davon ab das zu tun da eine Fülle an unvorhersehbaren Fehlern entstehen kann und wird.

Das grundlegende Verhalten könnte auch mit einer eigenen 1-Spalten-1-Zeilen-Tabelle simuliert werden, doch der Oracle Optimizer erkennt DUAL auch als Keyword und verfährt beim Execution Plan dann entsprechend.

Wie läuft das nun in Postgres?

Wenn Sie Code von Oracle auf Postgres portieren dann werden sie feststellen, dass das Statement von oben …

select 1 from dual;

…nicht funktioniert!
Postgres kennt keine DUAL Tabelle.

Nun gibt es zwei Möglichkeiten, je nachdem was die Ausgangslage und das Ziel ist:

  1. Man legt die DUAL Tabelle in Postgres als „Dummy“-View an
  2. Man passt das Statement an

Die erste Variante ist simpel und vor allem bei größeren Portierungen empfohlen, wo mehrere dutzend oder sogar hunderte Statements angepasst werden müssten.

Die zweite Variante ist bei wenigen Statements zu empfehlen, da sie dem Postgres Standard entspricht. Das entsprechende Statement in Postgres würde dann wie folgt aussehen:

select 1

Die komplette FROM-Klausel verschwindet hier!

Postgres macht durch diese Syntax die Verwendung einer Dummy-Tabelle unnötig um einen einzelnen Wert per SQL zu setzen oder abzufragen (z.B. auch das SYSDATE).

Das ist deutlich simpler.

English Version of this article

For the English Version of this article  >> click here

Managed Services für PostgeSQL

Kennen Sie schon unsere Managed Services für PostgreSQL Datenbanken? – hier klicken>

 

 

Migrieren ohne Downtime - ein Oracle 12.2 Feature

Migration OHNE Downtime – ein cooles Oracle 12.2 New Feature

Mit der Release 2 der Oracle Datenbank 12c bekommt der DBA eine Vielfalt neuer Funktionalitäten, die dem Datenbankadministrator die Arbeit in Zukunft um einiges erleichtern werden. Eines dieser neuen Features ist das Hot Cloning und Relocate von Pluggable Databases, das in diesem Artikel kurz näher vorgestellt wird.

Die Oracle Datenbank 12c Release 2 bringt mit der Multitenant-Architektur viele tolle Features, die man vorher nicht benutzen konnte. Zuallererst sollten wir aber die Aussage des Artikeltitels ein wenig einschränken und erläutern, welcher Themenkreis in diesem Artikel beleuchtet wird.
Eine Migration ohne Downtime ist mehr ein Wunsch als Wirklichkeit, aber mit der Version 12.2 kommen wir dem Ziel näher. Im gegenständlichen Artikel werden wir über die Möglichkeit Pluggable Databases online zu kopieren/migrieren beschreiben.

Ein Blick in die Vergangenheit

In der Version 11.2 hatte der Datenbankadministrator folgende Möglichkeiten, um eine Datenbank von einem System auf ein anderes System zu migrieren:

  • Duplicate
  • Datapump
  • Restore/Recover via RMAN
  • GoldenGate

Welchen Weg man letztendlich wählen wird, hängt ganz von der Situation und den Voraussetzungen ab. Es geht hier nicht um Cross-Platform-Migrationen oder um Migrationen zwischen Systemen mit unterschiedlicher Endianness. Jede Option verlangt gewisse Vorbereitungen und birgt auch mögliche, verborgene Gefahren mit sich, die dann erst im Zuge der Migration sichtbar werden.

Variante „Duplicate“

Bei einem Duplicate hat der Benutzer die Möglichkeit entweder direkt über SQL*Net, oder aus einem vorhandenen Backup, eine Kopie der Datenbank zu erstellen. Die Duplicate Methode ist eigentlich die einfachste, weil man hier entweder nur Firewall Freischaltungen zwischen den Servern benötigt oder man alternativ auf ein Backup, meistens von einem NFS Share oder einer Bandsicherung, zugreift. Wenn zwischen den Servern ein leistungsstarkes Netzwerk zur Verfügung steht, dann führt man am besten den Befehl DUPLICATE TARGET DATABASE TO … FROM ACTIVE DATABASE aus oder benutzt Backups die entweder in einer BACKUP LOCATION gespeichert oder aus Bandsicherungen gezogen werden.  Oracle automatisiert dabei einige Tätigkeiten die man bei Restores sonst manuell machen muss. Grundsätzlich ist es ein automatischer Restore/Recover der Quelldatenbank mit Veränderungen der Datafile Pfade und der DBID.

Variate „Datapump“

Die Variante Datapump ermöglicht dem Datenbankadministrator logische Datenbankbackups zu erstellen, auf ein neues Ziel zu übertragen und dort wieder zu importieren. Datapump bietet auch Features wie Transportable Tablespace oder Full Transportable Export. Dabei werden nur Metadaten exportiert und diese dann mit allen Datafiles kopiert und am Ziel importiert.

Variante „klassische Migration“

Bei der klassischen Migration einer Datenbank mittels RMAN Restore/Recover wird eine Datenbank auf Basis eines Backups neu aufgebaut.

Variante „GoldenGate“

Als letzte Option bleibt GoldenGate. Bei GoldenGate handelt es sich um eine kostenpflichtige Replikationssoftware, die es erlaubt nahezu Zero Downtime Migrationen durchzuführen. GoldenGate unterstützt zudem auch noch andere Datenbankanbieter wie MySQL, MS SQL und Hadoop.

Multitenant Architektur

Mit der Version 12.1 hat Oracle eine neue Datenbankarchitektur eingeführt – die Multitenant Architektur (CDB Architektur). Die Multitenant Architektur wird in Zukunft die derzeitige Non-CDB Architektur ersetzen. In der 12.1 Dokumentation findet man bereits den entsprechenden Hinweis auf die „Deprecation of Non-CDB Architecture“ für kommende Oracle Releases nach 12.2. Oracle empfiehlt hier auf die CDB Architektur zu schwenken. Multitenant ist prinzipiell eine kostenpflichtige Option zur Enterprise Edition, jedoch als Single tenant in allen Oracle Editionen enthalten.

Container Datenbank

Während es vor 12.1 nur eine 1:1-Beziehung zwischen Instanz und Datenbank gab (beim RAC ist es eine n:1-Beziehung), verändert sich das jetzt mit der Multitenant Architektur. In der Multitenant Architektur werden alle gemeinsamen Systemobjekte in eine neue, zentrale, Datenbank – die Container Datenbank (CDB) – gepackt, die alle notwendigen Funktionalitäten bereits vorinstalliert hat. Die eigentlichen Benutzerdaten befinden sich in sogenannten Pluggable Databases (PDB), die in die Containerdatenbank eingehängt werden.

In der Version 12.1 konnte man Datenbanken, die bereits als Pluggable Database definiert waren wesentlich einfacher kopieren, als noch in älteren Versionen ohne Multitenant Architektur. Man musste die Quelldatenbank lediglich in den Read Only Modus setzen und anschließend ein CREATE PLUGGABLE DATABASE Statement ausführen. Im einfachsten Fall bekommt man mit dem Kommando CREATE PLUGGABLE DATABASE ORCL2 FROM ORCL1 bereits eine lokale Kopie der Datenbank.

Mit der Multitenant Architektur bringt Oracle dem Benutzer eine Vielfalt von Features die das Provisionieren von Datenbanken leichter machen. Man bekommt eine Konsolidierungsplattform für Datenbanken in der man mit dem Ressource Manager Systemressourcen optimal zuteilen kann um wichtigeren Datenbanken mehr Performance zuzusichern. Weiters eine Automationsplattform in der man Datenbank Templates vordefinieren kann um im Anlassfall schnell PDBs zu erstellen oder klonen zu können. Das Kopieren von PDBs geht in 12.1 nicht ganz ohne Downtime ab. Im Internet findet man Artikel die ohne den Read Only Modus eine Kopie der PDBs ermöglichen, in 12.1 ist es jedoch nicht möglich PDBs online zu kopieren. Die Quelldatenbank geht hier in den Quiesce Modus, was mit einer Downtime vergleichbar ist.

Die Gegenwart – Hot Cloning und Relocate

Mit der Version 12.2 wird der Traum des DBAs jedoch endlich wahr: Die von vielen erwartete Lösung für das online kopieren von PDBs. Dieses Feature kommt in Begleitung mit einem anderen neuen Feature, den Local Undo Tablespaces. Um Datenbanken wirklich online kopieren zu können, braucht man die Container Datenbank im Local Undo Mode (und natürlich im Archivelog Mode).
Das Aktivieren des Local Undo Modes in der CDB erstellt automatisch in jeder PDB einen eigenen Undo Tablespace, welcher wiederum Voraussetzung für Flashback, PDBPITR und Hot Cloning in der PDB ist.

Um den Leser nicht weiter auf die Folter zu spannen, schauen wir uns das RELOCATE-Feature in Aktion an. Hier wird die Datenbank automatisch auf eine andere Instanz transferiert und alle DMLs und DDLs bis zum Öffnen der Pluggable Datenbank werden automatisch im Hintergrund verarbeitet, um die Datenbank konsistent zu halten.

In unserem Szenario haben wir 2 virtuelle Maschinen, ora01 und ora02 mit  Version 12.2.0.1 Enterprise Edition installiert, die ora01 verwaltet die cdb1 und ora02 die cdb2.

Beide Datenbanken sind im gleichen Netz, als Storage benutzen wir ASM, das erleichtert auch die Namenskonvention für die Datafiles. Diese Funktionalität (mit und ohne RELOCATE) funktioniert auch in der 12.2 SE2.

Um keine Lizenzverletzung zu begehen, denken Sie daran, dass in einer CDB nur eine Benutzer PDB existieren darf. Mehrere Benutzer PDBs verlangen die Multitenant Option und damit Enterprise Edition.

 

sys@cdb1 SQL> show pdbs

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO
         3 ORCL                           MOUNTED

sys@cdb1 SQL> alter pluggable database orcl open ;
Pluggable database altered.

sys@cdb2 SQL> show pdbs

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO

Um die Datenbank auf die neue Maschine zu transferieren, sind ein paar Vorbereitungen nötig. Zuerst ist ein Common User anzulegen, der die notwendigen Privilegien bekommt. Ein in der Dokumentation offensichtlich entdeckter Fehler wird am Ende des Beispiels beschrieben.

sys@cdb1 SQL> create user c##dba identified by oracle container=all ;
User created.

sys@cdb1 SQL> grant create session, create pluggable database to c##dba container=all ;
Grant succeeded.

sys@cdb1 SQL> grant sysoper to c##dba container=all ;
Grant succeeded.

Um die PDB zu erreichen, brauchen wir einen Connect-Descriptor, der die Datenbank identifiziert. In unserem Beispiel werden wir den Alias CDB1 in der tnsnames.ora der CDB2 anlegen.

CDB1 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = ora01)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = cdb1)
    )
  )

Als vorletzte Voraussetzung brauchen wir einen Public Database Link von der CDB2 in die CDB1.

 

sys@cdb2 SQL> create public database link cdb1 
connect to c##dba identified by oracle using 'cdb1' ;
Database link created.

Nachdem wir alle Voraussetzungen erfüllt haben, können wir die Datenbank auf die CDB2 migrieren. Diese Tätigkeit erledigt man mit einem Kommando auf der CDB2:

sys@cdb2 SQL> create pluggable database orcl from orcl@cdb1 relocate ;
Pluggable database created.

Nach dem Erstellen der PDB bleibt die neue PDB in der CDB2 im Zustand MOUNTED. Ohne den Zusatz RELOCATE würden wir einen Hot Clone der PDB „orcl“ erstellen. Um die Near Zero Downtime Migration zu zeigen, erstellen wir in der PDB „orcl“ auf CDB1, die immer noch im Read Write Modus ist, einen neuen Benutzer und eine Tabelle.

sys@cdb1 SQL> alter session set container=orcl ;
Session altered.

sys@cdb1 SQL> create tablespace data ;
Tablespace created.

sys@cdb1 SQL> create user psorger identified by oracle default tablespace data quota unlimited on data;
User created.

sys@cdb1 SQL> grant connect, resource to psorger ;
Grant succeeded.

sys@cdb1 SQL> create table PSORGER.EMP as select level id from dual connect by level<=10 ;
Table created.

Nachdem wir die Tabelle erstellt haben, können wir die neue PDB in der CDB2 öffnen.
Mit dem folgenden Kommando wird die PDB auf CDB1 gelöscht und die PDB auf CDB2 im Read Write Modus geöffnet.

sys@cdb2 SQL> alter pluggable database orcl open ;
 Pluggable database altered.

sys@cdb2 SQL> alter session set container=orcl ;
 Session altered.

sys@cdb2 SQL> select count(*) from psorger.emp ;

COUNT(*)
 ----------
 10

Nach dem Öffnen der neuen PDB sehen wir, dass alle DML/DDL Statements erfolgreich übertragen wurden.

Um den Fehler in der Dokumentation zu beschreiben werden wir dem C##DBA die Rolle SYSOPER entziehen und SYSDBA vergeben. Mit dieser Rolle vergibt man eigentlich Superprivilegien und logischerweise erwarten wir, dass das funktioniert.

sys@cdb1 SQL> revoke sysoper from C##DBA container=all ;
Revoke succeeded.

sys@cdb1 SQL> grant sysdba to C##DBA container=all ;
Grant succeeded.

sys@cdb2 SQL> select sysdate from dual@cdb1 ;

SYSDATE
---------
09-MAY-17

sys@cdb2 SQL> create pluggable database orcl from orcl@cdb1 ;
Pluggable database created.

sys@cdb2 SQL> create pluggable database orcl1 from orcl@cdb1 relocate ;
create pluggable database orcl1 from orcl@cdb1 relocate
*
ERROR at line 1:
ORA-17628: Oracle error 1031 returned by remote Oracle server
ORA-01031: insufficient privileges

Wie wir hier sehen ist ein Online Clone der Datenbank möglich, aber beim Relocate bekommt der C##DBA mit SYSDBA Privilegien dennoch einen insufficient Privileges Fehler.
Es scheint, dass man dieses Verhalten als einen Bug entweder in der Dokumentation oder in der Software bezeichnen kann.
In der Zukunft werden wir es bestimmt erfahren. 🙂

Um diesen Artikel abzurunden, möchte ich noch die neuen Features der Oracle Public Cloud erwähnen.

Ein neues Feature, der direkte SQL*Net Zugang in die Cloud, erleichtert ab 12.2 die Migration von On-Premise Datenbanken in die Cloud und zurück. Mit diesen zwei neuen Features bekommt der Benutzer die Möglichkeit sehr einfach in die Cloud zu wechseln, dort etwa Performance-Tests durchzuführen und dann die Datenbank wieder zurück ins eigene Rechenzentrum zu verschieben.

Gerade das ist ein Feature, das bei anderen Cloud-Anbietern aktuell kaum zu finden ist!