Recursive WITH clause (3)

Tags

, , ,

How to implement CONNECT BY features

ORACLE 11g release 2 introduced the recursive WITH clause as an alternative to the well known CONNECT BY clause.

This Blog entry shows how to implement all the CONNECT BY features using the new recursive WITH clause.


Cycle Detection

If the data contains a cylce the query would run indefinitely. ORACLE detects these situations and lets the query fail.

UPDATE emp
   SET mgr = 7499
 WHERE empno = 7839
/

SELECT LEVEL, e.ename
  FROM emp e
CONNECT BY PRIOR e.empno = e.mgr
 START WITH e.empno = 7839
 ORDER SIBLINGS BY e.ename
/

ERROR:
ORA-01436: CONNECT BY loop in user data

Recursive suquery factoring throws a different error code with a similar message.

WITH r_emps (ename, empno, mgr, lvl, path, root)
         AS (SELECT e.ename, e.empno, e.mgr
                  , 1 as lvl
                  , '/' || e.ename AS path
                  , e.ename        AS root
               FROM emp e
              WHERE e.empno = 7839
             UNION ALL
             SELECT e.ename, e.empno, e.mgr
                  , m.lvl + 1                AS lvl
                  , m.path || '/' || e.ename AS path
                  , m.root
               FROM            emp    e
                    INNER JOIN r_emps m ON (m.empno = e.mgr))
SEARCH DEPTH FIRST BY ename SET sorting
SELECT e.lvl, e.ename, e.empno, e.mgr, e.path, e.root
  FROM r_emps e
 ORDER BY e.sorting
/

ERROR at line ..:
ORA-32044: cycle detected while executing recursive WITH query

NOCYLCE

Since ORACLE 10g the CONNECT BY clause knows the NOCYLCE attribute as well as the CONNECT_BY_ISCYCLE pseudo column to ignore cycles and detect nodes with a connection to a node already included in the result set (cycle).

SELECT  LEVEL
      , CONNECT_BY_ISCYCLE                AS CYCLE
      , e.ename
      , SYS_CONNECT_BY_PATH(e.ename, '/') AS path 
      , e.mgr
  FROM emp e
CONNECT BY NOCYCLE PRIOR e.empno = e.mgr
 START WITH e.empno = 7839
 ORDER SIBLINGS BY e.ename
/

LEVEL  CYCLE  ENAME     PATH                    MGR
-----  -----  --------  ---------------------  ----
    1  0      KING     /KING                   7499
    2  0      BLAKE    /KING/BLAKE             7839
    3  1      ALLEN    /KING/BLAKE/ALLEN       7698
    3  0      JAMES    /KING/BLAKE/JAMES       7698
    3  0      MARTIN   /KING/BLAKE/MARTIN      7698
    3  0      TURNER   /KING/BLAKE/TURNER      7698
    3  0      WARD     /KING/BLAKE/WARD        7698
    2  0      CLARK    /KING/CLARK             7839
    3  0      MILLER   /KING/CLARK/MILLER      7782
    2  0      JONES    /KING/JONES             7839
    3  0      FORD     /KING/JONES/FORD        7566
    4  0      SMITH    /KING/JONES/FORD/SMITH  7902
    3  0      SCOTT    /KING/JONES/SCOTT       7566

The cycle was detected at node ALLAN (CONNECT_BY_ISCYCLE returned the value 1) and further processing of this branch is stopped.
The recursive WITH clause also offers a clause to handle cycles.

WITH r_emps (ename, empno, mgr, lvl, path)
         AS (SELECT e.ename, e.empno, e.mgr, 1 AS lvl, '/' || e.ename AS path
               FROM emp e
              WHERE e.empno = 7839
             UNION ALL
     SELECT e.ename, e.empno, e.mgr, m.lvl + 1 AS lvl, m.path || '/' || e.ename AS path
               FROM            r_emps m 
                    INNER JOIN emp    e ON (e.mgr = m.empno))
SEARCH DEPTH FIRST BY ename SET SORTING
CYCLE empno SET is_cycle TO 1 DEFAULT 0
SELECT e.lvl AS LEVEL, is_cycle AS CYCLE, e.ename, e.path ,e.mgr
  FROM r_emps e
 ORDER BY e.sorting
/

