Can a Pivot Resolve this? - sql-server

I'm using SQL Server 2016.
I have a table with 3 columns that describe an event: Location, StartTime and DayofWeek. Location can host the event on any DayofWeek and one or more that one StartTime on a Day or no StartTime on any DayofWeek.
The result table that I need should have 8 columns: Location, MonTime, TueTime, WedTime, ThuTime, FriTime, SatTime and SunTime.If there is more than one time on one or more days for a certain location, there will be a row fot every different StartTime against Location.
I have tried a pivot with MAX and MIN which sort of resolves the problem but only if there is a max of 2 different times on a day.
I also tried a Cursor loop with INSERT and dynamic UPDATE but that was a failure.
Any suggestions would be gratefully received. Thanks.

You can do it by first assigning a ranking to each of your "StartTime" partitioned by "Location" and "DayofWeek". Then you can easily apply your pivot with the CASE statements and the aggregation on both your "Location" and just computed ranking number:
WITH cte AS (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY Location, DayofWeek ORDER BY StartTime) AS rn
FROM tab
)
SELECT Location AS Site,
MAX(CASE WHEN DayofWeek='Mon' THEN StartTime END) AS MonTime,
MAX(CASE WHEN DayofWeek='Tue' THEN StartTime END) AS TueTime,
MAX(CASE WHEN DayofWeek='Wed' THEN StartTime END) AS WedTime,
MAX(CASE WHEN DayofWeek='Thu' THEN StartTime END) AS ThuTime,
MAX(CASE WHEN DayofWeek='Fri' THEN StartTime END) AS FriTime,
MAX(CASE WHEN DayofWeek='Sat' THEN StartTime END) AS SatTime,
MAX(CASE WHEN DayofWeek='Sun' THEN StartTime END) AS SunTime
FROM cte
GROUP BY Location,
rn
ORDER BY Site,
MonTime
Check the demo here.

If you want the PIVOT.
Note: Without a proper sequence (date, identity etc) both approaches will "FILL-IN" the matrix from top down.
Select Location
,MonTime = Mon
,TueTime = Tue
,WedTime = Wed
,ThuTime = Thu
,FriTime = Fri
,SatTime = Sat
,SunTime = Sun
From (
Select *,RN = row_number() over (partition by Location,DayofWeek Order by StartTime)
From YourTable
) Src
Pivot ( max( StartTime ) for DayofWeek in ([Mon],[Tue],[Wed],[Thu],[Fri],[Sat],[Sun] ) ) pvt
Order by Location
Results

Related

PivotTable issue on sqlserver- Data getting split to multiple rows

