MySQL – Get the First Day of the Month (for a given date)

This is one of those simple problems for which a multitude of answers present themselves. But which is better?

For starters, your "given date" could be the current date, or an arbitrary date in the past or in the future. It can be a string, or a date object, or datetime object.

Which is the best solution? It depends! Some are more "readable", others are faster.

In terms of speed, it seems that there are only slight differences depending on which methodology you use to get the first day of the month. What is more significant is whether you need a "date" or "datetime" result or just varchar. But you usually end up doing something with your result, so date or datetime is usually what you need.

Executive summary: all methods for playing around with dates are extremely fast,  so you should use the method that you find to be most readable. Still, our tests show that solutions that use string manipulation and return VARCHAR are the fastest. If you want DATETIME, then that'll thake about three times as long, while methodologies that result in a DATE take 6-10x as long.

In any case, here are some possible solutions (in no particular order) to this problem, along with benchmarks:

Solution # Resulting Type Benchmark
#1: LAST DAY + 2x date arithmetic DATE 1:11.1
#2: DATE + DAYOFMONTH + date arithmetic DATE 1:41.3
#3: DATE + DAYOF MONTH + 2x date arithmetic DATE 1:41.3
#4a: Just DATE_FORMAT it! VARCHAR 0:48.282
#4b: Just DATE_FORMAT it! +DATE DATE 1:38.3
#4c: Just DATE_FORMAT it! +TIMESTAMP DATETIME 1:06.9
#5: LAST_DAY + 2x date arithmetic DATE 1:13.8
#6a: CONCAT+LEFT VARCHAR 0:11.435
#6b: CONCAT+LEFT+DATE DATE 1:01.3
#6c: CONCAT+LEFT+CAST to DATE DATE 0:56.893
#6d: CONCAT+LEFT+CAST to DATETIME DATE 0:28.314
#6e: CONCAT+LEFT+TIMESTAMP DATETIME 0:28.892
#7: MAKEDATE + date arithmetic to DATE DATE 0:43.422

For all of our tests we set @date to an arbitrary DATETIME value:
SET @date := TIMESTAMP('2020-01-11 01:00:00');

You can run the tests yourself if you like:


SELECT BENCHMARK(100000000, LAST_DAY(@date) + INTERVAL 1 DAY - INTERVAL 1 MONTH) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE(@date) - INTERVAL (DAYOFMONTH(@date) - 1) DAY) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE(@date) - INTERVAL (DAYOFMONTH(@date)) DAY + INTERVAL 1 DAY) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE_FORMAT(@date,'%Y-%m-01')) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE(DATE_FORMAT(@date,'%Y-%m-01'))) AS first_day_of_month;
SELECT BENCHMARK(100000000, TIMESTAMP(DATE_FORMAT(@date,'%Y-%m-01'))) AS first_day_of_month;
SELECT	BENCHMARK(100000000, LAST_DAY(@date - INTERVAL 1 MONTH) + INTERVAL 1 DAY) AS first_day_of_month;
SELECT BENCHMARK(100000000, CONCAT(LEFT(@date, 7), '-01')) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE(CONCAT(LEFT(@date, 7), '-01'))) AS first_day_of_month;
SELECT BENCHMARK(100000000, CAST(CONCAT(LEFT(@date, 7), '-01') AS DATE)) AS first_day_of_month;
SELECT BENCHMARK(100000000, CAST(CONCAT(LEFT(@date, 7), '-01') AS DATETIME)) AS first_day_of_month;
SELECT BENCHMARK(100000000, TIMESTAMP(CONCAT(LEFT(@date, 7), '-01'))) AS first_day_of_month;
SELECT	BENCHMARK(100000000, MAKEDATE(YEAR(@date), 1) + INTERVAL MONTH(@date) - 1 MONTH) AS first_day_of_month;

#1: LAST DAY + 2x date arithmetic date

"Get first day of next month and backtrack one month"

SELECT DATE_ADD(DATE_ADD(LAST_DAY(@date), INTERVAL 1 DAY), INTERVAL -1 MONTH) AS first_day_of_month;

This is equivalent to using date arithmetic (as proven in this stackoverflow response by "Strawberry"):

SELECT LAST_DAY(@date) + INTERVAL 1 DAY - INTERVAL 1 MONTH AS first_day_of_month;