LEVEL  CYCLE  ENAME      PATH                      MGR
-----  -----  ---------  -----------------------  ----
    1      0  KING       /KING                    7499
    2      0  BLAKE      /KING/BLAKE              7839
    3      0  ALLEN      /KING/BLAKE/ALLEN        7698
    4      1  KING       /KING/BLAKE/ALLEN/KING   7499
    3      0  JAMES      /KING/BLAKE/JAMES        7698
    3      0  MARTIN     /KING/BLAKE/MARTIN       7698
    3      0  TURNER     /KING/BLAKE/TURNER       7698
    3      0  WARD       /KING/BLAKE/WARD         7698
    2      0  CLARK      /KING/CLARK              7839
    3      0  MILLER     /KING/CLARK/MILLER       7782
    2      0  JONES      /KING/JONES              7839
    3      0  FORD       /KING/JONES/FORD         7566
    4      0  SMITH      /KING/JONES/FORD/SMITH   7902
    3      0  SCOTT      /KING/JONES/SCOTT        7566
    4      0  ADAMS      /KING/JONES/SCOTT/ADAMS  7788

The difference between CONNECT BY and RECURSIVE WITH cycle detection is that with RECURSIVE WITH the cycle is decteted after the next recursion level wass processed. The erroneous node is repeated and the cycle flag is set one level lower than the CONNECT_BY_ISCYCLE pseudo column.

Recursive WITH clause (2)

Tags

, , ,

How to implement CONNECT BY features

 ORACLE 11g release 2 introduced the recursive WITH clause as an alternative to the well known CONNECT BY clause.
This Blog entry shows how to implement all the CONNECT BY features using the new recursive WITH clause.

Root and Path

CONNECT BY clause knows the CONNECT_BY_ROOT operator which returns the root(s) of a hierarchy. Furthermore the SYS_CONNECT_BY_PATH function may be used to get a path from the root to the current element within the hierarchy:

SELECT e.ename, e.empno, e.mgr
      ,SYS_CONNECT_BY_PATH(e.ename,'/') AS path
      ,CONNECT_BY_ROOT e.ename          AS root
  FROM emp e
CONNECT BY PRIOR e.empno = e.mgr
 START WITH e.mgr IS NULL
 ORDER SIBLINGS BY e.ename
/

ENAME    EMPNO   MGR  PATH                     ROOT
-------  -----  ----  -----------------------  ----
KING      7839        /KING KING               
BLAKE     7698  7839  /KING/BLAKE              KING
ALLEN     7499  7698  /KING/BLAKE/ALLEN        KING
JAMES     7900  7698  /KING/BLAKE/JAMES        KING
MARTIN    7654  7698  /KING/BLAKE/MARTIN       KING
TURNER    7844  7698  /KING/BLAKE/TURNER       KING
WARD      7521  7698  /KING/BLAKE/WARD         KING
CLARK     7782  7839  /KING/CLARK              KING
MILLER    7934  7782  /KING/CLARK/MILLER       KING
JONES     7566  7839  /KING/JONES              KING
FORD      7902  7566  /KING/JONES/FORD         KING
SMITH     7369  7902  /KING/JONES/FORD/SMITH   KING
SCOTT     7788  7566  /KING/JONES/SCOTT        KING
ADAMS     7876  7788  /KING/JONES/SCOTT/ADAMS  KING

Same result using the recursive WITH clause:

WITH r_emps (ename, empno, mgr, path, root)
         AS (SELECT e.ename, e.empno, e.mgr
                  , '/' || e.ename AS path
                  , e.ename        AS root
               FROM emp e
              WHERE e.mgr IS NULL
             UNION ALL
             SELECT e.ename, e.empno, e.mgr
                  , m.path || '/' || e.ename AS path
                  , m.root
               FROM            emp    e
                    INNER JOIN r_emps m ON (m.empno = e.mgr))
SEARCH DEPTH FIRST BY ename SET sorting
SELECT e.ename, e.empno, e.mgr, e.path, e.root
  FROM r_emps e
 ORDER BY e.sorting
/

Path and root are set to ename in the anchor query. While the path is enlarged along the hierarchy by adding separator and path the root is simply passed all the way down to the leafs.

Recursive WITH clause (1)

Tags

, , ,

how to implement CONNECT BY features

ORACLE 11g release 2 introduced the recursive WITH clause as an alternative to the well known CONNECT BY clause.

