Count the cycle, and count that already has been counted - sql

I have my query:
SELECT UserGroupCode,COUNT(UserGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY UserGroupCode;
Which shows me:
UserGroupCode CountofCycles
1 1
4 1
5 1
6 2 (gone into 2nd cycle)
7 1
8 1
9 1
10 1
11 1
12 1
13 1
14 1
15 1
16 1
17 1
18 1
19 1
When i try to count Total UserGroups where countofcycle=1
SELECT Count(t.CountOfCycle) AS 'totalgroups'
FROM
(SELECT CreateDate, COUNT(userGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY CreateDate,UserGroupCode)t
WHERE CountofCycle=1
I get result = 18 which should be 16, if i delete CreateDate from both SELECT And GROUP BY statement i can get correct number of CountofCycles,
and when i change condition to CountofCycle=2 or >1 it shows me 0
What is the problem with showing UserGroups with cycle > 1 ???!??
Here is my query to filter out onCreateDate, in 2nd table that i UNION with 1st one, i cant't use CreateDate, as it disturbs my query results
SELECT Count(t.CountOfCycle) AS 'total groups'
FROM
(SELECT COUNT(userGroupCode) AS [CountofCycle], CreateDate
FROM users.GroupCycles GROUP BY userGroupCode,CreateDate)t
WHERE t.CountOfCycle=1 AND t.CreateDate Between '03/16/2017' AND '04/25/2017'
UNION ALL
SELECT Count(t.CountOfCycle) AS 'group on date2'
FROM
(SELECT COUNT(userGroupCode) AS [CountofCycle] FROM users.GroupCycles GROUP BY userGroupCode)t
WHERE t.CountOfCycle=2

Firstly to address why you are not getting the results you are expecting, and the simple reason is that you are comparing two different queries and expecting the results to be the same.
Consider this very simple example data
UserGroupCode | CreateDate
----------------+----------------
A | 2017-05-10
B | 2017-05-10
B | 2017-05-11
C | 2017-05-10
You have two records where the UserGroupCode is B, so if you run:
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-05-10'), ('B', '2017-05-10'), ('B', '2017-05-11');
SELECT UserGroupCode, COUNT(*) AS [Count]
FROM #T
GROUP BY UserGroupCode
HAVING COUNT(*) = 2;
This returns:
UserGroupCode Count
-------------------------
B 2
However, if you were to add CreateDate to the grouping, "B" would be split into two groups, each with a count of 1:
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-05-10'), ('B', '2017-05-10'), ('B', '2017-05-11');
SELECT UserGroupCode, CreateDate, COUNT(*) AS [Count]
FROM #T
WHERE UserGroupCode = 'B'
GROUP BY UserGroupCode, CreateDate;
This returns:
UserGroupCode CreateDate Count
---------------------------------------
B 2017-05-10 1
B 2017-05-11 1
Now, based you your queries you have posted, it looks like you want to know
The number of groups that only have one record in the date range 16th March 2017 to 25th April 2017.
The number of groups that have two records in total.
For this, consider a slightly larger data set:
UserGroupCode | CreateDate
----------------+----------------
A | 2017-04-10
B | 2017-04-10
B | 2017-05-11
C | 2017-01-01
C | 2017-01-02
D | 2017-04-01
D | 2017-04-02
E | 2017-01-02
So here.
Group A has one record in total, and it falls within the date range
Group B has two records in total, on in the date range, one not
Group C has two records, neither in the date range
Group D has two records, both in the date range.
Group E has one record, not in the date range
So for your first requirement:
The number of groups that only have one record in the date range 16th March 2017 to 25th April 2017.
We would expect 2 groups, A and B, because C and E have no records in the date range, and D has two.
and the second we would expect three groups, B, C and D, since A and E only have one record each.
You can do this with a single query by using a conditional aggregate.
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-04-10'),
('B', '2017-04-10'), ('B', '2017-05-11'),
('C', '2017-01-01'), ('C', '2017-01-02'),
('D', '2017-04-01'), ('D', '2017-04-02'),
('E', '2017-01-02');
SELECT TotalGroups = COUNT(CASE WHEN RecordsInPeriod = 1 THEN 1 END),
GroupOnDate2 = COUNT(CASE WHEN TotalRecords = 2 THEN 1 END)
FROM ( SELECT UserGroupCode,
TotalRecords = COUNT(*),
RecordsInPeriod = COUNT(CASE WHEN CreateDate >= '20170316'
AND CreateDate <= '20170425' THEN 1 END)
FROM #T
GROUP BY UserGroupCode
) AS t;
Which gives:
TotalGroups GroupOnDate2
------------------------------
2 3

I'd expect to see a HAVING clause rather than a WHERE:
SELECT UserGroupCode, COUNT(UserGroupCode) [CountofCycle]
FROM [Users].[GroupCycles]
GROUP BY UserGroupCode
HAVING COUNT(UserGroupCode) > 1;

You could use HAVING, should work (and be more efficient)
select count(*)
from
(
SELECT CreateDate, COUNT(userGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY CreateDate,UserGroupCode
having count(userGroupCode) > 1 -- here is HAVING clause
) x1

Related

Calculate Rank Based on Shared Column Values and Consecutive Date Ranges (same rank for records with consecutive range)

I am trying to get the rank of a table that has specific id's and a start and end date for each record, as such:
id1
id2
flag
startdate
enddate
1
1
y
2007-01-10
2007-02-12
1
1
y
2007-02-13
2007-08-04
1
1
y
2007-08-05
2008-10-04
1
1
n
2008-10-05
2008-11-14
1
1
n
2008-11-15
2008-12-02
1
1
n
2008-12-08
2008-12-20
2
2
y
2012-01-10
2012-02-12
2
2
y
2012-02-13
2012-08-04
2
3
y
2012-01-10
2012-02-14
2
4
y
2012-08-14
2013-01-10
2
4
y
2013-01-15
2013-01-26
2
4
y
2013-01-27
2013-02-04
2
4
n
2016-03-14
2016-04-12
Where I essentially want to give the same count value to all records which share the same id1, id2, and flag, and are consecutive in their dates. Consecutive, meaning the start date of one record is equal to the end date of the previous record + 1 day.
The desired output should look like:
id1
id2
flag
startdate
enddate
rank_t
1
1
y
2007-01-10
2007-02-12
1
1
1
y
2007-02-13
2007-08-04
1
1
1
y
2007-08-05
2008-10-04
1
1
1
n
2008-10-05
2008-11-14
2
1
1
n
2008-11-15
2008-12-02
2
1
1
n
2008-12-08
2008-12-20
3
2
2
y
2012-01-10
2012-02-12
4
2
2
y
2012-02-13
2012-08-04
4
2
3
y
2012-01-10
2012-02-14
5
2
4
y
2012-08-14
2013-01-10
6
2
4
y
2013-01-15
2013-01-26
7
2
4
y
2013-01-27
2013-02-04
7
2
4
n
2016-03-14
2016-04-12
8
The output or rank does not have to be in that exact order, but the idea is still the same. Records which share the same id1, id2, and flag, and are consecutive in their dates should all have the same rank. And that rank value should not be used again for any other 'group' of records.
Here is the code to generate a temp table with this structure:
if object_id('tempdb..#temp1') is not null drop table #temp1
CREATE TABLE #temp1 (id1 INT, id2 int, flag varchar(10), startdate DATETIME, enddate DATETIME)
INSERT INTO #temp1 values
(1, 1, 'y', '2007-01-10', '2007-02-12'),
(1, 1, 'y', '2007-02-13', '2007-08-04'),
(1, 1,'y', '2007-08-05', '2008-10-04'),
(1, 1,'n', '2008-10-05', '2008-11-14'),
(1, 1,'n', '2008-11-15', '2008-12-02'),
(1, 1,'n', '2008-12-08', '2008-12-20'),
(2, 2,'y', '2012-01-10', '2012-02-12'),
(2, 2,'y', '2012-02-13', '2012-08-04'),
(2, 3,'y', '2012-01-10', '2012-02-14'),
(2, 4,'y', '2012-08-14', '2013-01-10'),
(2, 4,'y', '2013-01-15', '2013-01-26'),
(2, 4,'y', '2013-01-27', '2013-02-04'),
(2, 4,'n', '2016-03-14', '2016-04-12')
Thanks in advance for any help.
Same logic as existing answer... just done as 2 CTEs (which I find clearer) than a combination of CTE+Sub-query.
with cte1 as (
select *
-- Identify if there is a gap between the current startdate and the previous enddate
, case when lag(enddate,1,dateadd(day,-1,startdate)) over (partition by id1, id2, flag order by startdate asc) = dateadd(day,-1,startdate) then 0 else 1 end DateGap
from #temp1
), cte2 as (
select *
-- Sum every time a gap is detected to generate a new partition
, sum(DateGap) over (order by startdate asc) DateGapSum
from cte1
)
select id1, id2, flag, startdate, enddate
-- Use dense_rank to generate the ranking where ties are allocated the same value
, dense_rank() over (order by id1 asc, id2 asc, flag desc, DateGapSum asc) rank_t
from cte2
order by id1, id2, startdate;
Try the following:
WITH T AS
(
SELECT *,
IIF(
DATEDIFF(DAY,
LAG(enddate, 1, DATEADD(DAY, -1, startdate)) OVER (PARTITION BY id1, id2 ORDER BY enddate),
startdate)=1,
0, 1) AS chk
FROM #temp1
)
SELECT id1, id2, flag, startdate, enddate,
DENSE_RANK() OVER (ORDER BY id1, id2, flag DESC, grp) AS rank_t
FROM
(
SELECT *,
SUM(CHK) OVER (PARTITION BY id1, id2 ORDER BY enddate) AS grp
FROM T
) Groups
ORDER BY id1, id2, startdate
See a demo.
In the CTE T, we check if the date difference between the start date and the previous end date is equal to one, and set a value of zero if that condition is met and a value of one if that condition is not met.
Now, using a running sum of the chk value populated in the CTE, we can define unique groups for the consecutive rows.
At the end, we used DENSE_RANK function using the order (id1, id2, flag DESC, grp) to generate the required ranks.

Incremental Sum across different groups

I am trying to figure out how to count every product at every date such that count is incremental across all product,
this is dummy table for understanding , I have millions of records with thousands of different products
I am unable to query at every date for each product the count in incremental fashion along with miles as per date provided
CREATE TABLE Dummy_tab (
empid int,
date1_start date,
name_emp varchar(255),
product varchar(255),
miles varchar(20)
);
INSERT INTO Dummy_tab VALUES
(1, '2018-08-27', 'Eric', 'a',10),
(1, '2018-08-28', 'Eric','b',10),
(1, '2018-08-28', 'Eric','a',20),
(2, '2020-01-8', 'Jack','d',10),
(2, '2020-02-8', 'Jack','b',20),
(2, '2020-12-28', 'Jack','b',20),
(2, '2020-12-28', 'Jack','d',20),
(2,'2021-10-28', 'Jack','c',20),
(2, '2022-12-28', 'Jack','d',20),
(3, '2018-12-31', 'Jane','',10),
(3, '2018-12-31', 'Jane','',15);
My desired O/p is this
Id Date a b c d empty miles
1 2018-08-27 1 0 0 0 0 10
1 2018-08-28 2 1 0 0 0 20
2 2020-01-08 0 0 0 1 0 10
2 2020-02-08 0 1 0 1 0 20
2 2020-12-28 0 2 0 2 0 20
2 2021-10-28 0 2 1 2 0 20
2 2022-12-28 0 2 1 3 0 20
3 2018-12-31 0 0 0 0 1 10
3 2019-12-31 0 0 0 0 2 15
FOR EXAMPLE
Eric has 3 entry for ID =1 with product a on 2018 08 27 with product b on 2018 08 28 with product a on 2018 08 28
SO 1st entry a= 1 for ID=1 2nt entry is sum of previous and current so now a =2 for ID=1 and b= 1 as there were no entry earlier for b
Miles needs to be maximum miles for that date from past dates
You need to first (conditionally) aggregate your values here, and then you can do a cumulative SUM:
WITH Aggregates AS(
SELECT empid AS Id,
date1_start AS [Date],
COUNT(CASE product WHEN 'a' THEN 1 END) AS a,
COUNT(CASE product WHEN 'b' THEN 1 END) AS b,
COUNT(CASE product WHEN 'c' THEN 1 END) AS c,
COUNT(CASE product WHEN 'd' THEN 1 END) AS d,
COUNT(CASE product WHEN '' THEN 1 END) AS empty,
MAX(miles) AS miles
FROM dbo.Dummy_tab
GROUP BY empid, date1_start)
SELECT Id,
[Date],
SUM(a) OVER (PARTITION BY Id ORDER BY [Date]) AS a,
SUM(b) OVER (PARTITION BY Id ORDER BY [Date]) AS b,
SUM(c) OVER (PARTITION BY Id ORDER BY [Date]) AS c,
SUM(d) OVER (PARTITION BY Id ORDER BY [Date]) AS d,
SUM(empty) OVER (PARTITION BY Id ORDER BY [Date]) AS empty,
miles
FROM Aggregates
ORDER BY ID,
[Date];

Distinct counts based on dates in SQL Server

I'm attempting to create a new table based on the following table:
SubjectNumber TestDates
001 11/12/12
001 01/10/15
001 04/03/13
002 05/21/14
003 08/06/15
002 09/12/18
002 03/30/12
003 09/07/18
004 10/14/11
005 02/05/14
005 02/06/14
I need a new table that will include the following:
1) Subject number
2) Their first test date
3) Their last test date
4) A count of the total number of tests
5) A column with 0's and 1's indicating whether or not the subject had any two test dates that were at least 30 days apart
The new table should look like the following:
SubjectNumber FirstTestDate LastTestDate TestCount ThirtyDaysApart
001 11/12/12 01/10/15 3 1
002 03/30/12 09/12/18 3 1
003 08/06/15 09/07/18 2 1
004 10/14/11 1 0
005 02/05/14 02/06/14 2 0
I'm using SQL Server 2017.
I have a temporary table called #Temp1 that I'd like to store the data in. This table is called #Temp.
Insert into #Temp1
SELECT SubjectNumber, WHERE
CASE MIN(TestDates) then FirstTestDate = TestDates
END
CASE MAX(TestDates) then LastTestDate = TestDates
END
FROM #Temp;
You can use lag() and conditional aggregation:
select subjectnumber, min(testdate), max(testdate),
max(case when prev_testdate < dateadd(day, -30, testdate) then 1 else 0 end) as diff30
from (select t.*,
lag(testdate) over (partition by subjectnumber order by testdate) as prev_testdate
from t
) t
group by subjectnumber;
You can try using lag() function
select subjectnumber,min(TestDates),max(TestDates),count(TestDates),
case when count(case when pdatediff=30 then 1 end)>=2 then 1 else 0 end as ThirtyDaysApart
from
(
select subjectnumber,TestDates,COALESCE (DATEDIFF(DAY,
LAG(TestDates) OVER (PARTITION BY subjectnumber
ORDER BY TestDates), TestDates) ,0) as pdatediff
from tablenmae
)X group by subjectnumber
The only tricky part is checking if two dates within a group are 30 days apart. Note that the following query returns 1 if any two dates, not necessarily consecutive, are 30 days apart:
WITH cte AS (
SELECT SubjectNumber, MIN(TestDates) FirstTestDate, MAX(TestDates) LastTestDate, COUNT(TestDates) TestCount
FROM #yourdata
GROUP BY SubjectNumber
)
SELECT *
FROM cte AS t
CROSS APPLY (
SELECT CASE WHEN COUNT(*) = 0 THEN 0 ELSE 1 END AS ThirtyDaysApart
FROM #yourdata AS o
INNER JOIN #yourdata AS n ON o.SubjectNumber = n.SubjectNumber AND n.TestDates >= DATEADD(DAY, 30, o.TestDates)
WHERE o.SubjectNumber = t.SubjectNumber
) AS CA
DB Fiddle

