How to do this in SQL? (run a query over a window instead of just a single aggregate) - sql

Let's say I have some data with timestamps yyyy/mm/dd hh:mm:ss and some error stages (1 meaning an error has occurred). I have the data loaded in a dataframe I call df and want to compute Time_To_Next_Error (measured in seconds) a new column Time_To_Error based on the timestamp and Error_State.
Timestamp Error_State Time_To_Next_Error
2017-05-10 00:10:50 0 10
2017-05-10 00:10:55 0 5
2017-05-10 00:11:05 1 0
2017-05-10 00:11:10 0 5
2017-05-10 00:11:15 1 0
2017-05-10 00:11:20 0 15
2017-05-10 00:11:25 0 10
2017-05-10 00:11:30 0 5
2017-05-10 00:11:20 1 0
2017-05-10 00:11:20 0 0
For example, the first observation, there's 15 seconds before the first error occurs at 11:05 after which the count starts over from 0 seconds then the next "window" starts.
Is there a way to define a "window" spanning the next say 5 rows so I can 'look ahead' and check if any of those 5 rows satisfy some condition (like say one of the values is a 1 meaning an Error_Stage = 1 will happen soon)

Something like this perhaps:
SELECT
*,
DATEDIFF(second,
timestamp,
MIN(CASE WHEN error > 0 THEN timestamp END) OVER(ORDER BY timestamp ROWS BETWEEN 1 FOLLOWING AND 5 FOLLOWING
) as ttne
FROM yourtable
This will get the lowest(soonest) time stamp in the following 5 rows where the error code that occurs is greatest than 0, and datediff it with the timestamp of the current row
You could adjust the case when to do different logic
--time to next error code 1
MIN(CASE WHEN error = 1 THEN ...
If there is no error code 1 in the next 5 rows this should result in a null and datediff should also then output a null

Exactly what you're saying -- a window function!
Here's some code, SQL Server style:
DECLARE #tbl TABLE (
ts datetime,
Error_st int
);
INSERT INTO #tbl
VALUES
('2017-05-10 00:10:50', 0),
('2017-05-10 00:10:55', 0),
('2017-05-10 00:11:05', 1),
('2017-05-10 00:11:10', 0),
('2017-05-10 00:11:15', 1),
('2017-05-10 00:11:20', 0),
('2017-05-10 00:11:25', 0),
('2017-05-10 00:11:30', 0),
('2017-05-10 00:11:35', 1),
('2017-05-10 00:11:40', 0)
select *, DATEDIFF(second, ts,
min(CASE WHEN error_st=1 then ts else NULL END)
over (order by ts desc)) as time_to_Next_Err
-- , min(CASE WHEN error_st=1 then ts else NULL END)
-- over (order by ts desc) as NextErrorTS
from #tbl
order by ts
Here we rely on default behaviour of the SQL Server window-version of MIN():
the window is defined as "all previous rows and current" (ordering by descending timestamp). You can control the window and limit it to the "5 previous", if you only want to show "close-to-error" situations.
More details here:
https://learn.microsoft.com/en-us/sql/t-sql/queries/select-over-clause-transact-sql

Related

Calculating a value in SQL using previous row's values and current row value

I am trying to recreate the following in SQL where value at date of transaction needs to be calculated and value from other columns can be queried directly. It needs to add current value and transaction for first row of each type to get the value for 'value at date of transaction' and then for subsequent rows of that type, it needs to add 'value at date of transaction' from previous row to the 'transaction' value to get 'value at date of transaction' for current row. This process needs to start over for each type. Is this possible to recreate in SQL Server?
Type
Current Value
Transaction
Date of transaction
Value at date of transaction
A
5
2
12/31/2001
7
A
5
-3
12/30/2001
4
A
5
-1
12/29/2001
3
A
5
6
12/28/2001
9
B
100
20
12/31/2001
120
B
100
-50
12/30/2001
70
B
100
-10
12/29/2001
60
B
100
30
12/28/2001
90
C
20
7
12/31/2001
27
C
20
-3
12/30/2001
24
The structure seems odd to me.
But you can use the window function sum() over()
Declare #YourTable Table ([Type] varchar(50),[Current Value] int,[Transaction] int,[Date of transaction] date)
Insert Into #YourTable Values
('A',5,2,'12/31/2001')
,('A',5,-3,'12/30/2001')
,('A',5,-1,'12/29/2001')
,('A',5,6,'12/28/2001')
,('B',100,20,'12/31/2001')
,('B',100,-50,'12/30/2001')
,('B',100,-10,'12/29/2001')
,('B',100,30,'12/28/2001')
,('C',20,7,'12/31/2001')
,('C',20,-3,'12/30/2001')
Select *
,[Value at date] = [Current Value]
+ sum([Transaction]) over (partition by [Type] order by [Date of transaction] desc)
from #YourTable
Results
;with cte1
as (SELECT *,
/* conditional ROW_NUMBER to get the maxDate by Type and get only the Transaction Value+ immediately succeding row's Current Value*/
CASE
WHEN ROW_NUMBER() OVER (PARTITION BY Type ORDER BY Date_of_transaction DESC) = 1 then
LAG(Current_Value) OVER (PARTITION BY Type ORDER BY Date_of_transaction) + [Transaction]
else
[Transaction]
end as Base
FROM [Global_capturis_owner].[Book2]
)
select Type,
Current_Value,
[Transaction],
Date_of_transaction,
/* Windowed function to compute the Running Total*/
SUM(Base) OVER (PARTITION BY Type ORDER BY Date_of_transaction DESC) as RunningTotal
from cte1

Interval between next coming date

I have a table structure as below, Now i want to calculate the interval between next coming date from second column
TABLE STRUCTURE
Calendar |NEXT DAY
15-07-1997 15-07-1997
16-07-1997
17-07-1997
18-07-1997 18-07-1997
19-07-1997
20-07-1997
21-07-1997
22-07-1997
23-07-1997
24-07-1997
25-07-1997
26-07-1997 26-07-1997
EXPECTED RESULT
Calendar |NEXT DAY |DIFFDATE|
15-07-1997 15-07-1997 0
16-07-199 2
17-07-1997 1
18-07-1997 18-07-1997 0
19-07-1997 7
20-07-1997 6
21-07-1997 5
22-07-1997 4
23-07-1997 3
24-07-1997 2
25-07-1997 1
26-07-1997 26-07-1997 0
You can use lead(ignore nulls):
select t.*,
(coalesce(next_day,
lead(next_day ignore nulls) over (order by calendar)
) - calendar
) as diffdate
from t;

SQL calculate DayTime and NightTime between two DateTime values

I am way over my head with an SQL problem. I have a query which makes a temporary table, fills it with data from several other tables, makes some calculations and updates and provides this data to an app. The final step to do is calculate how many hours and how many minutes there are between two datetimes, but they should be divided in dayHours, dayMins, nightHours, nightMins (datetimes can be 20+ days in between). The following bulletpoints will visualize what I want to do:
Let say, night time is from 23:00 to 06:00.
We have DateTime1 = 20-04-2016 13:30.
We have DateTime2 = 21-04-2016 07:15.
NightTime: from 23:00 to 06:00 = 7 hours 0 minutes.
DayTime: from 13:30 to 23:00 (9h30m), and then again from 06:00 to 07:15(1h15m) the following day for a total of 10 hours 45 minutes.
I am providing a create table query, but I only need help with the calculation so you could ignore my table and data. Note, I have erased almost all formatting to reduce, as the post got really long.
CREATE TABLE [dbo].[myTestTable](
[JHID] [int] NULL, [ToDateTime] [datetime] NULL,
[startPayDateTime] [datetime] NULL, [opDayHour] [int] NULL,
[opDayMin] [int] NULL, [opNightHour] [int] NULL,
[opNightMin] [int] NULL, ) ON [PRIMARY] GO
Consider inserting this as a test data. The columns (for test purposes) are startPayDateTime and ToDateTime
INSERT INTO [myTestTable]
([JHID],[ToDateTime],[startPayDateTime],[opDayHour],[opDayMin],[opNightHour],[opNightMin])
VALUES (301533,'14-03-2016 01:54','14-03-2016 04:54',1,1,1,1),
(302488,'14-03-2016 01:54','14-03-2016 08:31',0,0,0,0),
(302676,'14-03-2016 01:54','28-03-2016 08:11',1,1,1,1) GO
So now I have to
UPDATE
SET opDayHour = (CASE WHEN ... THEN *value* ELSE 0 end),
opDayMin = (CASE WHEN ... THEN *value* ELSE 0 end),
opNightHour = (CASE WHEN ... THEN *value* ELSE 0 end),
opNightMin = (CASE WHEN ... THEN *value* ELSE 0 end),
How do I Thank you for your consideration, if my question is not clear enough leave a comment ! :)
The idea is to detect first day, a number of hole days (if present) and the last day (if not the same as first). So we need only one day long tally table for minutes. Drawback is more computations of those first/last intervals. When you need computations wich involve a number of intermidiate variables CROSS APPLY is a handy tool.
Try this, you may need to ajust +-1 logic to conform to your rules. This query computes minutes which can be easily converted to hours + minutes.
with myMinutes as (
select rn
-- day time is from 6:00 to 23:00
, mday = case when rn between 6*60 and 23*60-1 then 1 else 0 end
, mnight = 1 - case when rn between 6*60 and 23*60-1 then 1 else 0 end
from (select top(24*60) rn=row_number () over (order by (select null))
from sys.all_objects s1, sys.all_objects s2) t
)
select dayMinutes=r1.dayMin + case holedays when 0 then 0 else r2.dayMin + (holedays-1)*(23*60 - 6*60) end
, nightMinutes=r1.nightMin + case holedays when 0 then 0 else r2.nightMin + (holedays-1)*(24*60 -(23*60 - 6*60)) end
, totalMinutes= datediff(MINUTE, [FromDateTime], [ToDateTime]) -- control
,[JHID],[JetReg],[ArrFltID],[DepFltID],[ArrDateTime],[FromDateTime],[ToDateTime]
-- more columns sipped
from [myTestTable]
cross apply (select fD = dateadd(DAY,datediff(DAY,'19000101',[FromDateTime]),'19000101')
,tD = dateadd(DAY,datediff(DAY,'19000101',[ToDateTime]),'19000101')
,holedays = datediff(DAY,[FromDateTime],[ToDateTime]) ) xd
cross apply (select fFirstMin = datediff(MINUTE, fd, [FromDateTime])
,fLastMin = case holedays when 0 then datediff(MINUTE, td,[ToDateTime]) else 24*60 end - 1
,tFirstMin = 1
,tLastMin = datediff(MINUTE, td, [ToDateTime])
) xb
cross apply (select dayMin = sum(mm.mday)
, nightMin = sum(mm.mnight)
from myminutes mm
where mm.rn between fFirstMin and fLastMin ) r1
cross apply (select dayMin = sum(mm.mday)
, nightMin = sum(mm.mnight)
from myminutes mm
where mm.rn between tFirstMin and tLastMin ) r2
You can use cte for that count:
DECLARE
#DateTime1 datetime = '2016-04-20 13:30',
#DateTime2 datetime = '2016-04-21 07:15'
;WITH times AS(
SELECT #DateTime1 as d,
CASE WHEN DATEPART(hour,#DateTime1) between 6 and 22 then 'd' else 'n' end as a,
0 as m
UNION ALL
SELECT DATEADD(minute,1,d),
CASE WHEN DATEPART(hour,DATEADD(minute,1,d)) between 6 and 22 then 'd' else 'n' end as a,
DATEDIFF(minute,d,DATEADD(minute,1,d))
FROM times
WHERE DATEADD(minute,1,d) <= #DateTime2
)
SELECT CASE WHEN a = 'd' THEN 'DayTime' ELSE 'NightTime' END as TimePart,
sum(m)/60 as H,
sum(m) - (sum(m)/60)* 60 as M
FROM times
GROUP BY a
OPTION (MAXRECURSION 0)
Output be like:
TimePart H M
--------- ----------- -----------
DayTime 10 45
NightTime 7 0
(2 row(s) affected)

SQL query to find all timestamps covered by an interval in A but not covered by an interval in B ("subtract" or "except" between multiple intervals)

I have multiple tables in a PostgreSQL 9.4 database, where each row contains an interval as two columns "start" (inclusive) and "stop" (exclusive).
Consider the following pseudo-code (the tables are more complicated).
CREATE TABLE left (
start TIMESTAMP,
stop TIMESTAMP,
[...]
);
CREATE TABLE right (
start TIMESTAMP,
stop TIMESTAMP,
[...]
);
The intervals are inclusive of the start, but exclusive of the stop.
I now need a query to find all possible intervals of time where there is a row in "left" covering the interval, but not simultaneously a row in "right" covering the same interval.
One interval in "left" can be cut up into any number of intervals in the result, be shortened, or be entirely absent. Consider the following graph, with time progressing from left to right:
left [-----row 1------------------) [--row 2--) [--row 3----)
right [--row1--) [--row2--) [--row3--)
result [----) [----) [-------) [-----------)
In this tiny example, "left" has tree rows each representing three intervals and "right" has three rows, each representing three other intervals.
The result has four rows of intervals, which together cover all possible timestamps where there is a row/interval in "left" covering that timestamp, but not a row/interval in "right" covering the same timestamp.
The tables are of course in reality very much larger than three rows each - in fact I will frequently be wanting to perform the algorithm between two subqueries that have the "start" and "stop" columns.
I have hit a dead end (multiple dead ends, in fact), and am on the virge of just fetching all records into memory and applying some procedural programming to the problem...
Any solutions or suggestions of what thinking to apply is greatly appreciated.
Change the types of columns to tsrange (or create an appropriate views):
CREATE TABLE leftr (
duration tsrange
);
CREATE TABLE rightr (
duration tsrange
);
insert into leftr values
('[2015-01-03, 2015-01-20)'),
('[2015-01-25, 2015-02-01)'),
('[2015-02-08, 2015-02-15)');
insert into rightr values
('[2015-01-01, 2015-01-06)'),
('[2015-01-10, 2015-01-15)'),
('[2015-01-18, 2015-01-26)');
The query:
select duration* gap result
from (
select tsrange(upper(duration), lower(lead(duration) over (order by duration))) gap
from rightr
) inv
join leftr
on duration && gap
result
-----------------------------------------------
["2015-01-06 00:00:00","2015-01-10 00:00:00")
["2015-01-15 00:00:00","2015-01-18 00:00:00")
["2015-01-26 00:00:00","2015-02-01 00:00:00")
["2015-02-08 00:00:00","2015-02-15 00:00:00")
(4 rows)
The idea:
l [-----row 1------------------) [--row 2--) [--row 3----)
r [--row1--) [--row2--) [--row3--)
inv(r) [----) [----) [------------------------->
l*inv(r) [----) [----) [-------) [-----------)
If the type change to tsrange is not an option, here an alternative solution using window function.
The important idea is to realize that only the start and end points of the intervals are relavent. In the first step a transformation in a sequence of starting and ending timestamps is performed. (I use numbers to simplify the example).
insert into t_left
select 1,4 from dual union all
select 6,9 from dual union all
select 12,13 from dual
;
insert into t_right
select 2,3 from dual union all
select 5,7 from dual union all
select 8,10 from dual union all
select 11,14 from dual
;
with event as (
select i_start tst, 1 left_change, 0 right_change from t_left union all
select i_stop tst, -1 left_change, 0 right_change from t_left union all
select i_start tst, 0 left_change, 1 right_change from t_right union all
select i_stop tst, 0 left_change, -1 right_change from t_right
)
select tst, left_change, right_change,
sum(left_change) over (order by tst) as is_left,
sum(right_change) over (order by tst) as is_right,
'['||tst||','||lead(tst) over (order by tst) ||')' intrvl
from event
order by tst;
This ends with a two recods for each interval one for start (+1) and one for end (-1 in the CHANGE column).
TST LEFT_CHANGE RIGHT_CHANGE IS_LEFT IS_RIGHT INTRVL
1 1 0 1 0 [1,2)
2 0 1 1 1 [2,3)
3 0 -1 1 0 [3,4)
4 -1 0 0 0 [4,5)
5 0 1 0 1 [5,6)
6 1 0 1 1 [6,7)
7 0 -1 1 0 [7,8)
8 0 1 1 1 [8,9)
9 -1 0 0 1 [9,10)
10 0 -1 0 0 [10,11)
11 0 1 0 1 [11,12)
12 1 0 1 1 [12,13)
13 -1 0 0 1 [13,14)
14 0 -1 0 0 [14,)
The window SUM finction
sum(left_change) over (order by tst)
adds all changes so far, yielding the 1 for beeing in interval and 0 beeing out of the interval.
The filter to get all (sub)intervals that are left only ist therefore trivial
is_left = 1 and is_right = 0
The (sub)interval start with the timstamp of the current row and ends with the timstamp of the next row.
Final notes:
You may need to add logik to ignore intervals of leghth 0
I'm testing in Oracle, so pls re-check the Postgres functionality
For completeness: the naive method, without using interval types.
[I used the same sample data as #klin ]
CREATE TABLE tleft (
start TIMESTAMP,
stop TIMESTAMP,
payload text
);
INSERT INTO tleft(start,stop) VALUES
-- ('2015-01-08', '2015-03-07'), ('2015-03-21', '2015-04-14'), ('2015-05-01', '2015-05-15') ;
('2015-01-03', '2015-01-20'), ('2015-01-25', '2015-02-01'), ('2015-02-08', '2015-02-15');
CREATE TABLE tright (
start TIMESTAMP,
stop TIMESTAMP,
payload text
);
INSERT INTO tright(start,stop) VALUES
-- ('2015-01-01', '2015-01-15'), ('2015-02-01', '2015-02-14'), ('2015-03-01', '2015-04-07') ;
('2015-01-01', '2015-01-06'), ('2015-01-10', '2015-01-15'), ('2015-01-18', '2015-01-26');
-- Combine all {start,stop} events into one time series
-- , encoding the event-type into a state change.
-- Note: this assumes non-overlapping intervals in both
-- left and right tables.
WITH zzz AS (
SELECT stamp, SUM(state) AS state
FROM (
SELECT 1 AS state, start AS stamp FROM tleft
UNION ALL
SELECT -1 AS state, stop AS stamp FROM tleft
UNION ALL
SELECT 2 AS state, start AS stamp FROM tright
UNION ALL
SELECT -2 AS state, stop AS stamp FROM tright
) zz
GROUP BY stamp
)
-- Reconstruct *all* (sub)intervals
-- , and calculate a "running sum" over the state variable
SELECT * FROM (
SELECT zzz.stamp AS zstart
, LEAD(zzz.stamp) OVER (www) AS zstop
, zzz.state
, row_number() OVER(www) AS rn
, SUM(state) OVER(www) AS sstate
FROM zzz
WINDOW www AS (ORDER BY stamp)
) sub
-- extract only the (starting) state we are interested in
WHERE sub.sstate = 1
ORDER BY sub.zstart
;
Result:
DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
INSERT 0 3
CREATE TABLE
INSERT 0 3
zstart | zstop | state | rn | sstate
---------------------+---------------------+-------+----+--------
2015-01-06 00:00:00 | 2015-01-10 00:00:00 | -2 | 3 | 1
2015-01-15 00:00:00 | 2015-01-18 00:00:00 | -2 | 5 | 1
2015-01-26 00:00:00 | 2015-02-01 00:00:00 | -2 | 9 | 1
2015-02-08 00:00:00 | 2015-02-15 00:00:00 | 1 | 11 | 1
(4 rows)
If tsrange is not an option maybe stored procedure is?
Something like this:
--create tables
drop table if exists tdate1;
drop table if exists tdate2;
create table tdate1(start timestamp, stop timestamp);
create table tdate2(start timestamp, stop timestamp);
--populate tables
insert into tdate1(start, stop) values('2015-01-01 00:10', '2015-01-01 01:00');
insert into tdate2(start, stop) values('2015-01-01 00:00', '2015-01-01 00:20');
insert into tdate2(start, stop) values('2015-01-01 00:30', '2015-01-01 00:40');
insert into tdate2(start, stop) values('2015-01-01 00:50', '2015-01-01 01:20');
insert into tdate1(start, stop) values('2015-01-01 01:10', '2015-01-01 02:00');
insert into tdate1(start, stop) values('2015-01-01 02:10', '2015-01-01 03:00');
--stored procedure itself
create or replace function tdate_periods(out start timestamp, out stop timestamp)
returns setof record as
$$
declare
rec record;
laststart timestamp = null;
startdt timestamp = null;
stopdt timestamp = null;
begin
for rec in
select
t1.start as t1start,
t1.stop as t1stop,
t2.start as t2start,
t2.stop as t2stop
from tdate1 t1
left join tdate2 t2 on t2.stop > t1.start or t2.start > t1.stop
loop
if laststart <> rec.t1start or laststart is null then
if laststart is not null then
if startdt < stopdt then
start = startdt;
stop = stopdt;
return next;
startdt = stopdt;
end if;
end if;
startdt = rec.t1start;
stopdt = rec.t1stop;
laststart = startdt;
end if;
if rec.t2start is not null then
if startdt < rec.t2start then
start = startdt;
stop = rec.t2start;
return next;
end if;
startdt = rec.t2stop;
end if;
end loop;
if startdt is not null and startdt < stopdt then
start = startdt;
stop = stopdt;
return next;
end if;
end
$$ language plpgsql;
--call
select * from tdate_periods();

PIVOT SQL Server Assistance

Given the following table structure:
CrimeID | No_Of_Crimes | CrimeDate | Violence | Robbery | ASB
1 1 22/02/2011 Y Y N
2 3 18/02/2011 Y N N
3 3 23/02/2011 N N Y
4 2 16/02/2011 N N Y
5 1 17/02/2011 N N Y
Is there a chance of producing a result set that looks like this with T-SQL?
Category | This Week | Last Week
Violence 1 3
Robbery 1 0
ASB 3 1
Where last week shuld be a data less than '20/02/2011' and this week should be greater than or equal to '20/02/2011'
I'm not looking for someone to code this out for me, though a code snippet would be handy :), just some advice on whether this is possible, and how i should go about it with SQL Server.
For info, i'm currently performing all this aggregation using LINQ on the web server, but this requires 19MB being sent over the network every time this request is made. (The table has lots of categories, and > 150,000 rows). I want to make the DB do all the work and only send a small amount of data over the network
Many thanks
EDIT removed incorrect sql for clarity
EDIT Forget the above try the below
select *
from (
select wk, crime, SUM(number) number
from (
select case when datepart(week, crimedate) = datepart(week, GETDATE()) then 'This Week'
when datepart(week, crimedate) = datepart(week, GETDATE())-1 then 'Last Week'
else 'OLDER' end as wk,
crimedate,
case when violence ='Y' then no_of_crimes else 0 end as violence,
case when robbery ='Y' then no_of_crimes else 0 end as robbery,
case when asb ='Y' then no_of_crimes else 0 end as asb
from crimetable) as src
UNPIVOT
(number for crime in
(violence, robbery, asb)) as pivtab
group by wk, crime
) z
PIVOT
( sum(number)
for wk in ([This Week], [Last Week])
) as pivtab
Late to the party, but a solution with an optimal query plan:
Sample data
create table crimes(
CrimeID int, No_Of_Crimes int, CrimeDate datetime,
Violence char(1), Robbery char(1), ASB char(1));
insert crimes
select 1,1,'20110221','Y','Y','N' union all
select 2,3,'20110218','Y','N','N' union all
select 3,3,'20110223','N','N','Y' union all
select 4,2,'20110216','N','N','Y' union all
select 5,1,'20110217','N','N','Y';
Make more data - about 10240 rows in total in addition to the 5 above, each 5 being 2 weeks prior to the previous 5. Also create an index that will help on crimedate.
insert crimes
select crimeId+number*5, no_of_Crimes, DATEADD(wk,-number*2,crimedate),
violence, robbery, asb
from crimes, master..spt_values
where type='P'
create index ix_crimedate on crimes(crimedate)
From here on, check output of each to see where this is going. Check also the execution plan.
Standard Unpivot to break the categories.
select CrimeID, No_Of_Crimes, CrimeDate, Category, YesNo
from crimes
unpivot (YesNo for Category in (Violence,Robbery,ASB)) upv
where YesNo='Y'
Notes:
The filter on YesNo is actually applied AFTER unpivoting. You can comment it out to see.
Unpivot again, but this time select data only for last week and this week.
select CrimeID, No_Of_Crimes, Category,
Week = sign(datediff(d,CrimeDate,w.firstDayThisWeek)+0.1)
from crimes
unpivot (YesNo for Category in (Violence,Robbery,ASB)) upv
cross join (select DATEADD(wk, DateDiff(wk, 0, getdate()), 0)) w(firstDayThisWeek)
where YesNo='Y'
and CrimeDate >= w.firstDayThisWeek -7
and CrimeDate < w.firstDayThisWeek +7
Notes:
(select DATEADD(wk, DateDiff(wk, 0, getdate()), 0)) w(firstDayThisWeek) makes a single-column table where the column contains the pivotal date for this query, being the first day of the current week (using DATEFIRST setting)
The filter on CrimeDate is actually applied on the BASE TABLE prior to unpivoting. Check plan
Sign() just breaks the data into 3 buckets (-1/0/+1). Adding +0.1 ensures that there are only two buckets -1 and +1.
The final query, pivoting by this/last week
select Category, isnull([1],0) ThisWeek, isnull([-1],0) LastWeek
from
(
select Category, No_Of_Crimes,
Week = sign(datediff(d,w.firstDayThisWeek,CrimeDate)+0.1)
from crimes
unpivot (YesNo for Category in (Violence,Robbery,ASB)) upv
cross join (select DATEADD(wk, DateDiff(wk, 0, getdate()), -1)) w(firstDayThisWeek)
where YesNo='Y'
and CrimeDate >= w.firstDayThisWeek -7
and CrimeDate < w.firstDayThisWeek +7
) p
pivot (sum(No_Of_Crimes) for Week in ([-1],[1])) pv
order by Category Desc
Output
Category ThisWeek LastWeek
--------- ----------- -----------
Violence 1 3
Robbery 1 0
ASB 3 3
I would try this:
declare #FirstDayOfThisWeek date = '20110220';
select cat.category,
ThisWeek = sum(case when cat.CrimeDate >= #FirstDayOfThisWeek
then crt.No_of_crimes else 0 end),
LastWeek = sum(case when cat.CrimeDate >= #FirstDayOfThisWeek
then 0 else crt.No_of_crimes end)
from crimetable crt
cross apply (values
('Violence', crt.Violence),
('Robbery', crt.Robbery),
('ASB', crt.ASB))
cat (category, incategory)
where cat.incategory = 'Y'
and crt.CrimeDate >= #FirstDayOfThisWeek-7
group by cat.category;