This Blog entries show how to implement all the CONNECT BY features using the new recursive WITH clause.


Basic recursive query

CONNECT BY clause to show all employees starting with KING (who has no manager [mgr IS NULL]):

SELECT e.ename, e.empno, e.mgr
  FROM emp e
CONNECT BY PRIOR e.empno = e.mgr
 START WITH e.mgr IS NULL
/

Same result using the recursive WITH clause:

WITH r_emps (ename, empno, mgr)          AS (SELECT e.ename, e.empno, e.mgr
               FROM emp e
              WHERE e.mgr IS NULL
             UNION ALL
             SELECT e.ename, e.empno, e.mgr
               FROM            emp    e
                    INNER JOIN r_emps m ON (m.empno = e.mgr))
SELECT e.ename, e.empno, e.mgr
  FROM r_emps e
/

The first query block (anchor member) of the recursive WITH clause defines the root(s) of the hierarchy, the second the recursion. In the second block, we can see a join between the
anchor member and the emp table which is pretty much the same as the CONNECT BY clause in the traditional approach.


Formatting the result set

CONNECT BY queries often use the LEVEL pseudo column to format the output.

SELECT LEVEL, LPAD(' ',2*(LEVEL-1)) || e.ename AS ename
     , e.empno, e.mgr
  FROM emp e
CONNECT BY PRIOR e.empno = e.mgr
 START WITH e.mgr IS NULL
/

The same can be achieved using the following recursive WITH query:

WITH r_emps (ename, empno, mgr, lvl)
         AS (SELECT e.ename, e.empno, e.mgr, 1 AS lvl
               FROM emp e
              WHERE e.mgr IS NULL
             UNION ALL
            SELECT e.ename, e.empno, e.mgr, m.lvl + 1 AS lvl
               FROM            emp    e
                    INNER JOIN r_emps m ON (m.empno = e.mgr))
SELECT lvl, LPAD(' ',2*(lvl-1)) || e.ename AS ename
      ,e.empno, e.mgr
  FROM r_emps e
/

To simulate the LEVEL pseudo column using the recursive WITH clause, one has to add a virtual level column on the root elements, which will be incremented along the hierarchy.


Sorting the result set

The CONNECT BY clause has a dedicated ORDER BY clause (ORDER SIBLINGS BY) to sort the elements of a hierarchy.

SELECT LEVEL, LPAD(' ',2*(LEVEL-1)) || e.ename AS ename
     , e.empno, e.mgr
  FROM emp e
CONNECT BY PRIOR e.empno = e.mgr
 START WITH e.mgr IS NULL
 ORDER SIBLINGS BY e.ename
/

Same result can be achieved using the recursive WITH clause with the SEARCH clause.

WITH r_emps (ename, empno, mgr, lvl)
         AS (SELECT e.ename, e.empno, e.mgr, 1 AS lvl
               FROM emp e
              WHERE e.mgr IS NULL
             UNION ALL
             SELECT e.ename, e.empno, e.mgr, m.lvl + 1 AS lvl
               FROM            emp    e
                    INNER JOIN r_emps m ON (m.empno = e.mgr))
SEARCH DEPTH FIRST BY ename SET sorting
SELECT lvl, LPAD(' ',2*(lvl-1)) || e.ename AS ename
     , e.empno ,e.mgr
  FROM r_emps e
 ORDER BY e.sorting
/

SEARCH DEPTH FIRST will show the children before the siblings whereas SEARCH BREADTH FIRST would show the siblings before the children, the ename column is used as the order attribute within the hierarchy and assigned to the attribute sorting which can afterwards be used in the outermost query.

Working with a single date validity column

Tags

, , ,

Having a single column to determine the validity of a value has many advantages over a two column (valid_from / valid_to) approach. 

  • no need to care about gaps 
  • no need to care about overlaps
  • simple way to prevent duplicates possible (unique constraint)

Therefore this is often seen in parameter tables or any other tables storing different versions of an information with a time range validity.

The problem occurs when you try to use the correct information in your queries.


Prepare test case:

Our test case has an application parameter table (key,value,valid_from) holding information about the tax rate of the swiss vat.

CREATE TABLE appl_params (
    param_name VARCHAR2(20)  NOT NULL
   ,param_value       VARCHAR2(200) NOT NULL
   ,param_valid_from  DATE          NOT NULL)
