Blog programisty w Oracle PL/SQL

Jest to blog eksperymentatora programisty w PL/SQL dla Oracle. Wszystkie kody tutaj zamieszczone mogą być dowolnie wykorzystywane i zmieniane. A jeśli Ktoś z Gości znajdzie błąd, będę niezwykle wdzięczny...
Zapisz

Szukaj na tym blogu

wtorek, 4 marca 2025

How to show ODI scheduler for current day

Below is a query that shows in a fairly accurate way what the scheduled data flows are today. You just need to determine in which schema the SNP_PLAN_AGENT table is located and replace ODI_EXEC_REPO with the name of that schema.. 

The query returns the following columns: 

  • SCEN_NAME - scenario name 
  • SCEN_START_DATE - scenario start date (second resolution) 
  • DATE_FROM - start of the time window in which scenarios are started 
  • DATE_TO - end of the time window in which scenarios are started 
  • S_BEGIN_DATE - start of schedule activation period
  • S_END_DATE - end of schedule activation period
  • S_WEEK_DAY - on which day of the week scenarios are started
  • S_EX_DAYS_MONTH - which days of the week are excluded 
  • S_EX_DAYS_WEEK - which days of the week are excluded 
  • LAGENT_NAME - ODI agent name

WITH FUNCTION  FDb_IsDayOfMonthExcluded(
    pc_S_EX_DAYS_MONTH VARCHAR2,
    pn_Day NUMBER)
    RETURN NUMBER DETERMINISTIC
--- 1 DAY is excluded, 0 ELSE
IS
BEGIN
IF pc_S_EX_DAYS_MONTH IS NULL THEN
    RETURN 0;
END IF;
FOR EL IN
(
SELECT LP,
       TOKEN,
       CASE
           WHEN TOKEN LIKE '%-%'
           THEN
               REGEXP_SUBSTR (token,
                              '[[:digit:]]{1,2}',1,1)
           ELSE
               NULL
       END    AS START_PERIOD,
       CASE
           WHEN TOKEN LIKE '%-%'
           THEN
               REGEXP_SUBSTR (token,
                              '[[:digit:]]{1,2}', 1, 2)
           ELSE
               NULL
       END    AS END_PERIOD,
       CASE
           WHEN TOKEN NOT LIKE '%-%' THEN
                TO_NUMBER( TOKEN)
            ELSE
                NULL
            END AS DAY_OF_MONTH
  FROM (    SELECT LEVEL                    LP,
                   REGEXP_SUBSTR (pc_S_EX_DAYS_MONTH,
                                  '([[:digit:]]{1,2}((-)[[:digit:]]{1,2}){0,1})',
                                  1,
                                  LEVEL)    TOKEN
              FROM DUAL
        CONNECT BY LEVEL <=
                   REGEXP_COUNT (
                       pc_S_EX_DAYS_MONTH,
                       '([[:digit:]]{1,2}((-)[[:digit:]]{1,2}){0,1})',
                       1))
    )
    LOOP  
        IF EL.DAY_OF_MONTH IS NOT NULL THEN
            IF EL.DAY_OF_MONTH = pn_Day THEN
                RETURN 1;
            END IF;    
        ELSIF
            pn_Day BETWEEN EL.START_PERIOD AND EL.END_PERIOD THEN
                RETURN 1;
        END IF;  
    END LOOP;                   
    RETURN 0;               