I performed a Pivot on an existing table to get the Week data on the Column Titles, however instead of consolidating and giving me the data in just 3 rows, it splitting to multiple rows.
I am looking to see the data in just 3 rows and not the repetition.
Please let me know how to g about with this.
This is the query (looks a little messy coz of the details hidden)
This is the data I get with the Unpivot:
Expected result after the Pivot is:
I have done what I can based on your redacted SQL statement. You just need to group on Department and Type, then aggregate the pivoted values
select case when [REDACTED] END as Dept, Type, max([2019W01]) as [2019W01], max([2019W02]) as [2019W02], max([2019W03]) as [2019W03], max([2019W04]) as [2019W04]
from ( select
ltrim(rtrim(Plant)) as Plant
,cast(ltrim(rtrim(WeekNo)) as varchar) as WeekNo
,substring(cast(ltrim(rtrim(WeekNo)) as [REDACTED]
from [REDACTED] ) as a
unpivot
(Value for Type in (A,B,C)) as sq
pivot (
min(Value)
for WeekNo in ([2019W01], [2019W02], [2019W03], [2019W04])
) as Pvt
group by Dept, Type
This seems a lot clearer to me when pivoting data. You only need to copy-paste-replace on one place and you define how you group your columns.
SELECT Dept, Type,
MIN(CASE WHEN Weekno = '2019W01' THEN [value] END),
MIN(CASE WHEN Weekno = '2019W02' THEN [value] END),
MIN(CASE WHEN Weekno = '2019W03' THEN [value] END),
MIN(CASE WHEN Weekno = '2019W04' THEN [value] END),
MIN(CASE WHEN Weekno = '2019W05' THEN [value] END),
MIN(CASE WHEN Weekno = '2019W06' THEN [value] END)
FROM SomeTable
GROUP BY Dept, Type
ORDER BY Dept, Type;

SQL SMS 2008 -Count column ids and count duplicate ids if createddate is greater than 3 months between ids

*Edit (Hopefully to be more clear)
Table below, I would like to count ids and count duplicate ids where the createddate has a gap of 3 months or more for that ID.
Query I have so far...
if object_id('tempdb..#temp') is not null
begin drop table #temp end
select
top 100
a.id, a.CreatedDate
into #temp
from tbl a
where 1=1
--and year(CreatedDate) = '2015'
if object_id('tempdb..#temp2') is not null
begin drop table #temp2 end
select t.id, count(t.id) as Total_Cnt
into #temp2
from #temp t
group by id
select distinct #temp2.Total_Cnt, #temp2.id, #temp.CreatedDate, DENSE_RANK() over (partition by #temp.id order by createddate) RK
from #temp2
inner join #temp on #temp2.id = #temp.id
where 1=1
order by Total_Cnt desc
Results:
Total_cnt id createddate rk
3 1 01-01-2015 1
3 1 03-02-2015 2
3 1 01-02-2015 3
2 2 05-01-2015 1
2 2 05-02-2015 2
1 3 06-01-2015 1
1 4 07-01-2015 1
Count ids and only count duplicate ids when the createddate from the id is greater than 3 months.
Something like this...
Total_cnt id Countwith3monthgap
3 1 2
2 2 1
1 3 1
1 4 1
You can use a cte and ROW_NUMBER to get your order and self join the cte based on the order..
WITH cte AS
( SELECT
*,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY CreatedDate) Rn
FROM
Test
)
SELECT
c1.ID,
COUNT(CASE WHEN c2.CreatedDate IS NULL THEN 1
WHEN c1.CreatedDate >= DATEADD(month,3,c2.CreatedDate) THEN 1
END)
FROM
cte c1
LEFT JOIN cte c2 ON c1.ID = c2.ID
AND c1.RN = c2.RN + 1
GROUP BY
c1.ID
You also need to use a conditional count where the Previous CreatedDate is null or if the Current CreatedDate is >= the Previous CreatedDate + 3 months
If you happen to be using SQL 2012+ you can also use LAG here to get the same result
SELECT
ID,
COUNT(*)
FROM
(SELECT
ID,
CreatedDate CurrentDate,
LAG(CreatedDate) OVER (PARTITION BY ID ORDER BY CreatedDate) PreviousDate
FROM
Test
) T
WHERE
PreviousDate IS NULL
OR CurrentDate >= DATEADD(month, 3, PreviousDate)
GROUP BY
ID
You can use a lag to get the previous date, Null for the first in the list
SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t
You can use that as a subquery and get the difference in months using DATEDIFF
SELECT sub.id,DATEDiff(month, sub.PreviousCreateDate ,sub.CreatedDate)
FROM (SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t) sub
WHERE DATEDiff(month, sub.PreviousCreateDate ,sub.CreatedDate) >=3
OR sub.PreviousCreateDate IS NULL
You can then take your totals
SELECT sub.id,COUNT(sub.id) as cnt
FROM (SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t) sub
WHERE DATEDIFF(month, sub.PreviousCreateDate ,sub.CreatedDate) >=3
OR sub.PreviousCreateDate IS NULL
GROUP BY sub.id
Note that using datediff the last day of january is three months before the first day of march. That appears to be the logic you were after.
You might want to define your three month gap criteria as
WHERE sub.PreviousCreateDate <= DATEADD(month, -3, sub.CreatedDate)
OR sub.PreviousCreateDate IS NULL
or
WHERE sub.CreatedDate >= DATEADD(month, +3, sub.PreviousCreateDate )
OR sub.PreviousCreateDate IS NULL
I'm guessing that your desired definition of three-month gap doesn't coincide with datediff()'s. Most of the logic here is to look back at the previous date and decide if the gap is big enough to qualify.
When datediff() counts three months difference we still need to make sure the day of month is later than the first one (per example and ID 5). If difference is more than three months then we're good automatically.
But I'm also assuming that you would want to treat the distance from November 30th to February 28th (or 29th in a leap year) as a full three months because the end date falls on the final day of the month. By adjusting the end date by an extra day this is an easy scenario to snag as it will bump the date into the following month and increase the month difference by one as well. If that's not what you want then just remove the dateadd(day, 1, ...) portion and use only the raw CreatedDate value.
You sample data is limited so I'm also making the assumption that the gaps are measure between consecutive dates. If you're wanting to find blocks of runs that don't span more than three months across the set, then that's a different problem and you should clarify with more information.
Since you've indicated that you're probably on SQL Server 2008 you'll have to do without the lag() function. Although the first query could be adjusted for that it's likely easier to go with the second approach at the end.
with diffs as (
select
ID,
row_number() over (partition by ID order by CreatedDate) as RN,
case when
datediff(
month,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate),
CreatedDate
) = 3
and
datepart(
day,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate)
) <= datepart(day, CreatedDate)
or
datediff(
month,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate),
/* adding one day to handle gaps like Nov30 - Feb28/29 and Jan31 - Apr30 */
dateadd(day, 1, CreatedDate)
) >= 4
then 1
else 0
end as GapFlag
from <T> /* <--- your table name here */
), gaps as (
select
ID, RN,
sum(1 + GapFlag) over (partition by ID order by RN) as Counter
from diffs
)
select ID, count(distinct Counter - RN) as "Count"
from gaps
group by ID
The rest of the logic is a typical gaps and islands scenario looking for holes in the sum(1 + GapCount) sequence with the offset of 1 acting pretty much like row_number().
http://sqlfiddle.com/#!6/61b12/3
JamieD77's approach is also valid. I was originally thinking your problem involved more than looking at the rows in sequence. Here's how I would tweak it for the gap definition I've been running with:
with data as (
select ID, CreatedDate, row_number() over (partition by ID order by CreatedDate) as RN
from T
)
select ID, count(*) as "Count"
from data d1 left outer join data d0
on d0.ID = d1.ID and d0.RN = d1.RN - 1 /* connect to the one before */
where
datediff(month, d0.CreatedDate, d1.CreatedDate) = 3
and datepart(day, d0.CreatedDate) <= datepart(day, d0.CreatedDate)
or datediff(month, d0.CreatedDate, dateadd(day, 1, d0.CreatedDate)) >= 4
or d0.ID is null
group by ID
Edit: You have changed the question since yesterday.
Change this line in the first query to include the total count:
...
select count(*) as TotalCnt, ID, count(distinct Counter - RN) as GapCount
...
Second would look like:
with data as (
select ID, CreatedDate, row_number() over (partition by ID order by CreatedDate) as RN
from T
)
select
count(*) as TotalCnt, ID,
count(case when
datediff(month, d0.CreatedDate, d1.CreatedDate) = 3
and datepart(day, d0.CreatedDate) <= datepart(day, d0.CreatedDate)
or datediff(month, d0.CreatedDate, dateadd(day, 1, d0.CreatedDate)) >= 4
or d0.ID is null then 1 end
) as GapCount
from data d1 left outer join data d0
on d0.ID = d1.ID and d0.RN = d1.RN - 1 /* connect to the one before */
where
group by ID

