Oracle Database 10c SIG

Underestimated Powertools – Yes we can!

They have a nimbus of substitute solutions: Partition View , DBMS_PARALLE_EXECUTE , and Snapper. They are used, for example, in the Standard Edition, or when the Oracle options are not available. The exact application of the tools is sometimes poorly documented. The nimbus does not do these tools justice. Each of these can do things that the official option cannot. In this lecture you will not only learn how to really use these tools in practice, but you will also learn about their hidden strengths.

Check out the video here:

and the slides here

TOO_MANY_ROWS Exception

TOO_MANY_ROWS Exception

Wird in einem PL/SQL Block das Ergebnis einer Select Abfrage mittels INTO in eine Variable geschrieben, so muss das Statement EXAKT 1 Zeile zurück liefern. Wird keine Zeile zurückgegeben, so wird eine NO_DATA_FOUND Exception ausgelöst. Ist die Where-Klausel nicht exakt genug und es werden mehrere Zeilen zurück geliefert, so wird eine TOO_MANY_ROWS Exception ausgelöst. Der Unterschied zwischen diesen beiden Fehlern ist jedoch ein potentiell sehr relevanter:

Tritt der Fall einer NO_DATA_FOUND Exception auf, so bleibt der Wert der Variable unverändert – sprich wenn die Variable NULL ist, denn bleibt der Wert NULL, ist der Wert beispielsweise 2, so bleibt er 2 (siehe auch die Ergebnisse des Beispielskriptes).

Tritt der Fall einer TOO_MANY_ROWS Exception auf, so ändert sich der Wert der Variable – und zwar auf eventuell unvorhersehbare Weise. Der Wert der Variable wird auf den ersten gefundenen Wert der Abfrage gesetzt. Mit Order By kann das kontrolliert werden, aber ohne ist es nicht vorhersehbar, welcher Wert eingetragen wird.

Das wirklich wichtige an diesem Verhalten ist, dass bei einem größeren Block mit nachfolgenden Schritten berücksichtigt werden muss, dass bei einem TOO_MANY_ROWS Fehler die Variable einen Wert hat trotz der Exception.

Das folgende Skript verdeutlich diese Möglichkeiten:

declare
  v_value number := 2;
begin
  -- create no data found exception - v_value is NULL
  dbms_output.put_line('v_value am Ende = ' || v_value);
  begin
    with numbers as
     (select level eindeutig from dual connect by level <= 10),
    base as
     (select eindeutig, mod(eindeutig, 3) mehrdeutig from numbers)
    select eindeutig into v_value from base where eindeutig = 11;
  exception
    when no_data_found then
      dbms_output.put_line('v_value bei no_data_found = ' || v_value);
  end;
  -- create too many rows exception - v_value is NOT NULL
  begin
    with numbers as
     (select level eindeutig from dual connect by level <= 10),
    base as
     (select eindeutig, mod(eindeutig, 3) mehrdeutig from numbers)
    select eindeutig into v_value from base where mehrdeutig = 1;
  exception
    when too_many_rows then
      dbms_output.put_line('v_value bei too_many_rows ohne order = ' || v_value);
  end;
  -- create too many rows exception - v_value is NOT NULL
  begin
    with numbers as
     (select level eindeutig from dual connect by level <= 10),
    base as
     (select eindeutig, mod(eindeutig, 3) mehrdeutig from numbers)
    select eindeutig into v_value from base where mehrdeutig = 1 order by 1 desc;
  exception
    when too_many_rows then
      dbms_output.put_line('v_value bei too_many_rows mit order = ' || v_value);
  end;
end;

 

DBConcepts Sommerfest – die beste Veranstaltung des Jahres

Das DBC Sommerfest hat sich mittlerweile zu einem fixen Bestandteil des Sommers von vielen unseren Gästen etabliert. Auch dieses Jahr gibt es viel zu berichten.

Vergangenen Donnerstag (25.08.2022) haben sich im Laufe des Spätsommernachmittags zahlreiche Gäste zu unserem alljährlichen DBConcepts Sommerfest eingefunden und nutzten die Gelegenheit sich in einer malerischen Kulisse an der Alten Donau bei bestem Sommerwetter in einem entspannten Rahmen zu unterhalten.

Nach einer Stärkung am abwechslungsreichen Buffet, das keine Wünsche offenließ, starte das mit großer Spannung erwartete DBConcepts Pub Quiz.

