Learn by reading through in order

Date/Time Types and STRICT Tables

This article is part of the SQL Course, where you master practical SQL skills from scratch, from the fundamentals through to complex queries and SQL tuning.
Using the 50,000-row perf_sales table, learn the three date storage formats (ISO strings, julianday, unixepoch), year-based and range aggregation with date() / strftime(), and how the typed_strict table rejects type mismatches — all run live in the browser.

The data we'll use — perf_sales

Dates are stored as either strings or numbers, and you read or compute them with functions like date() / strftime().

In the first half, you'll use the sale_date column of the perf_sales sales table to check the three storage formats and how to extract values; in the second half, you'll create a STRICT table yourself to see what happens when you insert a value that doesn't match the declared type.

Before the exercises, take a look at the column definitions and a sample of the data of the perf_sales table used in this article. (When you run it correctly, the explanation will appear.)

① Run PRAGMA table_info(perf_sales); to check the column definitions.

② Run SELECT * FROM perf_sales LIMIT 5; to preview the first 5 rows. perf_sales has many rows, so always add LIMIT.

SQL Editor

Run a query to see results

The three date storage formats — ISO string / julianday / unixepoch

Dates and times can be stored in three main formats.

FormatExample valueMain use
ISO string'2024-03-15' (with time: '2024-03-15 12:30:00')Human-readable; sorting and range comparison work correctly as plain string comparison (the most common choice)
juliandayjulianday('2024-03-15')2460384.5 (a decimal counting days since antiquity)Computing the difference between two dates in days
unixepochstrftime('%s', ...)1710460800 (integer seconds since 1970-01-01)Second-level time math / interoperating between systems

Whichever format you store, you can read it back as 'YYYY-MM-DD' with date() or as 'YYYY-MM-DD HH:MM:SS' with datetime().

This course uses the ISO string by default because it's human-readable and easy to compare, and only converts to julianday / unixepoch when needed.

The three date storage formats
FormatStored valueMain useISO string'2024-03-15'Human-readable /range & sortjulianday2460384.5Difference betweendates in daysunixepoch1710460800Second-level math /system interop
The same 2024-03-15 can be stored in three formats: ISO string, julianday, and unixepoch. All three can be converted back to a readable form with date() / datetime(), and ISO strings let you do range comparisons directly as string comparisons.
-- The same date in 3 formats (read-only example)
SELECT '2024-03-15'                       AS iso_text,
       julianday('2024-03-15')            AS as_julianday,
       strftime('%s','2024-03-15 00:00:00') AS as_unixepoch;

-- Convert each of the 3 formats back to a readable date
SELECT date(julianday('2024-03-15'))            AS from_jd,
       datetime(1710460800,'unixepoch')         AS from_unix,
       date('2024-03-15')                       AS from_iso;

-- With ISO strings, range comparison works correctly as plain string comparison
SELECT '2024-03-15' BETWEEN '2024-01-01' AND '2024-12-31' AS in_2024;  -- 1

Suppose the requirement is: "For one sale in perf_sales, show its sale date side by side in the ISO string, julianday, and unixepoch formats."

① Narrow perf_sales down to the single row where sale_id = 12345 (since the table is large, always limit to one row).

② Read that row's sale_date as-is under the alias iso.

③ Alongside it, output julianday() applied to the same sale_date as jd, and strftime('%s', ...) as unixep.

SQL Editor

Run a query to see results

Manipulate and aggregate dates with date() and strftime()

  • date(value, modifier...): returns the date part as 'YYYY-MM-DD', and lets you do date math with modifiers like '+1 month' / '-7 days' / 'start of month'
  • strftime(format, value): extracts a piece of the date as a string using format specifiers like %Y (year), %m (month), %d (day), %w (day of week)
  • Combine strftime('%Y', sale_date) with GROUP BY to aggregate by year
  • Because sale_date is stored as an ISO string, range conditions like `WHERE sale_date >= '2024-01-01'` work correctly as plain string comparisons
What date() and strftime() do
FunctionWhat it doesdate(value, modifier)Date arithmetic'+1 month' /'start of month'strftime(format, value)Extract a part%Y year / %m month /%w weekday
date() handles date arithmetic (add/subtract with modifiers); strftime() pulls out a single part such as year, month, or weekday. With ISO strings, range conditions work correctly as plain string comparisons.
-- Date manipulation (read-only example)
SELECT date('2024-03-15','+1 month')      AS plus_1_month,   -- 2024-04-15
       date('2024-03-15','start of month') AS month_start,    -- 2024-03-01
       strftime('%Y',   '2024-03-15')      AS year_part,      -- 2024
       strftime('%Y-%m','2024-03-15')      AS year_month;     -- 2024-03

