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;
Related
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
The scenario is that for a particular date range, I need to return all rows where a field value has changed (FROM and TO date). The changed field values will then be shown on screen in a different colour.
I can return all employee records where a change has occurred for the specific time period. However, if an employee's record has changed multiple times within the selected time period, I need to return a single employee record with a combined value for each of the different X_FLAG columns. 1 indicates that a change has occurred, 0 indicates no change.
Table DDL is:
CREATE TABLE "EMPLOYEE_DATA"
( "EMPLOYEE_ID" NUMBER(20,0),
"EMPLOYEE_NAME" VARCHAR2(100 BYTE),
"EMPLOYEE_NAME_FLAG" NUMBER(1,0),
"EMPLOYEE_ROLE" VARCHAR2(100 BYTE),
"EMPLOYEE_ROLE_FLAG" NUMBER(1,0),
"EMPLOYEE_SALARY" VARCHAR2(100 BYTE),
"EMPLOYEE_SALARY_FLAG" NUMBER(1,0),
"DATE_VALID_FROM" DATE,
"DATE_VALID_TO" DATE,
"HAS_RECORD_CHANGED" NUMBER(1,0),
"CURRENT_ROW_IND" NUMBER(1,0)
);
Mock data is:
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (1,'John Smith',0,'Associate',0,'1',0,to_date('01-FEB-17','DD-MON-RR'),to_date('28-FEB-17','DD-MON-RR'),0,0);
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (2,'Katy Brown',0,'Team Leader',0,'7',0, to_date('01-FEB-17','DD-MON-RR'),to_date('28-FEB-17','DD-MON-RR'),0,0);
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (2,'Katy Brown',0,'Team Leader',0,'7',0, to_date('01-APR-17','DD-MON-RR'),to_date('31-DEC-99','DD-MON-RR'),1,1);
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (3,'Ian Jones',1,'Delivery Manager',1,'3',1, to_date('01-MAR-17','DD-MON-RR'),to_date('31-DEC-99','DD-MON-RR'),1,1);
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (1,'John Smith',0,'Analyst',1,'1',0, to_date('01-MAR-17','DD-MON-RR'),to_date('31-MAR-17','DD-MON-RR'),1,0);
Insert into EMPLOYEE_DATA (EMPLOYEE_ID,EMPLOYEE_NAME,EMPLOYEE_NAME_FLAG,EMPLOYEE_ROLE,EMPLOYEE_ROLE_FLAG,EMPLOYEE_SALARY,EMPLOYEE_SALARY_FLAG, DATE_VALID_FROM,DATE_VALID_TO,HAS_RECORD_CHANGED,CURRENT_ROW_IND) values (1,'John Smith',0,'Analyst',0,'2',1, to_date('01-APR-17','DD-MON-RR'),to_date('31-DEC-99','DD-MON-RR'),1,1);
My query is:
SELECT *
FROM EMPLOYEE_DATA
WHERE DATE_VALID_FROM <= TO_DATE('01/04/2017', 'dd/mm/yyyy')
AND DATE_VALID_TO >= TO_DATE('01/01/2017', 'dd/mm/yyyy')
AND HAS_RECORD_CHANGED = '1'
ORDER BY EMPLOYEE_ID ASC, DATE_VALID_FROM ASC;
In my final query, I will add following line to bring back the current record AND CURRENT_ROW_IND = '1'. I left this out to show how I need to combine data for "John Smith" record to merge EMPLOYEE_ROLE_FLAG and EMPLOYEE_SALARY_FLAG for the previous and current record (for John Smith)
EDIT - Added original and target results. If possible, I would need to aggregate to get the max of the X_FLAG columns for each unique employee.
Original
EMPLOYEE_ID EMPLOYEE_NAME EMPLOYEE_NAME_FLAG EMPLOYEE_ROLE EMPLOYEE_ROLE_FLAG EMPLOYEE_SALARY EMPLOYEE_SALARY_FLAG DATE_VALID_FROM DATE_VALID_TO HAS_RECORD_CHANGED CURRENT_ROW_IND
1 John Smith 0 Associate 0 1 0 01-Feb-17 28-Feb-17 0 0
2 Katy Brown 0 Team Leader 0 7 0 01-Feb-17 28-Feb-17 0 0
2 Katy Brown 0 Team Leader 0 7 0 01-Apr-17 31-Dec-99 1 1
3 Ian Jones 1 Delivery Manager 1 3 1 01-Mar-17 31-Dec-99 1 1
1 John Smith 0 Analyst 1 0 0 01-Mar-17 31-Mar-17 1 0
1 John Smith 0 Analyst 0 1 1 01-Apr-17 31-Dec-99 1 1
Target
EMPLOYEE_ID EMPLOYEE_NAME EMPLOYEE_NAME_FLAG EMPLOYEE_ROLE EMPLOYEE_ROLE_FLAG EMPLOYEE_SALARY EMPLOYEE_SALARY_FLAG DATE_VALID_FROM DATE_VALID_TO HAS_RECORD_CHANGED CURRENT_ROW_IND
1 John Smith 0 Analyst 1 1 1 01-Apr-17 31-Dec-99 1 1
2 Katy Brown 0 Team Leader 0 7 0 01-Apr-17 31-Dec-99 1 1
3 Ian Jones 1 Delivery Manager 1 3 1 01-Mar-17 31-Dec-99 1 1
Consider derived tables where unit level query joins with aggregate query that calculates max flags:
SELECT emp.*
FROM
(SELECT *
FROM EMPLOYEE_DATA
WHERE DATE_VALID_FROM BETWEEN TO_DATE('01/01/2017', 'dd/mm/yyyy')
AND TO_DATE('01/04/2017', 'dd/mm/yyyy')
AND HAS_RECORD_CHANGED = 1
) AS emp
INNER JOIN
(SELECT EMPLOYEE_ID, MAX(EMPLOYEE_NAME_FLAG) AS MAX_NAME_FLAG,
MAX(EMPLOYEE_ROLE_FLAG) AS MAX_ROLE_FLAG,
MAX(EMPLOYEE_SALARY_FLAG) AS MAX_SALARY_FLAG
FROM EMPLOYEE_DATA
WHERE DATE_VALID_FROM BETWEEN TO_DATE('01/01/2017', 'dd/mm/yyyy')
AND TO_DATE('01/04/2017', 'dd/mm/yyyy')
AND HAS_RECORD_CHANGED = 1
GROUP BY EMPLOYEE_ID
) AS agg
ON emp.EMPLOYEE_ID = agg.EMPLOYEE_ID
AND emp.EMPLOYEE_NAME_FLAG = agg.MAX_NAME_FLAG
AND emp.EMPLOYEE_ROLE_FLAG = agg.MAX_ROLE_FLAG
AND emp.EMPLOYEE_SALARY_FLAG = agg.MAX_SALARY_FLAG
ORDER BY emp.EMPLOYEE_ID ASC, emp.DATE_VALID_FROM ASC
If possible, I would need to aggregate to get the max of the X_FLAG columns for each unique employee.
For the above requirement GROUP BY will work, right?
Please find below query to achieve you Target,
SELECT EMPLOYEE_ID
, EMPLOYEE_NAME
, MIN(EMPLOYEE_NAME_FLAG) EMPLOYEE_NAME_FLAG
, EMPLOYEE_ROLE
, MAX(EMPLOYEE_ROLE_FLAG) EMPLOYEE_ROLE_FLAG
, MIN(EMPLOYEE_SALARY) EMPLOYEE_SALARY
, MAX(EMPLOYEE_SALARY_FLAG) EMPLOYEE_SALARY_FLAG
, MAX(DATE_VALID_FROM) DATE_VALID_FROM
, MAX(DATE_VALID_TO) DATE_VALID_TO
, HAS_RECORD_CHANGED
, MAX(CURRENT_ROW_IND) CURRENT_ROW_IND
FROM EMPLOYEE_DATA
WHERE HAS_RECORD_CHANGED = 1
AND DATE_VALID_FROM BETWEEN TO_DATE('01/01/2017', 'dd/mm/yyyy')
AND TO_DATE('01/04/2017', 'dd/mm/yyyy')
GROUP BY EMPLOYEE_ID
, EMPLOYEE_NAME
, EMPLOYEE_ROLE
, HAS_RECORD_CHANGED
ORDER BY EMPLOYEE_ID ASC
, DATE_VALID_FROM ASC;
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;
I have the data in the following format:
Tran No Date Store ID Register ID Tender
01 23-10-2015 1000 001 CASH
01 23-10-2015 1000 001 CRDT
02 23-10-2015 1000 001 CASH
02 23-10-2015 1000 001 GIFT
A new column has been added (Tran Seq) to the table
Such that the new data would become like
Tran No Date Store ID Register ID Tender Tran Seq
01 23-10-2015 1000 001 CASH 0
01 23-10-2015 1000 001 CRDT 1
02 23-10-2015 1000 001 CASH 0
02 23-10-2015 1000 001 GIFT 1
For every same TRAN NO, DATE, STORE ID and REGISTER ID, each line item would get a new sequence number in the field Tran Seq.
How do i achieve the above mentioned results?
You could use ROW_NUMBER() analytic function:
For example,
Setup
CREATE TABLE t
(tran_no NUMBER,
"DATE" VARCHAR2(10),
store_id NUMBER,
Register_ID NUMBER,
Tender varchar2(4));
INSERT ALL
INTO t (tran_no, "DATE", store_id, Register_ID, Tender)
VALUES (01, '23-10-2015', 1000, 001, 'CASH')
INTO t (tran_no, "DATE", store_id, Register_ID, Tender)
VALUES (01, '23-10-2015', 1000, 001, 'CRDT')
INTO t (tran_no, "DATE", store_id, Register_ID, Tender)
VALUES (02, '23-10-2015', 1000, 001, 'CASH')
INTO t (tran_no, "DATE", store_id, Register_ID, Tender)
VALUES (02, '23-10-2015', 1000, 001, 'GIFT')
INTO t (tran_no, "DATE", store_id, Register_ID, Tender)
VALUES (02, '23-10-2015', 1000, 001, 'CRDT')
SELECT * FROM dual;
Query
SQL> SELECT t.*,
2 row_number() OVER(PARTITION BY tran_no,
3 "DATE",
4 store_id,
5 register_id
6 ORDER BY tender) - 1 tran_seq
7 FROM t;
TRAN_NO DATE STORE_ID REGISTER_ID TEND TRAN_SEQ
---------- ---------- ---------- ----------- ---- ----------
1 23-10-2015 1000 1 CASH 0
1 23-10-2015 1000 1 CRDT 1
2 23-10-2015 1000 1 CASH 0
2 23-10-2015 1000 1 CRDT 1
2 23-10-2015 1000 1 GIFT 2
SQL>
Note :
You cannot have a space between column names. I have used underscore instead.
You cannot used Oracle keywords. If you want to use, then you need to use double-quotation marks. For example, "DATE" column. remember, you need to use the double-quotation marks everywhere you make the reference.
From documentation on Database Object Naming Rules:
A quoted identifier begins and ends with double quotation marks ("). If you name a schema object using a quoted identifier, then you must use the double quotation marks whenever you refer to that object.
UPDATE
OP wants to update the new column with the sequence generated. You could use MERGE statement.
SQL> ALTER TABLE t ADD tran_seq NUMBER;
Table altered.
SQL> MERGE INTO t
2 USING(
3 SELECT TRAN_NO, "DATE", STORE_ID, REGISTER_ID, TENDER,
4 row_number() OVER(PARTITION BY tran_no,
5 "DATE",
6 store_id,
7 register_id
8 ORDER BY tender) - 1 tran_seq
9 FROM t
10 ) s
11 ON (t.tran_no = s.tran_no
12 AND t."DATE" = s."DATE"
13 AND t.store_id = s.store_id
14 AND t.register_id = s.register_id
15 AND t.tender = s.tender
16 )
17 WHEN MATCHED THEN
18 UPDATE SET t.tran_seq = s.tran_seq
19 /
5 rows merged.
SQL> SELECT * FROM t ORDER BY tran_no, tran_seq;
TRAN_NO DATE STORE_ID REGISTER_ID TEND TRAN_SEQ
---------- ---------- ---------- ----------- ---- ----------
1 23-10-2015 1000 1 CASH 0
1 23-10-2015 1000 1 CRDT 1
2 23-10-2015 1000 1 CASH 0
2 23-10-2015 1000 1 CRDT 1
2 23-10-2015 1000 1 GIFT 2
You can use row_number with partition by clause
select tran_no,date,store_id,register_id,tender, row_number() over(partition
by tran_no,date,store_id,register_id order by tran_no)-1 tran_seq
from table_name
Note: I realized it lately after posting this answer that mine solution will work on MSSQLServer. So check #Lalit's answer for Oracle.
Hi you would need to write a script to do that, as this has many steps.
You need to create a temp table where you will store transit result like below.
To clean off table if it exist already..
IF OBJECT_ID('tempdb..##abc') IS NOT NULL
DROP TABLE ##abc
Now create a temp table with following query..
Declare #sql as nvarchar(max)
set #sql = N'select * into ##abc from (SELECT t.*,
row_number() OVER(PARTITION BY [Tran No],
DATE,
[Store ID],
[Register ID]
ORDER BY Tender) - 1 tran_seq
FROM #temp t) as T'
EXECUTE sp_executesql #sql
You may check this table, if created.
select * from ##abc
Now drop your existing table
drop table YourTableName
Now just recreate same from temp table.
select * into YourTableName from (select * from ##abc) as T
It's done. You may query your original table now.
select * from YourTableName
This particular case was distilled from an example where the programmer assumed that for two shipments into a tank car, line #1 would be loaded first. I corrected this to allow for the loading to be performed in any order - however, I discovered that MIN() OVER (PARTITION BY) allows an ORDER BY in Oracle (this is not allowed in SQL Server), and additionally, it alters the behavior of the function, causing the ORDER BY to apparently be added to the PARTITION BY.
WITH data AS (
SELECT 1 AS SHIPMENT_ID, 1 AS LINE_NUMBER, 2 AS TARE, 3 AS GROSS FROM DUAL
UNION ALL
SELECT 1 AS SHIPMENT_ID, 2 AS LINE_NUMBER, 1 AS TARE, 2 AS GROSS FROM DUAL
)
SELECT MIN(tare) OVER (PARTITION BY shipment_id) first_tare
,MAX(gross) OVER (PARTITION BY shipment_id) last_gross
,FIRST_VALUE(tare) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER) first_tare_incorrect
,FIRST_VALUE(gross) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER DESC) last_gross_incorrect
,MIN(tare) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER) first_tare_incorrect_still
,MAX(gross) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER DESC) last_gross_incorrect_still
,MIN(tare) OVER (PARTITION BY shipment_id, LINE_NUMBER) first_tare_incorrect_still2
,MAX(gross) OVER (PARTITION BY shipment_id, LINE_NUMBER) last_gross_incorrect_still2
FROM data
A SQL Server example (with non-applicable code commented out):
WITH data AS (
SELECT 1 AS SHIPMENT_ID, 1 AS LINE_NUMBER, 2 AS TARE, 3 AS GROSS -- FROM DUAL
UNION ALL
SELECT 1 AS SHIPMENT_ID, 2 AS LINE_NUMBER, 1 AS TARE, 2 AS GROSS -- FROM DUAL
)
SELECT MIN(tare) OVER (PARTITION BY shipment_id) first_tare
,MAX(gross) OVER (PARTITION BY shipment_id) last_gross
-- ,FIRST_VALUE(tare) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER) first_tare_incorrect
-- ,FIRST_VALUE(gross) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER DESC) last_gross_incorrect
-- ,MIN(tare) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER) first_tare_incorrect_still
-- ,MAX(gross) OVER (PARTITION BY shipment_id ORDER BY LINE_NUMBER DESC) last_gross_incorrect_still
,MIN(tare) OVER (PARTITION BY shipment_id, LINE_NUMBER) first_tare_incorrect_still2
,MAX(gross) OVER (PARTITION BY shipment_id, LINE_NUMBER) last_gross_incorrect_still2
FROM data
So question: What is Oracle doing and why and is it right?
If you add an ORDER BY to the MIN analytic function, you turn it into a "min so far" function rather than an overall minimum. For the final row for whatever you're partitioning by, the results will be the same. But the prior rows may have a different "min so far" than the overall minimum.
Using the EMP table as an example, you can see that the minimum salary so far for the department eventually converges on the overall minimum for the department. And you can see that the "min so far" value for any given department decreases as lower values are encountered.
SQL> ed
Wrote file afiedt.buf
1 select ename,
2 deptno,
3 sal,
4 min(sal) over (partition by deptno order by ename) min_so_far,
5 min(sal) over (partition by deptno) min_overall
6 from emp
7* order by deptno, ename
SQL> /
ENAME DEPTNO SAL MIN_SO_FAR MIN_OVERALL
---------- ---------- ---------- ---------- -----------
CLARK 10 2450 2450 1300
KING 10 5000 2450 1300
MILLER 10 1300 1300 1300
ADAMS 20 1110 1110 800
FORD 20 3000 1110 800
JONES 20 2975 1110 800
SCOTT 20 3000 1110 800
smith 20 800 800 800
ALLEN 30 1600 1600 950
BLAKE 30 2850 1600 950
MARTIN 30 1250 1250 950
SM0 30 950 950 950
TURNER 30 1500 950 950
WARD 30 1250 950 950
BAR
PAV
16 rows selected.
Of course, it would make more sense to use this form of the analytic function when you're trying to do something like compute a personal best that you can use as a comparison in future periods. If you're tracking an individual's decreasing golf scores, mile times, or weight, displaying personal bests can be a form of motivation.
SQL> ed
Wrote file afiedt.buf
1 with golf_scores as
2 ( select 1 golfer_id, 80 score, sysdate dt from dual union all
3 select 1, 82, sysdate+1 dt from dual union all
4 select 1, 72, sysdate+2 dt from dual union all
5 select 1, 75, sysdate+3 dt from dual union all
6 select 1, 71, sysdate+4 dt from dual union all
7 select 2, 74, sysdate from dual )
8 select golfer_id,
9 score,
10 dt,
11 (case when score=personal_best
12 then 'New personal best'
13 else null
14 end) msg
15 from (
16 select golfer_id,
17 score,
18 dt,
19 min(score) over (partition by golfer_id
20 order by dt) personal_best
21 from golf_scores
22* )
SQL> /
GOLFER_ID SCORE DT MSG
---------- ---------- --------- -----------------
1 80 12-SEP-11 New personal best
1 82 13-SEP-11
1 72 14-SEP-11 New personal best
1 75 15-SEP-11
1 71 16-SEP-11 New personal best
2 74 12-SEP-11 New personal best
6 rows selected.