Data Types and NULL
Data types and NULL are where SQL stops feeling like a spreadsheet. A column type is a promise about meaning, operations, and storage. NULL is the database’s marker for absence, unknown, or not applicable. This lesson teaches you to choose types deliberately, model money and dates safely, understand SQLite’s flexible typing, and write queries that handle missing values without lying.
A data type is a promise about meaning
NULL only when absence is a real state your queries are prepared to handle.NULL become unknown, and WHERE keeps only true rows.Start with type families, then verify your engine
Every database engine has its own exact type system, but most schema decisions start with a small set of value families. Text stores names, emails, codes, descriptions, and identifiers that are not meant for arithmetic. Integers store whole-number counts and stable identifiers. Decimal or exact numeric types store quantities that need fixed precision. Date/time types store moments, dates, or durations. Boolean values store yes/no states, even when the engine represents them internally as numbers. Binary types store bytes, not display text.
SQLite, which powers the browser runner, uses type affinity. A column declaration guides storage and conversion, but SQLite is more flexible than PostgreSQL or MySQL. That makes it excellent for learning SQL concepts in the browser, but it also means you must verify production behavior in the DBMS you deploy. A PostgreSQL integer column, a MySQL DECIMAL, and a SQLite NUMERIC column are related ideas, not identical contracts.
Choose a type by the value’s meaning and operations, not by how the value happens to look in a CSV file.
Names, emails, labels, slugs, SKUs, JSON text, and values sorted or searched as characters.
Counts, cents, identifiers, positions, and quantities that should never have a fractional part.
Money, measurements, and business numbers that need fixed precision in stricter engines.
Dates, timestamps, deadlines, periods, and values compared on a time axis.
Published, active, verified, archived, paid, or feature flags with a bounded set of states.
Hashes, encrypted payloads, images, or files when the database is truly the right storage boundary.
Run SQLite affinity and inspect storage classes
SQLite stores values using storage classes such as integer, real, text, blob, and null. Column declarations give SQLite an affinity, or preference, for how values should be stored. The typeof() function lets you inspect what actually happened. This matters because the same literal can behave differently when placed into columns with different affinities.
Inspect SQLite type affinity
Store similar-looking values in different declared column types and inspect the storage class SQLite chose for each value.
Edit the query, predict the rows it will return, then run it.
Choose numeric types by the risk of being wrong
A numeric column is not just “anything with digits.” An identifier such as postal_code can contain leading zeroes and should often be text. A count of seats is an integer. A currency amount needs exact handling. A scientific measurement may tolerate approximate floating-point math. Choosing the wrong representation can create subtle defects: postal codes lose leading zeroes, prices gain rounding errors, and averages accidentally ignore missing facts.
For portable beginner projects, storing money as integer cents is often clear and reliable. Production billing systems also need currency, tax, rounding, refund, discount, and audit semantics. The schema should make those responsibilities explicit instead of pretending every price is just a display string.
Model money and boolean flags deliberately
Store prices as integer cents, keep a currency code, and model active state with a constrained SQLite integer flag.
Edit the query, predict the rows it will return, then run it.
Text, dates, and booleans need conventions
Text columns should store text facts, not every fact that arrived from a form as a string. Use text for names, emails, descriptions, tags, URLs, slugs, and domain codes. Use date/time representations when the value participates in time comparisons. In SQLite, ISO-like text timestamps such as YYYY-MM-DD HH:MM:SS sort usefully as text and can be passed to date/time functions. In PostgreSQL and other stricter systems, prefer native date and timestamp types.
SQLite does not have a separate boolean storage class. A common convention is INTEGER NOT NULL CHECK (flag IN (0, 1)). PostgreSQL has a real boolean type. MySQL has aliases and conventions. The point is the same: encode the intended states and reject impossible ones.
Store and query date-like values
Use sortable timestamp text in SQLite, allow an open-ended end time with NULL, and protect impossible ranges with a CHECK.
Edit the query, predict the rows it will return, then run it.
Text
Best for character data and domain codes. Do not use it as a dumping ground for numbers that need math.
Date and time
Use sortable, engine-appropriate representations so filtering and ordering match time rather than display style.
Boolean states
Keep allowed values bounded. A flag with three real states may need an enum-like status instead.
NULL is not zero, false, or an empty string
NULL means the value is absent, unknown, or not applicable. It is a marker, not a normal value. Zero means a known number with value 0. An empty string means a known text value with length 0. False means a known negative boolean state. These differences matter because users, reports, constraints, and application behavior depend on them.
Consider shipped_at. A row with NULL shipped time may mean the order has not shipped or the time is unknown. A row with an empty string usually means bad imported text that should be cleaned. A row with a real date means the shipment event is known. The query below shows how IS NULL and = '' answer different questions.
Compare NULL with an empty string
Run two filters against shipment data and see why missing values and blank text are not the same data state.
Edit the query, predict the rows it will return, then run it.
- 01Required for every valid row?
Use
NOT NULLand require writers to supply or inherit a real value.ExampleA course title, product SKU, or account email usually cannot be absent.
- 02One honest default?
Use
DEFAULTwhen omission has a safe, truthful meaning.ExampleA new draft can default to unpublished; an unknown country should not default silently.
- 03Unknown or not applicable?
Allow
NULLand make reports handle that state explicitly.ExampleAn unresolved ticket has no resolved time yet.
- 04Known blank text?
Use an empty string only when blank text is a meaningful value, not a sloppy placeholder.
ExampleA blank optional display subtitle may be valid; a blank email is usually not.
Three-valued logic changes WHERE results
SQL comparisons do not return only true or false. When a comparison involves NULL, the result is usually unknown. WHERE keeps rows where the condition is true. It filters out false and unknown. That is why score >= 70 excludes both a score of 0 and a missing score, even though those rows mean different things.
Predict true, false, and unknown filters
Compare raw comparison output with WHERE behavior so NULL no longer feels random.
Edit the query, predict the rows it will return, then run it.
True
score >= 70 is true for known passing scores, so WHERE keeps those rows.
False
score >= 70 is false for known lower scores, so WHERE removes those rows.
Unknown
NULL >= 70 is unknown, not false. WHERE still removes it unless you explicitly include IS NULL.
Use COALESCE and aggregates with intention
COALESCE returns the first non-NULL expression. It is useful for display fallbacks, grouping labels, and optional values in reports. Use it carefully: replacing a missing numeric value with zero can be honest for “no discount applied,” but dishonest for “score not graded yet.” A fallback in the SELECT list changes the result presentation; it does not repair stored data.
Aggregates also treat NULL deliberately. COUNT(*) counts rows. COUNT(column) counts known values in that column. SUM and AVG ignore NULL inputs. That behavior is useful, but it means every report should state whether it is counting records, known values, or missing values.
Count rows, known values, and missing values
Use COUNT(*) and COUNT(column) to separate total tickets from tickets that have a resolved time.
Edit the query, predict the rows it will return, then run it.
Make required, optional, default, and unknown explicit
A column should allow NULL only when the model has a real missing state. Required identifiers, names, and state fields often deserve NOT NULL. Optional timestamps, completion dates, cancellation reasons, and external references may be nullable because the event has not happened or the value is not applicable. Defaults are powerful, but they should represent a true default rather than hiding incomplete writes.
Before you create a column, ask four questions: What does the value mean? Which operations must work correctly? Can the value be absent in a valid row? If it is absent, is that unknown, not applicable, not happened yet, or bad imported data? Good schemas are full of these small honest decisions.
Number stored as display text
Text prices sort and calculate like strings unless converted. Store a numeric representation and format at the edge.
Code treated as arithmetic
Not every digit string is a number. Codes can have leading zeroes and should often be text.
Blank string as missing date
Use NULL for missing date facts, then clean imports that use blanks as placeholders.
Unbounded state
A boolean or status column should reject states your application cannot reason about.
Independent lab: repair a messy import
Real data often arrives as text, even when the facts are not text. Your job is to turn the imported members into a cleaner result without pretending bad data is good data. Convert blank strings into NULL, cast numeric ages only when they look numeric, map yes/no marketing flags to 1 and 0, and keep uncertain facts as NULL for later review.
Clean text imports into typed facts
Use NULLIF, CASE, and CAST to convert imported text into a cleaner report while preserving unknown values honestly.
Edit the query, predict the rows it will return, then run it.
Lesson review
Types are part of the schema’s meaning, not decoration. Choose text, integer, exact numeric, date/time, boolean, and binary representations based on the fact being modeled and the operations required. SQLite’s affinity model is flexible, so inspect behavior while learning and verify stricter production engines separately. NULL is a missingness marker, distinct from zero, false, and empty strings. Comparisons with NULL produce unknown, WHERE keeps only true rows, and aggregates count rows versus known values differently. The best schemas make absence honest and visible.
- I can explain why a column type is a contract about meaning and operations.
- I can choose sensible representations for money, dates, booleans, identifiers, and display text.
- I can distinguish
NULL, zero, false, and an empty string in both schema design and query filters. - I can use
IS NULL,IS NOT NULL,NULLIF, andCOALESCEdeliberately. - I can predict why a nullable value may disappear from a
WHEREresult. - I can write reports that separate total rows, known values, and missing values.