/

A simple primary key on param_name, param_valid_from is sufficient to avoid duplicate validities for one parameter at a point in time.

ALTER TABLE appl_params 
   ADD CONSTRAINT appl_params_pk 
   PRIMARY KEY (param_name, param_valid_from)
/
INSERT INTO appl_params VALUES('SWISS VAT','7.6',TO_DATE('01.01.2005','dd.mm.yyyy'));
INSERT INTO appl_params VALUES('SWISS VAT','8.0',TO_DATE('01.01.2011','dd.mm.yyyy'));
INSERT INTO appl_params VALUES('SWISS VAT','7.0',TO_DATE('01.01.2014','dd.mm.yyyy'));
COMMIT;

To get the correct tax rate for a sale we need to find the rate belonging to the valid_from which is the latest before the sale takes place…. This is a rather complicated query.

WITH sales (prod, sale_date, price)
        AS (SELECT 'Product A', date '2009-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2010-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2011-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2013-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2015-07-27', 115 FROM dual)
 SELECT prod
      , sale_date
      , price
      , TO_NUMBER(ap.param_value)
      , price + (price * TO_NUMBER(ap.param_value)/100) AS price_incl_vat
   FROM sales s
       ,appl_params ap
  WHERE ap.param_name = 'SWISS VAT'
    AND ap.param_valid_from = (SELECT MAX(ap2.param_valid_from)
                                 FROM appl_params ap2
                                WHERE ap2.param_name = ap.param_name
                                  AND ap2.param_valid_from <= s.sale_date) 
/

To support easier and more intuitive queries we create a view which has an additional column param_valid_to. To populate this column we use the analytic function lead.

The solution underneath assumes that param_valid_to as well as sale_date do not include relevant time information (all date columns are truncated to midnight).

CREATE VIEW appl_params_vw
AS
SELECT param_name, param_value, param_valid_from
     , LEAD(param_valid_from,1,date '3000-01-01') 
             OVER (PARTITION BY param_name
  ORDER BY param_valid_from ASC) - 1 AS param_valid_to
  FROM appl_params             
/

After having created this view, the query to find the appropriate tax rate gets quite easy.

WITH sales (prod, sale_date, price)
        AS (SELECT 'Product A', date '2009-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2010-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2011-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2013-07-27', 115 FROM dual UNION ALL
            SELECT 'Product A', date '2015-07-27', 115 FROM dual)
SELECT s.prod
     , s.sale_date
     , s.price
     , TO_NUMBER(v.param_value) vat
     , s.price + (s.price * to_number(v.param_value)/100) AS price_incl_vat
  FROM            sales          s 
       INNER JOIN appl_params_vw v ON (    v.param_name = 'SWISS VAT'
                                       AND s.sale_date BETWEEN v.param_valid_from 
                                                           AND v.param_valid_to)
/

If the approach is not fast enough we would also have the possibility to materialize this view which is, for a table like the parameter table where changes do not happen to often, not a bad idea.

Calculating the number of weekdays between two dates

Tags

, ,

How can we calculate the number of business days (Monday – Friday) between two given dates….

This is a question often asked in the SQL community.

Two solutions for this problem:


Solution 1:

Calculate the number of days between two date values and subtract the number of Saturdays and Sundays in between.

WITH DATA AS (SELECT TO_DATE('05.09.2011','dd.mm.yyyy') date_from                    ,TO_DATE('31.10.2011','dd.mm.yyyy') date_to                FROM dual)
SELECT date_to - date_from + 1
     - (((NEXT_DAY(date_to-7,'Saturday') - NEXT_DAY(date_from,'Saturday'))/7) + 1)
     - (((NEXT_DAY(date_to-7,'Sunday')   - NEXT_DAY(date_from,'Sunday'))/7) + 1)
  FROM data
/

Solution 2:

Count the number of days that are neither Saturdays nor Sundays between two date values using a recursive query.

SELECT COUNT(DECODE(TO_CHAR(TO_DATE('05.09.2011','dd.mm.yyyy') + (ROWNUM - 1),'DY'),'SAT',NULL,'SUN',NULL,'1'))
   FROM dual
 CONNECT BY ROWNUM <= (TO_DATE('31.10.2011','dd.mm.yyyy') - TO_DATE('05.09.2011','dd.mm.yyyy') + 1) 