-- Count by year (ISO string, so extract the year with strftime and GROUP BY)
SELECT strftime('%Y', sale_date) AS yr, COUNT(*) AS cnt
FROM perf_sales
GROUP BY yr
ORDER BY yr;

Suppose the requirement is: "Group the sales in perf_sales by year and show the count and total amount for each year." Since sale_date is an ISO string, use strftime to extract the year and then aggregate.

① Extract the year from sale_date with strftime('%Y', ...) and alias it as yr.

② Output the count of each year as cnt, and the sum of amount as total.

③ Group by year (yr) and order the results in ascending year order (always aggregate — the table has many rows).

SQL Editor

Run a query to see results

Suppose the requirement is: "For Q1 2024 (January 1 to March 31) only, show the count and average amount." Because sale_date is an ISO string, range conditions work correctly as plain string comparisons.

① From perf_sales, keep only rows where sale_date is between 2024-01-01 and 2024-03-31 (inclusive).

② Output the count of those rows as cnt and the average of amount as avg_amount.

③ Don't group — return a single aggregate row covering the whole period.

SQL Editor

Run a query to see results

STRICT tables — enforce strict typing

The type affinity we've seen so far flexibly converts numeric-looking strings.

By contrast, when you add STRICT at the end of a table definition, that table rejects storing values whose type doesn't match the declared one.

If you try to insert a non-numeric string into a numeric column, the value isn't converted implicitly — you get an error.

Use this for tables where you want to catch type mix-ups early.

In a STRICT table, the allowed column types are limited to INTEGER / REAL / TEXT / BLOB / ANYINT and custom type names aren't allowed.

In the next exercise you'll create a typed_strict table with STRICT yourself, and in the following exercises you'll insert both matching and mismatched values to see the difference.

Type checking in a STRICT table
Normal table(type affinity)STRICT table(strict)INTEGER column gets '123' ->converted to 123 and storedINTEGER column gets '123' ->stored if it's an integer'abc' becomes 0 etc.and still runsNon-numeric valuesrejected with error
A normal table uses type affinity and converts numeric-looking strings. A STRICT table doesn't convert values that don't match the declared type and rejects the INSERT itself with an error.
-- Declaring and using a STRICT table (read-only example)
CREATE TABLE IF NOT EXISTS typed_strict(a INTEGER, b TEXT, c REAL) STRICT;

-- Rows with matching types insert just fine
INSERT INTO typed_strict VALUES (1, '2024-03-15', 1.5);

-- Inserting a non-numeric string into the a INTEGER column
-- is not implicitly converted -- the INSERT errors out:
--   cannot store TEXT value in INTEGER column typed_strict.a
INSERT INTO typed_strict VALUES ('not-a-number', 'x', 1.0);

Suppose the requirement is: "Define a table with STRICT myself so I can check the type-checking behavior in the following exercises." Before the next two exercises (where you'll insert matching and mismatched rows), create the table itself here.

① First run DROP TABLE IF EXISTS typed_strict; to clear any previous version, then CREATE a typed_strict table with three columns — a (INTEGER) / b (TEXT) / c (REAL) — adding STRICT at the end.

② Then run PRAGMA table_info(typed_strict); to confirm the three columns are defined as declared.

SQL Editor

Run a query to see results

Suppose the requirement is: "Add one row to the typed_strict table from Exercise 4 with values that match each column's declared type, and confirm it's stored."

① Insert one row into typed_strict (a INTEGER / b TEXT / c REAL): an integer in a, an ISO-format date string in b, and a decimal in c. You can pick the values freely (use a date-like string in b for example).

② Then run SELECT * FROM typed_strict; to confirm the inserted row is there.

SQL Editor

Run a query to see results

Let's check what happens when you try to insert a non-numeric string into the a column (INTEGER) of typed_strict. A STRICT table doesn't convert mismatched values — it rejects them, so this INSERT will error. This is the final exercise of the article, and the point is to experience the error itself.

① Run an INSERT that tries to put a non-numeric string like 'not-a-number' into the a column of typed_strict.

② Confirm that an error message appears (in this console, getting an error means you got it right).

SQL Editor

Run a query to see results
QUIZ

Knowledge Check

Answer each question one by one.

Q1Which of the following is a correct benefit of storing dates as ISO strings like '2024-03-15'?

Q2Which of the following correctly describes the role of strftime('%Y', sale_date)?

Q3Which of the following correctly describes what happens when you try to INSERT a non-numeric string into an INTEGER column of a STRICT table?