get data for first 3 months that have claims - sql

I have a table of members and their claims value, I'm interested in getting the claims values for the first 3 months for each member. Here's what I've tried so far:
WITH START as
(SELECT [HEALTH_ID]
,MIN([CLM_MONTH]) as DOS
FROM [TEST]
GROUP BY
[HEALTH_PLAN_ID])
SELECT HEALTH_ID
,DOS
,FORMAT(DATEADD(month, +1, DOS), 'MM/dd/yyyy')
,FORMAT(DATEADD(month, +2, DOS), 'MM/dd/yyyy')
FROM START
My plan is to get the dates of the first 3 months with claims then join the claim amounts to ID and dates. The problem here is not every member has claims in consecutive months and the dateadd function gives me consecutive months. For example if a member has claims in jan, feb, april, may etc...I'm interested in the claims for jan, feb and april since there were no claims in march. Using the dateadd function would give me dates jan, feb, march excluding april.
In summary, I need help getting the first 3 months that have claims values(months may or may not be consecutive).

Using dense_rank() to rank the months, partitioned by Health_Id, in order to filter for the first three months of each Health_Id.
;with cte as (
select *
, dr = dense_rank() over (
partition by Health_ID
order by dateadd(month, datediff(month, 0, CLM_Month) , 0) /* truncate to month */
)
from test
)
select *
from cte
where dr < 4 -- dense rank of 1-3
test data:
create table test (health_id int, clm_month date)
insert into test values
(1,'20170101'),(1,'20170201'),(1,'20170301'),(1,'20170401')
,(2,'20170101'),(2,'20170201'),(2,'20170401'),(2,'20170501') -- no March
,(3,'20170101'),(3,'20170115'),(3,'20170201'),(3,'20170215') -- Multiple per month
,(3,'20170401'),(3,'20170415'),(3,'20170501'),(3,'20170515')
rextester demo: http://rextester.com/MTZ16877
returns:
+-----------+------------+----+
| health_id | clm_month | dr |
+-----------+------------+----+
| 1 | 2017-01-01 | 1 |
| 1 | 2017-02-01 | 2 |
| 1 | 2017-03-01 | 3 |
| 2 | 2017-01-01 | 1 |
| 2 | 2017-02-01 | 2 |
| 2 | 2017-04-01 | 3 |
| 3 | 2017-01-01 | 1 |
| 3 | 2017-01-15 | 1 |
| 3 | 2017-02-01 | 2 |
| 3 | 2017-02-15 | 2 |
| 3 | 2017-04-01 | 3 |
| 3 | 2017-04-15 | 3 |
+-----------+------------+----+

Related

SQL Query to apply a command to multiple rows

I am new to SQL and trying to write a statement similar to a 'for loop' in other languages and am stuck. I want to filter out rows of the table where for all of attribute 1, attribute2=attribute3 without using functions.
For example:
| Year | Month | Day|
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 1 | 4 | 4 |
| 2 | 3 | 4 |
| 2 | 3 | 3 |
| 2 | 4 | 4 |
| 3 | 4 | 4 |
| 3 | 4 | 4 |
| 3 | 4 | 4 |
I would only want the row
| Year | Month | Day|
|:---- |:------:| -----:|
| 3 | 4 | 4 |
because it is the only where month and day are equal for all of the values of year they share.
So far I have
select year, month, day from dates
where month=day
but unsure how to apply the constraint for all of year
-- month/day need to appear in aggregate functions (since they are not in the GROUP BY clause),
-- but the HAVING clause ensure we only have 1 month/day value (per year) here, so MIN/AVG/SUM/... would all work too
SELECT year, MAX(month), MAX(day)
FROM my_table
GROUP BY year
HAVING COUNT(DISTINCT (month, day)) = 1;
year
max
max
3
4
4
View on DB Fiddle
So one way would be
select distinct [year], [month], [day]
from [Table] t
where [month]=[day]
and not exists (
select * from [Table] x
where t.[year]=x.[year] and t.[month] <> x.[month] and t.[day] <> x.[day]
)
And another way would be
select distinct [year], [month], [day] from (
select *,
Lead([month],1) over(partition by [year] order by [month])m2,
Lead([day],1) over(partition by [year] order by [day])d2
from [table]
)x
where [month]=m2 and [day]=d2

SQL - get summary of differences vs previous month