tsql UNION without nulls

I have the following query
SELECT MONTH, COUNT(DISTINCT VISITS) AS BRAND_VISITS, NULL AS NONB_VISITS
FROM Table1
WHERE KEYWORD_TYPE = BRAND(
AND DATE >= '2013-01-01'
GROUP BY MONTH
UNION ALL
SELECT MONTH, NULL, COUNT(DISTINCT VISITS) AS NONB_VSTS
FROM Table1
WHERE KEYWORD_TYPE = NON-BRAND
AND DATE >= '2013-01-01'
GROUP BY MONTH
I get the following results:
1 352540 NULL
2 309834 NULL
3 228764 NULL
4 236054 NULL
5 218096 NULL
6 172527 NULL
1 NULL 5337
2 NULL 14120
3 NULL 9954
4 NULL 23755
5 NULL 19771
6 NULL 30797
However, what I want is inline results without NULLS
1 352540 5337
2 309834 14120
3 228764 9954
4 236054 23755
5 218096 19771
6 172527 30797
You can do this with using a single statement with CASE or with an JOIN on month instead of a UNION. If you take the join approach you may need to account for null values (no visist for a keyword in a month). You will want to profile them to see which is faster with your data and table structure. It is really all about the indexes and the amount of data you need to aggregate.
Assuming you don't have to worry about nulls based on the counts in your example, here is what you want.
SELECT brand.month, brand.brand_visits,nonbrand.non_brand_visits
FROM (SELECT month, COUNT(visits) AS brand_visits
FROM Table1
WHERE keyword_type = 'BRAND'
AND date >= '2013-01-01'
GROUP BY month) brand
INNER JOIN
(SELECT month, COUNT(visits) AS non_brand_visits
FROM Table1
WHERE keyword_type = 'NON-BRAND'
AND date >= '2013-01-01'
GROUP BY month) nonbrand
ON brand.month=nonbrand.month
Here is the CASE approach. You should profile based on your actual data you are aggregating and your indexes to see which method is faster.
SELECT month,
SUM(CASE WHEN keyword_type = 'BRAND' THEN 1 ELSE 0 END) AS brand_visits,
SUM(CASE WHEN keyword_type = 'NON-BRAND' THEN 1 ELSE 0 END) AS non_brand_visits
FROM Table1
WHERE date >= '2013-01-01'
GROUP BY month
Finally, you did not provide table structure or example data so I made some assumptions above. I strongly believe you did not need the COUNT(DISTINCT in your original statement. I have removed it and verified the two statement above yield the same results. If COUNT(DISTINCT is required then the CASE approach will not work but the join approach will still work fine.
Using your columns:
SELECT month,
count(distinct CASE WHEN keyword_type = 'BRAND' THEN visits END) AS BRAND_VISITS,
count(distinct CASE WHEN keyword_type = 'NON-BRAND' THEN visits END) AS NONB_VSTS
FROM Table1
WHERE date >= '2013-01-01'
and keyword_type in ('BRAND','NON-BRAND')
GROUP BY month
Am tempted to believe that month is simply the month from the date column, I would prefer this solution, it con cover more years than 1 and the same query will still be valid in the year 2014
SELECT cast(dateadd(month, datediff(month, 0, date), 0) as date) month,
count(distinct CASE WHEN keyword_type = 'BRAND' THEN visits END) AS BRAND_VISITS,
count(distinct CASE WHEN keyword_type = 'NON-BRAND' THEN visits END) AS NONB_VSTS
FROM Table1
WHERE date >= '2013-01-01'
and keyword_type in ('BRAND','NON-BRAND')
GROUP BY datediff(month, 0, date)
If you want to stick with your old script, you can fix it this way:
SELECT MONTH, max(BRAND_VISITS) BRAND_VISITS, max(NONB_VISITS) NONB_VISITS
FROM
(
SELECT MONTH, COUNT(DISTINCT VISITS) AS BRAND_VISITS, NULL AS NONB_VISITS
FROM Table1
WHERE KEYWORD_TYPE = 'BRAND'
AND DATE >= '2013-01-01'
GROUP BY MONTH
UNION ALL
SELECT MONTH, NULL, COUNT(DISTINCT VISITS) AS NONB_VSTS
FROM Table1
WHERE KEYWORD_TYPE = 'NON-BRAND'
AND DATE >= '2013-01-01'
GROUP BY MONTH
) a
GROUP BY MONTH

ROW_NUMBER() without over in SQL

Is there any way to use ROW_NUMBER() in SQL without using OVER, because I want to use sorting.
I have a Grid with multiple sortable columns with configurable rows. In my scenario order by is variable that's why I am not able to put order by using ROWNUM.
select ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as number from Task order by RuleId
I approached this using a different logic. Instead of checking for every single day and verifying it is a weekday or a holiday I create a table which lists only the business days and then take the nth value.
CREATE OR REPLACE FUNCTION add_n_working_days (
start_date DATE, working_days PLS_INTEGER
) RETURN DATE AS
l_end_date DATE := start_date;
l_counter pls_integer := 0;
BEGIN
SELECT
business_day
INTO l_end_date
FROM
(
WITH
dates AS
(SELECT start_date + level - 1 as dt FROM dual CONNECT BY level < 100)
,weekdates AS
(SELECT dt as weekday FROM dates WHERE TO_CHAR(dt,'fmdy') NOT IN ('sat','sun'))
,business_days AS
(
SELECT weekday as business_day FROM weekdates
MINUS
SELECT holiday FROM so_holidays
)
SELECT business_day, ROW_NUMBER() OVER (ORDER BY 1) as rn from business_days
)
WHERE rn = working_days + 1;
RETURN l_end_date;
END add_n_working_days;
If working_days gets too high then I suggest making that value 100 a variable.
select
a.City,
a.len
from
(
select city,length(city) AS len,
row_number()over(order by length(city) desc, city) as hi,
row_number()over(order by length(city), city) as lo
from
STATION) as a where (a.lo=1 or a.hi=1);

How can I use Group By (instead of self-joins) to get correct data (specific example included)

Looking at the related questions, I don't think this specific question has been asked, so here goes.
I had a situation where I joined on a table three times to get different data based on dates.
This took too long, so in an effort to optimize, I rewrote it using a group by as defined here: http://weblogs.sqlteam.com/jeffs/jeffs/archive/2007/06/12/60230.aspx
I'm having a hard time with the logic, and I'm beginning to think it's not possible to get exactly what I want through this. I'll show you my current code then describe what I need from it (tables/variables changed to protect the innocent).
SELECT
upc,
MAX(CASE WHEN ip_start_date <= GETDATE() THEN ip_unit_price END) AS retail_amount,
MAX(CASE WHEN ip_start_date <= GETDATE() THEN ip_price_multiple END) AS retail_multiplier_num,
MAX(CASE WHEN ip_start_date BETWEEN GETDATE() AND DATEADD(ww,1,GETDATE()) THEN ip_unit_price END) AS retail_amt_nxt_wk,
MAX(CASE WHEN ip_start_date BETWEEN GETDATE() AND DATEADD(ww,1,GETDATE()) THEN ip_price_multiple END) AS retail_multipler_num_nxt_wk,
MAX(CASE WHEN ip_start_date BETWEEN DATEADD(ww,1,GETDATE()) AND DATEADD(ww,2,GETDATE()) THEN ip_unit_price END) AS retail_amt_wk_after_nxt,
MAX(CASE WHEN ip_start_date BETWEEN DATEADD(ww,1,GETDATE()) AND DATEADD(ww,2,GETDATE()) THEN ip_price_multiple END) AS retail_multiplier_num_wk_after_nxt
FROM
items AS im WITH (NOLOCK)
retails AS ip WITH (NOLOCK)
ON im.ID = ip.ID
GROUP BY
upc
So looking at first line, this gets me the max retail with a date less than today. I actually need the most recent one, not the largest. This used to be handled with a sub-query which got me the MAX(start_date) less than today. I can't do a MAX within a MAX, for what are most likely good reasons. I was considering LAST, but I'm not quite sure the last record will always be the most recent in our system (new system).
Does anyone see a solution to this? The BETWEENS work fine, MAX retail within that week is good enough as those are for estimation. The other one must be accurate, though.
(Feel free to edit title..I couldn't come up with a succinct way to ask this)
In SQL Server 2005+:
SELECT upc, retail_today.*, retail_next_week.*, retail_two_weeks.*
FROM items im
OUTER APPLY
(
SELECT TOP 1
ip_unit_price, ip_price_multiple
FROM retail ip
WHERE ip.ip_start_date <= GETDATE()
AND ip.id = im.id
ORDER BY
ip_start_date DESC
) retail_today
OUTER APPLY
(
SELECT TOP 1
ip_unit_price, ip_price_multiple
FROM retail ip
WHERE ip.ip_start_date BETWEEN GETDATE() AND DATEADD(ww, 1, GETDATE())
AND ip.id = im.id
ORDER BY
ip_start_date DESC
) retail_next_week
OUTER APPLY
(
SELECT TOP 1
ip_unit_price, ip_price_multiple
FROM retail ip
WHERE ip.ip_start_date BETWEEN DATEADD(ww, 1, GETDATE()) AND DATEADD(ww, 2, GETDATE())
AND ip.id = im.id
ORDER BY
ip_start_date DESC
) retail_two_weeks
In SQL Server 2000:
SELECT upc,
(
SELECT TOP 1
ip_price
FROM retail ip
WHERE ip.ip_start_date <= GETDATE()
AND ip.id = im.id
ORDER BY
ip_start_date DESC
) AS ip_price_today,
(
SELECT TOP 1
ip_price_multiple
FROM retail ip
WHERE ip.ip_start_date <= GETDATE()
AND ip.id = im.id
ORDER BY
ip_start_date DESC
) AS ip_price_multiple_today,
…
FROM items im

Resources