END;
SELECT *
    FROM (SELECT SCEN_NAME,
                SCEN_START_DATE
                 + NUMTODSINTERVAL (S_MINUTE, 'minute')
                 + NUMTODSINTERVAL (S_SECOND, 'second')
                     SCEN_START_DATE,
                S_BEGIN_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                    DATE_FROM,
                S_END_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                    DATE_TO,
                S_BEGIN_DATE,
                S_END_DATE,
                S_WEEK_DAY,
                S_EX_DAYS_MONTH,
                S_EX_DAYS_WEEK,
                LAGENT_NAME
            FROM (    SELECT   TRUNC (SYSTIMESTAMP)
                             + NUMTODSINTERVAL (LEVEL - 1, 'hour')    SCEN_START_DATE
                        FROM DUAL
                  CONNECT BY LEVEL <= 24),
                 ODI_EXEC_REPO.SNP_PLAN_AGENT PA
           WHERE S_TYPE = 'H' AND STAT_PLAN != 'D'
    UNION ALL
        SELECT PA.SCEN_NAME,
                TO_DATE (
                        EXTRACT (YEAR FROM SYSDATE)
                    || '-'
                    || LPAD (EXTRACT (MONTH FROM SYSDATE), 2, '0')
                    || '-'
                    || LPAD (EXTRACT (DAY FROM SYSDATE), 2, '0')
                    || ' '
                    || LPAD (S_HOUR, 2, '0')
                    || ':'
                    || LPAD (S_MINUTE, 2, '0')
                    || ':'
                    || LPAD (S_SECOND, 2, '0'),
                    'YYYY-MM-DD HH24:MI:SS')
                        AS SCEN_START_DATE,
                S_BEGIN_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_FROM,
                S_END_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_TO,
                S_BEGIN_DATE,
                S_END_DATE,
                S_WEEK_DAY,
                S_EX_DAYS_MONTH,
                S_EX_DAYS_WEEK,
                LAGENT_NAME
            FROM ODI_EXEC_REPO.SNP_PLAN_AGENT PA
           WHERE S_TYPE IN ('D', 'W') AND STAT_PLAN != 'D'
    UNION ALL
        SELECT PA.SCEN_NAME,
                TO_DATE (
                        EXTRACT (YEAR FROM SYSDATE)
                     || '-'
                     || LPAD (EXTRACT (MONTH FROM SYSDATE), 2, '0')
                     || '-'
                     || LPAD (S_MONTH_DAY, 2, '0')
                     || ' '
                     || LPAD (S_HOUR, 2, '0')
                     || ':'
                     || LPAD (S_MINUTE, 2, '0')
                     || ':'
                     || LPAD (S_SECOND, 2, '0'),
                     'YYYY-MM-DD HH24:MI:SS')
                     AS SCEN_START_DATE,
                S_BEGIN_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_FROM,
                S_END_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_TO,
                S_BEGIN_DATE,
                S_END_DATE,
                S_WEEK_DAY,
                S_EX_DAYS_MONTH,
                S_EX_DAYS_WEEK,
                LAGENT_NAME
        FROM ODI_EXEC_REPO.SNP_PLAN_AGENT PA
           WHERE     S_TYPE = 'M'
                AND STAT_PLAN != 'D'
                AND EXTRACT (DAY FROM SYSDATE) = S_MONTH_DAY
    UNION ALL
        SELECT PA.SCEN_NAME,
                TO_DATE (
                        S_YEAR
                     || '-'
                     || LPAD (S_MONTH, 2, '0')
                     || '-'
                     || LPAD (S_DAY, 2, '0')
                     || ' '
                     || LPAD (S_HOUR, 2, '0')
                     || ':'
                     || LPAD (S_MINUTE, 2, '0')
                     || ':'
                     || LPAD (S_SECOND, 2, '0'),
                     'YYYY-MM-DD HH24:MI:SS')
                     AS SCEN_START_DATE,
                S_BEGIN_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_FROM,
                S_END_HOUR + (TRUNC (SYSDATE) - TRUNC (S_BEGIN_HOUR))
                     DATE_TO,
                S_BEGIN_DATE,
                S_END_DATE,
                S_WEEK_DAY,
                S_EX_DAYS_MONTH,
                S_EX_DAYS_WEEK,
                LAGENT_NAME
            FROM ODI_EXEC_REPO.SNP_PLAN_AGENT PA
           WHERE S_TYPE = 'S' AND STAT_PLAN != 'D')
   WHERE     (SCEN_START_DATE >= DATE_FROM OR DATE_FROM IS NULL)
         AND (SCEN_START_DATE <= DATE_TO OR DATE_TO IS NULL)
         AND (SCEN_START_DATE >= S_BEGIN_DATE OR S_BEGIN_DATE IS NULL)
         AND (   SCEN_START_DATE <= S_END_DATE
             OR     S_END_DATE IS NULL
        AND (   S_WEEK_DAY LIKE '%' || TO_CHAR (SYSDATE, 'd') || '%'
            OR S_WEEK_DAY IS NULL))
    AND SCEN_START_DATE < SYSDATE        
    AND FDb_IsDayOfMonthExcluded(
        pc_S_EX_DAYS_MONTH =>  S_EX_DAYS_MONTH,
        pn_Day => EXTRACT( DAY FROM SYSDATE)
        ) = 0
        ORDER BY SCEN_START_DATE ;

sobota, 26 października 2024

My 4 Oracle dreams

Oracle introduces some fixes and new features in each release, but there are still problems 

