Fetch a record on maximum date of every month - sql

I want to fetch customers balances at the maximum date of every month, in every year in database. The Balance table has balances at the end of everyday when customer does transaction.
I just want to pick the balance at the maximum date of every month.Any help??
Below is a snip of My dataset.

You can try using window function - row_number()
select * from
(
SELECT *,row_number() over(partition by extract(YEAR FROM Date), extract(MONTH FROM Date) order by date desc) as rn
FROM t
)rn=1

You can do it also without a sub-query:
WITH b(ID, "date",bal) AS
(
SELECT 'CUST_I',DATE '2013-07-27', 14777.44 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-07-26', 71085.13 FROM dual UNION ALL
SELECT 'CUST_I',DATE '2013-08-27', 66431.35656 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-26', 63102.68622 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-20', 6310.68622 FROM dual UNION ALL
SELECT 'CUST_H',DATE '2013-08-10', 630.68622 FROM dual UNION ALL
SELECT 'CUST_G',DATE '2013-09-25', 89732.04889 FROM dual UNION ALL
SELECT 'CUST_E',DATE '2013-09-23', 83074.70822 FROM dual
)
SELECT ID,
MAX("date") KEEP (DENSE_RANK FIRST ORDER BY "date" desc) AS MAX_DATE,
MAX(bal) KEEP (DENSE_RANK FIRST ORDER BY "date" desc) AS MAX_BAL
FROM b
GROUP BY ID, TRUNC("date", 'MM');
+-----------------------------+
|ID |MAX_DATE |MAX_BAL |
+-----------------------------+
|CUST_E|23.09.2013|83074.70822|
|CUST_G|25.09.2013|89732.04889|
|CUST_H|26.07.2013|71085.13 |
|CUST_H|26.08.2013|63102.68622|
|CUST_I|27.07.2013|14777.44 |
|CUST_I|27.08.2013|66431.35656|
+-----------------------------+

You may use a self join for your table call cust_balances :
select c1.*
from cust_balances c1
join
(
select max("date") max_date
from cust_balances
group by to_char("date",'yyyymm')
) c2 on ( c1."date" = c2.max_date );
SQL Fiddle Demo

Related

SQL counting days with gap / overlapping