/ 

Rounding monetary values

Tags

, ,

A question that occurs from time to time is how to round a (monetary) amount to e.g. 5, 25, 50 cents. Basically this can be done easily using the ROUND function.

ROUNDING to 5 cents = ROUND(amount*20)/20
amount   multiplied by 20   rounded to integer   divided by 20
------   ----------------   ------------------   -------------
  1.02              20.40                   20            1.00 
  1.03              20.60                   21            1.05 
  1.37              27.40                   27            1.35

the same principle works for:

  •  round to 25 cents = ROUND(amount*4)/4
  •  round to 50 cents = ROUND(amount*2)/2

basically the general formula is:

ROUND(amount*(100/rounding_base)) / (100/rounding_base)

5 ways to aggregate columns into a comma separated string

Tags

, , , , ,

Connect By

The first example uses a recursive query which starts with the first node of every group and only shows those rows, where the whole string has been aggregated (connect_by_isleaf = 1).

SELECT deptno
     , rn
     , TRIM(leading ',' FROM SYS_CONNECT_BY_PATH(ename,',')) 
  FROM (SELECT deptno
             , ename
             , ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY ename) rn
          FROM emp)
  WHERE connect_by_isleaf = 1
  START WITH rn = 1
 CONNECT BY PRIOR deptno = deptno
       AND PRIOR rn + 1 = rn
 /

XMLAGG

another solution is to use xml functions like xmlagg (aggregate) and xmlelement.

SELECT deptno
      ,RTRIM(XMLAGG(XMLELEMENT(e,ename,',').EXTRACT('//text()')),',') name
  FROM emp
 GROUP BY deptno
 /

 WM_CONCAT

one more solution even if not documented…

SELECT deptno, WM_CONCAT(ename)
  FROM emp
 GROUP by deptno
 /

and if you need the enames to be ordered…

SELECT deptno
     , WM_CONCAT(ename)
 FROM (SELECT deptno, ename 
         FROM emp 
        ORDER BY ename)
 GROUP by deptno
 /

 User defined aggregator

Another solution is to write your own group function

CREATE OR REPLACE TYPE ListAggregator IS OBJECT (
   vList VARCHAR2(4000)
  ,STATIC FUNCTION ODCIAggregateInitialize (sctx IN OUT ListAggregator) RETURN NUMBER
  ,MEMBER FUNCTION ODCIAggregateIterate (SELF  IN OUT ListAggregator
                                       , value IN     VARCHAR2 ) RETURN NUMBER
  ,MEMBER FUNCTION ODCIAggregateTerminate (self        IN OUT ListAggregator
                                         , returnValue    OUT VARCHAR2
                                         , flags       IN     NUMBER ) RETURN NUMBER
  ,MEMBER FUNCTION ODCIAggregateMerge (self IN OUT ListAggregator
                                     , ctx2 IN     ListAggregator ) RETURN NUMBER
 );
 /

CREATE OR REPLACE TYPE BODY ListAggregator IS
   STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT ListAggregator) RETURN NUMBER IS
   BEGIN
      sctx := ListAggregator(NULL);
      RETURN ODCIConst.SUCCESS;
   END ODCIAggregateInitialize;

   MEMBER FUNCTION ODCIAggregateIterate (self  IN OUT ListAggregator
                                       , value IN     VARCHAR2 ) RETURN NUMBER IS
   BEGIN
      IF value IS NOT NULL
      THEN
         self.vList := concat(concat(self.vList,', '),value);
      END IF;

      RETURN ODCIConst.SUCCESS;
   END ODCIAggregateIterate;

   MEMBER FUNCTION ODCIAggregateTerminate (self        IN OUT ListAggregator
                                         , returnValue    OUT VARCHAR2
                                         , flags       IN     NUMBER ) RETURN NUMBER IS
   BEGIN
      returnValue := LTrim(self.vList, ', ');
      RETURN ODCIConst.SUCCESS;
   END ODCIAggregateTerminate;

   MEMBER FUNCTION ODCIAggregateMerge (self IN OUT ListAggregator
                                     , ctx2 IN     ListAggregator) RETURN NUMBER IS
   BEGIN
      IF self.vList IS NULL
      THEN
         self.vList := ctx2.vList;
      ELSIF ctx2.vList IS NOT NULL
      THEN
         self.vList := concat(concat(self.vList,', '),ctx2.vList);
      END IF;

      RETURN ODCIConst.SUCCESS;
   END ODCIAggregateMerge;