1. Oracle does not support transactional DDL: a transaction is considered closed when a CREATE, DROP, RENAME or ALTER command is executed, a hidden COMMIT is executed. If the transaction contains DML commands, Oracle commits the transaction as a whole, and then commits the DDL command as a separate transaction. This would be especially convenient for particularly complex DDL installation scripts

2.Problems with handling NULL in SQ

Note that NULL values ​​are not comparable to each other or to other non-NULL values... Oracle should treat the use of NULL in conditions after the WHERE clause of comparison operators as an error, any conditions like a = NULL, a < NULL.

SELECT * FROM HR.EMPLOYEES WHERE LAST_NAME = NULL;

SELECT * FROM HR.EMPLOYEES WHERE LAST_NAME <> NULL;

Such queries never return anything, and people with less experience are not aware of it. In my opinion, an error should be generated: invalid use of operators with NULL, use any of operators IS NULL or IS NOT NULL

Empty string '' should be forbidden, because it is de facto a NULL value.

SELECT NVL('', 'I am NULL')  FROM DUAL;

Result: I am NULL

3,CLOB handling is the same as VARCHAR2 handling. 

The idea is to have built-in functions instead of functions in package DBMS_LOB. 

  • LENGTH instead of DBMS_LOB.GETLEGTH
  • SUBSTR instead of DBMS_LOB.SUBSTR 
  • INSTR instead of DBMS_LOB.INSTR 
  • To have the = and <> operators work like DBMS_LOB.COMPARE 

4.To have a built-in function to convert the first 4000 characters of type LONG to VARCHAR2 .

LONG and LONG RAW are both deprecated. Yet they still exist in the data dictionary and legacy systems. For this reason, it is still quite common to see questions in Oracle forums about querying and manipulating LONGs. These questions are prompted because the LONG datatype is extremely inflexible and is subject to a number of restrictions.

niedziela, 22 września 2024

Mortgage repayments calculator in Oracle SQL - power of the MODEL clause

The post was inspired by late-night discussions about the sense of using mortgages. In the first approach, it was supposed to be a PL/SQL package, but I decided to make my life easier and wrote a query displaying the loan repayment plan for fixed and decreasing installments.

The query can be easily parameterized (parameters are dark green) by changing:

  • loan amount 
  • repayment period 
  • annual interest rate 

The following assumptions were made: 

  • the interest rate during loan repayment period is fixed
  •  installments are payable in advance from the current day every month 
  • we assume that the interest rate is the same within each month 
The query below, with a bit of imagination, can be expanded in an interesting way, e.g. by assuming different interest rates in subsequent years.

SELECT d + 1 AS "Repayments number",
          REPAYMENT_DATE AS "Repayments date",        
          ROUND (DECL_REPAYMENTS_DEBT_PART, 2) AS "Declining repayments debt part ",
          ROUND (DECL_REPAYMENTS_INTEREST_PART, 2) AS "Declining repayments interest part",
          ROUND (DECL_REPAYMENTS_DEBT_PART + DECL_REPAYMENTS_INTEREST_PART, 2) AS "Declining repayment amount",
          ROUND (DECL_REPAYMENTS_BALANCE, 2) AS "Declining repayment balance",
          ROUND (FIXED_REPAYMENTS_DEBT_PART, 2) AS "Fixed payments debt part",
          ROUND (FIXED_REPAYMENTS_INTEREST_PART, 2) AS "Fixed repayments interest part",
          ROUND (FIXED_REPAYMENTS_AMOUNT, 2) AS "Fixed repayments amount",
          ROUND (FIXED_REPAYMENTS_BALANCE, 2) AS "Fixed repayments balance"
     FROM (SELECT 1 FROM DUAL)
   MODEL
      DIMENSION BY