Achtzehn Teams fanden sich innerhalb kürzester Zeit zusammen und bildeten 4er Teams mit den kreativsten Namen und stellten ihr Wissen unter erschwerten Bedingungen (Handynutzung war Tabu sowie ein Zeit Limit für die Beantwortung der Fragen) unter Beweis. Nach einundzwanzig Fragen aus unterschiedlichsten Kategorien und einer Schätzfrage entschied das „Team die 4 Fragezeichen“ das Rennen für sich, setzte sich knapp gegen das Team „Margarethen“ durch und nahm den Hauptgewinn, Sonos Roam Lautsprecher mit nach Hause.

Ich freue mich jedes Jahr auf dieses stimmige Fest – und auch heuer wieder tat es richtig gut, dabei zu sein: inspirierende Örtlichkeit, herzlicher Empfang, ungezwungenes Beisammensein, anregende Gespräche und bestelltes Schönwetter, was will man mehr“, so Roland F. ein Gast.

Auch Melanie R. erklärte:“ Ich war heuer bei vielen Sommerfesten – aber das DBC Sommerfest war eindeutig das beste!

Wir freuen uns sehr über das Feedback und über das nette Beisammensein mit allen Gästen! Eines ist sicher – Nächstes Jahr ist die DBC Sommer Party wieder ein fixer Bestandteil des Sommers – also gleich den letzten Donnerstag im August in die Kalender notieren und reservieren!“ so Klaus-Michael Hatzinger abschließend.

Fotos vom Abend finden Sie hier.

Sommerparty 2022

It’s Summer time und der Countdown läuft – am Donnerstag (25.08) findet unser legendäres DBConcepts Sommerfest in der La Crêperie, An der Oberen Alten Donau 6, 1210 Wien, statt.

Wir möchten Sie gerne über die Anreise,- und Parkmöglichkeiten informieren.

Anreise mit den öffentlichen Verkehrsmitteln (Routenplaner).

  • U1 bis zur Station VIC. Anschließend mit Stadtbus 20A Richtung Neue Donau acht Halte bis Sandrockgasse. Der Bus fährt 2-mal die Stunde. Genau Fahrtzeiten können Sie dem Fahrplan Danach ist ein Fußweg von ca. 7 Minuten zu bewältigen.
  • Mit der U6 bis Station Wien Neue Donau (Fahrplan U6) – anschließend ein Fußweg von ca. acht Minuten.
  • S-Bahn-Station Wien Floridsdorf – danach noch ein Fußweg von 7 Minuten

Parkmöglichkeiten für die Anreise mit dem Auto

La Crêperie, An der Oberen Alten Donau 6, 1210 Wien

Ferdinand-Kaufmann-Platz 16 (parkopedia.at). Bitte beachten Sie, dass die Kurzparkzone bis 22 Uhr gilt.

 

Datum: 25. August 2022
Ort: La Crêperie, An der Oberen Alten Donau 6, 1210 Wien
Uhrzeit: ab 16:00 Uhr bis 23:00h

Wir freuen uns auf Sie!!

P.S.: Es erwartet Sie eine Tombola mit einem sensationellen Hauptgewinn. Sowie unser legendärer Pub-Quiz mit großartigen Preisen für die ersten 3 platzierten Teams! Mitmachen ist selbstverständlich kein „Muss“ aber der Spaß ist garantiert!

 

 

Anmeldung zur DBConcepts Sommer-Party 2022

DBConcepts Sommer Party Feiern Sie mit uns den Sommer ganz relaxed an der Alten Donau bei einem fantastischen Grill-Buffet und kühlen Getränken!

Oracle SQL Model Clause Vertiefung

Grundlegender Aufbau

Der grundlegende Aufbau der Model Clause ist bereits in einem anderen Blog-Eintrag beschrieben, den sie HIER finden.

Weitere Steuerungsmöglichkeiten

Im ersten Blog-Artikel wurde nur das main_model beachtet und auch von diesem nur die verpflichtenden Teile. Ergänzen wir das ganze nun um die cell_reference_options.

Die Cell Reference Options definieren wie mit fehlenden und leeren Werten umgegangen wird und wie streng die Eindeutigkeit geprüft wird. Im Detail sieht das wie folgt aus:

IGNORE NAV
Wenn diese Anweisung inkludiert wird, dann werden folgende Default Werte bei NULL-Werten oder fehlenden Werten zurückgegeben

  • 0 für numerische Spalten
  • 01.2000 für Datumsspalten
  • Ein leerer String für Textspalten
  • NULL für alle anderen Datentypen

