Tuesday, November 23, 2021

Fun - Query Generated Calendar

Here's a query that will generate a full year calendar given any year. Enter the 4-digit year in the first line and execute the code.

  

   WITH  year  as ( select  /*==>*/ 2022 /*<==*/ as yr from dual )
   select  tag as "Month", sun, mon, tue, wed, thu, fri, sat
     from  ( select  tag
                  ,  sum( decode(dw, '1', dm) ) sun
                  ,  sum( decode(dw, '2', dm) ) mon
                  ,  sum( decode(dw, '3', dm) ) tue
                  ,  sum( decode(dw, '4', dm) ) wed
                  ,  sum( decode(dw, '5', dm) ) thu
                  ,  sum( decode(dw, '6', dm) ) fri
                  ,  sum( decode(dw, '7', dm) ) sat
               from  ( select  to_char((yrdt-1+rownum),'MM')  as mn
                            ,  to_char((yrdt-1+rownum),'d')  as dw
                            ,  lpad(to_char((yrdt-1+rownum),'fmdd'),2)  as dm
                            ,  to_char((yrdt-1+rownum)+1,'iw')  as iw
                            ,  to_char((yrdt-1+rownum),'YYYY-MM (Mon)')  as tag
                         from  ( select  trunc(to_date( yr,'YYYY'),'YYYY') 
                                     as  yrdt  from  year )
                        where  rownum <= add_months(yrdt, 12) - yrdt
                      connect  by level <= 366 
                     )
              group  by  tag, mn, iw 
           )
    order  by  tag, sun NULLS first ;
  

Note: this code is not the product of my brain-steam. I found this algorithm somewhere on the web and have enjoyed playing with it. I apologize for not remembering where or who originally came up with it.

Fun - Sudoku Solver

Once upon a time, SUDOKU was a popular puzzle. The following query will attempt to solve a puzzle that is entered towards the top of this query. You'll need to enter the given numbers in their proper locations; the "-" represents an empty box. Note that this query is a very clever implementation of a 'recursive-WITH'. Have fun!

  

   with  
     sudoku_solver ( sol_str, ind )  as
       ( select  sud
              ,  instr( sud, '-' )
           from  ( select  '6--9-1--8'  -- Row #1
                       ||  '-4--8--3-'  -- Row #2
                       ||  '---4-6---'  -- Row #3
                       ||  '-9-----7-'  -- Row #4
                       ||  '5-7-1-2-9'  -- Row #5
                       ||  '-3-----5-'  -- Row #6
                       ||  '---1-2---'  -- Row #7
                       ||  '-1--5--9-'  -- Row #8
                       ||  '7--8-9--2'  -- Row #9
                       as  sud 
                     from dual 
                 )
       union all
         select  substr( sol_str, 1, ind - 1 ) || z 
             ||  substr( sol_str, ind + 1 )
              ,  instr( sol_str, '-', ind + 1 )
           from  sudoku_solver   
              ,  ( select  to_char( rownum ) z  
                     from  dual  connect by rownum <= 9 ) z
          where  ind > 0
            and  not exists 
              ( select  1
                  from  (select rownum lp from dual connect by rownum <= 9)
                 where  z = substr(sol_str, trunc((ind-1)/9)*9+lp, 1)
                    or  z = substr(sol_str, mod(ind-1, 9)-8+lp*9, 1)
                    or  z = substr(sol_str, mod(trunc((ind-1)/3), 3)*3+
                              trunc((ind-1)/27)*27+lp+trunc((lp-1)/3)*6, 1)
             )
       )
   , results  as  (select sol_str from sudoku_solver where ind = 0 )
   SELECT  'Row #' || rownum                as "Row Number"
        ,  substr(sol_str, rownum*9-8, 9)   as "Solution"
     from  results connect by level <= 9 ;
  

Note: this code is not the product of my brain-steam. I found this algorithm somewhere on the web and have enjoyed playing with it. I apologize for not remembering where or who originally came up with it; but, my hat is off to them!

Fun - Prime Factorization

This query will quickly find the prime factorization of a number -- Enter it on the first line and execute. This really would have been helpful in 7th grade math class!

  Example:  18 = 2 x 3 x 3

  Try some big, interesting numbers:  1234567890, 1212121212121, 9898989898989, 7171717171717

   WITH 
     step_0 as ( select /*==>*/ 1234567890 /*<==*/ as n from dual )
   , step_1 as ( select n, level i from step_0 connect by rownum <= sqrt(n) )
   , step_2 as ( select n, i, n/i as j from step_1 where mod(n,i) = 0 )
   , step_3 as ( select n, i from step_2 UNION select n, j from step_2 )
   , step_4 as ( select n, i
                      , ( select  min(i) 
                            from  step_3  x 
                           where  x.i/w.i = trunc(x.i/w.i) and  x.i > w.i
                        ) as idx 
                   from step_3  w )
   select max(n)||' = '
       || max(ltrim(rtrim(sys_connect_by_path(idx/i,'*'),'*'),'*')) as "Result"
     from step_4  
   connect by i = prior idx
   start with i = 1 ;
  

