Unclear on LAST_VALUE - Preceding - sql

I have a table that looks like this,
Date Value
01/01/2010 03:59:00 324.44
01/02/2010 09:31:00 NULL
01/02/2010 09:32:00 NULL
.
.
.
01/02/2010 11:42:00 NULL
I want the first valid value to appear in all following rows. This is what I did,
select date,
nvl(value, LAST_VALUE(value IGNORE NULLS) over (order by value RANGE BETWEEN 1 PRECEDING AND CURRENT ROW)) value
from
table
This shows no difference at all, but if I say RANGE BETWEEN 3 PRECEDING AND CURRENT ROW it copies the data to all the rows. I'm not clear why this is happening. Can anyone explain if I'm misunderstanding how to use preceding?

Analytic functions still work on sets of data. They do not process one row at a time, you would need PL/SQL or MODEL to do that. PRECEDING refers to the last X rows, but before the analytic function has been applied.
These problems can be confusing in SQL because you have to build the logic into defining the set, instead of trying to pass data from one row to another. That's why I used CASE with LAST_VALUE in my previous answer.
Edit:
I've added a simple data set so we can all run the exact same query. VALUE1 seems to work to me, am I missing something? Part of the problem with VALUE2 is that the analytic ORDER BY uses VALUE, instead of the date.
select id, the_date, value
,last_value(value ignore nulls) over
(partition by id order by the_date) value1
,nvl(value, LAST_VALUE(value IGNORE NULLS) over
(order by value RANGE BETWEEN 1 PRECEDING AND CURRENT ROW)) value2
from
(
select 1 id, date '2011-01-01' the_date, 100 value from dual union all
select 1 id, date '2011-01-02' the_date, null value from dual union all
select 1 id, date '2011-01-03' the_date, null value from dual union all
select 1 id, date '2011-01-04' the_date, null value from dual union all
select 1 id, date '2011-01-05' the_date, 200 value from dual
)
order by the_date;
Results:
ID THE_DATE VALUE VALUE1 VALUE2
1 1/1/2011 100 100 100
1 1/2/2011 100
1 1/3/2011 100
1 1/4/2011 100
1 1/5/2011 200 200 200

