SQL Server: Data between two dates - sql-server

Given that there are two dates like 2012-07-08 and 2013-02-06. I need to count record of each user for every month. If there is no data for a user in particular month then it should return zero. The output should look like:-
id name July-12 August-12 September-12 October-12 November-12 December-12 January-13 February-13
1 John 1 2 0 0 1 1 2 2
2 David 0 1 0 1 1 1 2 2
3 Marry 2 1 0 0 0 1 2 2

You cant use BETWEEN in case of date even if BETWEEN is inclusive.
For such cases it is better to use
startdate>='2012-07-08 ' and enddate<=2013-02-06
Because 2013-02-06 will be treated as 2013-02-06 00:00:00 12AM
Try this solution for your problem
DECLARE #temp AS TABLE(Month_Name varchar(25),ContactName varchar(50)) -- newly added
;WITH cte
AS
(
SELECT datename(month, #startdate) AS [Month_Name], #startdate AS dat
UNION ALL
SELECT datename(month, DateAdd(Month, 1, dat)), DateAdd(Month, 1, dat)
FROM cte
WHERE DateAdd(Month, 1, dat) < #enddate
)
INSERT INTO #temp
SELECT c.Month_Name,Cs.username
FROM cte c CROSS JOIN usertbale Cs
ORDER BY c.Month_Name
SELECT c.Month_Name,c.username,ISNULL(tt.coun,0) as coun
FROM #temp c LEFT JOIN
(
SELECT datename(month,OrderDate) AS month_Name
,count(OrderID) as coun
,ContactName
FROM tblpublisher p INNER JOIN tbluser u ON u.userid=p.userid
GROUP BY month_Name,datename(month,publishdate)
) as tt
ON c.Month_Name=tt.month_Name AND c.username=tt.username
EDIT
Using pivot in above query you can achieve the exact result as in your question
Check this

Hope you would require the below function to populate all dates between given condition and you may need to use pivoting to accomplish the task.
Exec: SELECT * FROM dbo.GetDates('2012-07-08','2013-02-06')
CREATE FUNCTION [dbo].[GetDates](#StartDate DATETIME, #EndDate DATETIME)
RETURNS TABLE AS
RETURN (
WITH
N0 AS (SELECT 1 AS N UNION ALL SELECT 1)
,N1 AS (SELECT 1 AS N FROM N0 T1, N0 T2)
,N2 AS (SELECT 1 AS N FROM N1 T1, N1 T2)
,N3 AS (SELECT 1 AS N FROM N2 T1, N2 T2)
,N4 AS (SELECT 1 AS N FROM N3 T1, N3 T2)
,N5 AS (SELECT 1 AS N FROM N4 T1, N4 T2)
,N6 AS (SELECT 1 AS N FROM N5 T1, N5 T2)
,NUMS AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS NUM FROM N6)
SELECT CAST(DATEADD(day,num-1,#StartDate) AS Date) as TheDate
FROM NUMS
WHERE NUM <= DATEDIFF(day,#StartDate,#EndDate) + 1
);

Related

Compare two tables and retrieve data

I have 2 tables in SQL Server and I want to compare them. I want to take 'NEEDED_AMOUNT' and 'min. 'ID'. I tried the following:
SELECT S_ID, NEEDED_AMOUNT, ID
FROM (
select T1.S_ID
, T2.NEEDED_AMOUNT
, T1.ID
from T1
INNER JOIN T2 MSD ON T1.S_ID = T2.S_ID
) TABLE1
GROUP BY S_ID, NEEDED_AMOUNT, ID
To explain this for example: in T1 table I have S_ID as '1' and its amount '20' and '30'. Also in T2 I have request for S_ID and I need '40' amount. So in T1 table how can I reach 40? I must take first row '20' amount and I split second row '30' to '20'. Below you can see what I want the output.
So here are the tables.
I can call this table T1 (ID is primary key and auto inc.):
ID AMOUNT S_ID
1 20 1
2 30 1
3 10 2
4 20 3
5 5 3
and I can call this table T2:
S_ID NEEDED_AMOUNT DATE
1 40 01.01.2020
2 5 02.01.2020
3 20 03.01.2020
So my output will be like this:
S_ID NEEDED_AMOUNT ID
1 20 1
1 20 2
2 5 3
3 20 4
Thanks for any opinion
I would use recursive approach for this :
with cte as (
select id, amount, s_id, needed_amount,
(case when amount = needed_amount then 1 else cnt end) as cnt
from (select t1.*, t2.needed_amount,
row_number() over (partition by t1.s_id order by t1.id) as seq,
count(*) over (partition by t1.s_id) as cnt
from t1 inner join
t2
on t2.s_id = t1.s_id
) t
where seq = 1
), cte1 as (
select c.needed_amount / c.cnt as amount, c.s_id, 1 as start, c.cnt
from cte c
union all
select amount, s_id, start + 1, cnt
from cte1 c1
where start < cnt
)
select s_id, amount, row_number() over (order by s_id) as id
from cte1;

Split number into rows so that they sum up to the original number

I have a table [tbl] with money values
id mon
1 10.17
2 36.00
I need to split these values into rows by a set of specific ranges [1.00,10.00,25.00]. The sum of the new values grouped by id will equal the original value.
id mon sum
1 1.00 1.00
1 9.17 10.17
2 1.00 1.00
2 10.00 11.00
2 25.00 36.00
Is there any way to do this without using a cursor?
Here's one way to do it:
;with CTE as (select t2.value, t1.id, sum(t2.value)
over (partition by t1.id order by t2.value asc) as total
from table1 t1 join table2 t2 on t1.mon >= t2.limit
)
select id, value, total from CTE
union all
select t1.id, t1.mon - c.total, t1.mon
from table1 t1
outer apply (select top 1 id, total from CTE c
where c.id = t1.id order by c.value desc) c
where t1.mon > c.total
order by 1,3
This uses additional table that has the limits stored to join with the original data and then uses running total in a CTE and joins that to the original table to get the remaining amounts
You can test the example in SQL Fiddle
Here is my attempt using window functions and CROSS APPLY:
;WITH Cte(s) AS(
SELECT CAST(1 AS MONEY) UNION ALL
SELECT 10 UNION ALL
SELECT 25
)
,CteRange AS(
SELECT
s,
e = SUM(s) OVER(ORDER BY s)
FROM Cte
)
SELECT
t.id,
mon = CASE WHEN t.mon > x.e THEN x.s ELSE mon - LAG(x.e) OVER(PARTITION BY t.id ORDER BY x.s) END,
[sum] = CASE WHEN t.mon < x.e THEN t.mon ELSE x.e END
FROM tbl t
CROSS APPLY(
SELECT * FROM CteRange
)x
WHERE t.mon > x.s
UNION ALL
SELECT
t.id,
mon = t.mon - x.e,
[sum] = t.mon
FROM tbl t
CROSS APPLY(
SELECT TOP 1 e
FROM CteRange
ORDER BY e DESC
)x(e)
WHERE t.mon > e
ORDER BY t.id, mon
SQL Fiddle
This works for your given example data, you just need to predefine ranges all by yourself (I've used CROSS JOIN VALUES, but this can be done however you want/prefer). I think that's not an issue. I've used running SUM and analytic functions to achieve that.
DECLARE #tbl TABLE
(
id INT IDENTITY (1, 1)
, mon MONEY
);
INSERT INTO #tbl (mon)
VALUES (10.17), (36.00);
SELECT id
, [sum] - SUM(lagRange) OVER (PARTITION BY ID ORDER BY rangeId) AS mon
, [sum]
FROM (
SELECT id, rangeId
, LAG(rangeValue, 1, 0) OVER (PARTITION BY ID ORDER BY rangeId) AS lagRange
, CASE
WHEN SUM(rangeValue) OVER (PARTITION BY ID ORDER BY rangeId) > mon THEN mon
ELSE SUM(rangeValue) OVER (PARTITION BY ID ORDER BY rangeId)
END AS [sum]
FROM #tbl
CROSS JOIN (VALUES ((1), (1.00)), ((2), (10.00)), ((3), (25.00))) AS T(rangeId, rangeValue)
WHERE rangeValue <= mon
) AS T;
Results:
id mon sum
-----------------
1 1.00 1.00
1 9.17 10.17
2 1.00 1.00
2 10.00 11.00
2 25.00 36.00

Finding sequence of the last numeric value in a varchar variable

I have a column in a table which has incremented values like:
AAA0000001
AAA0000002
... and so on
I want to find if the values stored in this column are in proper sequential order or if any value is missing in between or is deleted.
How can i achieve this?
Assuming the pattern is always: AAA[0-9][0-9][0-9][0-9][0-9][0-9][0-9], you can do this with a Tally Table.
Sample Data:
CREATE TABLE Tbl(val VARCHAR(10))
INSERT INTO Tbl VALUES
('AAA0000001'), ('AAA0000002'), ('AAA0000004'), ('AAA0000011');
val
----------
AAA0000001
AAA0000002
AAA0000004
AAA0000011
SQL Fiddle
;WITH Cte AS(
SELECT *,
num = CAST(SUBSTRING(val, 4, LEN(val) - 3) AS INT)
FROM Tbl
),
E1(N) AS(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b),
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b),
Tally(N) AS(
SELECT TOP(SELECT MAX(num) FROM Cte)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM E4
)
SELECT
N,
val = 'AAA' + RIGHT('0000000' + CAST(N AS VARCHAR(7)), 7)
FROM Tally
WHERE NOT EXISTS(
SELECT 1 FROM Cte WHERE num = N
)
RESULT
N val
-------------------- ----------
3 AAA0000003
5 AAA0000005
6 AAA0000006
7 AAA0000007
8 AAA0000008
9 AAA0000009
10 AAA0000010
Explanation:
The first CTE, named as Cte, extracts the numeric part of the strings and CASTs them to INT.
The succeeding CTEs, from E1 to Tally(N) generates a table with sequential values from 1 up to the MAX(num) - the INT return from the first CTE.
The final SELECT just checks for the non-existing num from the first CTE.
'AAA' + RIGHT('0000000' + CAST(N AS VARCHAR(7)), 7) transforms N so that it follows the pattern.
This is a Gaps problem. You can look into this article by Dwain Camps for more solutions on Gaps and Islands.
You can use ROW_NUMBER like this.
Sample Data
DECLARE #tab1 TABLE(id VARCHAR(20));
insert into #tab1 VALUES('AAA0000001'),('AAA0000002'),('AAA0000003'),('AAA0000004'),('AAA0000006'),('AAA0000007'),('AAA0000010');
Query
;WITH CTE as
(
SELECT convert(int,STUFF(id,1,3,'')) id,convert(int,STUFF(id,1,3,'')) - ROW_NUMBER()OVER(ORDER BY convert(int,STUFF(id,1,3,''))) rn
FROM #tab1
),CTE2 as
(
SELECT ROW_NUMBER()OVER(ORDER BY rn) as rn, MIN(id) series_start,MAX(id) series_end
FROM CTE
GROUP BY rn
)
SELECT C2.series_end,C1.series_start
FROM CTE2 C1
INNER JOIN CTE2 C2 ON C1.rn = C2.rn + 1;
SQL Fiddle
Explanation
Output of CTE is the difference of gaps between id values.
Output of CTE2 is the start and end of continuous series of numbers
Final Output gives the start and end of gaps within the series
Output
series_end series_start
4 6
7 10
If the schema is fixed then no need for complex queries. This works:
DECLARE #t TABLE ( v VARCHAR(100) );
INSERT INTO #t
VALUES ( 'AAA0000001' ),
( 'AAA0000002' ),
( 'AAA0000007' ),
( 'AAA0000008' ),
( 'AAA0000010' ),
( 'AAA0000011' ),
( 'AAA0000012' );
SELECT * FROM #t t1
CROSS APPLY(SELECT TOP 1 v FROM #t t2 WHERE t2.v > t1.v ORDER BY v) ca
WHERE RIGHT(t1.v, 7) <> RIGHT(ca.v, 7) - 1
Output:
v v
AAA0000002 AAA0000007
AAA0000008 AAA0000010
In sqlserver 2012, you can use LAG and LEAD
DECLARE #t table(col1 varchar(15))
INSERT #t values('AAA0000001'),('AAA0000002'),('AAA0000004')
SELECT
case when
stuff(lag(col1) over (order by col1), 1,3,'') + 1
= stuff(col1, 1,3,'') then 'Yes' else 'No' end previous_exists,
case when
stuff(lead(col1) over (order by col1), 1,3,'') - 1
= stuff(col1, 1,3,'') then 'Yes' else 'No' end next_exists,
col1
FROM #t
Result:
previous_exists next_exists col1
No Yes AAA0000001
Yes No AAA0000002
No No AAA0000004

SQL Server: Gap / Island, 365 day "contiguous" block

I have a table that looks like this:-
tblMeterReadings
id meter date total
1 1 03/01/2014 100.1
1 1 04/01/2014 184.1
1 1 05/01/2014 134.1
1 1 06/01/2014 132.1
1 1 07/01/2014 126.1
1 1 08/01/2014 190.1
This is an 8 day "contiguous block" from '2014-01-03' to '2014-01-08'.
In the real table there are "contiguous blocks" of years in length.
I need to select the MOST RESCENT CONTINUOUS 365 DAY BLOCK (filtered by meter column). If 365 cannot be found, then it should select next largest continuous block.
When I say CONTINUOUS I mean there must be no days missing.
This is beyond me, so if someone can solve... I will be very impressed.
using distinct to not count days with 2 sets of data
declare #gapdays int = 2 -- replace this with 365 in your case
;with x as
(
select datediff(d, '2014-01-01', [date])-dense_rank()over(order by [date]) grp
,[date]
from #t
)
select top 1 max([date]) last_date, min([date]) first_date, count(distinct [date]) days_in_a_row
from x
group by grp
having count(distinct [date]) >= #gapdays
order by max([date]) desc
There you go:
declare #tblMeterReadings table (id int, meter int, [date] date, total money)
insert into #tblMeterReadings ( id, meter, date, total )
values
(1, 1, '03/01/2014', 100.1),
(1, 1, '04/01/2014', 184.1),
(1, 1, '05/01/2014', 134.1),
(1, 1, '06/01/2014', 132.1),
(1, 1, '07/01/2014', 126.1),
(1, 1, '08/01/2014', 190.1),
(1, 1, '10/01/2014', 200.1),
(1, 1, '12/01/2014', 202.1),
(1, 1, '13/01/2014', 204.1)
;with data as (
select i = datediff(day, '2014', [date]), *
from #tblMeterReadings l
)
, islands as (
select island = l.i - row_number() over (order by i), l.*
from data l
)
, spans as (
select l = min(i), r = max(i)
from islands i
group by island
)
select *
from spans s
left join data l on s.l = l.i
left join data r on s.r = r.i
Most recent continuous block not exceeding 365 days in length will be as follows:
select top 1 *
from spans s
left join data l on s.l = l.i
left join data r on s.r = r.i
where s.l - s.r < 365
order by s.l - s.r desc, s.r desc
With recursive CTE and datepart(dayofyear, date):
with cte as
(
select id, meter, date, datepart(dayofyear, date) as x, cast(1 as int) as level, t1.date as startDate from tblMeterReadings t1
where meter = 1
and not exists(select * from tblMeterReadings t2 where (datepart(dayofyear, t1.date) - 1) = datepart(dayofyear, t2.date))
union all
select t1.id, t1.meter, t1.date, datepart(dayofyear, t1.date) as x, t2.level + 1, t2.startDate from tblMeterReadings t1
inner join cte t2 ON (datepart(dayofyear, t1.date)) = (datepart(dayofyear, t2.date) + 1)
)
select TOP 365 * from cte
where cte.startDate = (select top 1 startdate
from cte
--where Level <= 365
order by Level desc, startDate desc)
order by Level desc
OPTION ( MAXRECURSION 365 )
SQL Fiddle example

Cumulative Counts and the Dates the Counts Become Negative and Positive

I have the below scenario
Date Amount Item
6/17/08 208 1
9/24/08 -48 1
6/15/09 -160 1
9/23/09 40 1
For the same items, I want to get the date where the cumulative amount is less than or equal to 0 the first time
and if the cumulative amount becomes positive later, I want to get the date it turned positive as well.
Date Amount Item Cumulative Amount
6/17/08 208 1 208
9/24/08 -48 1 160
6/15/09 -160 1 0 --This date
9/23/09 40 1 40 --This date
Any suggestions on how to achieve this?
In SQL Server 2012:
SELECT date
FROM (
SELECT *,
SUM(amount) OVER (PARTITION BY item ORDER BY date) AS csum,
SUM(amount) OVER (PARTITION BY item ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS psum
FROM mytable
) q
WHERE (csum <= 0 AND psum > 0)
OR
(csum > 0 AND psum <= 0)
In earlier versions:
SELECT date
FROM (
SELECT *,
(
SELECT SUM(amount)
FROM mytable mi
WHERE mi.item = m.item
AND mi.date < m.date
) AS psum
FROM mytable m
) q
WHERE (psum + amount <= 0 AND psum > 0)
OR
(psum + amount > 0 AND psum <= 0)
For the date when it first turns non-positive:
SELECT MIN(t3.Date) AS dateCumAmountTurnsLTEZero
FROM
(SELECT Date,
(SELECT SUM(Amount)
FROM mytable t1
WHERE t1.Date <= t2.Date) AS cumulativeAmount
FROM mytable t2) t3
WHERE t3.cumulativeAmount <= 0
To get the date it turns positive again you could either inject this result into another query or assuming you want to do it all in SQL it gets a bit hairy! See below:
SELECT MIN(t6.Date)
FROM
(SELECT Date,
(SELECT SUM(Amount)
FROM mytable t4
WHERE t4.Date <= t5.Date) AS cumulativeAmount
FROM mytable t5) t6
WHERE t6.cumulativeAmount >= 0
AND t6.Date >
(SELECT MIN(t3.Date)
FROM
(SELECT Date,
(SELECT SUM(Amount)
FROM mytable t1
WHERE t1.Date <= t2.Date) AS cumulativeAmount
FROM mytable t2) t3
WHERE t3.cumulativeAmount <= 0)
See SQL Fiddle Demo

Resources