I have a table similar to this one:
| id | store | BOMdate |
| 1 | A | 01/10/2018 |
| 1 | B | 01/10/2018 |
| 1 | C | 01/10/2018 |
|... | ... | ... |
| 1 | A | 01/11/2018 |
| 1 | C | 01/11/2018 |
| 1 | D | 01/11/2018 |
|... | ... | ... |
| 1 | B | 01/12/2018 |
| 1 | C | 01/12/2018 |
| 1 | E | 01/12/2018 |
It contains the stores that are active at BOM (beginning of month).
How do I query it to get the amount of stores that are new that month - those that where not active the previous month?
The output should be this:
| BOMdate | #newstores |
| 01/10/2018 | 3 | * no stores on previous month
| 01/11/2018 | 1 | * D is the only new active store
| 01/12/2018 | 2 | * store B was not active on November, E is new
I now how to count the first time that each store is active (nested select, taking the MIN(BOMdate) and then counting). But I have no idea how to check each month vs its previous month.
I use SQL Server, but I am interested in the differences in other platforms if there are any.
Thanks
How do I query it to get the amount of stores that are new that month - those that where not active the previous month?
One option uses not exists:
select bomdate, count(*) cnt_new_stores
from mytable t
where not exists (
select 1
from mytable t1
where t1.store = t.store and t1.bomdate = dateadd(month, -1, t.bomdate)
)
group by bomdate
You can also use window functions:
select bomdate, count(*) cnt_new_stores
from (
select t.*, lag(bomdate) over(partition by store order by bomdate) lag_bomdate
from mytable t
) t
where bomdate <> dateadd(month, 1, lag_bomdate) or lag_bomdate is null
group by bomdate
you can compare a date with previous month's date using DATEDIFF function of TSQL.
Using NOT EXIST you can count the stores which did not appear in last month as well you can get the names in a list using STRING_AGG function of TSQL introduced from SQL 2017.
select BOMDate, NewStoresCount=count(1),NewStores= STRING_AGG(store,',') from
yourtable
where not exists
(
Select 1 from
yourtable y where y.store=store and DATEDIFF(m,y.BOMDate,BOMDate)=1
)
group by BOMDate

Showing date even zero value SQL

I have SQL Query:
SELECT Date, Hours, Counts FROM TRANSACTION_DATE
Example Output:
Date | Hours | Counts
----------------------------------
01-Feb-2018 | 20 | 5
03-Feb-2018 | 25 | 3
04-Feb-2018 | 22 | 3
05-Feb-2018 | 21 | 2
07-Feb-2018 | 28 | 1
10-Feb-2018 | 23 | 1
If you can see, there are days that missing because no data/empty, but I want the missing days to be shown and have a value of zero:
Date | Hours | Counts
----------------------------------
01-Feb-2018 | 20 | 5
02-Feb-2018 | 0 | 0
03-Feb-2018 | 25 | 3
04-Feb-2018 | 22 | 3
05-Feb-2018 | 21 | 2
06-Feb-2018 | 0 | 0
07-Feb-2018 | 28 | 1
08-Feb-2018 | 0 | 0
09-Feb-2018 | 0 | 0
10-Feb-2018 | 23 | 1
Thank you in advanced.
You need to generate a sequence of dates. If there are not too many, a recursive CTE is an easy method:
with dates as (
select min(date) as dte, max(date) as last_date
from transaction_date td
union all
select dateadd(day, 1, dte), last_date
from dates
where dte < last_date
)
select d.date, coalesce(td.hours, 0) as hours, coalesce(td.count, 0) as count
from dates d left join
transaction_date td
on d.dte = td.date;

Union in outer query