KEEP NAV (Default)
Wenn diese Anweisung inkludiert wird, wird immer NULL zurückgegeben, wenn ein Wert fehlt oder NULL ist

UNIQUE DIMENSION (Default)
Wenn diese Anweisung inkludiert ist, muss die Kombination der Spalten der PARTITION BY und der DIMENSION Spalten eine Zeile eindeutig identifizieren (die Spalten müssten also einen Unique Key definieren können)

UNIQUE SINGLE REFERENCEMit dieser Anweisung werden lediglich Referenzen auf eine einzelne Zelle auf der rechten Seite auf Uniqueness geprüft, nicht das gesamte Set

NAV Anweisung

Wenn Werte nicht gefunden werden, definiert diese Anweisung wie damit umgegangen wird. Als Beispiel auf Basis der vorhandenen Testdaten sollen die folgenden beiden Statements zeigen wie die Auswirkungen sind:

select schueler, note, schulstufe
  from schulnoten
 where schuelernummer = 1
   and fach = 'Deutsch' MODEL DIMENSION BY(schueler, schulstufe)
 MEASURES(note) keep nav(note [ 'Anton Anger',
           5 ] = note [ schueler = 'Anton Anger',
           schulstufe = 4 ]);

In diesem Fall mit KEEP NAV werden nicht vorhandene Werte mit NULL ausgegeben. Die Zeile mit Schulstufe 5 hat also in der Notenspalte einen NULL Wert stehen.

select schueler, note, schulstufe
  from schulnoten
 where schuelernummer = 1
   and fach = 'Deutsch' MODEL DIMENSION BY(schueler, schulstufe)
 MEASURES(note) ignore nav(note [ 'Anton Anger',
           5 ] = note [ schueler = 'Anton Anger',
           schulstufe = 4 ]);

Wenn nun IGNORE NAV verwendet wird, dann wird der Wert 0 eingetragen.

UNIQUE Anweisung

Die Unique Anweisung ist sehr simpel umzusetzen. Auf Basis der schon im ersten Blogeintrag verwendeten Testdaten würde folgendes Statement fehlschlagen:

  from schulnoten
 where schuelernummer = 1
   and fach = 'Deutsch' MODEL DIMENSION BY(schueler) MEASURES(note)
 unique dimension (note [ schueler = 'Anton Anger'
                 ] = round(avg(note) [ schueler = 'Anton Anger' ], 0));

Der Grund dafür ist simpel: Die Spalte SCHUELER definiert keine eindeutige Zuweisung. Um das Statement valid zu machen, müsste man das Jahr oder die Schulstufe ergänzen, das würde dann wie folgt aussehen:

