SQLite Date and Time: datetime(), strftime(), Modifiers and Storage

Dates in SQLite are stored as text or numbers, because there’s no date type, and a set of built-in date functions does the work. In this article I’ll walk you through storing dates and times and doing arithmetic on them.

TLDR

  • SQLite has no DATE or DATETIME type. Store timestamps as UTC text in the form YYYY-MM-DD HH:MM:SS, or as Unix seconds in an INTEGER column.
  • Get the current time with datetime('now') or CURRENT_TIMESTAMP. Both are UTC.
  • Calculate with modifiers such as '+7 days' and 'start of month', and format with strftime().
  • Filter a date range with >= the start and < the day after the end.

What are dates and times in SQLite?

In SQLite, a date or time is an ordinary value that the date functions know how to read. The docs: “SQLite does not have a storage class set aside for storing dates and/or times. Instead, the built-in Date And Time Functions of SQLite are capable of storing dates and times as TEXT, REAL, or INTEGER values” (Date and Time Datatype). So a date is either ISO-8601 text or a number. The numbers are Julian day numbers (REAL) or Unix timestamps (INTEGER).

SQLite date and time function syntax

SQLite has seven date and time functions (Date And Time Functions). Each takes a time value, followed by optional modifiers:

date(time-value, modifier, modifier, ...)
time(time-value, modifier, modifier, ...)
datetime(time-value, modifier, modifier, ...)
julianday(time-value, modifier, modifier, ...)
unixepoch(time-value, modifier, modifier, ...)
strftime(format, time-value, modifier, modifier, ...)
timediff(time-value, time-value)

The first five show the same moment in different forms. date() and time() return one part of it, datetime() the full timestamp, and the last two return numbers:

sqlite> SELECT date('2026-10-06 14:30:15') AS d, time('2026-10-06 14:30:15') AS t, datetime('2026-10-06 14:30:15') AS dt, unixepoch('2026-10-06 14:30:15') AS unix, julianday('2026-10-06 14:30:15') AS jd;
d           t         dt                   unix        jd
----------  --------  -------------------  ----------  ----------------
2026-10-06  14:30:15  2026-10-06 14:30:15  1791297015  2461320.10434028

How to store and query dates in SQLite

To show you how this works, I will use an orders table with fixed timestamps, so the results are the same every time you run them.

Step 1: Get the current date and time

'now' means the current date and time in UTC, returned as 19-character ISO text. The exact value changes on every run, so this shows its type and shape:

sqlite> SELECT typeof(datetime('now')) AS type, length(datetime('now')) AS len, datetime('now') LIKE '____-__-__ __:__:__' AS iso_shape;
type  len  iso_shape
----  ---  ---------
text  19   1

Step 2: Store dates in a table

ISO-8601 text sorts in time order, so plain string comparison works. CURRENT_TIMESTAMP as a default fills in the UTC time of the insert. Unix seconds in an INTEGER column convert back with the 'unixepoch' modifier:

sqlite> CREATE TABLE orders (id INTEGER PRIMARY KEY, placed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, placed_unix INTEGER);
sqlite> INSERT INTO orders (placed_at, placed_unix) VALUES ('2026-10-06 09:15:00', unixepoch('2026-10-06 09:15:00')), ('2026-09-30 23:59:59', unixepoch('2026-09-30 23:59:59')), ('2026-10-01 00:00:00', unixepoch('2026-10-01 00:00:00'));
sqlite> SELECT id, placed_at, placed_unix, datetime(placed_unix, 'unixepoch') AS from_unix FROM orders ORDER BY placed_at;
id  placed_at            placed_unix  from_unix
--  -------------------  -----------  -------------------
2   2026-09-30 23:59:59  1790812799   2026-09-30 23:59:59
3   2026-10-01 00:00:00  1790812800   2026-10-01 00:00:00
1   2026-10-06 09:15:00  1791278100   2026-10-06 09:15:00

Step 3: Filter a date range

Use a half-open interval: >= the first day and < the day after the last. It catches every time on the last day without guessing at 23:59:59:

sqlite> SELECT id, placed_at FROM orders WHERE placed_at >= '2026-10-01' AND placed_at < '2026-11-01' ORDER BY placed_at;
id  placed_at
--  -------------------
3   2026-10-01 00:00:00
1   2026-10-06 09:15:00

Step 4: Add and subtract with modifiers

Modifiers after the time value shift it or snap it to a boundary (modifiers):

sqlite> SELECT date('2026-10-06', '+7 days') AS next_week, date('2026-10-06', 'start of month') AS month_start, date('2026-10-06', 'start of month', '+1 month', '-1 day') AS month_end, date('2026-10-06', 'weekday 1') AS next_monday;
next_week   month_start  month_end   next_monday
----------  -----------  ----------  -----------
2026-10-13  2026-10-01   2026-10-31  2026-10-12

