SQL Oracle, what is done here? - sql

Can someone telle me what this code exactly does?
What does the 4 mean after count-function?
SELECT ROUND(SUM(correct)/COUNT(*),4) AS accuracy
FROM (SELECT DECODE(survived,
PREDICTION(DT_TITANIC USING *), 1, 0) AS correct
FROM titanic_test_data);

What does it do? Calculates an average and rounds it to 4 decimals.
SQL> select sum(sal) sum_salary,
2 count(*) num_of_employees,
3 --
4 sum(sal) / count(*) average_salary,
5 --
6 round(sum(sal) / count(*), 4) rounded_avg_salary
7 from emp;
SUM_SALARY NUM_OF_EMPLOYEES AVERAGE_SALARY ROUNDED_AVG_SALARY
---------- ---------------- -------------- ------------------
29025 14 2073,21429 2073,2143
sum_salary is sum of all salaries in the table
num_of_employees is number of employees
average_salary, as sum_salary divided by num_of_employees
rounded_avg_salary is what "your" code does
Note that we usually do it as
SQL> select avg(sal) from emp;
AVG(SAL)
----------
2073,21429

Related

Analytic functions and means of window clause

I'm using Oracle and SQL Developer. I have downloaded HR schema and need to do some queries with it. Now I'm working with table Employees. As an user I need to see employees with the highest gap between their salary and the average salary of all later hired colleagues in corresponding department. It seems quite interesting and really complicated. I have read some documentation and tried, for example LEAD(), that provides access to more than one row of a table at the same time:
SELECT
employee_id,
first_name
|| ' '
|| last_name,
department_id,
salary,
hire_date,
LEAD(hire_date)
OVER(PARTITION BY department_id
ORDER BY
hire_date DESC
) AS Prev_hiredate
FROM
employees
ORDER BY
department_id,
hire_date;
That shows for every person in department hiredate of later hired person. Also I have tried to use window clause to understand its concepts:
SELECT
employee_id,
first_name
|| ' '
|| last_name,
department_id,
hire_date,
salary,
AVG(salary)
OVER(PARTITION BY department_id
ORDER BY
hire_date ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING
) AS avg_sal
FROM
employees
ORDER BY
department_id,
hire_date;
The result of this query will be:
However, it is not exactly what I need. I need to reduce the result just by adding column with gap (salary-avr_sal), where the gap will be highest and receive one employee per department. How should the result look like: for example, we have 60 department. We have 5 employees there ordering by hire_date. First has salary 4800, second – 9000, third – 4800, fourth – 4200, fifth – 6000. If we do calculations: 4800 - ((9000+4800+4200+6000)/4)=-1200, 9000-((4800+4200+6000)/3)=4000, 4800 -((4200+6000)/2)=-300, 4200 - 6000=-1800 and the last person in department will have the highest gap: 6000 - 0 = 6000. Let's take a look on 20 department. We have two people there: first has salary 13000, second – 6000. Calculations: 13000 - 6000 = 7000, 6000 - 0 = 6000. The highest gap will be for first person. So for department 20 the result should be person with salary 13000, for department 60 the result should be person with salary 6000 and so on.
How should look my query to get the appropriate result (what I need is marked bold up, also I want to see column with highest gap, can be different solutions with analytic functions, but should be necessarily included window clause)?
You can get the average salary of employees that were hired prior to the current employee by just adapting the rows clause of your avg:
AVG(salary) OVER(
PARTITION BY department_id
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS avg_salary
The 1 PRECEDING clause tells the database not to include the current row in the window.
If you are looking for the employees with the greatest gap to that average, we can just order by the resultset:
SELECT e.*,
AVG(salary) OVER(
PARTITION BY department_id
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS avg_salary
FROM employees e
ORDER BY ABS(salary - avg_salary) DESC;
Finally, if you want the top "outlier salary" per department, then we need at least one more level. The shortest way to express this probably is to use ROW_NUMBER() to rank employees in each department by their salary gap to the average, and then to fetch all top rows per group using WITH TIES:
SELECT *
FROM (
SELECT e.*,
AVG(salary) OVER(
PARTITION BY department_id
ORDER BY hire_date
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS avg_salary
FROM employees e
) e
ORDER BY ROW_NUMBER() OVER(
PARTITION BY department_id
ORDER BY ABS(salary - avg_salary) DESC
)
FETCH FIRST ROW WITH TIES
Maybe this is what you are looking for.
Sample data:
WITH
emp (ID, EMP_NAME, HIRE_DATE, SALARY, DEPT) AS
(
Select 601, 'HILLER', To_Date('23-JAN-82', 'dd-MON-yy'), 4800, 60 From Dual Union All
Select 602, 'MILLER', To_Date('23-FEB-82', 'dd-MON-yy'), 9000, 60 From Dual Union All
Select 603, 'SMITH', To_Date('23-MAR-82', 'dd-MON-yy'), 4800, 60 From Dual Union All
Select 604, 'FORD', To_Date('23-APR-82', 'dd-MON-yy'), 4200, 60 From Dual Union All
Select 605, 'KING', To_Date('23-MAY-82', 'dd-MON-yy'), 6000, 60 From Dual Union All
Select 201, 'SCOT', To_Date('23-MAR-82', 'dd-MON-yy'), 13000, 20 From Dual Union All
Select 202, 'JONES', To_Date('23-AUG-82', 'dd-MON-yy'), 6000, 20 From Dual
),
Create CTE named grid with several analytic functions and windowing clauses. They are not all needed but the resulting dataset below shows the logic with all components included.
grid AS
(
Select
g.*, Max(GAP) OVER(PARTITION BY DEPT) "DEPT_MAX_GAP"
From
(
Select
ROWNUM "RN",
Sum(1) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN Unbounded Preceding And Current Row) "RN_DEPT",
ID, EMP_NAME, HIRE_DATE, DEPT, SALARY,
--
Nvl(Sum(SALARY) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following), 0) "SUM_SAL_LATER",
Nvl(Sum(1) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following), 0) "COUNT_EMP_LATER",
--
Nvl(Sum(SALARY) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following) /
Sum(1) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following), 0) "AVG_LATER",
--
SALARY -
Nvl((
Sum(SALARY) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following) /
Sum(1) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following)
), 0) "GAP"
from
emp
Order By
DEPT, HIRE_DATE, ID
) g
Order By
RN
)
CTE grid resultiing dataset:
RN
RN_DEPT
ID
EMP_NAME
HIRE_DATE
DEPT
SALARY
SUM_SAL_LATER
COUNT_EMP_LATER
AVG_LATER
GAP
DEPT_MAX_GAP
1
1
601
HILLER
23-JAN-82
60
4800
24000
4
6000
-1200
6000
2
2
602
MILLER
23-FEB-82
60
9000
15000
3
5000
4000
6000
3
3
603
SMITH
23-MAR-82
60
4800
10200
2
5100
-300
6000
4
4
604
FORD
23-APR-82
60
4200
6000
1
6000
-1800
6000
5
5
605
KING
23-MAY-82
60
6000
0
0
0
6000
6000
6
1
201
SCOT
23-MAR-82
20
13000
6000
1
6000
7000
7000
7
2
202
JONES
23-AUG-82
20
6000
0
0
0
6000
7000
Main SQL
SELECT
g.ID, g.EMP_NAME, g.HIRE_DATE, g.DEPT, g.SALARY, g.GAP
FROM
grid g
WHERE
g.GAP = g.DEPT_MAX_GAP
Order By
RN
Resulting as:
ID
EMP_NAME
HIRE_DATE
DEPT
SALARY
GAP
605
KING
23-MAY-82
60
6000
6000
201
SCOT
23-MAR-82
20
13000
7000
Without CTE and with all unnecessery columns excluded it looks like this:
SELECT ID, EMP_NAME, HIRE_DATE, DEPT, SALARY, GAP
FROM
(
( Select g.*, Max(GAP) OVER(PARTITION BY DEPT) "DEPT_MAX_GAP"
From( Select
ID, EMP_NAME, HIRE_DATE, DEPT, SALARY,
SALARY -
Nvl(( Sum(SALARY) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following) /
Sum(1) OVER(Partition By DEPT Order By DEPT, HIRE_DATE, ID ROWS BETWEEN 1 Following And Unbounded Following)
), 0) "GAP"
From emp
Order By DEPT, HIRE_DATE, ID
) g
)
)
WHERE GAP = DEPT_MAX_GAP
Order By DEPT, HIRE_DATE, ID
It seems like this is all you need.
Regards...