select schueler, note
  from schulnoten
 where schuelernummer = 1
   and fach = 'Deutsch' MODEL DIMENSION BY(schueler, schulstufe)
 MEASURES(note) unique
 dimension(note [ schueler = 'Anton Angera',
                 schulstufe = 4
                 ] = round(avg(note) [ schueler = 'Anton Angera',
                           schulstufe between 1 and 3 ],

Oder alternativ die UNIQUE Anweisung ändern, was dann so aussehen würde:

select schueler, note
  from schulnoten
 where schuelernummer = 1
   and fach = 'Deutsch' MODEL DIMENSION BY(schueler) MEASURES(note)
 unique single
 reference(note [ schueler = 'Anton Anger'
                 ] = round(avg(note) [ schueler = 'Anton Angera' ], 0));

 

APEX: Calendar Custom Classes

APEX: Calendar Custom Classes

Der native Kalender in APEX bietet bereits eine Vielzahl an möglichen Klassen um Termine passend anzuzeigen. Die Inline-Hilfe in APEX bietet folgende Liste von 14 Klassen an

 

Wie der letzte Satz andeutet, ist auch das Hinzufügen von eigenen zusätzlichen Klassen möglich.
Da die Dokumentation hier keine klaren Hinweise bietet, soll hier kurz aufgelistet werden, wie diese Klassen anzulegen sind.

Die beste Orientierung bietet hier eine der Klassen, welche von APEX bereitgestellt werden:
.fc .fc-event.apex-cal-black {
   background-color:#303030;
   border-color:#303030;
   color:#fff
}

Der Klassenname, welche in APEX in einer Spalte übergeben wird muss also in auf folgende Weise in einem CSS-File oder Inline auf der Seite eingebaut werden:

.fc .fc-event.[CUSTOM-CLASS_NAME] {
   CSS-Eigenschaften
}

Bei den CSS-Eigenschaften kann man sich ebenfalls an den Default-Klassen orientieren, die drei relevantesten Eigenschaften sind offensichtlicherweise Background, (Foreground-/Font-)Color und Border. Ein paar weitere Ideen, welche anderen CSS-Eigenschaften hier noch sinnvoll einsetzbar sind wären z.B.:

  • Alles was die Schrift betrifft wie
    *Font-weight
    *Font-family
    *Font-decoration

 

  •  Weitere Spielereien wie
    *Border-radius
    *Border-style

Als kleine Unterstützung bei der Recherche für weitere Styling Möglichkeiten:
Die Elemente selbst sind Links (<a>), welche über eine andere Klasse (fc-h-event) als Block-Element dargestellt wird, Klassen welche sich also lediglich auf Inline-Elemente auswirken werden keinerlei Effekt haben.

 

SQL Statements mithilfe von SQL Macros schlanker und dadurch effizienter schreiben

SQL Macros

SQL Macros

Des Öfteren kommt es vor, dass man ein und denselben Ausdruck über mehrere SELECT Befehle oder WHERE Bedingungen benötigt.
Mit den SQL Macros gibt es nun ein Werkzeug, um solche SQLs lesbarer und performanter zu gestalten.

Ein SQL Macro ist eine ausgelagerte Funktion, welche innerhalb eines SQL-Befehls aufgerufen wird. Dieses Konstrukt wird verwendet, um gängige SQL-Ausdrücke auszulagern und wiederzuverwenden. Überall, wo man normalerweise Funktionen aufrufen kann, können SQL Macros verwendet werden. In Oracle 20c nur als preview in der Cloud vorhanden, sind SQL Macros ab Oracle 21c fixer Bestandteil von Oracle Datenbanken, jedoch gibt es schon ab der Version 19.6 SQL TABLE Macros.

Generell gibt es zwei Arten von SQL Macros:

SCALAR: Liefert einen Ausdruck und kann im SELECT, WHERE, HAVING, GROUP BY und ORDER BY Teil des Befehls verwendet werden.

TABLE: Liefert einen Ausdruck und darf ausschließlich im FROM Teil des Befehls verwendet werden.

Aufbau:

Wird der Macro Typ nicht angegeben, wird das Macro defaultmäßig als TABLE Macro erstellt. Da in der Oracle Version 19.6 nur TABLE Macros zur Verfügung stehen, muss der Typ nicht angegeben werden.

Das SQL Macro wird nicht erst während der runtime ausgeführt, sondern bereits während dem parsen der Abfrage und liefert im Anschluss einen String (VARCHAR2, CHAR, CLOB) in die Hauptabfrage zurück. Dem Optimizer stehen daher schon alle nötigen Informationen zur Optimierung des SQLs zur Verfügung. Da das SQL Macro nur einmal während der parse time ausgeführt wird, entfällt daher auch der context switch zwischen SQL und PL/SQL engine. Des Weiteren können Parameter einem Macro mitübergeben werden. Dies ist besonders bei VIEWs interessant, denn bis dato konnte man VIEWs nicht parametrisieren.

Beispiele

Beispiel (Scalar SQL Macro): Wir wollen aus der Tabelle „emp“ alle Mitarbeiter ausgeben, welche schon mehr als 38 Jahre im Unternehmen angestellt sind.

Der Ausdruck „FLOOR(MONTHS_BETWEEN(TRUNC(SYSDATE), t1.hiredate)/12)” wird in diesem Statement zwei Mal verwendet, diesen können wir also auch in ein SQL Macro auslagern.

Die Funktion „job_duration“ wurde als SCALAR SQL Macro angelegt und ist nun verwendbar.



Beispiel (TABLE SQL Macro): Wir wollen aus der Tabelle „emp“ alle Mitarbeiter mit dem Jobtitel „ANALYST“ ausgeben.


Wir können den Ausdruck in der FROM Klausel auch als Macro auslagern.


Die Funktion „analyst_emp“ wurde als TABLE SQL Macro angelegt und ist nun verwendbar.

Folgendes Macro liefert zwar beim Kompilieren keinen Fehler, jedoch beim Aufruf im SQL Statement:

Fehler Nummer 1: Zwei Ergebnisspalten in einem Macro. Dies ist nicht möglich, da ein Macro immer nur eine Ergebnisspalte zurückliefert.

Fehler Nummer 2: Spalten Alias in einem Macro. Da Macros unter Anderem auch in der WHERE Klausel verwendet werden können und diese mit Spalten Aliase nicht arbeiten können, wird hier ein weiterer Fehler erzeugt.

Parameter

Um mehr Flexibilität in SQL Macros zu erlangen, können Parameter verwendet werden. Beispiel: Wir wollen anhand eines beliebig gewählten Jahres wissen, welche Mitarbeiter schon länger als 38 Jahre im Unternehmen tätig sind.

Nun haben wir das SQL Macro erstellt und können es nun in unserer Abfrage verwenden.

Data Dictionary

Da SQL Macros eigene Datenbankobjekte sind, sind diese auch im Data Dictionary ersichtlich.



 

CBQT-ORE and its FIRST_ROWS optimization inability

CBQT-ORE and its FIRST_ROWS optimization inability

Terminology:

CBQT: cost based query transformation
ORE: or expansion

Some days ago, I took a look on a customer’s performance issue which was introduced after their upgrade from 12.1 to 19c. Quite fast we could narrow it down to a new optimizer feature „cost based or expansion“.

To get a basic understanding of the feature, I’ll recommend reading Optimizer Transformations: OR Expansion by Oracle’s CBO PM Nigel Bayliss.

As a quick fix we simply disabled the feature by setting „_optimizer_cbqt_or_expansion“ = off and the performance went back to good again.

Afterwards I wanted to understand the root cause and built a model to reproduce the problem.

rem ######################################
rem # set environment                    #
rem ######################################
alter session set statistics_level=ALL;

rem ######################################
rem # prophylactic cleanup               #
rem ######################################
drop table asc_t2;
drop table asc_t1;
drop table asc_t3;

rem ######################################
rem # create testdata                    #
rem ######################################

--will hold 1M rows with a unique ID
create table asc_t1
as
with gen as
(
   select rownum dummy from dual connect by level <= 1e4
)
select rownum id
  from gen, gen
 where rownum <= 1e6;

--will hold 1M rows with 500 distinct values of T1_ID (20000 records per value)
create table asc_t2
as
select id, mod(id, 500) t1_id, lpad('*', 250, '*') pad
  from asc_t1;

--will hold 1M rows with 500 distinct values of T1_ID (20000 records per value)
create table asc_t3
as
select id, t1_id, pad
  from asc_t2;

--indexes
create unique index asc_t1_uk on asc_t1 (id);
create unique index asc_t2_uk on asc_t2(id);
create index asc_t2_i1 on asc_t2(t1_id);
create unique index asc_t3_uk on asc_t3(id);
create index asc_t3_i1 on asc_t3(t1_id);

And this is the query to be examined:

select t1.*
  from asc_t1 t1
 where id between 1 and 10
   and exists (select 1
                 from asc_t2 t2, asc_t3 t3
                where t2.id = t3.id
                  and (t2.t1_id = t1.id or t3.t1_id = t1.id)
              );

With no hints or further parameters set, this is the plan including some rowsource execution statistics pulled from memory after its execution:
So at first we can see that ORE was used and the query was split into two disjunct union all branches. What’s really interesting for me at a first sight is the presence of a blocking operation in the VW_ORE block (HASH JOIN at ID 4), despite the fact it is called in an exists clause and therefore has the ability to leave after the first row is found.

So for each row we got from ASC_T1 an in-memory hash table was build after scanning the appropriate index on T2 and visiting the table block (IDs 6 and 5).  In total 20.000 rows on 20.078 buffers were read on these operations. After that index ASC_T3_UK was probed against that hash table and here we see the effects of “exists” very cleary:  despite the index contains 1 million entries just 55 rows over all 10 calls needed to be read to find a first match and therefore be able to quit, because the exists clause was fulfilled. The second union-all branch wasn’t called at all.

Let’s look at two more examples.

First: forbid CBQT-ORE from kicking in.

select t1.*
  from asc_t1 t1where id between 1 and 10 
 and exists (select /*+ no_or_expand */1
                          from asc_t2 t2, asc_t3 t3
                        where t2.id = t3.id
                              and (t2.t1_id = t1.id or t3.t1_id = t1.id)
                            );

And its rowsource execution statistics:
It has got a much lesser cost than the ORE plan and much less buffers were visited to execute the query. It gives also the impression that the CBO is now aware that it has to deal with an “exists” clause here, because that part of the plan was optimized to find a first matching row very quickly. E.g low cost for the FTS on ASC_T2 or the absence of blocking operations are indicators for this strategy.

Second: Switch back to LORE (Legacy OR Expansion)

select /*+ opt_param('_optimizer_cbqt_or_expansion', 'off') */ t1.* 
  from asc_t1 t1
 where id between 1 and 10
    and exists (select 1
                             from asc_t2 t2, asc_t3 t3
                          where t2.id = t3.id
                                and (t2.t1_id = t1.id or t3.t1_id = t1.id)
                            );

Again rowsource execution statistics:
This one now shows the lowest cost and least buffers visited overall. It also seems to be aware of the exists clause and adapts a first_rows strategy in the relevant parts of the execution plan! This is basically what I would have expected from the CBQT-ORE.

So we now have a theory that CBQT-ORE loses track that its query block is called in an “exists” clause and provides an execution plan as if it was a standalone query, where it would need to fetch all the rows.

There’s some more evidence to this theory.

1.) The transformed query from the VW_ORE query block shows the same cost and execution plan when it is costed as a standalone query. The correlated value from the outer rowsource was replaced with a bind variable in the following example:

select 0
  from (
             select 1
                 from asc_t3 t3, asc_t2 t2
              where t2.id = t3.id and t2.t1_id = :b1
            union all
           select 1
               from asc_t3 t3, asc_t2 t2
            where t2.id = t3.id      
                 and t3.t1_id = :b1
                 and lnnvl (t2.t1_id = :b1)               );

generates this plan, which matches the “VW_ORE_82971ECB” from our first query.



2.) The 10053 traces for both the disabled CBQT-ORE and the old-style OR-Expansion (LORE) show lots of references that a first_rows optimization approach was chosen for the query blocks in question.