Examples

Each example below uses the same orders table or fixed dates.

Format a date with strftime

sqlite> SELECT strftime('%Y-%m', '2026-10-06') AS year_month, strftime('%d/%m/%Y %H:%M', '2026-10-06 14:30:15') AS uk, strftime('%w', '2026-10-06') AS weekday_0_sunday, strftime('%j', '2026-10-06') AS day_of_year;
year_month  uk                weekday_0_sunday  day_of_year
----------  ----------------  ----------------  -----------
2026-10     06/10/2026 14:30  2                 279

%w counts Sunday as 0. The full list of substitutions is in the strftime() reference.

Group rows by month

Grouping by a formatted date gives per-month totals:

sqlite> SELECT strftime('%Y-%m', placed_at) AS month, count(*) AS orders FROM orders GROUP BY month ORDER BY month;
month    orders
-------  ------
2026-09  1
2026-10  2

Find the difference between two dates

Subtract julianday() values for days, or unixepoch() values for seconds. timediff() returns a human-readable span (timediff()):

sqlite> SELECT julianday('2026-12-25') - julianday('2026-10-06') AS days, unixepoch('2026-10-06 18:00') - unixepoch('2026-10-06 09:15') AS seconds, timediff('2026-12-25', '2026-10-06') AS timediff;
days  seconds  timediff
----  -------  ------------------------
80.0  31500    +0000-02-19 00:00:00.000

Edge cases

Dates in SQLite are text and numbers underneath, so a few things behave differently from a database with a real date type.

Adding months to the 31st

Adding a month to 31 January is ambiguous. SQLite’s default, ceiling, overflows into the next month. floor “resolves ambiguities by resolving to the last day of the previous month” (ambiguous dates):

sqlite> SELECT date('2026-01-31', '+1 month') AS plain, date('2026-01-31', '+1 month', 'floor') AS floor;
plain       floor
----------  ----------
2026-03-03  2026-02-28

Invalid dates aren’t always rejected

The functions return NULL for text they can’t parse, and for month 13 or day 32. A day from 29 to 31 that doesn’t exist in that month isn’t rejected, though. It rolls forward into the next month, so 30 February becomes 2 March. Validate input before you store it:

sqlite> SELECT date('2026-02-30') AS feb_30, date('2026-02-31') AS feb_31, date('2026-04-31') AS apr_31, date('2026-01-32') AS jan_32, date('2026-13-01') AS month_13, date('not a date') AS garbage;
feb_30      feb_31      apr_31      jan_32  month_13  garbage
----------  ----------  ----------  ------  --------  -------
2026-03-02  2026-03-03  2026-05-01

Local time depends on the machine

The localtime modifier “assumes the time-value to its left is in Universal Coordinated Time (UTC) and adjusts that time value so that it is in localtime” (localtime). The result depends on the machine’s time zone. I ran this with the time zone set to Asia/Kolkata (UTC+5:30):

sqlite> SELECT datetime('2026-10-06 09:15:00', 'localtime') AS local;  -- run with TZ=Asia/Kolkata
local
-------------------
2026-10-06 14:45:00

Store UTC and convert at display time. Mixing local and UTC values in one column breaks ranges and sorting.

Python’s datetime adapter is deprecated

Passing a Python datetime straight into a query still works, but Python 3.12 deprecated the adapter that does it. An aware datetime is also stored with a +00:00 suffix. Formatting the value yourself stores the plain form:

import sqlite3, warnings
from datetime import datetime, timezone

warnings.simplefilter("always")  # show deprecation warnings
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE orders (placed_at TEXT)")
when = datetime(2026, 10, 6, 9, 15, tzinfo=timezone.utc)

con.execute("INSERT INTO orders VALUES (?)", (when,))  # relies on the default adapter
con.execute("INSERT INTO orders VALUES (?)", (when.strftime("%Y-%m-%d %H:%M:%S"),))  # explicit text
for (value,) in con.execute("SELECT placed_at FROM orders"):
    print(repr(value))
con.close()
$ python3 dt_python.py
dt_python.py:9: DeprecationWarning: The default datetime adapter is deprecated as of Python 3.12; see the sqlite3 documentation for suggested replacement recipes
  con.execute("INSERT INTO orders VALUES (?)", (when,))  # relies on the default adapter
'2026-10-06 09:15:00+00:00'
'2026-10-06 09:15:00'

Conclusion

Store dates in SQLite as UTC text in YYYY-MM-DD HH:MM:SS form, or as Unix seconds. Use datetime('now') for the current time. Calculate with modifiers, and format with strftime(). Validate dates before storing them, and convert to local time only when you display them.

Further reading

Tested with SQLite 3.51.0 and Python 3.13.14 on 7 October 2026.