Related
I need to get all week days in a given time interval.
In postgresql, there are dow and isodow
By mixing them together, may I write a function to retrieve weekdays?
demo:db<>fiddle
SELECT
generated_date,
to_char(generated_date, 'Day'), -- 1
EXTRACT(isodow FROM generated_date), -- 2
EXTRACT(dow FROM generated_date) -- 3
FROM
generate_series('2020-11-01'::date, '2020-11-10'::date, interval '1 day') AS generated_date
Returns the name for the weekday
Returns the number of the weekday (Monday = 1, Sunday = 7)
Returns the number of the weekday (Sunday = 0, Saturday = 6)
Edit:
If you want to get the days without weekend, you can filter by the dow/isodow values, e.g.:
SELECT
generated_date::date
FROM
generate_series('2020-11-01'::date, '2020-11-10'::date, interval '1 day') AS generated_date
WHERE
EXTRACT(isodow FROM generated_date) < 6
As far as I understand you need to extract all Monday..Fridays between two dates. Here is an illustration with 2020-11-30 as the beginning of the interval and 2020-12-12 as the end of it.
select d
from generate_series('2020-11-30'::date, '2020-12-12'::date, '1 day'::interval) t(d)
where extract(isodow from d) between 1 and 5;
I've been trying to calculate 7 Day Return Rate (also known as Classic Retention Rate, as described here: https://www.braze.com/blog/calculate-retention-rate/) and then taking a 30 day average to reduce noise in Postgresql.
However, I'm sure I'm doing something wrong. First of all, the numbers look waaay higher than intuitively I feel they should be (generally around 5% for the rest of the sector). Also, I believe the first 7 days should show 0, as theoretically users should take at least 7 days to count as a "return". However, I get around 40-70%, as shown below.
Would someone mind taking a look at the code below and seeing if there are any errors? 7 Day Return Rate is a really common metric for apps, and I haven't found any questions using postgresql that calculate it to this level of sophistication on Stack Exchange (or even the rest of the web), so I feel like a solid response could be very useful to a lot of people.
Sample data
Wednesday, August 1, 2018 12:00 AM 71.14
Thursday, August 2, 2018 12:00 AM 55.44
Friday, August 3, 2018 12:00 AM 50.09
Saturday, August 4, 2018 12:00 AM 45.81
Sunday, August 5, 2018 12:00 AM 43.27
Monday, August 6, 2018 12:00 AM 40.61
Tuesday, August 7, 2018 12:00 AM 39.38
Wednesday, August 8, 2018 12:00 AM 38.46
Thursday, August 9, 2018 12:00 AM 36.81
Friday, August 10, 2018 12:00 AM 35.94
with
user_first_event as (
select distinct id, min(timestamp)::date as first_event_date
from log
where
timestamp <= current_date
and timestamp >= {{start_date}} and timestamp <= {{end_date}}
group by id),
event as (
select distinct id, timestamp::date as user_event_date
from log
where timestamp <= current_date and timestamp >= {{start_date}}),
gap as (
select
user_first_event.id,
user_first_event.first_event_date,
event.user_event_date,
event.user_event_date - user_first_event.first_event_date as days_since_signup
from user_first_event
join event on user_first_event.id = event.id
where user_first_event.first_event_date <= event.user_event_date),
conversion_rate as (
select
first_event_date,
(sum(case when days_since_signup = 7 then 1 else 0 end) * 100.0 /
count(distinct id)
) as seven_day_retention_rate
from gap
group by first_event_date
)
SELECT first_event_date,
AVG(seven_day_retention_rate)
OVER(ORDER BY first_event_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW) AS rolling_avg_retention_rate
FROM conversion_rate
The problem is a bit easier than your query makes it seem, you can actually do it with just one subquery and one out query as follows:
select first_event_date
, avg(seven_day_return) as seven_day_return_day_only
, avg( avg(seven_day_return) ) OVER(ORDER BY first_event_date asc ROWS BETWEEN 29 preceding AND CURRENT ROW ) AS thirty_day_rolling_retention
from (
--inner query to get value for user, 1 if they retain and 0 if they do not
select min(timestamp)::date as first_event_date
, case when array_agg(timestamp::date) #> ARRAY[ (min(timestamp)::date + 7) ] then 1 else 0 end as seven_day_return
from log
group by id ) t
group by t.first_event_date;
Note that this weights each day equally rather than each user equally across days. If you want to weight the average by user across days then you can update the outer calculation using more aggregates and windows to compute the value with weightings.
Reference: http://sqlfiddle.com/#!17/ee17e/1/0
If you don't have access to array_agg (but have access to window functions) you can use:
select first_event_date
, avg(seven_day_return) as day_seven_day_return
, avg( avg(seven_day_return) ) OVER(ORDER BY first_event_date asc ROWS BETWEEN 29 preceding AND CURRENT ROW ) AS thirty_day_rolling_retention
from (
--inner query to get value for user
select min(timestamp)::date as first_event_date
, case when exists(select 1 from log l2 where l2.id = log.id and l2.timestamp::date = min(log.timestamp)::date + 7) then 1 else 0 end as seven_day_return
from log
group by id ) t
group by t.first_event_date;
How to write a sql statement that always returns data from last Monday to the last Sunday? Any guidance is appreciated.
Thanks.
t-clausen.dk's answer does work, but it may not be clear why it works (neither to you nor to the developer who comes after you). Since added clarity sometimes comes at the cost of conciseness and performance, I'd like to explain why it works in case you'd prefer to use that shorter syntax.
SELECT t.*
FROM <table> t CROSS JOIN
(SELECT DATEDIFF(day, 0, getdate() - DATEDIFF(day, 0, getdate()) %7) lastmonday) a
WHERE t.yourdate >= a.lastmonday - 7 and yourdate < a.lastmonday
How SQL Server Stores datetime Internally
SQL Server stores datetime as two 4-byte integers; the first four bytes represents the number of days since 1/1/1900 (or before 1/1/1900, for negative numbers) and the second four bytes represents the number of milliseconds since midnight.
Using datetime with int or decimal
Because datetime is stored as two 4-byte integers, it is easy to move between numeric and date data types in T-SQL. For example, SELECT GETDATE() + 1 returns the same as SELECT DATEADD(day, 1, GETDATE()), and CAST(40777.44281 AS datetime) is the same as 2011-08-24 10:37:38.783.
1/1/1900
Since the first integer portion of datetime is, as was mentioned above, the number of days since 1/1/1900 (also called the SQL Server Epoch), CAST(0 as datetime) is by definition equivalent to
1900-01-01 00:00:00
DATEDIFF(day, 0, GETDATE())
Here's where things start to get both tricky and fun. First, we've already established that when 0 is treated as a date, it's the same as 1/1/1900, so DATEDIFF(day, '1/1/1900', GETDATE()) is the same as DATEDIFF(day, 0, GETDATE())—both will return the current number of days since 1/1/1900. But, wait: the current number of days is exactly what is represented by the first four bytes of datetime! In a single statement we have done two things: 1) we've removed the "time" portion returned by GETDATE() and we've got an integer value we can use to make the calculation for finding the most recent Sunday and the previous Monday a little easier.
Modulo 7 for Monday
DATEDIFF(day, 0, GETDATE()) % 7 takes advantage of the fact that DATEPART(day, 0) (which, at the risk of overemphasizing the point, is the same as DATEPART(day, '1/1/1900')) returns 2 (Monday).1 Therefore, DATEDIFF(day, 0, GETDATE()) % 7 will always yield the number of days from Monday for the current date.
1 Unless you have changed the default behavior by using DATEFIRST.
Most Recent Monday
It's almost too trivial for its own heading, but at this point we can put together everything we've got so far:
GETDATE() - DATEDIFF(day, 0, GETDATE()) %7 gives you the most recent Monday
DATEDIFF(day, 0, GETDATE() - DATEDIFF(day, 0, GETDATE()) %7) gives you the most recent Monday as a whole date, with no time portion.
But the poster wanted everything from Monday to Sunday...
Instead of using the BETWEEN operator, which is inclusive, the posted answer excluded the last day, so that there was no need to do any shifting or calculating to get the right date range:
t.yourdate >= a.lastmonday - 7 returns records with dates since (or including) midnight on the second-most-recent Monday
t.yourdate < a.lastmonday returns records with dates before (but not including) midnight on the most recent Monday.
I am a big proponent of writing code that is easy to understand, both for you and for the developer who comes after you a year later (which might also be you). I believe that the answer I posted earlier should be understandable even to novice T-SQL programmers. However, t-clausen.dk's answer is concise, performs well, and could be encountered in production code, so I wanted to give some explanation to help future visitors understand why it works.
I realized my code was too complex, so I changed my answer to this. The minus one after getdate() corrects this to return the last Monday on Sunday instead of this weeks Monday (tomorrow if today is Sunday).
SELECT t.*
FROM <table> t CROSS JOIN
(SELECT DATEDIFF(week, 0, getdate() - 1) * 7 lastmonday) a
WHERE t.yourdate >= a.lastmonday - 7 and yourdate < a.lastmonday
Old code
SELECT t.*
FROM <table> t CROSS JOIN
(SELECT DATEDIFF(day, 0, getdate() - DATEDIFF(day, 0, getdate()) %7) lastmonday) a
WHERE t.yourdate >= a.lastmonday - 7 and yourdate < a.lastmonday
You did not specify which SQL dialect, so I will answer for T-SQL, which is what I know best, and you've used the tsql tag.
In T-SQL, use the DATEPART function to find the day of the week. When you know the current day of the week, you can get the date of the most recent Sunday and the Monday before it.
In a stored procedure, it's easier—at least, more readable and easier to maintain, in my opinion—to calculate the values for the most recent Sunday and the preceding Monday and store the values in variables. Then those variables can be used in calculations later in the procedure.
CREATE PROCEDURE SomeProcedure
AS
DECLARE #CurrentDayOfWeek int, #LastSunday date, #LastMonday date
SET #CurrentWeekday = DATEPART(weekday, GETDATE())
-- Count backwards from today to get to the most recent Sunday.
-- (#CurrentWeekday % 7) - 1 will give the number of days since Sunday;
-- -1 negates for subtraction.
SET #LastSunday = DATEADD(day, -1 * (( #CurrentWeekday % 7) - 1), GETDATE())
-- Preceding Monday is obviously six days before last Sunday.
SET #LastMonday = DATEADD(day, -6, #LastSunday)
SELECT ReportColumn1, ReportColumn2
FROM ReportTable
WHERE DateColumn BETWEEN #LastMonday AND #LastSunday
If you need to be able to do the calculation in a SELECT statement or a view, it's trivial now that we've worked out the steps, though the query itself is a little messier:
SELECT ReportColumn1, ReportColumn2
FROM ReportTable
WHERE DateColumn
BETWEEN
(
-- Last Monday is six days before...
DATEADD(day, -6,
-- ... last Sunday.
DATEADD(day, -1 * (( DATEPART(weekday, GETDATE()) % 7) - 1), GETDATE())
)
)
AND
(
-- Last Sunday has to be calculated again each time it is used inline.
DATEADD(day, -1 * (( DATEPART(weekday, GETDATE()) % 7) - 1), GETDATE())
)
The parentheses I added are not necessary, but are only there to help you see how the WHERE clause is built.
Finally, note that these use the SQL 2008 date data type; for the datetime data type, you may need to perform your own conversion/truncation to compare whole dates instead of date-plus-time values.
The short code in #t-clausen.dk answer doesn't work for Sunday nor does the first result I found on Google. To test them out, try.
DECLARE #date as DATETIME;
SET #date = '2014-11-23 10:00:00'; -- Sunday at 10a
SELECT DATEADD(wk, DATEDIFF(wk,0,#date), 0) -- 2014-11-24 00:00:00
SELECT CAST(DATEDIFF(week, 0, #date)*7 as datetime) -- 2014-11-24 00:00:00
SELECT CAST(DATEDIFF(day, 0, CAST(#date as DATETIME) - DATEDIFF(day, 0,#date) %7) as DATETIME) --2014-11-17 00:00:00
SELECT DATEADD(wk, DATEDIFF(wk,0,#date-1), 0) --2014-11-17 00:00:00
Thus, you should use the long code from the original answer or modified short code.
SELECT t.*
FROM <table> t CROSS JOIN
(SELECT DATEDIFF(week, 0, getdate() - 1) * 7 lastmonday) a
WHERE t.yourdate >= a.lastmonday - 7 and yourdate < a.lastmonday
How can i calculate the last working five days which is monday to Friday. my current script gets the last monday's date, but i cannot get the last friday's date. Please help
declare #StartDate datetime
declare #EndDate datetime
--Calculate date range for report
select #EndDate = Cast(convert(char(10), getdate(), 101)+' 00:00:00' as datetime)
select #StartDate = DateAdd(d, -7, #EndDate)
select #EndDate = Cast(convert(char(10), getdate(), 101)+' 23:59:59' as datetime)
select #StartDate startdate
select #EndDate enddate
Datepart offers you an easy way to get the week day:
SET DATEFIRST 1 -- monday is first day of the week
SELECT DATEPART(weekday, '20110725')
-- result is 1
See T-SQL Date functions and SET DATEFIRST for more information.
Using the weekday, you can work out how many days ago last monday and friday are and use 'AddDate' (like you are doing now) to calculate those.
Note that you should really use DATEDIFF for this type of date range selection. If you select everything up to 23:59:59 there's always a chance that some records are left out. For example 23:59:59.001 is out of range but it's still on the same day. With DATEDIFF you can test whether it's on the same day, discarding the time part. No need to bother with casting to string, adding time and casting back.
The answer is more complex than people are assuming. What you need is to go 5 days back and find the first monday before that, and the first friday after that. You can eather use ##datefirst, a calculation or 'set firstdate 1' for that. I prefer not using the last one, because it can't be done in functions. As you can see i used the calculation, ##datefirst is just as good.
Assuming you want the last group of monday to friday that is in the past. This query will get that. If you trust your current monday, you can just add 5 days and subtract 1 minute (I wouldn't trust it, it only returns last monday if you run the query on a monday).
In my sql, I am not aiming for simplicity, I am aiming for effectivity.
DECLARE #getdate datetime = dateadd(day, cast(getdate() as int), 0)
-- the 'Declare' can also be written like this thanks to #Andriy M
--DECLARE #getdate = CAST(GETDATE() - 0.5 AS int)
SELECT #getdate - 5 - CAST(#getdate- 5 as int) % 7 monday,
dateadd(minute, -1, #getdate) - CAST(#getdate- 5 as int) % 7 friday
Result:
Monday Friday
2011-07-18 0:00:00 2011-07-22 23:59:00
*first solution was a day off #AndriyM pointed it out, it has been solved.
Answer to #Andriy M
For some reason it acted different than I expected. I can't explain it but try this
select cast(dateadd(day, cast(getdate() as int) - .5, 0) as datetime),
cast(dateadd(day, cast(getdate() as int), 0) as datetime),
cast(dateadd(day, cast(getdate() as int) + .5, 0) as datetime)
in the morning the last 2 fields has same value, in the evening the first 2 fields has the same value. I am as surprised as you are, I wish I could explain it. It was tested here
https://data.stackexchange.com/stackoverflow/query/new
These two questions:
Find last sunday
How to get last day of last week in sql?
might be of some help.
Although, if you already know how to find the last Monday, you can easily find the corresponding Friday by adding 4 days to the Monday date using the DATEADD() function. For example:
SELECT #EndDate = DATEADD(DAY, 4, #StartDate)
It's all relative to TODAY's date, right?
So find out what today is DATEPART(weekday, getdate())
And then turn that into an adjustment variable -- e.g.:
declare #adjustment int
set #adjustment = CASE DATEPART(weekday, getdate()) WHEN 'MONDAY' THEN 0 WHEN 'TUESDAY' THEN 1, ...END
So then the Monday you want would be today - 7 - #adjustment
...and the Friday you want would be Monday + 5
Declare #myMonday smalldatetime,
#myFriday smalldatetime
set #myMonday = getdate() - 7 - #adjustment
set #myFriday = #myMonday + 5
DECLARE #my int
DECLARE #myDeduct int
DECLARE #day INT
DECLARE #mydate DATETIME
SET #mydate = '2012-08-01'
SET #myDeduct = 0
SET DateFirst 1 -- Set it monday=1 (value)
--Saturday and Sunday on the first and last day of a month will Deduct 1
IF (DATEPART(weekday,(DATEADD(dd,-(DAY(#mydate)-1),#mydate))) > 5)
SET #myDeduct = #myDeduct + 1
IF (DATEPART(weekday,(DATEADD(dd,-(DAY(DATEADD(mm,1,#mydate))),DATEADD(mm,1,#mydate)))) > 5)
SET #myDeduct = #myDeduct + 1
SET #my = day(DATEADD(dd,-(DAY(DATEADD(mm,1,#mydate))),DATEADD(mm,1,#mydate)))
select (((#my/7) * 5 + (#my%7)) - #myDeduct) as Working_Day_per_month
I'll throw my hat in the ring too. :-)
DECLARE #Date datetime = '01/11/2015'
DECLARE #StartDate datetime = DATEADD(d, (1 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date - 6) % 7)), #Date - 6) -- MONDAY
DECLARE #EndDate datetime = DATEADD(d, (5 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date - 6) % 7)), #Date - 6) -- FRIDAY
SELECT '#Date' as Variable ,CONVERT(date, #Date) as DateValue ,DATENAME(dw, #Date) as DayOfTheWeek
UNION SELECT '#StartDate' as Variable ,CONVERT(date, #StartDate) as DateValue ,DATENAME(dw, #StartDate) as DayOfTheWeek
UNION SELECT '#EndDate' as Variable ,CONVERT(date, #EndDate) as DateValue ,DATENAME(dw, #EndDate) as DayOfTheWeek
-- Variable DateValue DayOfTheWeek
-- ---------- ---------- ------------
-- #Date 2015-01-11 Sunday
-- #StartDate 2015-01-05 Monday
-- #EndDate 2015-01-09 Friday
BONUS: Here you can generate a quick table of the 5 weekdays using the same technique.
SELECT DATENAME(dw, DATEADD(d, TT.DaysToAdd, DATEADD(d, (1 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date - 6) % 7)), #Date - 6))) as DayOfTheWeek
, DATEADD(d, TT.DaysToAdd, DATEADD(d, (1 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date - 6) % 7)), #Date - 6)) as DateValue
FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 as DaysToAdd FROM (VALUES(0),(0),(0),(0),(0)) a(n)) as TT
-- DayOfTheWeek DateValue
-- ------------------------------ -----------------------
-- Monday 2015-01-05 00:00:00.000
-- Tuesday 2015-01-06 00:00:00.000
-- Wednesday 2015-01-07 00:00:00.000
-- Thursday 2015-01-08 00:00:00.000
-- Friday 2015-01-09 00:00:00.000
Here's an explanation:
1) First we need to know what date to begin our evaluation from. For this example, we chose to use Sunday, January 11th, 2015.
DECLARE #Date2 datetime = '01/11/2015'
1b) Here's a nice to have bonus technique of getting the name for the day of the week, given a date value
SELECT #Date2 as DateValue, DATENAME(dw, #Date2) as DayOfTheWeek
2) Next we need to know deterministically (based on the US calendar) what the given day of the week is numerically between 1 and 7
NOTE: 1899.12.31 is the first Sunday before 1900.01.01, which is the MINIMUM value for the SmallDateTime data type.
NOTE: Yes, you could use DATEPART(dw, #Date) like this more simply, but it is not deterministic given that certain server environments could have different configurations
RESULTS: 1 = Sunday | 2 = Monday | 3 = Tuesday | 4 = Wednesday | 5 = Thursday | 6 = Friday | 7 = Saturday
SELECT ((DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date2) % 7) + 1) [DayOfWeek Deterministic (Based on US)]
3) Now, given any date, you should have a deterministic way of determining the Monday for that given week
SELECT DATEADD(d, (1 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date2) % 7)), #Date2) as [Monday Day of the Week - Deterministic (Based on US)]
4) Now, given any date, you should have a deterministic way of determining the Friday for that given week
SELECT DATEADD(d, (5 - (DATEDIFF(d, CAST('1899.12.31' AS datetime), #Date2) % 7)), #Date2) as [Monday Day of the Week - Deterministic (Based on US)]
5) The last date manipulation technique we need is to know how to get into a week that has the first full week of weekdays happening before it. For instance, if we are on a Sunday and we subtract 1 day from it, then we get to Saturday, which puts us in the previous week, which is the first full week of weekdays. Alternatively, if we also subtracted 1 day from Monday, it would only get us to Sunday, which is not the previous week, so subtracting 1 day is not enough. On the flip side, if we were on a Saturday and subtracted 7 days, it would take us past the previous full week of weekday, into the week before it, which is too far. Here's a run down of the analysis to figure out what the magic numbers is that you can subtract by that will work with any day of the week. As you can see below, the magic number is 6.
-- DAYS TO SUBTRACT
-- Day of the Week - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7
-- =============== ==== ==== ==== ==== ==== ==== ==== ====
-- Sunday Bad Good Good Good Good Good Good Good
-- Monday Bad Bad Good Good Good Good Good Good
-- Tuesday Bad Bad Bad Good Good Good Good Good
-- Wednesday Bad Bad Bad Bad Good Good Good Good
-- Thursday Bad Bad Bad Bad Bad Good Good Good
-- Friday Bad Bad Bad Bad Bad Bad Good Good
-- Saturday Good Good Good Good Good Good Good Bad
BONUS) If you want to have all the weekdays in little table, then you would want to also use a quick zero based "tally table". There are many ways to do this, so pick your flavor. Here are few.
SELECT * FROM (SELECT 0 as DaysToAdd UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) as TT
SELECT * FROM (SELECT TOP 5 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 as DaysToAdd FROM sys.all_columns a CROSS JOIN sys.all_columns b) as TT
SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 as DaysToAdd FROM (VALUES(0),(0),(0),(0),(0)) a(n)) as TT
I work at a college and our student management systems academic year start date is determined by the Monday on or before the 1st of August. I need to match this in my query, is there a way to easily get the date of the Monday on or before this date.
The accepted answer didn't work for me because I needed both a Sunday week and a Monday week in the same query. This works across different "datefirst" settings:
SELECT DATEADD(d, -((DATEPART(WEEKDAY, '20110515') - DATEPART(dw, '19000101') + 7) % 7), '20110515')
"DATEPART(dw, '19000101')" will determine your "datefirst" setting since 1900-01-01 was on a Monday. If you want a Tuesday based week, you can change 19000101 to 19000102.
BTW, '20110515' is the only date format that works across all SQL Server culture settings. Dates like '2011-05-06' will get mis-interpreted in certain countries. (credit to Itzik Ben-Gan for pointing this out)
set datefirst 1; -- Make Monday the first day of the week
select dateadd(dd, -1*(datepart(dw, '2009-08-01')-1), '2009-08-01')
Returns July 27th, 2009, which is the Monday on or before August 1. Change it to 2005 when Aug 1 was a Monday and the query will return 08-01
You could use datepart to get the weekday, and then do a little math to back into your monday. This example is using the US default of datefirst 7 (in which Monday is day 2 of the week). Adjust the days to add to be which day of the week Monday is for your locale.
select dateadd(dd, -datepart(dw, '2009-08-01') + 2, '2009-08-01')
very hacky
DECLARE #weekday int
SELECT #weekday = DATEPART(WEEKDAY, '1-Aug-2009')
SELECT CASE
WHEN #weekday = 1 THEN '1-Aug-2009' ELSE DATEADD ( dd,(#weekday-2)*-1, '1-Aug-2009')
END
This is a generic algorithm that will return the first Monday of any month (#inputdate):
DATEADD(wk, DATEDIFF(wk, 0, dateadd(dd, 6 - datepart(day, #inputDate), #inputDate)), 0)
It is a common method for getting the first monday of the month in SQL Server. This link explains how the above calculation works along with many other date calculations.
Here is how the above algorithm could be used to get the Monday on or before the 1st day of a month:
-- Set month to get Monday before or at 1st of month.
DECLARE #inputDate DATETIME
SET #inputDate = '2009-08-01'
-- Get first Monday of month.
DECLARE #firstMonday DATETIME
SET #firstMonday = DATEADD(wk, DATEDIFF(wk, 0, dateadd(dd, 6 - datepart(day, #inputDate), #inputDate)), 0)
-- Determine date for first Monday on or before 1st of month.
DECLARE #startDate DATETIME
SET #startDate = #firstMonday
IF #firstMonday > #inputDate
SET #startDate = DATEADD(wk, -1, #firstMonday)
SELECT #startDate