Note: this code is not the product of my brain-steam. I found this algorithm somewhere on the web and have enjoyed playing with it. I apologize for not remembering where or who originally came up with it.

Select Top(n) or nth-Ranked Rows (Top-N Analysis)

Sometimes you wish to select the top or highest 'n' rows of a sorted data-set. (or the bottom 'n' rows -- Reversing the sort will switch from top to bottom.) Simialarly, you may wish to only return the 'nth' row. For example, from a data set sorted by date, you may wish to find the oldest 10 records, or the one 3rd youngest record. In my experience there are 3 main ways to do this:

  
  1) Use the psudo-column ROWNUM.
  
  2) Use the Analytic functions Rank(), Dense_Rank(), or Row_Number().
  
  3) Use the Fetch and Offset keywords.
  
  
  
For the following examples, I'll use the a 'WITH' data set. Pre-pend it to all the other examples that follow to run them.

   with 
     w_data ( id, nm )  as
       ( select 30, 'Ted' from dual union all
         select 68, 'Dan' from dual union all
         select 21, 'Joe' from dual union all
         select 49, 'Ben' from dual union all
         select 12, 'Sam' from dual union all
         select 57, 'Art' from dual )
   select  id, nm
     from  w_data
    order  by nm ;
    
---

   {with...}
   select  id, nm
     from  w_data
    order  by id ;

The ROWNUM Psudo-Colum

The 'rownum' psudo column can be confusing; it returns a sequential number for each row as it is pulled from the source. It doesn't conform to any sort unless you build that into your query. Notice the example below; the second query returns nothing. It's trying to limit the pull to the 4th row -- but each row that's tentatively pulled and tested against the 'WHERE' clause, is assigned to be the first row, so it fails to qualify.


   {with...}
   select  id, nm      /* Returns 3 rows */
     from  w_data
    where  rownum < 4 ;  
    
   {with...}
   select  id, nm      /* Returns no rows */
     from  w_data
    where  rownum = 4 ;
    

However, if you use 'ROWNUM' properly, it can find your requested rows. Use a double sub-query as shown below, you can assign the 'ROWNUM' as a ranking after sorting the data (in this example sorting by "nm"). you can then use the 'WHERE' clause to select out the 'rankings' for which you are looking: top-3 (where rnk < 4), 3rd row (where rnk = 3).


And, notice that if you want to select the Bottom(n) rows, you just need to change the initial sort to "nm desc"

   {with...}
   select  id, nm, rnk
     from  ( select  id, nm
                  ,  rownum as rnk
               from  ( select id, nm from w_data order by nm ) 
           )
    where  rnk < 4
   ;
    
   /* Try other 'WHERE' clauses: */
    
     a)  where  rnk between 2 and 4
     b)  where  rnk = 2
     c)  where  rnk >= 3

Analytic Ranking

Using the analytic functions Rank(), Dense_Rank(), or Row_Number -- you can 'rank' the rows and select out the nth row or the top(n) rows as shown in this example.


Here I use the Row_Number() function, it does differ from the two 'rank' functions, but I'll leave that to another post focusing on those differences. Make sure you specify the correct sort order in the "(order by ---)" part of the analytic function.

   {with...}
   select  id, nm, rnk
     from  ( select  id, nm
                  ,  row_number() over (order by nm) as rnk
               from  w_data 
           )
    where  1=1
      and  rnk <= 4          /* top 4 records */
   -- and  rnk in (2,4,6)    /* rank is 2nd, 4th, or 6th */
   -- and  rnk = 1           /* first record */
   ;


Offset and Fetch

The "OFFSET" & "FETCH" offer a very similar functionality -- for more details, see my other post on these keywords. Put these after the sort (order by __ ). The "OFFSET" specifies how many rows to skip, and the "FETCH" specifies how many rows to return.


   Examples:
   
   a) offset 0 rows fetch next 10 rows only   -- get the top 10 rows
   
   b) offset 2 rows fetch next 1 row only     -- get the 3rd row 

   {with...}
   select  id, nm
     from  w_data 
    order  by nm
   OFFSET 2 rows
    FETCH next 3 rows only
   ;

Monday, November 22, 2021

Inline Functions (Built into a 'SELECT').

Since Oracle 12, it's possible to include a function declaration right in your query. You define it much like a normal function, but it is included right after the "WITH" clause; and it is only available in the attached query that follows.

In the following examples, Note these details:

  1- The function needs to immediately follow the "WITH" keyword.
  2- The PL/SQL code that defines the function ends with a semicolon.
  3- Other "WITH" clause subqueries can follow the function declaration.
  4- The query can be terminated with a semicolon; but ...
  5- To run it in a SQL-Developer like tool, you may need a terminating slash "/".
  