Like:

On the other hand when looking at the 10053 trace of the first query, interestingly before the ORE checks kicked in, the same plan was already found which was used in query 2, where I explicitly disabled the transformation.

Final cost for query block SEL$2 (#2) – First K Rows Plan:
  Best join order: 1
  Cost: 506.142990  Degree: 1  Card: 2.000000  Bytes: 71964.000000
  Resc: 506.142990  Resc_io: 506.000000  Resc_cpu: 4233492
  Resp: 506.142990  Resp_io: 506.000000  Resc_cpu: 4233492

Later in the same tracefile, the ORE transformation and costing was performed and produced a final cost (8591) which was (way) higher than the formerly calculated plan with a final cost of 506. But however at the end it was not picked and the CBO stayed with the cheapest ORE-plan.

Summary:
CBQT-ORE does not seem to be aware when its query block resides in an “exists” clause and therefore doesn’t optimize for first rows access patterns. The query block gets optimized as if all rows would be needed to fetch. Additionally in certain scenarios it seems to “forget” if cheaper plans were found during the whole optimization process. If this effects you in a negative way, as a first action the cost based transformation can be turned off and the legacy version used instead. Additionally an SR was raised to tackle this issue.

Further Reading:
Excellent articles on the topic of CBQT Or-Expansion can also be found on the blogs of Patrick Joliffe, Mohamed Houri and Nenad Noveljic

Update 1:
After playing with the same testcase in my 21c lab I noticed the that the problem went away. After quick look into v$system_fix_control I found bug 28414968 “expansion with some constant branches;fkr1 in (NOT)EXISTS subque” which is first available in 19.11 and has to be activated proactively. That plan now gets produced in in 21c and after setting alter session set „_fix_control“=’28414968:3′; in >=19.11
10 buffers less visited compared to the LORE plan, as ID 8 was superfluous in the LORE plan.

Funnily, if I code the union-all instead of the or-predicate myself, I still get the old plan. But that’s one topic for another post I guess.

select t1.id id
  from demo.asc_t1 t1
 where t1.id >= 1
   and t1.id <= 10
   and exists (select 0
                            from demo.asc_t3 t3, demo.asc_t2 t2
                         where t2.id = t3.id
                               and t2.t1_id = t1.id 
                         union all
                         select 1
                             from demo.asc_t3 t3, demo.asc_t2 t2
                          where t2.id = t3.id
                                and t3.t1_id = t1.id
                                and lnnvl (t2.t1_id = t1.id)
                            );