I am working on a "counting days" problem almost identical to this one. I have a list of date(s), and need to count how many days used excluding duplicate, and handling the gaps. Same input and output.
From: Markus Jarderot
Input
ID d1 d2
1 2011-08-01 2011-08-08
1 2011-08-02 2011-08-06
1 2011-08-03 2011-08-10
1 2011-08-12 2011-08-14
2 2011-08-01 2011-08-03
2 2011-08-02 2011-08-06
2 2011-08-05 2011-08-09
Output
ID hold_days
1 11
2 8
SQL to find time elapsed from multiple overlapping intervals
But for the life of me I couldn't understand Markus Jarderot's solution.
SELECT DISTINCT
t1.ID,
t1.d1 AS date,
-DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1) AS n
FROM Orders t1
LEFT JOIN Orders t2 -- Join for any events occurring while this
ON t2.ID = t1.ID -- is starting. If this is a start point,
AND t2.d1 <> t1.d1 -- it won't match anything, which is what
AND t1.d1 BETWEEN t2.d1 AND t2.d2 -- we want.
GROUP BY t1.ID, t1.d1, t1.d2
HAVING COUNT(t2.ID) = 0
Why is DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1) picking from the min(d1) from the entire list? Is that regardless of ID.
And what does t1.d1 BETWEEN t2.d1 AND t2.d2 do? Is that to ensure only overlapped interval are calculated?
Same thing with group by, I think because if in the event the same identical period will be discarded? I tried to trace the solution by hand but getting more confused.
This is mostly a duplicate of my answer here (including explanation) but with the inclusion of grouping on an id column. It should use a single table scan and does not require a recursive sub-query factoring clause (CTE) or self joins.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE your_table ( id, usr, start_date, end_date ) AS
SELECT 1, 'A', DATE '2017-06-01', DATE '2017-06-03' FROM DUAL UNION ALL
SELECT 1, 'B', DATE '2017-06-02', DATE '2017-06-04' FROM DUAL UNION ALL -- Overlaps previous
SELECT 1, 'C', DATE '2017-06-06', DATE '2017-06-06' FROM DUAL UNION ALL
SELECT 1, 'D', DATE '2017-06-07', DATE '2017-06-07' FROM DUAL UNION ALL -- Adjacent to previous
SELECT 1, 'E', DATE '2017-06-11', DATE '2017-06-20' FROM DUAL UNION ALL
SELECT 1, 'F', DATE '2017-06-14', DATE '2017-06-15' FROM DUAL UNION ALL -- Within previous
SELECT 1, 'G', DATE '2017-06-22', DATE '2017-06-25' FROM DUAL UNION ALL
SELECT 1, 'H', DATE '2017-06-24', DATE '2017-06-28' FROM DUAL UNION ALL -- Overlaps previous and next
SELECT 1, 'I', DATE '2017-06-27', DATE '2017-06-30' FROM DUAL UNION ALL
SELECT 1, 'J', DATE '2017-06-27', DATE '2017-06-28' FROM DUAL UNION ALL -- Within H and I
SELECT 2, 'K', DATE '2011-08-01', DATE '2011-08-08' FROM DUAL UNION ALL -- Your data below
SELECT 2, 'L', DATE '2011-08-02', DATE '2011-08-06' FROM DUAL UNION ALL
SELECT 2, 'M', DATE '2011-08-03', DATE '2011-08-10' FROM DUAL UNION ALL
SELECT 2, 'N', DATE '2011-08-12', DATE '2011-08-14' FROM DUAL UNION ALL
SELECT 3, 'O', DATE '2011-08-01', DATE '2011-08-03' FROM DUAL UNION ALL
SELECT 3, 'P', DATE '2011-08-02', DATE '2011-08-06' FROM DUAL UNION ALL
SELECT 3, 'Q', DATE '2011-08-05', DATE '2011-08-09' FROM DUAL;
Query 1:
SELECT id,
SUM( days ) AS total_days
FROM (
SELECT id,
dt - LAG( dt ) OVER ( PARTITION BY id
ORDER BY dt ) + 1 AS days,
start_end
FROM (
SELECT id,
dt,
CASE SUM( value ) OVER ( PARTITION BY id
ORDER BY dt ASC, value DESC, ROWNUM ) * value
WHEN 1 THEN 'start'
WHEN 0 THEN 'end'
END AS start_end
FROM your_table
UNPIVOT ( dt FOR value IN ( start_date AS 1, end_date AS -1 ) )
)
WHERE start_end IS NOT NULL
)
WHERE start_end = 'end'
GROUP BY id
Results:
| ID | TOTAL_DAYS |
|----|------------|
| 1 | 25 |
| 2 | 13 |
| 3 | 9 |
The brute force method is to create all days (in a recursive query) and then count:
with dates(id, day, d2) as
(
select id, d1 as day, d2 from mytable
union all
select id, day + 1, d2 from dates where day < d2
)
select id, count(distinct day)
from dates
group by id
order by id;
Unfortunately there is a bug in some Oracle versions and recursive queries with dates don't work there. So try this code and see whether it works in your system. (I have Oracle 11.2 and the bug still exists there; so I guess you need Oracle 12c.)
I guess Markus' idea is to find all starting points that are not within other ranges and all ending points that aren't. Then just take the first starting point till the first ending point, then the next starting point till the next ending point, etc. As Markus isn't using a window function to number starting and ending points, he must find a more complicated way to achieve this. Here is the query with ROW_NUMBER. Maybe this gives you a start what to look for in Markus' query.
select startpoint.id, sum(endpoint.day - startpoint.day)
from
(
select id, d1 as day, row_number() over (partition by id order by d1) as rn
from mytable m1
where not exists
(
select *
from mytable m2
where m1.id = m2.id
and m1.d1 > m2.d1 and m1.d1 <= m2.d2
)
) startpoint
join
(
select id, d2 as day, row_number() over (partition by id order by d1) as rn
from mytable m1
where not exists
(
select *
from mytable m2
where m1.id = m2.id
and m1.d2 >= m2.d1 and m1.d2 < m2.d2
)
) endpoint on endpoint.id = startpoint.id and endpoint.rn = startpoint.rn
group by startpoint.id
order by startpoint.id;
If all your intervals start at different dates, consider them in ascending order by d1 counting how many days are from d1 to the next interval.
You can discard an interval of it is contained in another one.
The last interval won't have a follower.
This query should give you how many days each interval give
select a.id, a.d1,nvl(min(b.d1), a.d2) - a.d1
from orders a
left join orders b
on a.id = b.id and a.d1 < b.d1 and a.d2 between b.d1 and b.d2
group by a.id, a.d1
Then group by id and sum days

Oracle SQL: Get the First Value of each complex Group/Partition

How can I in Oracle with SQL retrieve for a table each first Column A,B, in case column B changes the value ordered by A???
Assume I have a table with date and value:
DATE;VALUE
01-2015;1
02-2015;1
01-2016;2
01-2016;2
01-2017:1
So what I want now, is each first line once the value changes (based on certain orderning here DATE) so from this set I want:
DATE;VALUE
01-2015;1
01-2016;2
01-2017:1
Now I cannot use a simply GROUP BY VALUE, because the value can flip back again (in this case to 1 in 2015 and 2017) and MIN(DATECOL) GROUP BY VALUECOL will not report this 2017.
So I was looking into Analytical functions something like:
SELECT FIRST_VALUE(DATECOL),FIRST_VALUE(VALUECOL) OVER (PARTITION BY
VALUECOL ORDER BY DATECOL) FROM DATATABLE
But I cannot get this working!
Tabibtosan makes this easy:
with table1 as (select to_date('01/01/2015', 'dd/mm/yyyy') dt, 1 val from dual union all
select to_date('01/02/2015', 'dd/mm/yyyy') dt, 1 val from dual union all
select to_date('01/01/2016', 'dd/mm/yyyy') dt, 2 val from dual union all
select to_date('01/01/2016', 'dd/mm/yyyy') dt, 2 val from dual union all
select to_date('01/01/2017', 'dd/mm/yyyy') dt, 1 val from dual)
-- end of mimicking a table "table1" with data in it. See sql below:
select min(dt) dt,
val
from (select dt,
val,
dense_rank() over (order by dt)
- dense_rank() over (partition by val order by dt) grp
from table1)
group by val,
grp;
DT VAL
---------- ----------
01/01/2015 1
01/01/2016 2
01/01/2017 1
I think LAG() is the appropriate function, along with some other logic:
select t.*
from (select t.*, lag(value) over (order by date) as prev_value
from datatable t
) t
where prev_value is null or prev_value <> value;
The only issue with your data is that the rows are not unique. This can cause a problem, because sorting in databases is not stable (that is, two rows can be in either order). Hopefully, in your actual data, the dates are unique or you have another id you can add to the order by to make the sort stable.
One brute force way of doing this is:
with dt as (
select dt.*, rownum as rn
from datatable dt
)
select t.*
from (select dt.*, lag(value) over (order by date, rn) as prev_value
from datatable dt
) t
where prev_value is null or prev_value <> value;

Get Date of Change

I have a table containing Dates and Statuses. I wish to get the date that the status changed to the most recent status. Sample data:
DATE STATUS
01/01/2000 P
02/01/2000 A
03/01/2000 C
04/01/2000 A
05/01/2000 A
06/01/2000 A
So in this instance the most recent status is A and it changed to this on 04/01/2000. (The 02/01/2000 row should be ignored in this situation)
Any suggestions for how to go about selecting this row?
At first, I misunderstood the question. You need to get the earliest date of the last status.
You can group sequences of like statuses using a trick -- a difference of row numbers. The difference (in the query below) is constant for sequences that are the same. Then you can use aggregation to get the minimum date and select the latest one:
select mindate
from (select min(date) as mindate
from (select t.*,
row_number() over (order by date) as seqnum1,
row_number() over (partition by status order by date) as seqnum2
from table t
) t
group by status, (seqnum1 - seqnum2)
order by mindate desc
) t
where rownum = 1
EDIT:
In any case, the right way to do this is using lag():
select max(date)
from (select t.*, lag(status) over (order by date) as prev_status
from table t
)
where prev_status <> status or prev_status is null;
Here is the SQL Fiddle.
You can do this using lag or lead. Here I'm using lead, ordering by date descending to find the previous status date (if it's null I'm just supplying the date, which is needed in case there's only one record).
select max(date)
from (
select status, date, nvl(lead(status) over (order by date desc),date) as previous_status
from t
order by date desc
)
where status <> previous_status;
Something like this ought to do the trick:
with sample_data as (select to_date('01/01/2000', 'dd/mm/yyyy') dt, 'P' status from dual union all
select to_date('02/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('03/01/2000', 'dd/mm/yyyy') dt, 'C' status from dual union all
select to_date('04/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('05/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual union all
select to_date('06/01/2000', 'dd/mm/yyyy') dt, 'A' status from dual),
results1 as (select dt,
status,
row_number() over (order by dt) - row_number() over (partition by status order by dt) grp
from sample_data),
results2 as (select status, min(dt) min_dt, grp, max(min(dt)) over () max_min_dt
from results1
group by status, grp)
select status, min_dt
from results2
where min_dt = max_min_dt;
STATUS MIN_DT
------ ----------
A 04/01/2000

Lowest continuous date without break

I have a table and each record has a date. We can assume that a date range is contiguous if there's not a 3 month break. How can I find the start of the most recent contiguous date range?
For example, imagine if I had this data:
1990-5-1
1990-6-4
1990-10-28
1990-11-14
1990-12-19
1991-1-20
1991-4-30
1991-5-13
I'd like for it to return 1991-4-30 because it's the start of the most recent contiguous range of dates.
I think this does what you're looking for. Using my own table and column names as test data. This is on Oracle.
select * from (
select * from sm_ss_tickets t1 where exists (
select * from sm_ss_tickets t2 where t2.created_date between t1.created_date and t1.created_date+90 and t1.rowid <> t2.rowid
) order by created_date asc
) where rownum = 1;
Maybe something like the following would work:
WITH d1 AS (
SELECT date'1990-05-01' AS dt FROM dual
UNION ALL
SELECT date'1990-06-04' AS dt FROM dual
UNION ALL
SELECT date'1990-10-28' AS dt FROM dual
UNION ALL
SELECT date'1990-11-14' AS dt FROM dual
UNION ALL
SELECT date'1990-12-19' AS dt FROM dual
UNION ALL
SELECT date'1991-01-20' AS dt FROM dual
UNION ALL
SELECT date'1991-04-30' AS dt FROM dual
UNION ALL
SELECT date'1991-05-13' AS dt FROM dual
)
SELECT MAX(dt) FROM (
SELECT dt, LAG(dt) OVER ( ORDER BY dt ) AS prev_dt, LEAD(dt) OVER ( ORDER BY dt ) AS next_dt
FROM d1
) WHERE ( dt > ADD_MONTHS(prev_dt, 3) OR prev_dt IS NULL )
AND dt > ADD_MONTHS(next_dt, -3)
In the above, a date can only be the start of a contiguous sequence if there is no prior date within 3 months (either it is more than three months ago or it doesn't exist at all) and there is also a subsequent date within 3 months.
You can use LAG and LEAD. Find the query below. I think it works fine.
tmp_year is the table I have created. tdate is the column.
The records in the table are
28-JAN-15
27-JAN-15
26-JAN-15
25-JAN-15
12-JUL-14
11-JUL-14
10-JUL-14
09-JUL-14
24-DEC-13
23-DEC-13
22-DEC-13
21-DEC-13
15-SEP-13
07-JUN-13
27-FEB-13
19-NOV-12
11-AUG-12
Please find the query which returns 25th Jan 2015.
select max(d.tdate) from (
select c.tdate,c.next_date,c.date_diff,lag(date_diff) over( order by tdate) prev_diff from (
select b.tdate ,b.next_date,(next_date-tdate) date_diff from
(select a.tdate,lead(a.tdate) over(order by a.tdate) next_date from tmp_year a ) b ) c) d where d.date_diff<90 and d.prev_diff>=90;

SQL Oracle---get last record for every month

I have a table that contain some Inspection Data. Every commodity needs to be inspected every month. The goal here is to find the last inspected record for each month.
Table Inspection:
INSPECTION_I--------INSPECTION_TS
200--------------------------- 10/20/2011
201----------------------------10/24/2011
202----------------------------10/26/2011
Table Product_Inspection:
INSPECTION_I------------------ASSET_I
200------------------------------------1000
201------------------------------------2000
Table Box_Inspection
INSPECTION_I--------ASSET_I
202------------------------3000
Table Product
ASSET_I------------ASSOCIATED_BOX_ASSET_I
1000---------------------------3000
Table BOX:
ASSET_I------------OTHER_STUFF
3000--------------------#####
Now in this case what I want is 201 and not 200. I tried to do MAX(to_char(inspection_ts, 'mm/yyyy')) but that is not helping. There is one more issue. For some reason, I keep getting the Cartesian in a case where a Product or a Box is inspected twice or more in a month. All I want is one inspection every month and it should be the last inspection for each month. I am really close to getting it but if someone can help, I would really appreciate it. I was able to get it done through a nested cursor but I don't want that.
I tend to use analytic functions to do this kind of query. Something like:
with data as
(
select 1 product_id, 100 inspection_id, to_date('09/04/2011', 'MM/DD/YYYY') inspection_date from dual union all
select 1 product_id, 101 inspection_id, to_date('09/14/2011', 'MM/DD/YYYY') inspection_date from dual union all
select 1 product_id, 103 inspection_id, to_date('10/04/2011', 'MM/DD/YYYY') inspection_date from dual union all
select 1 product_id, 105 inspection_id, to_date('11/01/2011', 'MM/DD/YYYY') inspection_date from dual union all
select 2 product_id, 102 inspection_id, to_date('09/24/2011', 'MM/DD/YYYY') inspection_date from dual union all
select 2 product_id, 104 inspection_id, to_date('10/05/2011', 'MM/DD/YYYY') inspection_date from dual
)
select *
from
(
select
product_id,
inspection_id,
inspection_date,
row_number() over (
partition by
product_id,
trunc(inspection_date, 'MM') -- Month
order by
inspection_date desc
) rn
from
data
)
where rn = 1 -- indicates last inspection date of the month for each product
use a subquery
Select [ColName List]
From TableName a
Where DateTimeColumname
= (Select Max(DateTimeColumnName)
From TableName
Where DateTimeColumnName < 1+Last_Day(a.DateTimeColumnName))