The following is an example of a function that uses the 'calculator' like functionality of the "SELECT" to evaluate an expression. If the expression or 'numeric value' can be represented as a number, then that value is returned. If not, then the 'exception' handler in the function returns a "NULL". The 'table' being queried is built into the "WITH" clause in the example below.

   WITH
     FUNCTION to_nmbr(f_exp in varchar2) 
       return number 
     is
       v_num number ;
     BEGIN
       execute immediate 'select '||f_exp||' from dual' 
         into v_num ;
       return v_num ;
     exception  
       when others then return NULL ;
     END ;
   -----
     w_data  as
       ( select null as id, null as val from dual where 1=2
         ----------------------------------------------------
         union all select   1, '  218.95  '      from dual
         union all select   2, ' +56  '          from dual
         union all select   3, ' -84  '          from dual
         union all select   4, '   4.549e6   '   from dual
         union all select   5, ' +(((4.8)))  '   from dual
         union all select   6, ' 13+7+9-15  '    from dual
         union all select   7, '  40-2*(11-2) '  from dual
         union all select   8, '  -(123)  '      from dual
         union all select   9, ' -(5) - -(7)  '  from dual
         union all select  10,    NULL           from dual
         union all select  11, '  words  '       from dual
         union all select  12, ' a 721 '         from dual
         union all select  13, ' 2 3  '          from dual
         union all select  14, ' 3 - -2  '       from dual
         union all select  15, ' -321.7e-5  '    from dual
         ----------------------------------------------------
       )
   select  id,  val,  to_nmbr(val)  as rslt
     from  w_data 
    order  by 1 ;
   /


The following function tests a date value stored in a text string against a given format string. If the date evaluates against the given format, then the DATE value is returned; if not, then it returns "NULL". In the SQL query, the 'coalesce()' tests several format strings for each date; the first successfull one is returned as the 'best-guess' DATE value.

   WITH
     FUNCTION try_dt
       ( f_dt in varchar2
       , f_fmt in varchar2 
       )  return date is
       v_dt date ;
     begin
       v_dt := to_date( f_dt, f_fmt ) ;
       return v_dt ;
     exception  when others then return NULL ;
     end ;
   select  id
        ,  dt
        ,  coalesce
             ( try_dt(dt, 'YYYY-MM-DD') 
             , try_dt(dt, 'YYYY-Mon-DD') 
             , try_dt(dt, 'MM-DD-YYYY') 
             , try_dt(dt, 'DD-MM-YYYY') )  as best_dt_guess
     from  ( select null as id, null as dt from dual where 1=2
             -------------------------------------------------
             union all select  1, '2018-Oct-17' from dual
             union all select  2, '1949-01-05'  from dual
             union all select  3, '4/25-2004'   from dual
             union all select  4, '25/4/2018'   from dual
             union all select  5, '3-7-1984'    from dual
             union all select  6, '11.5.1978'   from dual
             union all select  7, '2:22:2021'   from dual
             union all select  8, '2020-2-30'   from dual
           )
    order  by 1 
   /


One last example. This query has a built in 'Multi-Replace' function, allowing a Replace() like functionality but with lists of search/replace values.

   
   WITH
   ---------- /* Function Definition */
     function  replace_mltpl
       ( f_txt    varchar2
       , f_srch   varchar2
       , f_rplc   varchar2
       , f_dlim   char       default ','  
       )
     RETURN  VARCHAR2
     AS
        v_str   varchar2(4000) ;
        v_cnt   integer        ;
        v_exp   varchar2(20)   ;
        v_sep   char           := f_dlim ;
     BEGIN
        v_str := f_txt ; 
        v_cnt := regexp_count(f_srch, '\'||v_sep)+1 ;  
        v_exp := '\'||v_sep||'?([^\'||v_sep||']*)' ;
        for n in 1..v_cnt loop
           v_str := regexp_replace
                      ( v_str
                      , regexp_substr(f_srch, v_exp, 1, n, 'i', 1)
                      , '®¤'||n||'¤®'
                      ) ;
        end loop ;
        for n in 1..v_cnt loop
           v_str := regexp_replace
                      ( v_str
                      , '®¤'||n||'¤®'
                      , regexp_substr(f_rplc, v_exp, 1, n, 'i', 1)
                      ) ;
        end loop ;
        RETURN  v_str ; 
     END ;
   ---------- /* Sample Data */
     w_data  as
       ( select  NULL  as txt
              ,  NULL  as srch
              ,  NULL  as rplc
              ,  NULL  as dlm
           from  dual  where 1=2
         union all select  'A quick brown fox jumps over the lazy dog.' 
                        , 'fox~dog~lazy~quick' , 'dog~fox~quick~lazy'       
                        , '~'  
                     from dual
         union all select  'A quick brown fox jumps over the lazy dog.' 
                        , 'fox~dog~lazy~quick' , 'walaby~dingo~mangy~agile' 
                        , '~'  
                     from dual
         union all select  'a~b=c; x-y~z; 2=3~4; e=f-g'                 
                        , '~|=|-|;| '          , '=|-|~| |/'                
                        , '|'  
                     from dual
         union all select  'one two three; four five six; seven 8 9'    
                        , '; , ,e'             , ': ,-,E'                   
                        , ','  
                     from dual
       )
   ---------- /* Query */
   select  txt, srch, rplc, dlm
        ,  replace_mltpl( txt, srch, rplc, dlm )  as rslt
     from  w_data 
   /