(0 d)
      MEASURES (600000 AMOUNT_OF_CREDIT, 6 BANK_RATE, 240 NUMBER_REPAYMENTS,
      TRUNC (SYSDATE) REPAYMENT_DATE,
             0 DECL_REPAYMENTS_BALANCE,
             0 DECL_REPAYMENTS_DEBT_PART,
             0 DECL_REPAYMENTS_INTEREST_PART,            
             0 FIXED_REPAYMENTS_AMOUNT,
             0 FIXED_REPAYMENTS_DEBT_PART,
             0 FIXED_REPAYMENTS_INTEREST_PART,
             0 FIXED_REPAYMENTS_BALANCE
             )
      RULES
         ITERATE (10000) UNTIL (ITERATION_NUMBER = NUMBER_REPAYMENTS[0] -1)
         (              
         NUMBER_REPAYMENTS [ITERATION_NUMBER ] =  
               NVL (NUMBER_REPAYMENTS[ITERATION_NUMBER - 1],                                     NUMBER_REPAYMENTS[0]),
          BANK_RATE [ITERATION_NUMBER ] =
               NVL (BANK_RATE[ITERATION_NUMBER - 1], BANK_RATE[0]/1200),
         DECL_REPAYMENTS_DEBT_PART [ITERATION_NUMBER ] =
               AMOUNT_OF_CREDIT[0] / NUMBER_REPAYMENTS[0],
         REPAYMENT_DATE [ITERATION_NUMBER ] =
                ADD_MONTHS (REPAYMENT_DATE[0], ITERATION_NUMBER ),
         DECL_REPAYMENTS_BALANCE [ITERATION_NUMBER ] =
                 NVL (DECL_REPAYMENTS_BALANCE[ITERATION_NUMBER - 1],
                      AMOUNT_OF_CREDIT[0])
               - NVL (DECL_REPAYMENTS_DEBT_PART[ITERATION_NUMBER ], 0),
         DECL_REPAYMENTS_INTEREST_PART [ITERATION_NUMBER ] =
                 DECL_REPAYMENTS_BALANCE[ITERATION_NUMBER ]
               * BANK_RATE[ITERATION_NUMBER ]  ,                                       
         FIXED_REPAYMENTS_DEBT_PART [ITERATION_NUMBER ]              
         = AMOUNT_OF_CREDIT[0] *BANK_RATE [ITERATION_NUMBER ] *POWER( 1+BANK_RATE [ITERATION_NUMBER ], ITERATION_NUMBER )/
          ( POWER( 1+BANK_RATE [ITERATION_NUMBER ],          NUMBER_REPAYMENTS[ITERATION_NUMBER ]) -1  ),
           FIXED_REPAYMENTS_BALANCE [ITERATION_NUMBER ] =
                 NVL (FIXED_REPAYMENTS_BALANCE[ITERATION_NUMBER - 1],
                      AMOUNT_OF_CREDIT[0])
               - FIXED_REPAYMENTS_DEBT_PART[ITERATION_NUMBER ],
           FIXED_REPAYMENTS_INTEREST_PART [ITERATION_NUMBER ] =
                 FIXED_REPAYMENTS_BALANCE[ITERATION_NUMBER ]
               * BANK_RATE[ITERATION_NUMBER ],
           FIXED_REPAYMENTS_AMOUNT[ITERATION_NUMBER ] = FIXED_REPAYMENTS_DEBT_PART[ITERATION_NUMBER ] + FIXED_REPAYMENTS_INTEREST_PART[ITERATION_NUMBER ]       
          )

środa, 7 sierpnia 2024

Calculating Euler's number in Oracle SQL

 

I use the MODEL clause again to calculate the Euler's number.
Euler proved that e is the sum of the infinite series e = 1/0!  +1/1!  +1/2! +1/3!  +1/4! ........
There is one dimension - the iteration number.
The measures are:
  • E_EXP -the Euler's number calculated from the EXP function
  • FACTORIAL - value of the factorial function
  • E_COMPUTED - our estimate of the number e
  • DIFF -the difference between EXP(1) and the current estimate

My proposal is to change the precision in the stop condition
- the second parameter of the POWER function: 
UNTIL (DIFF[ITERATION_NUMBER] < POWER(10, -30))
----------------------------------------------------------- 

SELECT
       d              "Iteration number",
       FACTORIAL      "Factorial",
       E_EXP          "Euler's number from EXP",
       E_COMPUTED     "Euler's number calculated",
       DIFF           "Difference"
  FROM (SELECT 1 d FROM DUAL)
MODEL
    DIMENSION BY (0 d)
    MEASURES (CAST (0  AS NUMBER) AS E_EXP,
              CAST (1 AS NUMBER) AS FACTORIAL,
              CAST (1 AS NUMBER) AS E_COMPUTED,
              CAST (0 AS NUMBER) AS DIFF)
    RULES
    ITERATE (1000) UNTIL (DIFF[ITERATION_NUMBER] < POWER(10, -30))
    (
        FACTORIAL [ITERATION_NUMBER] =
            CASE
                WHEN ITERATION_NUMBER=0  THEN 1
                ELSE FACTORIAL[ITERATION_NUMBER- 1] * (ITERATION_NUMBER)
            END,
        E_EXP [ITERATION_NUMBER] =  
          CASE
            WHEN ITERATION_NUMBER= 0 THEN    
                EXP(1)
            WHEN ITERATION_NUMBER> 0 THEN
                E_EXP[ITERATION_NUMBER-1]
            END,
        E_COMPUTED [ITERATION_NUMBER] =
            CASE ITERATION_NUMBER
                WHEN 0 THEN 1
                ELSE
                      E_COMPUTED[ITERATION_NUMBER- 1]
                    + 1 / FACTORIAL[ITERATION_NUMBER]
            END,
        DIFF [ITERATION_NUMBER] =
            ABS (E_COMPUTED[ITERATION_NUMBER] - E_EXP[ITERATION_NUMBER]))

