I have the following two tables in a PostgreSQL database:
dummy=# select * from employee;
id | name
----+-------
1 | John
2 | Susan
3 | Jim
4 | Sarah
(4 rows)
dummy=# select * from stats;
id | arrival | day | employee_id
----+----------+------------+-------------
2 | 08:31:34 | monday | 2
4 | 08:15:00 | monday | 3
5 | 08:43:00 | monday | 4
1 | 08:34:00 | monday | 1
7 | 08:29:00 | midweek | 1
8 | 08:31:00 | midweek | 2
9 | 08:10:00 | midweek | 3
10 | 08:40:00 | midweek | 4
11 | 08:28:00 | midweek | 1
12 | 08:33:00 | midweek | 2
14 | 08:21:00 | midweek | 3
15 | 08:45:00 | midweek | 4
16 | 08:25:00 | midweek | 1
17 | 08:35:00 | midweek | 2
18 | 08:44:00 | midweek | 4
19 | 08:10:00 | friday | 1
20 | 08:40:00 | friday | 2
21 | 08:30:00 | friday | 3
22 | 08:30:00 | friday | 4
(19 rows)
I want to select all employees that arrive between 8:25 and 8:35 on midweek and friday. I can accomplish that relatively easy with the following query:
SELECT * FROM stats
WHERE
arrival >= (time '8:30' - interval '5 minutes')
AND
arrival <= (time '8:30' + interval '5 minutes')
AND
(day = 'midweek' or day = 'friday');
However, an additional criterion is that I only want to select those employees that arrive at least 60% of the time within the aforementioned time window. This is where I am stuck. I do not know how to calculate that ratio.
What does the Query look like which fulfills all the criteria?
CLARIFICATION
Apparently the above description for the ratio is misleading.
When calculating the ratio then only the rows that meet the criteria (day = 'midweek' or day = 'friday') shall be considered. So in the sample data John and Susan show up four times for work on midweek and friday. Three out of those four times they are punctual. Hence, the ratio for Susan and John is 75%.
Use a common table expression to calculate needed counts, e.g.
with in_time as (
select *
from stats
where arrival >= (time '8:30' - interval '5 minutes')
and arrival <= (time '8:30' + interval '5 minutes')
and (day = 'midweek' or day = 'friday')
),
count_in_time as (
select employee_id, count(*)
from in_time
group by employee_id
),
total_count as (
select employee_id, count(*)
from stats
where day = 'midweek' or day = 'friday'
group by employee_id
)
select
i.*,
c.count as in_time,
t.count as total_count,
round(c.count* 100.0/t.count, 2) as ratio
from in_time i
join count_in_time c using(employee_id)
join total_count t using(employee_id);
Results:
id | arrival | day | employee_id | in_time | total_count | ratio
----+----------+---------+-------------+---------+-------------+-------
16 | 08:25:00 | midweek | 1 | 3 | 4 | 75.00
11 | 08:28:00 | midweek | 1 | 3 | 4 | 75.00
7 | 08:29:00 | midweek | 1 | 3 | 4 | 75.00
17 | 08:35:00 | midweek | 2 | 3 | 4 | 75.00
12 | 08:33:00 | midweek | 2 | 3 | 4 | 75.00
8 | 08:31:00 | midweek | 2 | 3 | 4 | 75.00
21 | 08:30:00 | friday | 3 | 1 | 3 | 33.33
22 | 08:30:00 | friday | 4 | 1 | 4 | 25.00
(8 rows)
You can add an appropriate condition in the WHERE clause of the final query.
If you want to get aggregated data only with employees and their ratios, use count() with filter:
select employee_id, name, in_time* 1.0/ total as ratio
from (
select
employee_id,
count(*) filter (where arrival >= time '8:30' - interval '5 minutes' and arrival <= time '8:30' + interval '5 minutes') as in_time,
count(*) as total
from stats
where day in ('midweek', 'friday')
group by 1
) s
join employee e on e.id = s.employee_id
where in_time* 1.0/ total >= 0.6;
employee_id | name | ratio
-------------+-------+------------------------
1 | John | 0.75000000000000000000
2 | Susan | 0.75000000000000000000
(2 rows)
You can get the arrival rate like so, for example:
SELECT name,
AVG(CASE WHEN arrival >= (time '8:30' - interval '5 minutes') AND
arrival <= (time '8:30' + interval '5 minutes') THEN 1 ELSE 0 END) AS arrival_rate
FROM employee
INNER JOIN stats ON stats.employee_id = employee.id
GROUP BY name
and to select only those where rate > 60% you just use having condition
SELECT name,
AVG(CASE WHEN arrival >= (time '8:30' - interval '5 minutes') AND
arrival <= (time '8:30' + interval '5 minutes') THEN 1 ELSE 0 END) AS arrival_rate
FROM employee
INNER JOIN stats ON stats.employee_id = employee.id
GROUP BY name
HAVING
AVG(CASE WHEN arrival >= (time '8:30' - interval '5 minutes') AND
arrival <= (time '8:30' + interval '5 minutes') THEN 1 ELSE 0 END)
> 0.6
Related
I want to calculate the effectiveness of a discount.
I want to update the last column as 'pozitive' if s_quantity increased and negative, if decreased. Neutral, if no change. I've written the code:
DECLARE #count AS INT
SET #count=1
WHILE #count< 316
BEGIN
IF product_id = #count
WHEN s_quantity > (LAG(s_quantity) OVER (ORDER BY product_id ASC, discount ASC))
UPDATE [SampleRetail].[sale].[Analiz] SET hesap_kitap = 'pozitif'
SET #count +=1
IF #count > 316
BREAK
ELSE
CONTINUE
END
Where do I make the mistake? Can you help me?
We can do this with the function LAG() in a SELECT.
(see the dbFiddle link below for the test schema.)
SELECT
period,
sales,
LAG(sales) OVER (ORDER BY period) p_1,
sales - LAG(sales) OVER (ORDER BY period) increase
FROM sales
ORDER BY period;
period | sales | p_1 | increase
-----: | ----: | ---: | -------:
1 | 100 | null | null
2 | 200 | 100 | 100
3 | 300 | 200 | 100
4 | 250 | 300 | -50
5 | 500 | 250 | 250
6 | 500 | 500 | 0
WITH sale as (
SELECT
period,
sales,
LAG(sales) OVER (ORDER BY period) p_1
FROM sales)
SELECT
period,
sales,
sales - p_1 increase,
CASE WHEN sales < p_1 THEN 'negative'
WHEN sales = p_1 THEN 'stable'
ELSE 'positive' END AS "change"
FROM sale;
period | sales | increase | change
-----: | ----: | -------: | :-------
1 | 100 | null | positive
2 | 200 | 100 | positive
3 | 300 | 100 | positive
4 | 250 | -50 | negative
5 | 500 | 250 | positive
6 | 500 | 0 | stable
db<>fiddle here
EDIT: I changed my example and made it more simple. First Quote is how the source table looks like, second quote is how the result should look like.
Hello everyone,
I have multiple parking that only sends changing states.
It sends a "1" when a car arrived at the parking, then it doesn't send anything until the car leaves again. At that moment the parking sends a "0". I need to do analysis over a long time, so it would be awesome to see the amount of time per hour or so to not get too many rows (compared by minute).
The data looks like this (as requested I reduce it to parking-ID 10 and just the last record from 19.12. and the records from 20.12.):
+------------+------------------+--------+-------------+
| Parking-ID | DateTime | Status | Comment |
+------------+------------------+--------+-------------+
| 10 | 20.12.2019 16:35 | 0 | Car left |
+------------+------------------+--------+-------------+
| 10 | 20.12.2019 08:22 | 1 | Car arrived |
+------------+------------------+--------+-------------+
| 10 | 19.12.2019 22:47 | 0 | Car left |
+------------+------------------+--------+-------------+
Now to not make it too easy for me, next to the "free" and "taken" status there is also a warm status. 1 hour after a car left the parking should be marked as "warm" because some cars have to come and go fast in a few minutes and this time range should be shown as "warm".
To not get too many rows (like for every minute), I would appreciate if it would be possible to get the summary per hour. For my analysis I should be able to see how many hours per day the parking was taken, how many hours it was warm and how many hours it was free.
So the result should look something like this (for Parking-ID 10 and for 20.12.2019):
+------------+------------------+--------+----------+---------+
| Parking-ID | DateTime | Status | Duration | Comment |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 23:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 22:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 21:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 20:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 19:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 18:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 17:00 | 0 | 0.42 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 17:00 | 2 | 0.58 | Warm |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 16:00 | 2 | 0.42 | Warm |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 16:00 | 1 | 0.58 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 15:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 14:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 13:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 12:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 11:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 10:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 09:00 | 1 | 1.00 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 08:00 | 1 | 0.63 | Taken |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 08:00 | 0 | 0.37 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 07:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 06:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 05:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 04:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 03:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 02:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 01:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
| 10 | 20.12.2019 00:00 | 0 | 1.00 | Free |
+------------+------------------+--------+----------+---------+
Does someone have a good approach? I already searched and tried but couldn't find a working approach.
Thank you and best regards
First, your duration output is still wrong,if you cross check.
For example 20.12.2019 08:00 it should be 22.00 and 38.00.Clear this ?
Secondly,Two rows on for 20.12.2019 17:00 is not clear.Why it it will contain 2 rows ?Clear this also.
Create Calendar table in whatever way you want.
CREATE TABLE [dbo].[CalendarDate](
[Dates] [datetime2](0) NOT NULL
PRIMARY KEY CLUSTERED
(
[Dates] ASC
)
) ON [PRIMARY]
GO
insert into [CalendarDate] with(tablock)
select top (100000)
dateadd(day,ROW_NUMBER()over(order by (select null))
,'1950-01-01 00:00:00')
from sys.objects a, sys.objects b, sys.objects c
Then Create number table also
-- Real or #temp your wish
create Table #Number(Hrs int)
insert into #Number (Hrs)
select top 24 ROW_NUMBER()over(order by number)-1
from master..spt_values
Your table sample data.I have kept Parking status in seperate table,follow Normalization.
-- your real table
create table #Parking( ParkingID int, ParkingDateTime Datetime2(0),ParkingStatus tinyint )
insert into #Parking values(10,'2019-12-20 16:35',0),(10,'2019-12-20 08:22',1)
,(10,'2019-12-19 22:47',0)
-- It should be your real table
create table #ParkingStatus( ParkingStatus tinyint,StatusName varchar(50) )
insert into #ParkingStatus values(0,'Car left')
,(1,'Car arrived'),(2,'Free'),(3,'Taken')
,(4,'Warm')
The Script,
declare #From Datetime2(0)='2019-12-20'
declare #To Datetime2(0)=dateadd(second,-1,dateadd(day,1,#From))
-- Put require data in #temp table,since it will be use many times
create table #ParkingTemp(ParkingID int,ParkingDateTime Datetime2(0)
,ParkingDate Date,ParkingStatus tinyint )
insert into #ParkingTemp (ParkingID,ParkingDateTime
,ParkingDate,ParkingStatus)
select P.ParkingID,ParkingDateTime
,p.ParkingDateTime
,ParkingStatus
from #Parking P
where ParkingDateTime>=#From
and ParkingDateTime<=#To
;With CTE as
(
select ParkingID,ParkingDateTime ,count(*)+1 SplitCount
,ParkingStatus as InitialStatus
from #ParkingTemp
group by ParkingID,ParkingDateTime,ParkingStatus
)
, DistinctIDCTE as
(
select distinct ParkingID
from #ParkingTemp
)
, CTE1 as
(
select Dates
,dateadd(hour,hrs,Dates)ReportDateTime
,ParkingID
from [CalendarDate],#Number N,DistinctIDCTE
where dates>=#From and Dates<=#To
),
CTE2 as
(
select c.ParkingID
,dateadd(minute,-datepart(minute,ParkingDateTime),ParkingDateTime) ParkingDate
,ParkingDateTime,hrs as rownum,InitialStatus
from CTE C
cross apply(select hrs from #Number N where c.SplitCount>n.Hrs)ca
)
,CTE3 as
(
select parkingid,ParkingDateTime as FromDatetime
,ToDatetime
from #ParkingTemp C
cross apply(select top 1 ParkingDateTime as ToDatetime
from #ParkingTemp C1 where c.ParkingID=c1.ParkingID
and c1.ParkingStatus=0 and
c1.ParkingDateTime>c.ParkingDateTime
order by c1.ParkingDateTime )c1
where ParkingStatus=1
)
,CTE4 as
(
select c.ParkingID,c.ReportDateTime
from CTE1 C
outer apply(select top 1 FromDatetime ,ToDatetime
from CTE3 c1 where c.ParkingID=c1.ParkingID
and (ReportDateTime>= FromDatetime and ReportDateTime<=ToDatetime))ca
)
--select * from CTE2
,CTE5 as
(
select c4.ParkingID,c4.ReportDateTime
,case when rownum=0 and InitialStatus=1 then 2
when rownum=1 and InitialStatus=1 then 3
when rownum=0 and InitialStatus=0 then 4
when rownum=1 and InitialStatus=0 then 3
else 2 end as ParkingStatusid
,case when rownum=0 then datediff(minute,ReportDateTime,ParkingDateTime)
when rownum=1 then 60- datepart(minute,ParkingDateTime)
else 1.00 end Duration
,ParkingDateTime
,rownum,InitialStatus
from CTE4 c4
left join CTE2 c2 on c4.ParkingID=c2.ParkingID and c2.ParkingDate =c4.ReportDateTime
)
select c5.ParkingID,c5.ReportDateTime,c5.ParkingStatusid
,Duration,PS.StatusName AS Comment
from CTE5 c5
inner join #ParkingStatus ps on c5.ParkingStatusid=ps.ParkingStatus
order by ReportDateTime desc
Clean Up
drop table #Parking,#ParkingStatus,#Number,#ParkingTemp
Alternate and improve :
;WITH CTE
AS (SELECT ParkingID,
ParkingDateTime,
COUNT(*) + 1 SplitCount,
ParkingStatus AS InitialStatus
FROM #ParkingTemp
GROUP BY ParkingID,
ParkingDateTime,
ParkingStatus),
DistinctIDCTE
AS (SELECT DISTINCT
ParkingID
FROM #ParkingTemp),
CTE1
AS (SELECT Dates,
DATEADD(hour, hrs, Dates) ReportDateTime,
ParkingID
FROM [CalendarDate],
#Number N,
DistinctIDCTE
WHERE dates >= #From
AND Dates <= #To),
CTE2
AS (SELECT c.ParkingID,
DATEADD(minute, -DATEPART(minute, ParkingDateTime), ParkingDateTime) ParkingDate,
ParkingDateTime,
hrs AS rownum,
InitialStatus
FROM CTE C
CROSS APPLY
(
SELECT hrs
FROM #Number N
WHERE c.SplitCount > n.Hrs
) ca),
CTE5
AS (SELECT c4.ParkingID,
c4.ReportDateTime,
CASE
WHEN rownum = 0
AND InitialStatus = 1
THEN 2
WHEN rownum = 1
AND InitialStatus = 1
THEN 3
WHEN rownum = 0
AND InitialStatus = 0
THEN 4
WHEN rownum = 1
AND InitialStatus = 0
THEN 3
ELSE 2
END AS ParkingStatusid,
CASE
WHEN rownum = 0
THEN DATEDIFF(minute, ReportDateTime, ParkingDateTime)
WHEN rownum = 1
THEN 60 - DATEPART(minute, ParkingDateTime)
ELSE 1.00
END Duration,
ParkingDateTime,
rownum,
InitialStatus
FROM CTE1 c4
LEFT JOIN CTE2 c2 ON c4.ParkingID = c2.ParkingID
AND c2.ParkingDate = c4.ReportDateTime)
SELECT c5.ParkingID,
c5.ReportDateTime,
c5.ParkingStatusid,
Duration,
PS.StatusName AS Comment
FROM CTE5 c5
INNER JOIN #ParkingStatus ps ON c5.ParkingStatusid = ps.ParkingStatus
ORDER BY ReportDateTime DESC;
Note : clear my doubts.Throw diffrent sample data such within one hour there more than 2 parking staus for same parkingid.
This query gives me Event values from 1 to 20 within an hour, how to add to that if a consecutive Event value is >=200 as well?
SELECT ID, count(Event) as numberoftimes
FROM table_name
WHERE Event >=1 and Event <=20
GROUP BY ID, DATEPART(HH, AtHour)
HAVING DATEPART(HH, AtHour) <= 1
ORDER BY ID desc
In this dummy 24h table:
+----+-------+--------+
| ID | Event | AtHour |
+----+-------+--------+
| 1 | 1 | 11:00 |
| 1 | 4 | 11:01 |
| 1 | 1 | 11:02 |
| 1 | 20 | 11:03 |
| 1 | 200 | 11:04 |
| 1 | 1 | 13:00 |
| 1 | 1 | 13:05 |
| 1 | 2 | 13:06 |
| 1 | 500 | 13:07 |
| 1 | 39 | 13:10 |
| 1 | 50 | 13:11 |
| 1 | 2 | 13:12 |
+----+-------+--------+
I would like to select IDs with Event with values with range between 1 and 20 followed immediately by value greater than or equal to 200 within an hour.
Expected result should be something like that:
+----+--------+
| ID | AtHour |
+----+--------+
| 1 | 11 |
| 1 | 13 |
| 2 | 11 |
| 2 | 14 |
| 3 | 09 |
| 3 | 12 |
+----+--------+
or just how many times it has happened for unique ID instead of which hour.
Please excuse me I am still rusty with post formatting!
CREATE TABLE data (Id INT, Event INT, AtHour SMALLDATETIME);
INSERT data (Id, Event, AtHour) VALUES
(1,1,'2017-03-16 11:00:00'),
(1,4,'2017-03-16 11:01:00'),
(1,1,'2017-03-16 11:02:00'),
(1,20,'2017-03-16 11:03:00'),
(1,200,'2017-03-16 11:04:00'),
(1,1,'2017-03-16 13:00:00'),
(1,1,'2017-03-16 13:05:00'),
(1,2,'2017-03-16 13:06:00'),
(1,500,'2017-03-16 13:07:00'),
(1,39,'2017-03-16 13:10:00')
;
; WITH temp as (
SELECT rownum = ROW_NUMBER() OVER (PARTITION BY id ORDER BY AtHour)
, *
FROM data
)
SELECT a.id, DATEPART(HOUR, a.AtHour) as AtHour, COUNT(*) AS NumOfPairs
FROM temp a JOIN temp b ON a.rownum = b.rownum-1
WHERE a.Event BETWEEN 1 and 20 AND b.Event >= 200
AND DATEDIFF(MINUTE, a.AtHour, b.AtHour) <= 60
GROUP BY a.id, DATEPART(HOUR, a.AtHour)
;
Let's assume I have in SQL Server the following table with only seven days available (SUN - SAT):
Orders
| Day | ProductType | Price |
| SUN | 1 | 10 |
| MON | 1 | 15 |
| MON | 2 | 20 |
| MON | 3 | 10 |
| TUE | 1 | 5 |
| TUE | 3 | 5 |
...
I need to group the data in a way so that to see the Total sum of Prices by each distinct Day and two groups of ProductType (= 1 and > 1):
| Day | FirstProductTypeTotal | RestProductsTypesTotal | GrandTotal |
| SUN | 10 | 0 | 10 |
| MON | 15 | 30 | 45 |
| TUE | 5 | 5 | 10 |
...
where FirstProductTypeTotal is ProductType = 1 and RestProductTypesTotal is ProductType > 1.
Is it possible to select this in one select instead of writing two different selects:
Select Day, SUM(Price) as FirstTotal from Orders where ProductType = 1 group by Day
and
Select Day, SUM(Price) as SecondTotal from Orders where ProductType > 1 group by Day
And then add FirstTotal and SecondTotal manually in the code to get the Grand total for each day of the week?
Use CASE Expression
Select Day, SUM(CASE WHEN ProductType = 1 THE Price ELSE 0 END) AS FirstTotal,
SUM(CASE WHEN ProductType > 1 THE Price ELSE 0 END) AS SecondTotal,
SUM(Price) AS GrandTotal
FROM Orders
group by Day
Try conditional aggregation;
Sample data;
CREATE TABLE #Orders ([Day] varchar(10), ProductType int, Price int)
INSERT INTO #Orders ([Day],ProductType, Price)
VALUES
('SUN',1,10)
,('MON',1,15)
,('MON',2,20)
,('MON',3,10)
,('TUE',1,5)
,('TUE',3,5)
Query;
SELECT
o.[Day]
,SUM(CASE WHEN o.ProductType = 1 THEN o.Price ELSE 0 END) FirstTotal
,SUM(CASE WHEN o.ProductType > 1 THEN o.Price ELSE 0 END) SecondTotal
,SUM(o.Price) GrandTotal
FROM #Orders o
GROUP BY o.[Day]
Result
Day FirstTotal SecondTotal GrandTotal
MON 15 30 45
SUN 10 0 10
TUE 5 5 10
You'd just need to sort out the ordering of the days because SQL Server by definition doesn't store the data in any particular order.
I having trouble to count items biweekly (end at Friday)
Table is like:
+------------+------------+
| ItemNumber | Date |
+------------+------------+
| 235 | 2016-03-02 |
| 456 | 2016-03-04 |
| 454 | 2016-03-08 |
| 785 | 2016-03-10 |
| 123 | 2016-03-15 |
| 543 | 2016-03-18 |
| 863 | 2016-03-20 |
| 156 | 2016-03-26 |
+------------+------------+
Result:
+-------+------------+
| Total | biWeek |
+-------+------------+
| 4 | 2016-03-11 |
| 3 | 2016-03-25 |
| 1 | 2016-04-08 |
+-------+------------+
If I understood your problem correctly, something like this should work:
select
sum(1),
dateadd(day, ceiling(datediff(day,4, [Date]) / 14.0) * 14, 4)
from
yourtable
group by
dateadd(day, ceiling(datediff(day,4, [Date]) / 14.0) * 14, 4)
This calculates the date difference in days to "day 4" aka. 5.1.1900, divides it with 14 (rounding up) and multiplies by 14 to get biweeks and then adds that to the "day 4".
SELECT anyColumn, …, dateadd(week, datediff(week, 0, dateColumn) / 2 * 2 , 0) as biweekly
FROM table
WHERE condition
GROUP BY anyColumn, …, dateadd(week, datediff(week, 0, dateColumn) / 2 * 2 , 0)
This calculates how many weeks dateColumn is far from date 0, then the quotient of dividing this difference by 2 is multiplied by 2 which gives you 2 week intervals. By adding these 2 week intervals to date 0, you will get 2 week periods.