Since using DATE_ADD is no different than doing date arithmetic - and date arithmetic is more readable in my opinion - in all further examples we will just use date arithmetic even if it could be written using MySQL functions.

#2: DATE + DAYOFMONTH + date arithmetic DATE

"Subtract one less than the current day of the month"

SELECT DATE(@date) - INTERVAL (DAYOFMONTH(@date) - 1) DAY AS first_day_of_month;
#Equivalent to:
SELECT DATE_ADD(DATE(@date), INTERVAL -DAYOFMONTH(@date)+1 DAY) AS first_day_of_month;

Note that it is necessary to initially cast our value to a date, otherwise we will get a VARCHAR value that includes any time component we already have in our value. So, for example this returns "2020-01-01 01:00:00" - which is probably not what we want:

SET @date := TIMESTAMP('2020-01-11 01:00:00');
SELECT DATE_ADD(@date, INTERVAL -DAYOFMONTH(@date)+1 DAY) AS first_day_of_month;

Also note that there is no difference between DAYOFMONTH and DAY, the two are synonyms for each other as stated in the MySQL manual:

DAY() is a synonym for DAYOFMONTH().

#3: DATE + DAYOF MONTH + 2x date arithmetic DATE

This is very similar to #2 except perhaps a bit more readable:

SELECT DATE(@date) - INTERVAL (DAYOFMONTH(@date)) DAY + INTERVAL 1 DAY AS first_day_of_month;

#4: Just DATE_FORMAT it! VARCHAR(10) / DATE / DATETIME

"Cast the date to string (and back to a date is needed) using DATE_FORMAT"

The result here is VARCHAR unless you then cast it to DATE or DATETIME:

4a: SELECT DATE_FORMAT(@date,'%Y-%m-01') AS first_day_of_month;
4b: SELECT DATE(DATE_FORMAT(@date,'%Y-%m-01')) AS first_day_of_month;
4c: SELECT TIMESTAMP(DATE_FORMAT(@date,'%Y-%m-01')) AS first_day_of_month;

#5: LAST_DAY + 2x date arithmetic date

"Get the last day of the previous month and add a day"

SELECT	LAST_DAY(@date - INTERVAL 1 MONTH) + INTERVAL 1 DAY AS first_day_of_month;

#6: CONCAT+LEFT VARCHAR(10) / DATE / DATETIME

"Cast the date to string, take the first 7 characters, and append "-01", and if necessary cast it back to DATE or DATETIME"

6a: SELECT CONCAT(LEFT(@date, 7), '-01') AS first_day_of_month;
6b: SELECT DATE(CONCAT(LEFT(@date, 7), '-01')) AS first_day_of_month;
6c: SELECT CAST(CONCAT(LEFT(@date, 7), '-01') AS DATE) AS first_day_of_month;
6d: SELECT CAST(CONCAT(LEFT(@date, 7), '-01') AS DATETIME) AS first_day_of_month;
6e: SELECT TIMESTAMP(CONCAT(LEFT(@date, 7), '-01')) AS first_day_of_month;

#7: MAKEDATE + 1x date arithmetic date

"Get the first day of the given year and add months to is"

SELECT BENCHMARK(100000000, CAST('2020-01-11 01:00:00' AS DATETIME)) AS first_day_of_month; 

Bonus: From VARCHAR to DATE/DATETIME

Several of the solutions above return a VARCHAR result, which can then be converted to date or datetime depending on our needs. This conversion seems to require the most overhead, and it can be done with DATE(...), CAST(... as ...), or CONVERT(..., ...). Is there a difference? Between CAST and CONVERT NO, there is no difference (as explained in this article, which refers to SQL server but the same applies to MySQL server).

From our tests it looks like it doesn't matter how you convert your values, however, it does take about half as long to convert to DATETIME than to DATE:

Method Benchmark
CAST(... AS DATETIME) 26.879
CONVERT(..., DATETIME) 26.785
TIMESTAMP(...) 26.707
CAST(... AS DATE) 52.011
CONVERT(..., DATE) 53.196
DATE(...) 51.980
DATE(TIMESTAMP(...)) 54.569

You can run the tests yourself to validate our findings:

SELECT BENCHMARK(100000000, CAST('2020-01-11 01:00:00' AS DATETIME)) AS first_day_of_month; 
SELECT BENCHMARK(100000000, CONVERT('2020-01-11 01:00:00', DATETIME)) AS first_day_of_month;
SELECT BENCHMARK(100000000, TIMESTAMP('2020-01-11 01:00:00')) AS first_day_of_month; 
SELECT BENCHMARK(100000000, CAST('2020-01-11 01:00:00' AS DATE)) AS first_day_of_month; 
SELECT BENCHMARK(100000000, CONVERT('2020-01-11 01:00:00', DATE)) AS first_day_of_month;
SELECT BENCHMARK(100000000, DATE('2020-01-11 01:00:00')) AS first_day_of_month; 
SELECT BENCHMARK(100000000, DATE(TIMESTAMP('2020-01-11 01:00:00'))) AS first_day_of_month;

Methodology

To determine the actual return type of the result, we stored it in a temp table and used DESCRIBE to see what the resulting column type was (as suggested by "Brilliand" in a related question on StackOverflow). For example:

DROP TEMPORARY TABLE IF EXISTS temp;
CREATE TEMPORARY TABLE temp AS
SELECT DATE_ADD(DATE_ADD(LAST_DAY(@date), INTERVAL 1 DAY), INTERVAL -1 MONTH) AS first_day_of_month;
DESCRIBE temp;

To benchmark the results, we used...the BENCHMARK function! Continuing our example above:

SELECT BENCHMARK(100000000, LAST_DAY(@date) + INTERVAL 1 DAY - INTERVAL 1 MONTH) AS first_day_of_month;

Another option would have been to create a table of many dates (as described by "IvanD" on stackoverflow) and run the query on that - but this is more messy than the simple BENCHMARK above, especially since our table can only have so many rows because the MySQL date datatype is limited in range, and running the above solutions on only 99 000 rows takes less than a second:

DROP TABLE IF EXISTS t;
CREATE TABLE t (d DATE);

INSERT INTO t 
SELECT (CURDATE() - INTERVAL c.number DAY) AS date
FROM (
	SELECT singles + tens + hundreds + thousands + tenthousands AS number 
	FROM 
		( 
			SELECT 0 singles
			UNION ALL SELECT   1 UNION ALL SELECT   2 UNION ALL SELECT   3
			UNION ALL SELECT   4 UNION ALL SELECT   5 UNION ALL SELECT   6
			UNION ALL SELECT   7 UNION ALL SELECT   8 UNION ALL SELECT   9
		) singles JOIN 
		(
			SELECT 0 tens
			UNION ALL SELECT  10 UNION ALL SELECT  20 UNION ALL SELECT  30
			UNION ALL SELECT  40 UNION ALL SELECT  50 UNION ALL SELECT  60
			UNION ALL SELECT  70 UNION ALL SELECT  80 UNION ALL SELECT  90
		) tens  JOIN 
		(
			SELECT 0 hundreds
			UNION ALL SELECT  100 UNION ALL SELECT  200 UNION ALL SELECT  300
			UNION ALL SELECT  400 UNION ALL SELECT  500 UNION ALL SELECT  600
			UNION ALL SELECT  700 UNION ALL SELECT  800 UNION ALL SELECT  900
		) hundreds  JOIN 
		(
			SELECT 0 thousands
			UNION ALL SELECT  1000 UNION ALL SELECT  2000 UNION ALL SELECT  3000
			UNION ALL SELECT  4000 UNION ALL SELECT  5000 UNION ALL SELECT  6000
			UNION ALL SELECT  7000 UNION ALL SELECT  8000 UNION ALL SELECT  9000
		) thousands  JOIN 
		(
			SELECT 0 tenthousands
			UNION ALL SELECT  10000 UNION ALL SELECT  20000 UNION ALL SELECT  30000
			UNION ALL SELECT  40000 UNION ALL SELECT  50000 UNION ALL SELECT  60000
			UNION ALL SELECT  70000 UNION ALL SELECT  80000 UNION ALL SELECT  90000
		) tenthousands
		# going past tenthousands results in datetime field overflow...
	ORDER BY number DESC
	) c 
;
   
SELECT DATE_ADD(DATE_ADD(LAST_DAY(d), INTERVAL 1 DAY), INTERVAL -1 MONTH) AS first_day_of_month
FROM t;


SELECT COUNT(DISTINCT DATE_ADD(DATE_ADD(LAST_DAY(d), INTERVAL 1 DAY), INTERVAL -1 MONTH)) AS first_day_of_month
FROM t;

Sources and related content: