Min date with condition using window functions - sql

I'm trying to use window functions to get the first date when ColC became True (grouped by id) and create a new column.
id date ColC
1 2017-04-20 t
1 2017-04-19 t
1 2017-04-18 t
2 2017-04-20 t
2 2017-04-19 f
So as the result I want to get the following:
id first_date
1 2017-04-18
2 2017-04-20
So far I've got an idea of using either FIRST_VALUE(date) over(partition by id order by date) or min(). However, it's still a question how to incorporate the condition ColC = t.
Is it possible to get the result with one line?
Thank you in advance!

Below is for BigQuery Standard SQL and handles more generic cases when ColC can changes its status from f to t and back to f and then back to t again multiple times
#standardSQL
WITH yourTable AS (
SELECT 1 AS id, DATE '2017-04-20' AS date, 't' AS ColC UNION ALL
SELECT 1, DATE '2017-04-19', 't' UNION ALL
SELECT 1, DATE '2017-04-18', 't' UNION ALL
SELECT 2, DATE '2017-04-21', 't' UNION ALL
SELECT 2, DATE '2017-04-20', 't' UNION ALL
SELECT 2, DATE '2017-04-19', 'f' UNION ALL
SELECT 2, DATE '2017-04-18', 't' UNION ALL
SELECT 2, DATE '2017-04-17', 'f'
)
SELECT id, date AS switch_date
FROM (
SELECT
id, date, ColC,
LAG(ColC) OVER(PARTITION BY id ORDER BY date) AS prevColC
FROM yourTable
)
WHERE ColC = 't' AND IFNULL(prevColC, 'f') != 't'
-- ORDER BY id, date DESC

I would be tempted to do:
select min(case when colc = 't' then date end) over (partition by id)
Of course, for your particular result, group by is simpler:
select id, min(date)
from t
where colc = 't'
group by id;

how about:
select id, min(date) as first_date from tablename where ColC='t' group by id

Related

How can I use the LAG FUNCTION to show revenue this year vs last year in Snowflake with?

I would like to show the current revenue vs last's year's revenue in the same row per region per type. Example:For 2022-04-01, US, Type 1 --> REVENUE: 2456, REVENUE_LAST_YEAR: 4000
2021-04-01, US, Type 1 --> REVENUE: 4000, REVENUE_LAST_YEAR: 0
For some reason, the Lag formula in Snowflake is showing wrong values. Could someone please help ?
WITH
indata(dt,region,type,revenue) AS (
SELECT DATE '2021-04-01','US','Type 1',4000 UNION ALL SELECT DATE '2021-05-01','Europe','Type 2',5777
UNION ALL SELECT DATE '2021-06-01','US','Type 1',45433 UNION ALL SELECT DATE '2021-07-01','Europe','Type 2',8955
UNION ALL SELECT DATE '2021-08-01','US','Type 1',45777 UNION ALL SELECT DATE '2021-09-01','Asia','Type 1',7533
UNION ALL SELECT DATE '2021-10-01','US','Type 1',8866 UNION ALL SELECT DATE '2021-11-01','Asia','Type 2',5534
UNION ALL SELECT DATE '2021-12-01','US','Type 2',4000 UNION ALL SELECT DATE '2022-01-01','Asia','Type 1',7244
UNION ALL SELECT DATE '2022-02-01','US','Type 1',6678 UNION ALL SELECT DATE '2022-03-01','Asia','Type 1',5654
UNION ALL SELECT DATE '2022-04-01','US','Type 1',2456 UNION ALL SELECT DATE '2022-05-01','Asia','Type 1',4525
UNION ALL SELECT DATE '2022-06-01','US','Type 1',6677 UNION ALL SELECT DATE '2022-07-01','Asia','Type 1',6654
UNION ALL SELECT DATE '2022-08-01','US','Type 1',6677 UNION ALL SELECT DATE '2022-09-01','Asia','Type 2',5754
UNION ALL SELECT DATE '2022-10-01','US','Type 1',7744 UNION ALL SELECT DATE '2022-11-01','Asia','Type 2',5644
UNION ALL SELECT DATE '2022-12-01','Europe','Type 2',6775 UNION ALL SELECT DATE '2023-01-01','Asia','Type 2',6777
UNION ALL SELECT DATE '2023-02-01','Europe','Type 2',7755
)
SELECT indata.*,
lag(REVENUE, 1, 0) over (partition by region,type,revenue order by year(dt)) REVENUE_last_year
FROM indata
order by year(dt)
Partitioning by region, type and month-day:
SELECT indata.*,
LAG(REVENUE, 1, 0) over (partition by region,type, TO_VARCHAR(dt, 'mmdd')
order by dt) AS REVENUE_last_year
FROM indata
ORDER BY dt;
Output:

How to count days between 2 rows multiple scenarios

I would like to count days from table like this:
[]]1
I have 2 columns with status and date, I need to count how many days one row had status with 1 and 3, for example: first row has staus 1 second row has status 2 the difference between those two rows is 3 days, the same eight and nine row. All those calculation I neet to do in sql.
This will work for sql-server 2012. I was unable to find any documentation that would prevent this syntax in express.
DECLARE #t table(Status int, Date datetime)
INSERT #t
VALUES
(1,'2018-02-15'),
(2,'2018-02-18'),
(3,'2018-02-20'),
(2,'2018-02-23')
;WITH CTE as
(
SELECT Date, lead(Date)over(order by date) NextDate, Status
FROM #t
)
SELECT sum(DateDiff(day, Date, NextDate))
FROM CTE
WHERE Status in (1,3)
You can do that if you have analytic (aka window) functions available (Oracle, SQL Server, Postgresql, MySQL). The basic idea would be (using Oracle SQL dialect):
select
sum(trunc(next_date) - trunc(dat))
from (
select
status,
dat,
lead(dat) over (order by dat asc) next_date
from
( -- This is a "fake" data table
select 1 as status, to_date('15/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 2 as status, to_date('18/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 3 as status, to_date('20/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 2 as status, to_date('23/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 4 as status, to_date('24/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 7 as status, to_date('27/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 9 as status, to_date('28/2/2018' ,'dd/mm/yyyy') dat from dual
union all
select 3 as status, to_date('1/3/2018' ,'dd/mm/yyyy') dat from dual
union all
select 14 as status, to_date('4/3/2018' ,'dd/mm/yyyy') dat from dual
union all
select 15 as status, to_date('7/3/2018' ,'dd/mm/yyyy') dat from dual
union all
select 1 as status, to_date('10/3/2018' ,'dd/mm/yyyy') dat from dual
union all
select 2 as status, to_date('14/3/2018' ,'dd/mm/yyyy') dat from dual
) d
) d2
where
status in (1, 3);
Other dialects may be slightly different but the idea is the same.

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

How do I find the total number of used days in a month?

I am arriving at the total number of days a service has been used in a month.
(Start_Date and End_Date are - both inclusive)
Sample Data 1:
User Start_Date End_Date
A 01-Jun-2017 30-Jun-2017
B 06-Jun-2017 30-Jun-2017
Ans: Service used days = 30 days.
Sample Data 2:
User Start_Date End_Date
C 06-Jun-2017 10-Jun-2017
D 02-Jun-2017 02-Jun-2017
Ans: Service used days = 6 days.
How do I write a code to find the same, preferable in SQL to PLSQL.
Test Data:
CREATE TABLE your_table ( usr, start_date, end_date ) AS (
SELECT 'A', DATE '2017-06-01', DATE '2017-06-03' FROM DUAL UNION ALL
SELECT 'B', DATE '2017-06-02', DATE '2017-06-04' FROM DUAL UNION ALL -- Overlaps previous
SELECT 'C', DATE '2017-06-06', DATE '2017-06-06' FROM DUAL UNION ALL
SELECT 'D', DATE '2017-06-07', DATE '2017-06-07' FROM DUAL UNION ALL -- Adjacent to previous
SELECT 'E', DATE '2017-06-11', DATE '2017-06-20' FROM DUAL UNION ALL
SELECT 'F', DATE '2017-06-14', DATE '2017-06-15' FROM DUAL UNION ALL -- Within previous
SELECT 'G', DATE '2017-06-22', DATE '2017-06-25' FROM DUAL UNION ALL
SELECT 'H', DATE '2017-06-24', DATE '2017-06-28' FROM DUAL UNION ALL -- Overlaps previous and next
SELECT 'I', DATE '2017-06-27', DATE '2017-06-30' FROM DUAL UNION ALL
SELECT 'J', DATE '2017-06-27', DATE '2017-06-28' FROM DUAL; -- Within H and I
Query:
SELECT SUM( days ) AS total_days
FROM (
SELECT dt - LAG( dt ) OVER ( ORDER BY dt ) + 1 AS days,
start_end
FROM (
SELECT dt,
CASE SUM( value ) OVER ( 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';
Output:
TOTAL_DAYS
----------
25
Explanation:
SELECT dt, value
FROM your_table
UNPIVOT ( dt FOR value IN ( start_date AS 1, end_date AS -1 ) )
This will UNPIVOT the table so that the start and end dates are in the same column (dt) and are given a corresponding value of +1 for a start and -1 for an end date.
SELECT dt,
SUM( value ) OVER ( ORDER BY dt ASC, value DESC, ROWNUM ) AS total,
value
FROM your_table
UNPIVOT ( dt FOR value IN ( start_date AS 1, end_date AS -1 ) )
Will give the start and end dates and the cumulative sum of those generated values. The start of a range will always have value=1 and total=1 and the end of a range will always have total=0. If a date is mid-way through a range then it will either have total>1 or value=-1 and total=1. Using this, if you multiply value and total then the start of a range is when value*total=1 and the end of a range is when value*total=0 and any other value indicates a date that is midway through a range.
Which is what this gives:
SELECT dt,
CASE SUM( value ) OVER ( 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 ) )
You can then filter out the dates when the start_end is NULL which will leave you with a table with alternating start and end rows which you can use LAG to calculate the number of days difference:
SELECT dt - LAG( dt ) OVER ( ORDER BY dt ) + 1 AS days,
start_end
FROM (
SELECT dt,
CASE SUM( value ) OVER ( 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
All you need to do then is to SUM all the differences for the end - start; which gives the query above.
As #Pravin Satav addressed, your requirement it's not very clear, something like this is what I understood from your explanation:
SELECT sum(CASE WHEN end_date=start_date THEN 1 ELSE (end_date-start_date)+1 END) as total_days
FROM my_table
WHERE <conditions that determine your "sample data">;

SQL: Dynamic Date creation issue

Need Suggestion to make it dynamic On Dates.
Expected:
Date, Total Sellers, Sellers From Previous Date
Currently:
Data in table(active_seller_codes): date, seller_code
Queries:
-- Date Wise Sellers Count
select date,count(distinct seller_code) as Sellers_COunt
from active_seller_codes where date between '2016-12-15' AND '2016-12-15'
-- Sellers from previous Days
select date,count(distinct seller_code) as Last_Day_Seller
from active_seller_codes
where date between '2016-12-15' AND '2016-12-15'
and seller_code IN(
select seller_code from active_seller_codes
where date between '2016-12-14' AND '2016-12-14'
)
group by 1
Database Using: Vertica
Reading attentively, you seem to want one row in the report, with the data from the search date in the first two columns and the data of the day before the search date in the third and fourth column, like so:
sales_date|sellers_count|prev_date |prev_sellers_count
2016-12-15| 8|2016-12-14| 5
The solution could be something like this (without the first Common Table Expression, which, in my case, contains the data, but in your case, the data would be in your active_seller_codes table.
WITH
-- initial input
(sales_date,seller_code) AS (
SELECT DATE '2016-12-15',42
UNION ALL SELECT DATE '2016-12-15',43
UNION ALL SELECT DATE '2016-12-15',44
UNION ALL SELECT DATE '2016-12-15',45
UNION ALL SELECT DATE '2016-12-15',46
UNION ALL SELECT DATE '2016-12-15',47
UNION ALL SELECT DATE '2016-12-15',48
UNION ALL SELECT DATE '2016-12-15',49
UNION ALL SELECT DATE '2016-12-14',42
UNION ALL SELECT DATE '2016-12-14',44
UNION ALL SELECT DATE '2016-12-14',46
UNION ALL SELECT DATE '2016-12-14',48
UNION ALL SELECT DATE '2016-12-14',50
UNION ALL SELECT DATE '2016-12-13',42
UNION ALL SELECT DATE '2016-12-13',43
UNION ALL SELECT DATE '2016-12-13',44
UNION ALL SELECT DATE '2016-12-13',45
UNION ALL SELECT DATE '2016-12-13',46
UNION ALL SELECT DATE '2016-12-13',47
UNION ALL SELECT DATE '2016-12-13',48
UNION ALL SELECT DATE '2016-12-13',49
)
,
-- search argument this, in the real query, would come just after the WITH keyword
-- as the above would be the source table
search_dt(search_dt) AS (SELECT DATE '2016-12-15')
,
-- the two days we're interested in, de-duped
distinct_two_days AS (
SELECT DISTINCT
sales_date
, seller_code
FROM active_seller_codes
WHERE sales_date IN (
SELECT search_dt FROM search_dt -- the search date
UNION ALL SELECT search_dt - 1 FROM search_dt -- the day before
)
)
,
-- the two days we want one above the other,
-- with index for the final pivot
vertical AS (
SELECT
ROW_NUMBER() OVER (ORDER BY sales_date DESC) AS idx
, sales_date
, count(DISTINCT seller_code) AS seller_count
FROM distinct_two_days
GROUP BY 2
)
SELECT
MAX(CASE idx WHEN 1 THEN sales_date END) AS sales_date
, SUM(CASE idx WHEN 1 THEN seller_count END) AS sellers_count
, MAX(CASE idx WHEN 2 THEN sales_date END) AS prev_date
, SUM(CASE idx WHEN 2 THEN seller_count END) AS prev_sellers_count
FROM vertical
;
sales_date|sellers_count|prev_date |prev_sellers_count
2016-12-15| 8|2016-12-14| 5