It is possible to copy one row at time because i had done that using java Logic and Sql query
Statement sta;
ResultSet rs,rslast;
try{
//Connection creation code and "con" is an object of Connection Class so don't confuse about that.
sta = con.createStatement();
rs=sta.executeQuery("SELECT * FROM TABLE NAME");
rslast=sta.executeQuery("SELECT * FROM TABLENAME WHERE ID = (SELECT MAX(ID) FROM TABLENAME)");
rslast.next();
String digit =rslast.getString("ID");
System.out.print("ID"+rslast.getString("ID")); // it gives ID of the Last Record.
Instead using this method u can also use ORDER by Date in Descending order.
Now i hope u make logic that only insert Last record.

Related

Using SQL, how do I select which column to add a value to, based on the contents of the row?

I'm having a difficult time phrasing the question, so I think the best thing to do is to give some example tables. I have a table, Attribute_history, I'm trying to pull data from that looks like this:
ID Attribute_Name Attribute_Val Time Stamp
--- -------------- ------------- ----------
1 Color Red 2022/09/28 01:00
2 Color Blue 2022/09/28 01:30
1 Length 3 2022/09/28 01:00
2 Length 4 2022/09/28 01:30
1 Diameter 5 2022/09/28 01:00
2 Diameter 10 2022/09/28 01:30
2 Diameter 11 2022/09/28 01:32
I want to create a table that pulls the attributes of each ID, and if the same ID and attribute_name has been updated, pull the latest info based on Time Stamp.
ID Color Length Diameter
---- ------ ------- --------
1 Red 3 5
2 Blue 4 11
I've achieved this by nesting several select statements, adding one column at a time. I achieved selecting the latest date using this stack overflow post. However, this code seems inefficient, since I'm selecting from the same table multiple times. It also only chooses the latest value for an attribute I know is likely to have been updated multiple times, not all the values I'm interested in.
SELECT
COLOR, DIAMETER, DATE_
FROM
(
SELECT
COLORS.COLOR, ATTR.ATTRIBUTE_NAME AS DIAMETER, ATTR.TIME_STAMP AS DATE_, RANK() OVER (PARTITION BY COLORS.COLOR ORDER BY ATTR.TIME_STAMP DESC) DATE_RANK -- https://stackoverflow.com/questions/3491329/group-by-with-maxdate
FROM
(
SELECT
ATTRIBUTE_HISTORY.ATTRIBUTE_VAL
FROM
ATTRIBUTE_HISTORY
WHERE
ATTRIBUTE_HISTORY.ATTRIBUTE_NAME = 'Color'
GROUP BY ATTRIBUTE_HISTORY.ID
) COLORS
INNER JOIN ATTRIBUTE_HISTORY ATTR ON COLORS.ID = ATTR.ID
WHERE
ATTR.ATTRIBUTE_NAME = 'DIAMETER'
)
WHERE
DATE_RANK = 1
(I copied my real query and renamed values with Find+Replace to obscure the data so this code might not be perfect, but it gets across the idea of how I'm achieving my goal now.)
How can I rewrite this query to be more concise, and pull the latest date entry for each attribute?
For MS SQL Server
Your Problem has 2 parts:
Identify the latest Attribute value based on Time Stamp Column
Convert the Attribute Names to columns ( Pivoting ) in the final
result.
Solution:
;with CTEx as
(
select
row_number() over(partition by id, Attr_name order by Time_Stamp desc) rnum,
id,Attr_name, Attr_value, time_stamp
from #temp
)
SELECT * FROM
(
SELECT id,Attr_name,Attr_value
FROM CTEx
where rnum = 1
) t
PIVOT(
max(Attr_value)
FOR Attr_name IN (Color,Diameter,[Length])
) AS pivot_table;
First part of the problem is taken care of by the CTE with the help of ROW_NUMBER() function. Second part is achieved by using PIVOT() function.
Definition of #temp for reference
Create table #temp(id int, Attr_name varchar(200), Attr_value varchar(200), Time_Stamp datetime)

How to Select Last Entry If None On Input Date SQL

I have a set of data that I am passing a user input of date to pull from a table.
What I am trying to do is setup up logic as such:
Here is the original table:
User DATE_TIME Value
HH1 5/20/2018 1:00 50
HH1 5/20/2018 10:00 50
HH1 5/20/2018 18:00 120
HH1 5/25/2018 12:00 10
HH1 5/26/2018 10:00 15
User passes 05/20/2018 into the sql query for DATE_TIME
The output is as follows:
User DATE_TIME Value
HH1 5/20/2018 1:00 50
HH1 5/20/2018 10:00 50
HH1 5/20/2018 18:00 120
Now the user passes 05/21/2018 into DATE_TIME
Result is nothing
What I am trying to accomplish is...if there are no results on the current day, then pull the latest value in the database, in this case:
User DATE_TIME Value
HH1 5/20/2018 18:00 120
I am not sure how to find this most recent value.
Any help would be appreciated, thanks!
In my solution below, I named the base table TBL and I changed the name of the first column to USR since "user" is an Oracle keyword.
Most of the work is done in the subquery. Look at the FROM clause first: I cross-join to a small subquery that creates an actual date from the input (assumed to be given as a string in MM/DD/YYYY format). Depending on your application, you may be able to input a date directly and not have to convert to date in the query. One way or another, you should be able to use the input date.
The WHERE clause limits the rows to dates up to the input date given (with that date included). Then we rank the rows in descending order, but with a modification we make first: If there are any rows on the input date, their time-of-day component is truncated to zero. (The DATE_TIME value is replaced with the input date for those rows only.) So if there are any rows for the input date, all the rows for that date will get rank = 1, and all other rows will get higher ranks. However, if there are no rows for the input date, then the most recent row before that date will get rank = 1 (and only that row, assuming there are no duplicates in the DATE_TIME column).
So then the job of the outer query is easy: keep only the row OR ROWS where rank = 1. This means either ALL the rows for the input date (if there were any), or the single most recent row before that date.
The subquery-outer query structure cannot be avoided, because the WHERE clause is evaluated before the ranks can be calculated in the same query. If we want the WHERE clause to reference ranks, it must be in an outer query (at a higher level than the query that generates the ranks).
The query should be efficient, since the optimizer is smart enough to see we only want rank = 1 in the outer query, so it will not actually compute ALL the ranks (it will not fully order the subquery rows by DATE_TIME). And, of course, if there is an index on DATE_TIME, it will be used.
You didn't say anything about the role played by USR. If it plays no role, then the solution should work as-is. If you also input a USR, then add the filter in the WHERE clause of the subquery. If you need a result for each USR separately, for the same input date, add PARTITION BY USR in the analytic clause of the RANK() function.
select usr, date_time, value
from (
select usr, date_time, value,
rank() over (order by case when date_time >= input_date then input_date
else date_time end desc) as rnk
from tbl cross join
(select to_date(:input_date, 'mm/dd/yyyy') as input_date from dual)
where date_time < input_date + 1
)
where rnk = 1
;
Below query will make use of window function ROW_NUMBER PARTITION BY ORDER BY. It will combine (UNION ALL) the records equal to input date OR if not found, get the rec with max date. Rownum =1 and rank=1 will only get the first record. If date is same with user input then the max date will be at the bottom and will not be selected. If date is not found then record with max date is the only record, thus will be shown. See demo at http://sqlfiddle.com/#!4/931c2/1.
SELECT usr as "user", date_time, valu as "value"
FROM (
SELECT usr, date_time, valu,
row_number() over (partition by trunc(date_time)
order by date_time desc) as rnk
FROM TEST
WHERE to_char(DATE_TIME, 'yyyy-mm-dd') = '2018-05-21'
UNION ALL
SELECT usr, date_time, valu,
row_number() over (order by date_time desc) as rnk
FROM TEST) t
WHERE rnk = 1
AND rownum = 1;
Ugly as hell, who knows how it'll behave on large tables (probably not very well), but - kind of works on a simple case (based on Oracle).
SQL> create table test (usern varchar2(3), date_time date, hour number, value number);
Table created.
SQL> insert into test
2 (select 'HH1', date '2018-05-20', 1, 50 from dual union
3 select 'HH1', date '2018-05-20', 10, 50 from dual union
4 select 'HH1', date '2018-05-20', 18, 120 from dual union
5 select 'HH1', date '2018-05-25', 12, 10 from dual union
6 select 'HH1', date '2018-05-26', 10, 15 from dual
7 );
5 rows created.
SQL>
Testing:
SQL> select *
2 from test t
3 where t.date_time = (select max(t1.date_time) from test t1
4 where t1.usern = t.usern
5 and t1.date_time <= to_date('&&date_you_enter', 'dd.mm.yyyy')
6 )
7 and t.hour = (select max(case when e.it_exists = 'Y' then t.hour
8 else t2.hour
9 end)
10 from test t2 left join
11 (select t3.usern, 'Y' it_exists
12 from test t3
13 where t3.date_time = to_date('&&date_you_enter', 'dd.mm.yyyy')
14 ) e on e.usern = t2.usern
15 )
16 order by t.date_time, t.hour;
Enter value for date_you_enter: 20.05.2018
USE DATE_TIME HOUR VALUE
--- ---------- ---------- ----------
HH1 20.05.2018 1 50
HH1 20.05.2018 10 50
HH1 20.05.2018 18 120
SQL> undefine date_you_enter
SQL> /
Enter value for date_you_enter: 21.05.2018
USE DATE_TIME HOUR VALUE
--- ---------- ---------- ----------
HH1 20.05.2018 18 120
SQL> undefine date_you_enter
SQL> /
Enter value for date_you_enter: 25.05.2018
USE DATE_TIME HOUR VALUE
--- ---------- ---------- ----------
HH1 25.05.2018 12 10
SQL>
It will be helpful if you post your SQL Statement. But try this
Select
column1, column2,
FROM
YourTable
Where Date <= #dateInputByUserGoesHere

show recent records only

I have a requirement to show most recent records when user selects the option to view most recent records. I have 3 different tables from which I take data and display on the screen.
Below are the sample tables created.
Create table one(sealID integer,product_ser_num varchar2(20),create_time timestamp,status varchar2(10));
create table two(transID integer,formatID integer, formatStatus varchar,ctimeStamp timestamp,sealID integer);
create table three(transID integer,fieldStatus varchar,fieldValue varchar,exctype varchar);
I'm joining above 3 tables and showing the results in a single screen. I want to display the most recent records based on the timestamp.
Please find the sample data on the screen taken from 3 different tables.
ProductSerialNumber formatID formatStatus fieldStatus TimeStamp
ASD100 100 P P 2015-09-03 10:30:22
ASD100 200 p P 2015-09-03 10:30:22
ASD100 100 p P 2015-09-03 10:22:11
ASD100 200 p P 2015-09-03 10:22:11
I want to display the most recent records from the above shown table which should return first 2 rows as they are the recent records when checked with the timestamp column.
Please suggest what changes to be done to the below query to show most recent records.
SELECT transId,product_ser_num,status, to_char(timestamp, 'yyyy-mm-dd hh24:mi:ss') timestamp,
cnt
FROM (SELECT one.*,
row_number() over(ORDER BY
CASE
WHEN :orderDirection like '%asc%' THEN
CASE
WHEN :orderBy='product_ser_num' THEN product_ser_num,
WHEN :orderBy='status' THEN status
WHEN :orderBy='timestamp' THEN to_char(timestamp, 'yyyy-mm-dd hh24:mi:ss')
ELSE to_char(timestamp, 'yyyy-mm-dd hh24:mi:ss')
END
END ASC,
CASE
WHEN :orderDirection like '%desc%' THEN
CASE
WHEN :orderBy='product_ser_num' THEN product_ser_num,
WHEN :orderBy='status' THEN status
WHEN :orderBy='timestamp' THEN to_char(timestamp, 'yyyy-mm-dd hh24:mi:ss')
ELSE to_char(timestamp, 'yyyy-mm-dd hh24:mi:ss')
END
END DESC , transId ASC) line_number
FROM (select one_inner.*, COUNT(1) OVER() cnt
from (select two_tran.transaction_id,
one_res.product_serial_number productSerialNumber,
one_res.status status,from one one_res
left outer join two two_trans on two_trans.sealID = one_res.sealID
left outer join three three_flds on two_tran.transID = three_flds.transID and (three_flds.fieldStatus = 'P')
I don't think you are looking for a Top-n query as your topic title suggests.
It seems like you want to display the data in a customized order, as you have shown in the first image. You want the set of three rows to be grouped together on the basis of timestamp.
I have prepared a small test case to demonstrate the custom order of the rows:
SQL> WITH DATA(ID, num, datetime) AS(
2 SELECT 10, 1001, SYSDATE FROM dual UNION ALL
3 SELECT 10, 6009, SYSDATE FROM dual UNION ALL
4 SELECT 10, 3951, SYSDATE FROM dual UNION ALL
5 SELECT 10, 1001, SYSDATE -1 FROM dual UNION ALL
6 SELECT 10, 6009, SYSDATE -1 FROM dual UNION ALL
7 SELECT 10, 3951, SYSDATE -1 FROM dual
8 )
9 SELECT ID,
10 num,
11 TO_CHAR(DATETIME, 'yyyy-mm-dd hh24:mi:ss') TIMESTAMP
12 FROM
13 (SELECT t.*,
14 row_number() OVER(ORDER BY DATETIME DESC,
15 CASE num
16 WHEN 1001
17 THEN 1
18 WHEN 6009
19 THEN 2
20 WHEN 3951
21 THEN 3
22 END, num) rn
23 FROM DATA t
24 );
ID NUM TIMESTAMP
---------- ---------- -------------------
10 1001 2015-09-04 11:04:48
10 6009 2015-09-04 11:04:48
10 3951 2015-09-04 11:04:48
10 1001 2015-09-03 11:04:48
10 6009 2015-09-03 11:04:48
10 3951 2015-09-03 11:04:48
6 rows selected.
Now, you can see that for the same ID 10, the NUM values are grouped and also in a custom order.
This query seems very large and complex, so this may be oversimplifying things:
Add a clause to the end limit 3 ?
What I think you need to do is:
select
max(timestamp), engine_serial_number, formatID
from
<
joins here
>
group by engine_serial_number, formatID
This will basically give you the lines you want, but not all metadata.
Hence, you will just have to re-join all this with the main join to get the rest of the info (join on all three columns, engine serial number, formatID AND timestamp).
That should work.
Hope this helps!
It's hard to give you a precise answer, because your query is incomplete. But I'll give you the general idea, and you can tweak it into your query.
One way to accomplish what you want is by using the dense_rank() analytical function to number your rows by timestamp in descending order (You could use rank() too in this case, it doesn't actually matter). All rows with the same timestamp will be assigned the same "rank", so you can then filter by rank to only get the most recent records.
Try to adjust your query to something like this:
select ...
from (select ...,
dense_rank() over (order by timestamp desc) as timestamp_rank
from ...)
where timestamp_rank = 1
...
I suspect that with a better understanding of your data model and query, there would probably be a better solution. But based on the information provided, I think that the above should yield the results you are looking for.

Joining next Sequential Row

I am planing an SQL Statement right now and would need someone to look over my thougts.
This is my Table:
id stat period
--- ------- --------
1 10 1/1/2008
2 25 2/1/2008
3 5 3/1/2008
4 15 4/1/2008
5 30 5/1/2008
6 9 6/1/2008
7 22 7/1/2008
8 29 8/1/2008
Create Table
CREATE TABLE tbstats
(
id INT IDENTITY(1, 1) PRIMARY KEY,
stat INT NOT NULL,
period DATETIME NOT NULL
)
go
INSERT INTO tbstats
(stat,period)
SELECT 10,CONVERT(DATETIME, '20080101')
UNION ALL
SELECT 25,CONVERT(DATETIME, '20080102')
UNION ALL
SELECT 5,CONVERT(DATETIME, '20080103')
UNION ALL
SELECT 15,CONVERT(DATETIME, '20080104')
UNION ALL
SELECT 30,CONVERT(DATETIME, '20080105')
UNION ALL
SELECT 9,CONVERT(DATETIME, '20080106')
UNION ALL
SELECT 22,CONVERT(DATETIME, '20080107')
UNION ALL
SELECT 29,CONVERT(DATETIME, '20080108')
go
I want to calculate the difference between each statistic and the next, and then calculate the mean value of the 'gaps.'
Thougts:
I need to join each record with it's subsequent row. I can do that using the ever flexible joining syntax, thanks to the fact that I know the id field is an integer sequence with no gaps.
By aliasing the table I could incorporate it into the SQL query twice, then join them together in a staggered fashion by adding 1 to the id of the first aliased table. The first record in the table has an id of 1. 1 + 1 = 2 so it should join on the row with id of 2 in the second aliased table. And so on.
Now I would simply subtract one from the other.
Then I would use the ABS function to ensure that I always get positive integers as a result of the subtraction regardless of which side of the expression is the higher figure.
Is there an easier way to achieve what I want?
The lead analytic function should do the trick:
SELECT period, stat, stat - LEAD(stat) OVER (ORDER BY period) AS gap
FROM tbstats
The average value of the gaps can be done by calculating the difference between the first value and the last value and dividing by one less than the number of elements:
select sum(case when seqnum = num then stat else - stat end) / (max(num) - 1);
from (select period, row_number() over (order by period) as seqnum,
count(*) over () as num
from tbstats
) t
where seqnum = num or seqnum = 1;
Of course, you can also do the calculation using lead(), but this will also work in SQL Server 2005 and 2008.
By using Join also you achieve this
SELECT t1.period,
t1.stat,
t1.stat - t2.stat gap
FROM #tbstats t1
LEFT JOIN #tbstats t2
ON t1.id + 1 = t2.id
To calculate the difference between each statistic and the next, LEAD() and LAG() may be the simplest option. You provide an ORDER BY, and LEAD(something) returns the next something and LAG(something) returns the previous something in the given order.
select
x.id thisStatId,
LAG(x.id) OVER (ORDER BY x.id) lastStatId,
x.stat thisStatValue,
LAG(x.stat) OVER (ORDER BY x.id) lastStatValue,
x.stat - LAG(x.stat) OVER (ORDER BY x.id) diff
from tbStats x

How to write Oracle query to find a total length of possible overlapping from-to dates

I'm struggling to find the query for the following task
I have the following data and want to find the total network day for each unique ID
ID From To NetworkDay
1 03-Sep-12 07-Sep-12 5
1 03-Sep-12 04-Sep-12 2
1 05-Sep-12 06-Sep-12 2
1 06-Sep-12 12-Sep-12 5
1 31-Aug-12 04-Sep-12 3
2 04-Sep-12 06-Sep-12 3
2 11-Sep-12 13-Sep-12 3
2 05-Sep-12 08-Sep-12 3
Problem is the date range can be overlapping and I can't come up with SQL that will give me the following results
ID From To NetworkDay
1 31-Aug-12 12-Sep-12 9
2 04-Sep-12 08-Sep-12 4
2 11-Sep-12 13-Sep-12 3
and then
ID Total Network Day
1 9
2 7
In case the network day calculation is not possible just get to the second table would be sufficient.
Hope my question is clear
We can use Oracle Analytics, namely the "OVER ... PARTITION BY" clause, in Oracle to do this. The PARTITION BY clause is kind of like a GROUP BY but without the aggregation part. That means we can group rows together (i.e. partition them) and them perform an operation on them as separate groups. As we operate on each row we can then access the columns of the previous row above. This is the feature PARTITION BY gives us. (PARTITION BY is not related to partitioning of a table for performance.)
So then how do we output the non-overlapping dates? We first order the query based on the (ID,DFROM) fields, then we use the ID field to make our partitions (row groups). We then test the previous row's TO value and the current rows FROM value for overlap using an expression like: (in pseudo code)
max(previous.DTO, current.DFROM) as DFROM
This basic expression will return the original DFROM value if it doesnt overlap, but will return the previous TO value if there is overlap. Since our rows are ordered we only need to be concerned with the last row. In cases where a previous row completely overlaps the current row we want the row then to have a 'zero' date range. So we do the same thing for the DTO field to get:
max(previous.DTO, current.DFROM) as DFROM, max(previous.DTO, current.DTO) as DTO
Once we have generated the new results set with the adjusted DFROM and DTO values, we can aggregate them up and count the range intervals of DFROM and DTO.
Be aware that most date calculations in database are not inclusive such as your data is. So something like DATEDIFF(dto,dfrom) will not include the day dto actually refers to, so we will want to adjust dto up a day first.
I dont have access to an Oracle server anymore but I know this is possible with the Oracle Analytics. The query should go something like this:
(Please update my post if you get this to work.)
SELECT id,
max(dfrom, LAST_VALUE(dto) OVER (PARTITION BY id ORDER BY dfrom) ) as dfrom,
max(dto, LAST_VALUE(dto) OVER (PARTITION BY id ORDER BY dfrom) ) as dto
from (
select id, dfrom, dto+1 as dto from my_sample -- adjust the table so that dto becomes non-inclusive
order by id, dfrom
) sample;
The secret here is the LAST_VALUE(dto) OVER (PARTITION BY id ORDER BY dfrom) expression which returns the value previous to the current row.
So this query should output new dfrom/dto values which dont overlap. It's then a simple matter of sub-querying this doing (dto-dfrom) and sum the totals.
Using MySQL
I did haves access to a mysql server so I did get it working there. MySQL doesnt have results partitioning (Analytics) like Oracle so we have to use result set variables. This means we use #var:=xxx type expressions to remember the last date value and adjust the dfrom/dto according. Same algorithm just a little longer and more complex syntax. We also have to forget the last date value any time the ID field changes!
So here is the sample table (same values you have):
create table sample(id int, dfrom date, dto date, networkDay int);
insert into sample values
(1,'2012-09-03','2012-09-07',5),
(1,'2012-09-03','2012-09-04',2),
(1,'2012-09-05','2012-09-06',2),
(1,'2012-09-06','2012-09-12',5),
(1,'2012-08-31','2012-09-04',3),
(2,'2012-09-04','2012-09-06',3),
(2,'2012-09-11','2012-09-13',3),
(2,'2012-09-05','2012-09-08',3);
On to the query, we output the un-grouped result set like above:
The variable #ld is "last date", and the variable #lid is "last id". Anytime #lid changes, we reset #ld to null. FYI In mysql the := operators is where the assignment happens, an = operator is just equals.
This is a 3 level query, but it could be reduced to 2. I went with an extra outer query to keep things more readable. The inner most query is simple and it adjusts the dto column to be non-inclusive and does the proper row ordering. The middle query does the adjustment of the dfrom/dto values to make them non-overlapped. The outer query simple drops the non-used fields, and calculate the interval range.
set #ldt=null, #lid=null;
select id, no_dfrom as dfrom, no_dto as dto, datediff(no_dto, no_dfrom) as days from (
select if(#lid=id,#ldt,#ldt:=null) as last, dfrom, dto, if(#ldt>=dfrom,#ldt,dfrom) as no_dfrom, if(#ldt>=dto,#ldt,dto) as no_dto, #ldt:=if(#ldt>=dto,#ldt,dto), #lid:=id as id,
datediff(dto, dfrom) as overlapped_days
from (select id, dfrom, dto + INTERVAL 1 DAY as dto from sample order by id, dfrom) as sample
) as nonoverlapped
order by id, dfrom;
The above query gives the results (notice dfrom/dto are non-overlapping here):
+------+------------+------------+------+
| id | dfrom | dto | days |
+------+------------+------------+------+
| 1 | 2012-08-31 | 2012-09-05 | 5 |
| 1 | 2012-09-05 | 2012-09-08 | 3 |
| 1 | 2012-09-08 | 2012-09-08 | 0 |
| 1 | 2012-09-08 | 2012-09-08 | 0 |
| 1 | 2012-09-08 | 2012-09-13 | 5 |
| 2 | 2012-09-04 | 2012-09-07 | 3 |
| 2 | 2012-09-07 | 2012-09-09 | 2 |
| 2 | 2012-09-11 | 2012-09-14 | 3 |
+------+------------+------------+------+
How about constructing an SQL which merges intervals by removing holes and considering only maximum intervals. It goes like this (not tested):
SELECT DISTINCT F.ID, F.From, L.To
FROM Temp AS F, Temp AS L
WHERE F.From < L.To AND F.ID = L.ID
AND NOT EXISTS (SELECT *
FROM Temp AS T
WHERE T.ID = F.ID
AND F.From < T.From AND T.From < L.To
AND NOT EXISTS ( SELECT *
FROM Temp AS T1
WHERE T1.ID = F.ID
AND T1.From < T.From
AND T.From <= T1.To)
)
AND NOT EXISTS (SELECT *
FROM Temp AS T2
WHERE T2.ID = F.ID
AND (
(T2.From < F.From AND F.From <= T2.To)
OR (T2.From < L.To AND L.To < T2.To)
)
)
with t_data as (
select 1 as id,
to_date('03-sep-12','dd-mon-yy') as start_date,
to_date('07-sep-12','dd-mon-yy') as end_date from dual
union all
select 1,
to_date('03-sep-12','dd-mon-yy'),
to_date('04-sep-12','dd-mon-yy') from dual
union all
select 1,
to_date('05-sep-12','dd-mon-yy'),
to_date('06-sep-12','dd-mon-yy') from dual
union all
select 1,
to_date('06-sep-12','dd-mon-yy'),
to_date('12-sep-12','dd-mon-yy') from dual
union all
select 1,
to_date('31-aug-12','dd-mon-yy'),
to_date('04-sep-12','dd-mon-yy') from dual
union all
select 2,
to_date('04-sep-12','dd-mon-yy'),
to_date('06-sep-12','dd-mon-yy') from dual
union all
select 2,
to_date('11-sep-12','dd-mon-yy'),
to_date('13-sep-12','dd-mon-yy') from dual
union all
select 2,
to_date('05-sep-12','dd-mon-yy'),
to_date('08-sep-12','dd-mon-yy') from dual
),
t_holidays as (
select to_date('01-jan-12','dd-mon-yy') as holiday
from dual
),
t_data_rn as (
select rownum as rn, t_data.* from t_data
),
t_model as (
select distinct id,
start_date
from t_data_rn
model
partition by (rn, id)
dimension by (0 as i)
measures(start_date, end_date)
rules
( start_date[for i
from 1
to end_date[0]-start_date[0]
increment 1] = start_date[0] + cv(i),
end_date[any] = start_date[cv()] + 1
)
order by 1,2
),
t_network_days as (
select t_model.*,
case when
mod(to_char(start_date, 'j'), 7) + 1 in (6, 7)
or t_holidays.holiday is not null
then 0 else 1
end as working_day
from t_model
left outer join t_holidays
on t_holidays.holiday = t_model.start_date
)
select id,
sum(working_day) as network_days
from t_network_days
group by id;
t_data - your initial data
t_holidays - contains list of holidays
t_data_rn - just adds unique key (rownum) to each row of t_data
t_model - expands t_data date ranges into a flat list of dates
t_network_days - marks each date from t_model as working day or weekend based on day of week (Sat and Sun) and holidays list
final query - calculates number of network day per each group.