I'm attempting to combine multiple rows using a UNION but I need to pull in additional data as well. My thought was to use a UNION in the outer query but I can't seem to make it work. Or am I going about this all wrong?
The data I have is like this:
+------+------+-------+---------+---------+
| ID | Time | Total | Weekday | Weekend |
+------+------+-------+---------+---------+
| 1001 | AM | 5 | 5 | 0 |
| 1001 | AM | 2 | 0 | 2 |
| 1001 | AM | 4 | 1 | 3 |
| 1001 | AM | 5 | 3 | 2 |
| 1001 | PM | 5 | 3 | 2 |
| 1001 | PM | 5 | 5 | 0 |
| 1002 | PM | 4 | 2 | 2 |
| 1002 | PM | 3 | 3 | 0 |
| 1002 | PM | 1 | 0 | 1 |
+------+------+-------+---------+---------+
What I want to see is like this:
+------+---------+------+-------+
| ID | DayType | Time | Tasks |
+------+---------+------+-------+
| 1001 | Weekday | AM | 9 |
| 1001 | Weekend | AM | 7 |
| 1001 | Weekday | PM | 8 |
| 1001 | Weekend | PM | 2 |
| 1002 | Weekday | PM | 5 |
| 1002 | Weekend | PM | 3 |
+------+---------+------+-------+
The closest I've come so far is using UNION statement like the following:
SELECT * FROM
(
SELECT Weekday, 'Weekday' as 'DayType' FROM t1
UNION
SELECT Weekend, 'Weekend' as 'DayType' FROM t1
) AS X
Which results in something like the following:
+---------+---------+
| Weekday | DayType |
+---------+---------+
| 2 | Weekend |
| 0 | Weekday |
| 2 | Weekday |
| 0 | Weekend |
| 10 | Weekday |
+---------+---------+
I don't see any rhyme or reason as to what the numbers are under the 'Weekday' column, I suspect they're being grouped somehow. And of course there are several other columns missing, but since I can't put a large scope in the outer query with this as inner one, I can't figure out how to pull those in. Help is greatly appreciated.
It looks like you want to union all a pair of aggregation queries that use sum() and group by id, time, one for Weekday and one for Weekend:
select Id, DayType = 'Weekend', [time], Tasks=sum(Weekend)
from t
group by id, [time]
union all
select Id, DayType = 'Weekday', [time], Tasks=sum(Weekday)
from t
group by id, [time]
Try with this
select ID, 'Weekday' as DayType, Time, sum(Weekday)
from t1
group by ID, Time
union all
select ID, 'Weekend', Time, sum(Weekend)
from t1
group by ID, Time
order by order by 1, 3, 2
Not tested, but it should do the trick. It may require 2 proc sql steps for the calculation, one for summing and one for the case when statements. If you have extra lines, just use a max statement and group by ID, Time, type_day.
Proc sql; create table want as select ID, Time,
sum(weekday) as weekdayTask,
sum(weekend) as weekendTask,
case when calculated weekdaytask>0 then weekdaytask
when calculated weekendtask>0 then weekendtask else .
end as Task,
case when calculated weekdaytask>0 then "Weekday"
when calculated weekendtask>0 then "Weekend"
end as Day_Type
from have
group by ID, Time
;quit;
Proc sql; create table want2 as select ID, Time, Day_Type, Task
from want
;quit;

Count concurrent dates in user-input date range using SQL

The user will input a date range, and I want to output in SQL every date between and including that range in the number of concurrent uses of said equipment.
In this example, the user date range is 03/08/2016 to 03/09/2016, so you can see below I include anything on or between those dates (grouped by category, but I've simplified here by only using 'powerchair')
The table schema is as follows;
trans_date | trans_end_date | eq_category
17/03/2016 | 16/10/2016 | POWERCHAIR
08/08/2016 | 08/08/2016 | POWERCHAIR
12/08/2016 | 12/08/2016 | POWERCHAIR
17/08/2016 | 18/08/2016 | POWERCHAIR
22/08/2016 | 22/08/2016 | POWERCHAIR
26/08/2016 | 26/08/2016 | POWERCHAIR
02/09/2016 | 02/09/2016 | POWERCHAIR
And I would like to output;
date | concurrent_use
03-08-2016 | 1
04-08-2016 | 1
05-08-2016 | 1
06-08-2016 | 1
07-08-2016 | 1
08-08-2016 | 2
09-08-2016 | 1
10-08-2016 | 1
11-08-2016 | 1
12-08-2016 | 2
13-08-2016 | 1
14-08-2016 | 1
15-08-2016 | 1
16-08-2016 | 1
17-08-2016 | 2
18-08-2016 | 2
19-08-2016 | 1
20-08-2016 | 1
21-08-2016 | 1
22-08-2016 | 2
23-08-2016 | 1
24-08-2016 | 1
25-08-2016 | 1
26-08-2016 | 2
27-08-2016 | 1
28-08-2016 | 1
29-08-2016 | 1
30-08-2016 | 1
31-08-2016 | 1
01-09-2016 | 1
02-09-2016 | 2
03-09-2016 | 1
Anything 1 or 0, I can then filter out as there mustn't have been any equipment out concurrently that day.
I don't think this is a gaps/islands problem, but I'm drawing a blank trying to get this in an SQL statement.
Try like below. You need to generate dates using recursive cte. Then we need to count the no of occurrences of each date falling in range.
;WITH CTE
AS (SELECT CONVERT(DATE, '2016-08-03', 103) DATE1
UNION ALL
SELECT Dateadd(DAY, 1, DATE1) AS DATE1
FROM CTE
WHERE Dateadd(DD, 1, DATE1) <= '2016-09-03')
SELECT C.DATE1,
Count(1) OCCURENCES
FROM CTE C
JOIN #TABLE1 T
ON C.DATE1 BETWEEN [TRANS_DATE] AN [TRANS_END_DATE]
GROUP BY C.DATE1
You need a set of numbers or dates. So, if you want everything in that range:
with d as (
select cast('2016-08-03' as date) as d
union all
select dateadd(day, 1, d.d)
from d
where d < '2016-09-03'
)
select d.d, count(s.trans_date)
from d left join
schema s
on d.d between s.trans_date and s.trans_date_end
group by d.d;
I'm not sure if both the start and end dates are included in the range.