oracle database query for getting monthly salary and total salary

I have a table where monthly salary of employees are stored.
create table myemp
(
empno number ,
month number,
year number,
salary number
);
Now i need a query to get results like below
empno|month|Year|salary
0001 2 2016 10000
0001 3 2016 11000
0001 4 2016 12000
0001 -- ---- (10000+11000+12000)
0002 2 2016 15000
0002 3 2016 16000
0002 4 2016 15000
0002 -- ----(15000+16000+15000)
We can set total and subtotal using Rollup function of oracle like given below
select empno,month,year,sum(salary) from myemp
GROUP BY year,ROLLUP (empno,month)
here empno and month are in rollup function that gives total and subtotal of
empno and month group.
i hope this will help.
Here you go:
SELECT *
FROM (
(
SELECT empno, month, year, salary
FROM myemp
)
UNION ALL
(
SELECT empno, NULL AS month, NULL AS year, sum(salary)
FROM myemp
GROUP BY empno
)
) AS foo
ORDER BY empno, year IS NULL, year, month
would look like this
select lastname , dept_no, salary,
sum(salary) over (partition by dept_no order by lastname) dept_total
from myemp
order by salary, lastname;

How do i find who got maximum % salary from employee table?

Here the emp table :
To show the employee who got the maximum % salary between 01-jan-81 and 31-dec-81
EMPNO ENAME SAL HIREDATE
------- ---------- ---------- ---------
7369 SMITH 800 17-DEC-80
7499 ALLEN 1600 20-FEB-81
7521 WARD 1250 22-FEB-81
7566 JONES 2975 02-APR-81
7654 MARTIN 1250 28-SEP-81
7698 BLAKE 2850 01-MAY-81
7782 CLARK 2450 09-JUN-81
7788 SCOTT 3000 09-DEC-82
7839 KING 5000 17-NOV-81
7844 TURNER 1500 08-SEP-81
7876 ADAMS 1100 12-JAN-83
7900 JAMES 950 03-DEC-81
7902 FORD 3000 03-DEC-81
7934 MILLER 1300 23-JAN-82
The typical way to do this is to use order by and some sort of limit. In Oracle 12c+, you can do:
select t.*
from emptable t
where hiredate between date '1981-01'01' and date '1981-12-31'
order by sal desc
fetch first 1 row only;
In earlier versions, you can use a subquery:
select x.*
from (select t.*
from emptable t
where hiredate between date '1981-01'01' and date '1981-12-31'
order by sal desc
) x
where rownum = 1;
Note: This only shows one employee. If there are ties, an arbitrary employee is chosen. To get all of them, use rank() or dense_rank():
select x.*
from (select t.*, dense_rank() over (order by sal desc) as seqnum
from emptable t
where hiredate between date '1981-01'01' and date '1981-12-31'
order by sal desc
) x
where seqnum = 1;
SELECT EMPNO, ENAME from EMPTABLE
WHERE
sal = (SELECT MAX(sal) from EMPTABLE where hiredate BETWEEN '01-JAN-81' and '31-DEC-81')
AND
hiredate BETWEEN '01-JAN-81' and '31-DEC-81'
Employee with the maximum percentage salary also has the maximum salary in the given period.
PS: This is generic sql syntax
First we create a temp table that includes all salary of everyone in specified date and then we select the person who got the most using max.
SELECT ENAME,max(ALLSAL)
FROM (SELECT ENAME,SUM(SAL) AS ALLSAL
FROM t1 GROUP BY ENAME
WHERE hiredate between date '1981-01'01' and date '1981-12-31' ) AS t2
Just in case you want to print % salary also
SELECT *
FROM
(SELECT emp.*,
round((salary/
(SELECT SUM(salary) FROM emp
))*100,2) pcent
FROM emp
WHERE hiredate BETWEEN DATE '1981-01-01' AND DATE '1981-12-31'
ORDER BY pcent DESC
)
WHERE ROWNUM<=1;
You can use KEEP ( DENSE_RANK [FIRST|LAST] ORDER BY ... ) to get the maximum of two columns:
SELECT MAX( EName ) KEEP ( DENSE_RANK LAST ORDER BY Salary ) AS EName,
100 * MAX( Salary ) / SUM( Salary ) AS Percentage_Of_Total_Salary
FROM Employees
WHERE HIRE_DATE >= DATE '1981-01-01'
AND HIRE_DATE < DATE '1982-01-01';
Something like this should work.
with selected_rows as (
select empno, ename, sal
from scott.emp
where hiredate between date '1981-01-01' and date '1981-12-31'
),
max_and_total_sal (max_sal, total_sal) as (
select max(sal), sum(sal)
from selected_rows
)
select empno, ename, sal, to_char(100*sal/total_sal, '99.99') || '%' as percent_sal
from selected_rows cross join max_and_total_sal
where sal = max_sal;
Result:
EMPNO ENAME SAL PERCENT_SAL
---------- ---------- ---------- -----------
7839 KING 5000 21.91%
1 row selected.
Note that the percentage is based on the sum of salaries of the employees hired in 1981 (NOT the sum of all salaries in the table). As a quick verification, clearly King has the highest salary (and therefore also the highest percentage) of all employees, and indeed he was hired in 1981, so he must be the (unique) answer to the "WHO" part of the question.
Note also that the solution depends on KNOWING that the hiredate column in the table only stores "pure dates" (that is, with the time set to 00:00:00); otherwise the "between ... and ..." part would have to be refined.

Oracle Database sql query. Having?

Show the name of all the employees who were hired on the day of the week on which the highest number of employees were hired.
Table:
Steven 06/17/1987
Neena 09/21/1989
Lex 01/13/1993
Alex 01/03/1990
Bruce 05/21/1991
Diana 02/07/1999
Kevin 11/16/1999
Trenna 10/17/1995
Curtis 01/29/1997
Randall 03/15/1998
Peter 07/09/1998
Eleni 01/29/2000
Ellen 05/11/1996
Jonath 03/24/1998
Kimber 05/24/1999
Jenni 09/17/1987
Michael 02/17/1996
Pat 08/17/1997
Shelley 06/07/1994
William 06/07/1994
What I have so far.
SELECT FIRST_NAME, to_char(hire_date,'d') AS DOW FROM EMPLOYEES;
Steven 4
Neena 5
Lex 4
Alex 4
Bruce 3
Diana 1
Kevin 3
Trenna 3
Curtis 4
Randall 1
Peter 5
Eleni 7
Ellen 7
Jonath 3
Kimbe 2
Jenni 5
Michael 7
Pat 1
Shelley 3
William 3
Sunday is 1, monday is 2, ... so on...
Now i need to select the one with the max repeating number.
Which by looking at the table we will know it's 3 (tuesday). I know i will need to use a subquery to get it, is it having?
I would be inclined to use analytic functions for this:
select e.*
from (SELECT to_char(hire_date, 'd') AS DOW, count(*) as cnt,
row_number() over (order by count(*) desc) as seqnum
FROM EMPLOYEES
) dow join
EMPLOYEEs e
on dow.DOW = to_char(e.hire_date, 'd') and seqnum = 1;
One way, extending your query above (SQL Fiddle Example):
SELECT FIRST_NAME, to_char("hire_date", 'd') AS DOW
FROM EMPLOYEES
WHERE to_char("hire_date", 'd') =
(
SELECT b.DOW
FROM
(
select a.*, ROWNUM rnum
from (
SELECT to_char("hire_date", 'd') AS DOW, COUNT(1) AS cnt
FROM EMPLOYEES
GROUP BY to_char("hire_date", 'd')
ORDER BY cnt DESC
) a
where rownum = 1
) b
)
select *
from employees
where to_char(hire_date, 'd') = (
select max(to_char(hire_date, 'd')) keep (dense_rank last order by count(*))
from employees
group by to_char(hire_date, 'd')
);
SQLFiddle

(Oracle) how to group rows for pagination

I have a table that outputs similar to this (although in thousands):
EMPNO ENAME TRANDATE AMT
---------- ---------- --------- -------
100 Alison 21-MAR-96 45000
100 Alison 12-DEC-78 23000
100 Alison 24-OCT-82 11000
101 Linda 15-JAN-84 16000
101 Linda 30-JUL-87 17000
102 Celia 31-DEC-90 78000
102 Celia 17-SEP-96 21000
103 James 21-MAR-96 45000
103 James 12-DEC-78 23000
103 James 24-OCT-82 11000
104 Robert 15-JAN-84 16000
104 Robert 30-JUL-87 17000
My desired output would be similar to this:
EMPNO ENAME TRANDATE AMT PAGE
---------- ---------- --------- ------- ----
100 Alison 21-MAR-96 45000 1
100 Alison 12-DEC-78 23000 1
100 Alison 24-OCT-82 11000 1
101 Linda 15-JAN-84 16000 2
101 Linda 30-JUL-87 17000 2
102 Celia 31-DEC-90 78000 2
102 Celia 17-SEP-96 21000 2
103 James 21-MAR-96 45000 3
104 Robert 12-DEC-78 23000 4
104 Robert 24-OCT-82 11000 4
104 Robert 15-JAN-84 16000 4
104 Robert 30-JUL-87 17000 4
Basically, it should insert a new field to identify the page it belongs to. The page break is based on the rows. And, as if "kept together" in EMPNO, it adds 1 to PAGE when the rows cannot add the next EMPNO batch. It's for the Excel limit since Excel does not allow more than 65000 rows (or so) in a single Sheet. In the sample's case, it's only 4 rows. The limit number is static.
ThinkJet is right that that some of the other answers don't cater for the 'keep together' requirement. However I think this can be done without resorting to a user-defined aggregate.
Sample data
create table test (empno number, ename varchar2(20), trandate date, amt number);
insert into test values (100, 'Alison' , to_date('21-MAR-1996') , 45000);
insert into test values (100, 'Alison' , to_date('12-DEC-1978') , 23000);
insert into test values (100, 'Alison' , to_date('24-OCT-1982') , 11000);
insert into test values (101, 'Linda' , to_date('15-JAN-1984') , 16000);
insert into test values (101, 'Linda' , to_date('30-JUL-1987') , 17000);
insert into test values (102, 'Celia' , to_date('31-DEC-1990') , 78000);
insert into test values (102, 'Celia' , to_date('17-SEP-1996') , 21000);
insert into test values (103, 'James' , to_date('21-MAR-1996') , 45000);
insert into test values (103, 'James' , to_date('12-DEC-1978') , 23000);
insert into test values (103, 'James' , to_date('24-OCT-1982') , 11000);
insert into test values (104, 'Robert' , to_date('15-JAN-1984') , 16000);
insert into test values (104, 'Robert' , to_date('30-JUL-1987') , 17000);
Now, determine the end row of each empno segment (using RANK to find the start and COUNT..PARTITION BY to find the number in the segment).
Then use ceil/4 from APC's solution to group them into their 'pages'. Again, as pointed out by ThinkJet, there is a problem in the specification as it doesn't cater for the situation when there are more records in the empno 'keep together' segment than can fit in a page.
select empno, ename,
ceil((rank() over (order by empno) +
count(1) over (partition by empno))/6) as chunk
from test
order by 1;
As pointed out by ThinkJet, this solution isn't bullet proof.
drop table test purge;
create table test (empno number, ename varchar2(20), trandate date, amt number);
declare
cursor csr_name is
select rownum emp_id,
decode(rownum,1,'Alan',2,'Brian',3,'Clare',4,'David',5,'Edgar',
6,'Fred',7,'Greg',8,'Harry',9,'Imran',10,'John',
11,'Kevin',12,'Lewis',13,'Morris',14,'Nigel',15,'Oliver',
16,'Peter',17,'Quentin',18,'Richard',19,'Simon',20,'Terry',
21,'Uther',22,'Victor',23,'Wally',24,'Xander',
25,'Yasmin',26,'Zac') emp_name
from dual connect by level <= 26;
begin
for c_name in csr_name loop
for i in 1..11 loop
insert into test values
(c_name.emp_id, c_name.emp_name, (date '2010-01-01') + i,
to_char(sysdate,'SS') * 1000);
end loop;
end loop;
end;
/
select chunk, count(*)
from
(select empno, ename,
ceil((rank() over (order by empno) +
count(1) over (partition by empno))/25) as chunk
from test)
group by chunk
order by chunk
;
So with chunk size of 25 and group size of 11, we get the jumps where it fits 33 people in the chunk despite the 25 limit. Large chunk sizes and small groups should make this infrequent, but you'd want to allow some leeway. So maybe set the chunks to 65,000 rather than going all the way to 65,536.
It's too tricky or even impossible to do such thing in plain SQL.
But with some limitations problem can be resolved with help of user-defined aggregate functions .
First, create object with ODCIAggregate interface implementation:
create or replace type page_num_agg_type as object
(
-- Purpose : Pagination with "leave together" option
-- Attributes
-- Current page number
cur_page_number number,
-- Cumulative number of rows per page incremented by blocks
cur_page_row_count number,
-- Row-by-row counter for detect page overflow while placing single block
page_row_counter number,
-- Member functions and procedures
static function ODCIAggregateInitialize(
sctx in out page_num_agg_type
)
return number,
member function ODCIAggregateIterate(
self in out page_num_agg_type,
value in number
)
return number,
member function ODCIAggregateTerminate(
self in page_num_agg_type,
returnValue out number,
flags in number
)
return number,
member function ODCIAggregateMerge(
self in out page_num_agg_type,
ctx2 in page_num_agg_type
)
return number
);
Create type body:
create or replace type body PAGE_NUM_AGG_TYPE is
-- Member procedures and functions
static function ODCIAggregateInitialize(
sctx in out page_num_agg_type
)
return number
is
begin
sctx := page_num_agg_type(1, 0, 0);
return ODCIConst.Success;
end;
member function ODCIAggregateIterate(
self in out page_num_agg_type,
value in number
)
return number
is
-- !!! WARNING: HARDCODED !!!
RowsPerPage number := 4;
begin
self.page_row_counter := self.page_row_counter + 1;
-- Main operations: determine number of page
if(value > 0) then
-- First row of new block
if(self.cur_page_row_count + value > RowsPerPage) then
-- If we reach next page with new block of records - switch to next page.
self.cur_page_number := self.cur_page_number + 1;
self.cur_page_row_count := value;
self.page_row_counter := 1;
else
-- Just increment rows and continue to place on current page
self.cur_page_row_count := self.cur_page_row_count + value;
end if;
else
-- Row from previous block
if(self.page_row_counter > RowsPerPage) then
-- Single block of rows exceeds page size - wrap to next page.
self.cur_page_number := self.cur_page_number + 1;
self.cur_page_row_count := self.cur_page_row_count - RowsPerPage;
self.page_row_counter := 1;
end if;
end if;
return ODCIConst.Success;
end;
member function ODCIAggregateTerminate(
self in page_num_agg_type,
returnValue out number,
flags in number
)
return number
is
begin
-- Returns current page number as result
returnValue := self.cur_page_number;
return ODCIConst.Success;
end;
member function ODCIAggregateMerge(
self in out page_num_agg_type,
ctx2 in page_num_agg_type
)
return number
is
begin
-- Can't act in parallel - error on merging attempts
raise_application_error(-20202,'PAGE_NUM_AGG_TYPE can''t act in parallel mode');
return ODCIConst.Success;
end;
end;
Create agrreation function to use with type:
create function page_num_agg (
input number
) return number aggregate using page_num_agg_type;
Next prepare data and use new function to calculate page numbers:
with data_list as (
-- Your example data as source
select 100 as EmpNo, 'Alison' as EmpName, to_date('21-MAR-96','dd-mon-yy') as TranDate, 45000 as AMT from dual union all
select 100 as EmpNo, 'Alison' as EmpName, to_date('12-DEC-78','dd-mon-yy') as TranDate, 23000 as AMT from dual union all
select 100 as EmpNo, 'Alison' as EmpName, to_date('24-OCT-82','dd-mon-yy') as TranDate, 11000 as AMT from dual union all
select 101 as EmpNo, 'Linda' as EmpName, to_date('15-JAN-84','dd-mon-yy') as TranDate, 16000 as AMT from dual union all
select 101 as EmpNo, 'Linda' as EmpName, to_date('30-JUL-87','dd-mon-yy') as TranDate, 17000 as AMT from dual union all
select 102 as EmpNo, 'Celia' as EmpName, to_date('31-DEC-90','dd-mon-yy') as TranDate, 78000 as AMT from dual union all
select 102 as EmpNo, 'Celia' as EmpName, to_date('17-SEP-96','dd-mon-yy') as TranDate, 21000 as AMT from dual union all
select 103 as EmpNo, 'James' as EmpName, to_date('21-MAR-96','dd-mon-yy') as TranDate, 45000 as AMT from dual union all
select 103 as EmpNo, 'James' as EmpName, to_date('12-DEC-78','dd-mon-yy') as TranDate, 23000 as AMT from dual union all
select 103 as EmpNo, 'James' as EmpName, to_date('24-OCT-82','dd-mon-yy') as TranDate, 11000 as AMT from dual union all
select 104 as EmpNo, 'Robert' as EmpName, to_date('15-JAN-84','dd-mon-yy') as TranDate, 16000 as AMT from dual union all
select 104 as EmpNo, 'Robert' as EmpName, to_date('30-JUL-87','dd-mon-yy') as TranDate, 17000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('30-JUL-88','dd-mon-yy') as TranDate, 31000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('01-JUL-87','dd-mon-yy') as TranDate, 19000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('31-JAN-97','dd-mon-yy') as TranDate, 11000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('17-DEC-93','dd-mon-yy') as TranDate, 33000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('11-DEC-91','dd-mon-yy') as TranDate, 65000 as AMT from dual union all
select 105 as EmpNo, 'Monica' as EmpName, to_date('22-OCT-89','dd-mon-yy') as TranDate, 19000 as AMT from dual
),
ordered_data as (
select
-- Source table fields
src_data.EmpNo, src_data.EmpName, src_data.TranDate, src_data.AMT,
-- Calculate row count per one employee
count(src_data.EmpNo) over(partition by src_data.EmpNo)as emp_row_count,
-- Calculate rank of row inside employee data sorted in output order
rank() over(partition by src_data.EmpNo order by src_data.EmpName, src_data.TranDate) as emp_rnk
from
data_list src_data
)
-- Final step: calculate page number for rows
select
-- Source table data
ordered_data.EmpNo, ordered_data.EmpName, ordered_data.TranDate, ordered_data.AMT,
-- Aggregate all data with our new function
page_num_agg(
-- pass count of rows to aggregate function only for first employee's row
decode(ordered_data.emp_rnk, 1, ordered_data.emp_row_count, 0)
)
over (order by ordered_data.EmpName, ordered_data.TranDate) as page_number
from
ordered_data
order by
ordered_data.EmpName, ordered_data.TranDate
And, finally ...
Disadvantages of this solution:
Hardcoded page row count.
Requires some specific data preparation in query to use aggregate function properly.
Advantages of this solution:
Just works :)
Updated: improved to handle oversized blocks, example modified.
The following SQL statement splits the twenty records in my EMP table into five pages of four rows each:
SQL> select empno
2 , ename
3 , deptno
4 , ceil((row_number() over (order by deptno, empno)/4)) as pageno
5 from emp
6 /
EMPNO ENAME DEPTNO PAGENO
---------- ---------- ---------- ----------
7782 BOEHMER 10 1
7839 SCHNEIDER 10 1
7934 KISHORE 10 1
7369 CLARKE 20 1
7566 ROBERTSON 20 2
7788 RIGBY 20 2
7876 KULASH 20 2
7902 GASPAROTTO 20 2
7499 VAN WIJK 30 3
7521 PADFIELD 30 3
7654 BILLINGTON 30 3
7698 SPENCER 30 3
7844 CAVE 30 4
7900 HALL 30 4
8083 KESTELYN 30 4
8084 LIRA 30 4
8060 VERREYNNE 50 5
8061 FEUERSTEIN 50 5
8085 TRICHLER 50 5
8100 PODER 50 5
20 rows selected.
SQL>
How about this one: (100 is the row limit per page)
select
a.*, floor(count(1) over (order by empno, empname)/100)+1 as page
from source_table a
order by page, empno, empname;