poniedziałek, 15 lipca 2024

Recursive algorithms in SQL

Possible?? Yes, just like the model clause..  

Below, the two queries contain algorithms written in a recursive version:

  •   Fibonacci sequence 
  •   Factorial
  •   Euclid's algorithm for determining the greatest common divisor (GCD)

Two techniques are presented:  

  • iterative models - are perfect for implementing unary recursive functions, and their notation is almost intuitive. Unfortunately, with a larger number of parameters, problems arise with cyclical cell enumeration 
  • non-iterative models - formulas are a bit more complicated, but multi-argument functions can be handled
SELECT                                                    
      d  , f AS FIBBONACCI,
      S AS FACTORIAL
  FROM   (SELECT   0 d  FROM DUAL)
MODEL
   DIMENSION BY (0 d)
   MEASURES (0 f, 0 s)
   RULES
      ITERATE (100) UNTIL (ITERATION_NUMBER = 50)
       (f [ITERATION_NUMBER] =
                            CASE ITERATION_NUMBER
                                WHEN 0 THEN 0
                                WHEN 1 THEN 1
                              ELSE
                                f[ITERATION_NUMBER - 2] + f[ITERATION_NUMBER - 1]
                            END,
      s [ITERATION_NUMBER] =
                  CASE ITERATION_NUMBER
                    WHEN 0 THEN 1
                    ELSE           
                    ITERATION_NUMBER * s[ITERATION_NUMBER - 1]
                  END)


The example below can be simplified a bit :)

SELECT   L1, L2, GCD
  FROM   (SELECT   L1, l2
            FROM   (    SELECT   LEVEL - 1 L1
                          FROM   DUAL
                    CONNECT BY   LEVEL <= 20),
                   (    SELECT   LEVEL - 1 L2
                          FROM   DUAL
                          CONNECT BY   LEVEL <= 20))
MODEL
   DIMENSION BY (L1, L2)
   MEASURES (0 GCD)
   RULES SEQUENTIAL ORDER
   (GCD [ANY, ANY] =
            CASE
               WHEN CV (l2) = 0  AND CV (L1) =0
               THEN
                  1
               WHEN CV (l2) = 0  OR CV (L1) =0
               THEN
                  GREATEST(CV (l1), CV(l2))
               WHEN CV (l2) <= CV (l1)
               THEN
                  GCD[CV (l2), MOD (CV (l1), CV (l2))]
            END,
      GCD[ANY, ANY] =
            CASE
               WHEN CV (l2) > CV (l1) THEN
                        GCD[CV (l2), CV (l1)]
               ELSE GCD[CV (l1), CV (l2)]
            END)

 

niedziela, 17 maja 2020

Obsługa XML - część 3

Cały czas  działamy na tabeli  TBL_XML - opisanej tutaj. Analizowany XML  zawiera powtarzający się element pozycja, a w ramach tego elementu zagnieżdżone są pola:
  • nazwa_waluty
  • przelicznik
  • kod_waluty
  • kurs sredni
 
    dolar amerykański
1 USD 4,2396
 
Poznamy teraz konstrukcji, która w w elegancki sposób będzie pokazywać elementy i atrybuty z powtarzających się elementów

SELECT   EXTRACTVALUE( x.cxml, '//data_publikacji') data_pubblikacji ,
   EXTRACTVALUE( x.cxml, '//numer_tabeli') nr_tabeli ,
  xt.*