END;
/

CREATE OR REPLACE FUNCTION LIST(vValue IN VARCHAR2) RETURN VARCHAR2
   PARALLEL_ENABLE
   AGGREGATE
   USING ListAggregator;
/

SELECT deptno
     , LIST(ename)
  FROM emp
 GROUP BY deptno
/

LISTAGG

and finally if you are on ORACLE 11R2, there is a built-in function to do list aggregation

SELECT LISTAGG(ename,',') WITHIN GROUP (ORDER BY ename)
 FROM emp
 GROUP BY deptno
 /

Finding Gaps

Tags

,

One of the things often asked for is how to find gaps in a list of numbers. So here’s a rather old suggestion:

WITH gaps (id) AS (SELECT  1 FROM dual UNION ALL
                   SELECT  3 FROM dual UNION ALL
                   SELECT  4 FROM dual UNION ALL
                   SELECT  7 FROM dual UNION ALL
                   SELECT 15 FROM dual)
    ,borders AS (SELECT MIN(id) mins
                       ,MAX(id) maxs
                   FROM gaps)
SELECT mins + (ROWNUM - 1) AS missing_value
  FROM borders
CONNECT BY mins + (ROWNUM - 1) <> maxs
MINUS
SELECT id
  FROM gaps
/

Calendar Query

Tags

,

This post shows how to create a calendar view using pure SQL.

E.g. September 2016 should look like this:

WEEK MON TUE WED THU FRI SAT SUN
---- --- --- --- --- --- --- ---
  35              01  02  03  04
  36  05  06  07  08  09  10  11
  37  12  13  14  15  16  17  18
  38  19  20  21  22  23  24  25
  39  26  27  28  29  30

To create this kind of calendar you can use recursive queries (aka connect by queries), to create a list of all days belonging to the current month.

SELECT TRUNC(SYSDATE,'Month') + (ROWNUM - 1) AS theDate
  FROM dual
CONNECT BY TRUNC(SYSDATE,'Month') + (ROWNUM - 1) <= LAST_DAY(SYSDATE);

Having this list of all days the only additional thing we have to consider is how to group and format this data. I have choosen a DECODE approach to group the dates to the appropriate weekday column.

WITH days_of_curr_month AS (SELECT TRUNC(SYSDATE,'Month') + (ROWNUM - 1) AS theDate
                               FROM dual
                            CONNECT BY TRUNC(SYSDATE,'Month') + (ROWNUM - 1) <= LAST_DAY(SYSDATE))   
    ,data_formatter     AS (SELECT TO_CHAR(theDate,'IYYY')    AS Iyear                                   
                                  ,TO_CHAR(theDate,'IW')      AS Iweek                                   
                                  ,TO_CHAR(theDate,'DY','NLS_DATE_LANGUAGE=AMERICAN') AS MyDateDay                                   
                                  ,TO_CHAR(theDate,'DD')      AS MyDate                               
                               FROM days_of_curr_month) 
SELECT IWeek                               AS Week       
      ,MAX(DECODE(MyDateDay,'MON',MyDate)) AS MON       
      ,MAX(DECODE(MyDateDay,'TUE',MyDate)) AS TUE       
      ,MAX(DECODE(MyDateDay,'WED',MyDate)) AS WED       
      ,MAX(DECODE(MyDateDay,'THU',MyDate)) AS THU       
      ,MAX(DECODE(MyDateDay,'FRI',MyDate)) AS FRI       
      ,MAX(DECODE(MyDateDay,'SAT',MyDate)) AS SAT       
      ,MAX(DECODE(MyDateDay,'SUN',MyDate)) AS SUN 
  FROM data_formatter 
 GROUP BY IYear ,Iweek
 ORDER BY IYear ,IWeek;

The IYear information is needed as in the ISO-Calendar dates like January 1 – January 3 or December 29 – December 31 may belong to the previous or the following calendar year (week 53 or 1) and we have to make sure, that week 1 of the following year is placed at the end of the list and week 53 of the previous year is placed on top.