Select except where different in SQL

I need a bit of help with a SQL query.
Imagine I've got the following table
id | date | price
1 | 1999-01-01 | 10
2 | 1999-01-01 | 10
3 | 2000-02-02 | 15
4 | 2011-03-03 | 15
5 | 2011-04-04 | 16
6 | 2011-04-04 | 20
7 | 2017-08-15 | 20
What I need is all dates where only one price is present.
In this example I need to get rid of row 5 and 6 (because there is two difference prices for the same date) and either 1 or 2(because they're duplicate).
How do I do that?
select date,
count(distinct price) as prices -- included to test
from MyTable
group by date
having count(distinct price) = 1 -- distinct for the duplicate pricing
The following should work with any DBMS
SELECT id, date, price
FROM TheTable o
WHERE NOT EXISTS (
SELECT *
FROM TheTable i
WHERE i.date = o.date
AND (
i.price <> o.price
OR (i.price = o.price AND i.id < o.id)
)
)
;
JohnHC answer is more readable and delivers the information the OP asked for ("[...] I need all the dates [...]").
My answer, though less readable at first, is more general (allows for more complexes tie-breaking criteria) and also is capable of returning the full row (with id and price, not just date).
;WITH CTE_1(ID ,DATE,PRICE)
AS
(
SELECT 1 , '1999-01-01',10 UNION ALL
SELECT 2 , '1999-01-01',10 UNION ALL
SELECT 3 , '2000-02-02',15 UNION ALL
SELECT 4 , '2011-03-03',15 UNION ALL
SELECT 5 , '2011-04-04',16 UNION ALL
SELECT 6 , '2011-04-04',20 UNION ALL
SELECT 7 , '2017-08-15',20
)
,CTE2
AS
(
SELECT A.*
FROM CTE_1 A
INNER JOIN
CTE_1 B
ON A.DATE=B.DATE AND A.PRICE!=B.PRICE
)
SELECT * FROM CTE_1 WHERE ID NOT IN (SELECT ID FROM CTE2)

How to COUNT rows according to specific complicated rules?

I have the following table:
custid custname channelid channel dateViewed
--------------------------------------------------------------
1 A 1 ABSS 2016-01-09
2 B 2 STHHG 2016-01-19
3 C 4 XGGTS 2016-01-09
6 D 4 XGGTS 2016-01-09
2 B 2 STHHG 2016-01-26
2 B 2 STHHG 2016-01-28
1 A 3 SSJ 2016-01-28
1 A 1 ABSS 2016-01-28
2 B 2 STHHG 2016-02-02
2 B 7 UUJKS 2016-02-10
2 B 8 AKKDC 2016-02-10
2 B 9 GGSK 2016-02-10
2 B 9 GGSK 2016-02-11
2 B 7 UUJKS 2016-02-27
And I want the results to be:
custid custname month count
------------------------------
1 A 1 1
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
According to the following rules:
All channel views subscription is billed every 15 days. If the
customer viewed the same channel within the 15 days, he will only be
billed once for that channel. For instance, custid 2, custname B his billing cycle is 19 Jan - 3 Feb (one billing cycle), 4 Feb - 20 Feb (one billing cycle) and so on. Therefore, he is billed only 1 time in Jan since he watch the same channel throughout the billing cycle; and he is billed 4 times in Feb for watching (channelid 7, 8, 9) and channelid 7 watched on 27 Feb (since this falls in another billing cycle, customer B is also charged here). Customer B is not charged on 2 Feb for watching channel 2 since he was already billed in 19 jan - 3 Feb billing cycle.
An invoice is generated every month for each customer, therefore, the
results should show the 'Month' and the 'Count' of the channels
viewed for each customer.
Can this be done in SQL server?
;WITH cte AS (
SELECT custid,
custname,
channelid,
channel,
dateViewed,
CAST(DATEADD(day,15,dateViewed) as date) as dateEnd,
ROW_NUMBER() OVER (PARTITION BY custid, channelid ORDER BY dateViewed) AS rn
FROM (VALUES
(1, 'A', 1, 'ABSS', '2016-01-09'),(2, 'B', 2, 'STHHG', '2016-01-19'),
(3, 'C', 4, 'XGGTS', '2016-01-09'),(6, 'D', 4, 'XGGTS', '2016-01-09'),
(2, 'B', 2, 'STHHG', '2016-01-26'),(2, 'B', 2, 'STHHG', '2016-01-28'),
(1, 'A', 3, 'SSJ', '2016-01-28'),(1, 'A', 1, 'ABSS', '2016-01-28'),
(2, 'B', 2, 'STHHG', '2016-02-02'),(2, 'B', 7, 'UUJKS', '2016-02-10'),
(2, 'B', 8, 'AKKDC', '2016-02-10'),(2, 'B', 9, 'GGSK', '2016-02-10'),
(2, 'B', 9, 'GGSK', '2016-02-11'),(2, 'B', 7, 'UUJKS', '2016-02-27')
) as t(custid, custname, channelid, channel, dateViewed)
), res AS (
SELECT custid, channelid, dateViewed, dateEnd, 1 as Lev
FROM cte
WHERE rn = 1
UNION ALL
SELECT c.custid, c.channelid, c.dateViewed, c.dateEnd, lev + 1
FROM res r
INNER JOIN cte c ON c.dateViewed > r.dateEnd and c.custid = r.custid and c.channelid = r.channelid
), final AS (
SELECT * ,
ROW_NUMBER() OVER (PARTITION BY custid, channelid, lev ORDER BY dateViewed) rn,
DENSE_RANK() OVER (ORDER BY custid, channelid, dateEnd) dr
FROM res
)
SELECT b.custid,
b.custname,
MONTH(f.dateViewed) as [month],
COUNT(distinct dr) as [count]
FROM cte b
LEFT JOIN final f
ON b.channelid = f.channelid and b.custid = f.custid and b.dateViewed between f.dateViewed and f.dateEnd
WHERE f.rn = 1
GROUP BY b.custid,
b.custname,
MONTH(f.dateViewed)
Output:
custid custname month count
----------- -------- ----------- -----------
1 A 1 3
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
(5 row(s) affected)
I don't know why you get 1 in count field for customer A. He got:
ABSS 2016-01-09 +1 to count (+15 days = 2016-01-24)
SSJ 2016-01-28 +1 to count
ABSS 2016-01-28 +1 to count (28-01 > 24.01)
So in January there must be count = 3.
Whenever I am trying to count things with complex criteria, I use a sum and case statement. Something like below:
SELECT custid, custname,
SUM(CASE WHEN somecriteria
THEN 1
ELSE 0
END) As CriteriaCount
FROM whateverTable
GROUP BY custid, custname
You can make that somecriteria variable as complicated a statement as you like, so long as it returns a boolean. If it passes, this row returns a 1. If it fails, the row reutrns a 0, then we sum up the values returned to get the count.
Generally this is how you can get any number (10 in this example) of fixed 15 day intervals starting at the given date (#dd in this example).
DECLARE #dd date = CAST('2016-01-19 17:30' AS DATE);
WITH 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, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10,000 rows max
tally(N) AS (SELECT TOP (10) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4)
SELECT
startd = DATEADD(D,(N-1)*15, #dd),
endd = DATEADD(D, N*15-1, #dd)
FROM tally
Adapt it to the rules defining how start date must be calculated for the user (and probably chanel).
#Sturgus what if I want to define it in the code? Any other
alternatives besides defining it in the table? How to write a query
that can be run every month to generate the monthly invoice. –
saturday 15 mins ago
Well, one way or another, you will have to save each customer's billing start date (minimally). If you want to do this entirely in SQL without 'editing the database', something like the following should work. The drawback to this approach is that you would need to manually edit the "INSERT INTO" statement every month to suit your needs. If you were allowed to edit the already existing customers table or create a new one, then it would reduce this manual effort.
DECLARE #CustomerBillingPeriodsTVP AS Table(
custID int UNIQUE,
BillingCycleID int,
BillingStartDate Date,
BillingEndDate Date
);
INSERT INTO #CustomerBillingPeriodsTVP (custID, BillingCycleID, BillingStartDate, BillingEndDate) VALUES
(1, 1, '2016-01-03', '2016-01-18'), (2, 1, '2016-01-18', '2016-02-03'), (3, 1, '2016-01-15', '2016-01-30'), (6, 1, '2016-01-14', '2016-01-29');
SELECT A.custid, A.custname, B.BillingCycleID AS [month], COUNT(DISTINCT A.channelid) AS [count]
FROM dbo.tblCustomerChannelViews AS A INNER JOIN #CustomerBillingPeriodsTVP AS B ON A.custid = B.CustID
GROUP BY A.custid, A.custname, B.BillingCycleID;
GO
Where are you getting your customers' billing start dates as it is?
I'm not sure how this solution will scale - but with some good index candidates and decent data housekeeping, it'll work..
You're going to need some extra info for starters, and to normalize your data. You will need to know the first charging period start date for each customer. So store that in a customer table.
Here are the tables I used:
create table #channelViews
(
custId int, channelId int, viewDate datetime
)
create table #channel
(
channelId int, channelName varchar(max)
)
create table #customer
(
custId int, custname varchar(max), chargingStartDate datetime
)
I'll populate some data. I won't get the same results as your sample output, because I don't have the appropriate start dates for each customer. Customer 2 will be OK though.
insert into #channel (channelId, channelName)
select 1, 'ABSS'
union select 2, 'STHHG'
union select 4, 'XGGTS'
union select 3, 'SSJ'
union select 7, 'UUJKS'
union select 8, 'AKKDC'
union select 9, 'GGSK'
insert into #customer (custId, custname, chargingStartDate)
select 1, 'A', '4 Jan 2016'
union select 2, 'B', '19 Jan 2016'
union select 3, 'C', '5 Jan 2016'
union select 6, 'D', '5 Jan 2016'
insert into #channelViews (custId, channelId, viewDate)
select 1,1,'2016-01-09'
union select 2,2,'2016-01-19'
union select 3,4,'2016-01-09'
union select 6,4,'2016-01-09'
union select 2,2,'2016-01-26'
union select 2,2,'2016-01-28'
union select 1,3,'2016-01-28'
union select 1,1,'2016-01-28'
union select 2,2,'2016-02-02'
union select 2,7,'2016-02-10'
union select 2,8,'2016-02-10'
union select 2,9,'2016-02-10'
union select 2,9,'2016-02-11'
union select 2,7,'2016-02-27'
And here is the somewhat unweildy query, in a single statement.
The two underlying sub-queries are actually the same data, so there may be more appropriate / efficient ways to generate these.
We need to exclude from billing any channel charged in the same charging period C for the previous Month. This is the essence of the join. I used a right-join so that I could exclude all such matches from the results (using old.custId is null).
select c.custId, c.[custname], [month], count(*) [count] from
(
select new.custId, new.channelId, new.month, new.chargingPeriod
from
(
select distinct cv.custId, cv.channelId, month(viewdate) [month], (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15 chargingPeriod
from #channelViews cv join #customer c on cv.custId = c.custId
) old
right join
(
select distinct cv.custId, cv.channelId, month(viewdate) [month], (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15 chargingPeriod
from #channelViews cv join #customer c on cv.custId = c.custId
) new
on old.custId = new.custId
and old.channelId = new.channelId
and old.month = new.Month -1
and old.chargingPeriod = new.chargingPeriod
where old.custId is null
group by new.custId, new.month, new.chargingPeriod, new.channelId
) filteredResults
join #customer c on c.custId = filteredResults.custId
group by c.custId, [month], c.custname
order by c.custId, [month], c.custname
And finally my results:
custId custname month count
1 A 1 3
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
This query does the same thing:
select c.custId, c.custname, [month], count(*) from
(
select cv.custId, min(month(viewdate)) [month], cv.channelId
from #channelViews cv join #customer c on cv.custId = c.custId
group by cv.custId, cv.channelId, (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15
) x
join #customer c
on c.custId = x.custId
group by c.custId, c.custname, x.[month]
order by custId, [month]