Skip to Main Content
  • Questions
  • Methods to find which rows are part of result of analytic function with window clause

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Kim Berg.

Asked: November 12, 2015 - 9:27 am UTC

Last updated: November 13, 2015 - 9:17 am UTC

Version: 12.1.0.2

Viewed 1000+ times

You Asked

I'm working on a little something to demonstrate use of analytic functions and how window clause works. For that I wish for each row that has the result of an analytic function to know which of all the rows is part of that result.

I did a little script on LiveSql: https://livesql.oracle.com/apex/livesql/s/cfppbobpjp4lgmqp4i9ioflh9

I would have liked to be able to do something like this:

select department_id, employee_id, last_name, salary
     , sum(salary) over (
          partition by department_id
          order by salary
          range between unbounded preceding and current row
       ) range_sum
     , listagg(employee_id,',') within group (
          order by salary
       ) over (
          partition by department_id
          order by salary
          range between unbounded preceding and current row
       ) who_in_range
  from hr.employees
 where department_id in (30,90)
 order by department_id, salary
/


It fails with ORA-30487: ORDER BY not allowed here, where I would have liked it to return this instead:

DEPARTMENT_ID EMPLOYEE_ID LAST_NAME  SALARY  RANGE_SUM WHO_IN_RANGE
------------- ----------- ---------- ------ ---------- ------------
           30         119 Colmenares   2500       2500 119
           30         118 Himuro       2600       5100 119,118
           30         117 Tobias       2800       7900 119,118,117
           30         116 Baida        2900      10800 119,118,117,116
           30         115 Khoo         3100      13900 119,118,117,116,115
           30         114 Raphaely    11000      24900 119,118,117,116,115,114
           90         102 De Haan     17000      34000 101,102
           90         101 Kochhar     17000      34000 101,102
           90         100 King        24000      58000 101,102,100


Or even better yet, I would have liked this to work:

create type employees_table_type as table of number
/

select department_id, employee_id, last_name, salary
     , sum(salary) over (
          partition by department_id
          order by salary
          range between unbounded preceding and current row
       ) range_sum
     , cast(collect(employee_id order by salary) over (
          partition by department_id
          order by salary
          range between unbounded preceding and current row
       ) as employees_table_type) who_in_range
  from hr.employees
 where department_id in (30,90)
 order by department_id, salary
/


But it fails with ORA-06553: PLS-306: wrong number or types of arguments in call to 'SYS_NT_COLLECT'

I have made a "somewhat" solution:

select department_id, employee_id, last_name, salary
     , sum(salary) over (
          partition by department_id
          order by salary
          range between unbounded preceding and current row
       ) range_sum
     , employees_table_type(
          nth_value(employee_id,1) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,2) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,3) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,4) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,5) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,6) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,7) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,8) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
        , nth_value(employee_id,9) over (
             partition by department_id
             order by salary
             range between unbounded preceding and current row
          )
       ) who_in_range
  from hr.employees
 where department_id in (30,90)
 order by department_id, salary
/


For my purposes it is "doable" as my demonstration setup I know exactly the number of rows, so I can do like this where I have 9 NTH_VALUE calls.

But I keep thinking there ought to be a better way somehow and hope someone might come up with something ;-)

The important bit is, that what I want is something that can accept the identical window clause as the SUM() call, and then give me which rows are part of that window. So I can change the window clause (both in the SUM() call and in the "something") and still get which rows gave each individual sum.

Any ideas?

Thanks
Kim Berg Hansen

and Chris said...

Although the window clause doesn't work with listagg, it does with Tom Kyte's predecessor for this stragg:

create or replace type stragg_type as object
(
  string varchar2(4000),

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

  member function ODCIAggregateIterate
    ( self  in out stragg_type ,
      value in     varchar2
    ) return number ,

  member function ODCIAggregateTerminate
    ( self        in  stragg_type,
      returnvalue out varchar2,
      flags in number
    ) return number ,

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

create or replace type body stragg_type
is

  static function ODCIAggregateInitialize
  ( sctx in out stragg_type )
  return number
  is
  begin

    sctx := stragg_type( null ) ;

    return ODCIConst.Success ;

  end;

  member function ODCIAggregateIterate
  ( self  in out stragg_type ,
    value in     varchar2
  ) return number
  is
  begin

    self.string := self.string || ',' || value ;

    return ODCIConst.Success;

  end;

  member function ODCIAggregateTerminate
  ( self        in  stragg_type ,
    returnvalue out varchar2 ,
    flags       in  number
  ) return number
  is
  begin

    returnValue := ltrim( self.string, ',' );

    return ODCIConst.Success;

  end;

  member function ODCIAggregateMerge
  ( self in out stragg_type ,
    ctx2 in     stragg_type
  ) return number
  is
  begin

    self.string := self.string || ctx2.string;

    return ODCIConst.Success;

  end;

end;
/

create or replace function stragg
  ( input varchar2 )
  return varchar2
  deterministic
  parallel_enable
  aggregate using stragg_type;
/

select employee_id, salary,
       sum(salary) over (
         partition by department_id 
         order by salary range between unbounded preceding and current row
       ) tot_all_prev , 
       stragg(employee_id) over (
         partition by department_id 
         order by salary range between unbounded preceding and current row
       ) str_all_prev,
       sum(salary) over (
         partition by department_id 
         order by salary rows between 2 preceding and current row
       ) tot_2_prev , 
       stragg(employee_id) over (
         partition by department_id 
         order by salary rows between 2 preceding and current row
       ) str_2_prev
from   hr.employees
where  department_id in (30,90)
order  by department_id, salary;

EMPLOYEE_ID     SALARY TOT_ALL_PREV STR_ALL_PREV              TOT_2_PREV STR_2_PREV    
----------- ---------- ------------ ------------------------- ---------- ---------------
        119       2500         2500 119                             2500 119            
        118       2600         5100 119,118                         5100 119,118        
        117       2800         7900 119,118,117                     7900 119,118,117    
        116       2900        10800 119,118,117,116                 8300 118,117,116    
        115       3100        13900 119,118,117,116,115             8800 117,116,115    
        114      11000        24900 119,118,117,116,115,114        17000 116,115,114    
        102      17000        34000 102,101                        17000 102            
        101      17000        34000 102,101                        34000 102,101        
        100      24000        58000 102,101,100                    58000 102,101,100


And here it is in LiveSQL:

https://livesql.oracle.com/apex/livesql/file/content_CFRK6W2PJ04EQTSVJQ2E1G4FI.html

Rating

  (1 rating)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Excellent

Kim Berg Hansen, November 13, 2015 - 9:29 am UTC

Excellent that using ODCI interface to create our own aggregate/analytic functions support windowing clause. I think I can create a custom COLLECT function that would work nicely ;-)

More to Explore

Analytics

Analytic SQL got you confused? Check out Connor McDonald's complete video course.