FROM   TBL_XML x,
       XMLTABLE('//pozycja'
         PASSING x.cxml --nazwa_kolumny XML
         COLUMNS
           NAZWA_WALUTY     VARCHAR2(30 CHARPATH 'nazwa_waluty',
           KOD_WALUTY     VARCHAR2(3 CHARPATH '//*/kod_waluty',
           PRZELICZNIK    VARCHAR2(5 CHAR) PATH '//przelicznik',
           KURS_DZIENNY    VARCHAR2(10 CHAR) PATH 'kurs_sredni'
         ) xt;
  
Wynik: Zauważmy - wszystkie pola sa tekstowe !!!!






Jak powyższa konstrukcja działa?
  1.  Tworzona jest w pamięci tabela aliasowana przez xt
  2. Każdy wiersz tabeli xt to powtarzający się element  pozycja, opisujący parametry waluty
  3. W nawiasie po słowie kluczowym XMLTABLE podaje się ścieżkę do analizowanego elementu
  4. Po słowie kluczowym PASSING podaje się nazwę pola zawierającego XML
  5. Po słowie kluczowym COLUMNS podaje się  definicje i mapowania kolumn. Mapowanie występuje po słowie kluczowym PATH  i może być, jak powyżej, wykonane na różne sposoby
  6. Wyrażenie xt.* zwraca kolumny zdefiniowane w mapowaniu
Uwagi:
  1. W sekcji COLUMNS  najlepiej jest robić mapowania na typ znakowy lub na typ numeryczny(ale tylko dla liczb całkowitych). W powyższym przykładzie  mapowanie typ NUMBER(4) zadziałałoby dla pola PRZELICZNIK.  Dla liczb zmiennopozycyjnych (jak pole w  powyższym przypadku KURS_DZIENNY) i dat, automatyczna konwersja jest źródłem problemów.  Właściwą konwersję najlepiej przeprowadzić  w głównym zapytaniu, a nie na poziomie mapowania  
  2. Jeśli zdefiniujemy  typ o niewystarczającej długości, to podczas mapowania teksty zostaną obcięte
  3. Jedli w danym elemencie nie wystąpi ścieżka z mapowania, wtedy dla odpowiadającej temu mapowaniu kolumny  zwracana jest wartość NULL
  4. W przypadku zmiany kolumny lub dodania nowych pól, modyfikacja istniejącego zapytania jest banalna

    

niedziela, 12 kwietnia 2020

Obsługa XML - część 2

Bazujemy na tej tabeli co w poprzednim wpisie. W tabeli z kursami jest jeden wiersz zawierający  w formacie XML  kursy następujących walut (w kolejności ich występowania):
THB
USD
AUD
HKD
CAD

 Napiszemy kilka prostych zapytań.

Wyświetlenie kodu waluty z pierwszego węzła:

SELECT EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty' ) FROM TBL_XML;

Wyświetlenie kodu waluty z pierwszego węzła:

SELECT EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty' ) FROM TBL_XML;

Wyświetlenie kodu waluty z pierwszego węzła

SELECT EXTRACTVALUE(CXML,'//pozycja[ last() ]/kod_waluty' ) FROM TBL_XML;
lub
SELECT EXTRACTVALUE(CXML,'//pozycja[ position() = last() ]/kod_waluty' ) FROM TBL_XML;
Uwaga: wielkość liter ścieżce XPath ma znaczenie. Następująca ścieżka ,'//pozycja[ Position() = last() ]/kod_waluty' jest już niepoprawna

Wyświetlenie kodu waluty z przedostatniego węzła:

SELECT EXTRACTVALUE(CXML,'//pozycja[ last() -1 ]/kod_waluty' ) FROM TBL_XML;

Wyświetlenie wszystkich kodów walut (przy założeniu, że  procesujemy jeden wiersz) - czyli warto dodać odwołanie do klucza w tabeli. W klauzuli CONNECT  BY LEVEL należy dodać sensowne górne oszacowanie liczby węzłów

  SELECT EXTRACTVALUE(CXML,'//pozycja[' || LEVEL || ']/kod_waluty' ) FROM TBL_XML
    WHERE EXTRACTVALUE(CXML,'//pozycja[' || LEVEL || ']/kod_waluty' ) IS NOT NULL  AND CID = 1
    CONNECT  BY LEVEL < 100;

Wyświetlenie wszystkich kodów walut zawartych w węzłach od drugiego do przedostatniego

SELECT EXTRACTVALUE(CXML,'//pozycja[' || LEVEL || ']/kod_waluty' ) FROM TBL_XML WHERE EXTRACTVALUE(CXML,'//pozycja[ position() = ' || LEVEL || ' and position() >=2 and position() <= last() -1 ]/kod_waluty' ) IS NOT NULL  AND CID = 1
    CONNECT  BY LEVEL < 100;

Policzenie ile wierszy w tabeli zawiera w polu CXML kod waluty dolara:

  SELECT COUNT(*) FROM TBL_XML WHERE  EXISTSNODE(CXML, '//pozycja[kod_waluty="USD"]') = 1;
wynik 1
 
 

   

piątek, 10 kwietnia 2020

Obsługa XML - część 1

Zacznijmy od stworzenia tabeli 
CREATE TABLE TBL_XML
(
  CID                                 NUMBER(6)       NOT NULL,
  CXML                            SYS.XMLTYPE    NOT NULL,
  UZYTKOWNIK            VARCHAR2(30 CHAR)      DEFAULT USER  NOT NULL,
  DATA_WSTAWIENIA  DATE  DEFAULT SYSDATE    NOT NULL
)

Dlaczego został wybrany typ XMLTYPE  , a nie CLOB ?? Ponieważ  umożliwia wstawienie do kolumny CXML tylko  poprawnego składniowo.
Jako xml wezmę początek pliku XML  z kursami walut ze strony NBP na dzień 3 kwietnia 2020, zawierający pierwsze pięć węzłów.


<tabela_kursow typ="A" uid="20a066">
  <numer_tabeli>066/A/NBP/2020</numer_tabeli>
  <data_publikacji>2020-04-03</data_publikacji>
  <pozycja>
    <nazwa_waluty>bat (Tajlandia)</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>THB</kod_waluty>
    <kurs_sredni>0,1287</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar amerykański</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>USD</kod_waluty>
    <kurs_sredni>4,2396</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar australijski</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>AUD</kod_waluty>
    <kurs_sredni>2,5527</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar Hongkongu</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>HKD</kod_waluty>
    <kurs_sredni>0,5469</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar kanadyjski</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>CAD</kod_waluty>
    <kurs_sredni>2,9956</kurs_sredni>
  </pozycja>
</tabela_kursow>  

Wstawiamy dane do tabeli:

INSERT INTO TBL_XML (CID, CXML)
VALUES
( 1,  '<tabela_kursow typ="A" uid="20a066">
  <numer_tabeli>066/A/NBP/2020</numer_tabeli>
<data_publikacji>2020-04-03</data_publikacji>
  <pozycja>
    <nazwa_waluty>bat (Tajlandia)</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>THB</kod_waluty>
    <kurs_sredni>0,1287</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar amerykański</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>USD</kod_waluty>
    <kurs_sredni>4,2396</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar australijski</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>AUD</kod_waluty>
    <kurs_sredni>2,5527</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar Hongkongu</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>HKD</kod_waluty>
    <kurs_sredni>0,5469</kurs_sredni>
  </pozycja>
  <pozycja>
    <nazwa_waluty>dolar kanadyjski</nazwa_waluty>
    <przelicznik>1</przelicznik>
    <kod_waluty>CAD</kod_waluty>
    <kurs_sredni>2,9956</kurs_sredni>
  </pozycja>
</tabela_kursow>')




Spróbujmy z pola XML pobrać kod waluty pierwszego węzła w sposób prawidłowy, aby wynik zapytania był typu VARCHAR2. Można do tego celu wykorzystać funkcje EXTRACT i EXTRACTVALUE. Ta druga jest lepsza co poniżej pokażemy:

Poniższe polecenie jest niepoprawne:
SELECT EXTRACT(CXML,'//pozycja[1]/kod_waluty' ) FROM TBL_XML;

Co nam zwróci - to zależy od edytora SQL.. w TOAD jest to  tekst  "THB.  Ale jakiego rzeczywiście typu typu jest wynik  powyższego zapytania ????

SELECT DUMP(EXTRACT(CXML,'//pozycja[1]/kod_waluty' )) FROM TBL_XML;

Otrzymamy:
Typ=58 Len=48: 128,68,239,156,252,127,0,0,192,142,121,1,178,1,0,0,32,3,128,1,178,1,0,..
Taki sam wynik otrzymamy dla zapytania:
SELECT DUMP(EXTRACT(CXML,'//pozycja[1]/kod_waluty/text()' )) FROM TBL_XML;

Co to jest typ  58 ??  wyjaśnienie znajduje się w pakiecie DBMS_TYPES:
TYPECODE_OPAQUE          PLS_INTEGER := 58;
W dokumentacji przeczytamy, że jest to typ abstrakcyjny (w znaczeniu nie można  wykorzystywać w PL/SQL), zaimplementowany jako ciąg  bajtów..
Niektóre  edytory SQL potrafią sobie z tym poradzić i wtedy mamy mylne złudzenie, że zapytanie zwraca wartość tekstową..

Wymusimy konwersję na tekst:
SELECT CAST(EXTRACT(CXML,'//pozycja[1]/kod_waluty' ) AS VARCHAR2 (30 CHAR)) FROM TBL_XML;

Wynik:
THB

Jeśli wynusimy konwersję typu  TYPECODE_OPAQUE do typu  VARCHAR2  za pomocą funkcji GestStringVal()

SELECT EXTRACT(CXML,'//pozycja[1]/kod_waluty' ).GestStringVal() FROM TBL_XML;
Wynik:
THB
wynik typu TYPECODE_VARCHAR        

SELECT EXTRACT(CXML,'//pozycja[1]/kod_waluty/text()' ).GestStringVal() FROM TBL_XML;
Wynik:
THB
Wynik typu TYPECODE_VARCHAR     

Z funkcję EXTRACTVALUE parsowanie  jest prostsze.

SELECT EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty' ) FROM TBL_XML;
i
SELECT EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty/text()' ) FROM TBL_XML;
zwraca:
THB

Zarówno
SELECT DUMP(EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty' ) ) FROM TBL_XML;
jak i 
SELECT DUMP(EXTRACTVALUE(CXML,'//pozycja[1]/kod_waluty/text()' )) FROM TBL_XML;
 zwraca  wynik:
Typ=1 Len=3: 84,72,66
czyli długość 3 znaki, gdzie  84,72,66 to kody ASCII poszczególnych liter


W pakiecie DBMS_TYPES znajdziemy
 TYPECODE_VARCHAR         PLS_INTEGER :=   1;

Tu jest mała pułapka, bo typ VARCHAR2  nie rozróżnia wartości NULL i pustego tekstu, a typ VARCHAR to rozróżnia.  Jest ona niegroźna gdyż puste znaczniki i atrybuty zwracane są jako NULL.

Podsumowując do prostego parsowania najlepsza jest funkcja EXTRACTVALUE,  ponieważ wyłuskuje wartość znacznika 

poniedziałek, 12 sierpnia 2019

Dzielimy kwotę na banknoty i monety

W jaki sposób  w SQL można rozbić dana kwotę na minimalną liczbę banknotów i monet??. To jest proste przy wykorzystaniu niezapomnianej klauzuli MODEL, kod przypomina prosty arkusz w MS Excel. Kwota, która ns interesuje wstawiana jest zamiast  przykładowej zielonej kwoty w kolorze zielonym Polecam analizę tego zapytania, ma kilka smaczków takich jak:
  • podzapytanie ze złączeniem kartezjańskim
  • ciekawy warunek stopu w iteracji
  • indeks do iteracji liczony  w funkcji analitycznej


SELECT *
  FROM ( SELECT KWOTA,
                NOMINAL,
                LICZBA_NOMINALOW,
                RESZTA
           FROM (   SELECT LICZBA * MNOZNIK                                       NOMINAL,
                           ROW_NUMBER( ) OVER (ORDER BY LICZBA * MNOZNIK DESC)    LP
                      FROM( SELECT 1 AS LICZBA FROM DUAL
                            UNION
                            SELECT 2 FROM DUAL
                            UNION
                            SELECT 5 FROM DUAL),
                           (SELECT 100 AS MNOZNIK FROM DUAL
                            UNION
                            SELECT 10 FROM DUAL
                            UNION
                            SELECT 1 FROM DUAL
                            UNION
                            SELECT 0.1 FROM DUAL
                            UNION
                            SELECT 0.01 FROM DUAL)
                  ORDER BY 1 DESC )
         MODEL
             DIMENSION BY( LP - 1 LP )
             MEASURES( NOMINAL,
                       15329.98 KWOTA,
                       0 LICZBA_NOMINALOW,
                       1 RESZTA,
                       0 Z )
             RULES
             ITERATE( 100 ) UNTIL (RESZTA[ITERATION_NUMBER] = 0)
             (
                 UPSERT
                 KWOTA [ITERATION_NUMBER] =
                     CASE ITERATION_NUMBER
                         WHEN 0 THEN TRUNC( KWOTA[0], 2 )
                         ELSE RESZTA[ITERATION_NUMBER - 1]
                     END,
                 LICZBA_NOMINALOW [ITERATION_NUMBER] =
                     TRUNC(
                         KWOTA[ITERATION_NUMBER] / NOMINAL[ITERATION_NUMBER] ),
                 RESZTA [ITERATION_NUMBER] =
                       KWOTA[ITERATION_NUMBER]
                     -   NOMINAL[ITERATION_NUMBER]
                       * LICZBA_NOMINALOW[ITERATION_NUMBER] ) )
 WHERE LICZBA_NOMINALOW <> 0