Chapter 02 · Tables, Schemas, and Data Types
Choosing Data Types Without Losing Meaning
Model the meaning of a value first, then select a type that preserves its range, precision, validation rules, and future use.
Learning outcomes
A technically valid type can still be a poor model. Good type selection begins with the meaning of a value, then accounts for range, precision, units, validation, operations, indexing, portability, and change.
Use a structured decision process instead of choosing types by habit.
Model identifiers, money, measurements, temporal values, text, categories, and flags appropriately.
Recognize weak-schema patterns that silently discard business meaning.
Refactor a permissive SQLite table into a STRICT, constrained design.
Meaning first, storage second
Start with the domain. A type is successful when valid values are easy to store and invalid values are difficult to store.
Ask what the value represents before asking how many bytes it requires. The string 00127 could be a quantity, an identifier, a postal code, or a formatted label. Only one of those meanings supports arithmetic.
| Question | Why it matters |
|---|---|
| Is it an identifier or a measurable quantity? | Identifiers should not usually support arithmetic. |
| Can the value be absent? | Determines nullability and possibly a separate status model. |
| What are the minimum and maximum valid values? | Controls range and overflow risk. |
| Must decimal values be exact? | Distinguishes decimal or scaled integers from floating point. |
| Which comparisons and calculations are required? | The type must support intended operations. |
| Which database engines must support it? | Specialized types may need an adapter or fallback. |
Identifiers are labels, not quantities
An identifier distinguishes one entity from another. Even when composed only of digits, it may be text. Leading zeros, fixed formatting, check digits, or external ownership are strong signals.
Surrogate key
A database-generated integer or UUID used only for identity and relationships.
Natural key
A real-world identifier such as a standardized code, constrained for uniqueness.
External identifier
A value owned by another system; preserve its exact spelling and leading zeros.
Not a number
Phone numbers, postal codes, account references, and serial labels generally should not be added or averaged.
Use integer keys when compact sequential identity is desirable and exposure is acceptable. Use UUIDs or another distributed identifier when independent systems must generate identifiers without coordination. Do not assume “UUID as text” is always best; many engines provide native UUID types.
Money and exact decimal values
Money requires a currency, amount, rounding policy, and scale. Two common representations are:
- an exact decimal type such as
DECIMAL(19,4)in a server database; - an integer count of a documented minor unit, such as cents, when the currency and precision policy permit it.
CREATE TABLE payment ( payment_id INTEGER PRIMARY KEY, currency_code TEXT NOT NULL CHECK (length(currency_code) = 3), amount_minor INTEGER NOT NULL CHECK (amount_minor >= 0)) STRICT;INSERT INTO payment (currency_code, amount_minor)VALUES ('EUR', 1299); -- EUR 12.99SELECT currency_code, amount_minor, printf('%.2f', amount_minor / 100.0) AS display_amountFROM payment;Formatting a value with a currency symbol and decimal separator belongs at an interface boundary. Store the currency and amount in machine-readable columns, then format them for users.
Measurements require units and precision
A numeric column named temperature is incomplete. Does it store Celsius, Fahrenheit, or Kelvin? What sensor precision is meaningful? Are values estimates or calibrated measurements?
| Design | Assessment |
|---|---|
temperature REAL | Ambiguous unit and quality policy |
temperature_c REAL | Unit is explicit; floating point may suit sensor measurement |
mass_mg INTEGER | Exact scaled quantity in milligrams |
latitude DECIMAL(9,6) | Explicit decimal precision in engines with exact decimal support |
Value plus unit_code | Flexible but requires compatibility constraints and conversions |
Use floating point for scientific or sensor values when approximate representation is acceptable. Use exact decimal or scaled integers when business rules require exact decimal arithmetic. Record units in the column name, metadata, or a constrained companion column.
Dates, times, instants, and durations
Temporal modeling errors often survive for years. Decide whether the value is:
- a calendar date without a time, such as
birth_date; - a local civil time, such as a recurring store opening time;
- a local date-time tied to a named time zone, such as an appointment;
- a global instant, such as
created_at; - a duration with an explicit unit, such as
latency_ms.
For global events, normalize storage to a consistent instant representation and preserve the originating time-zone context when the business needs it. Never infer a time zone from the server location.
Text length, Unicode, and collation
Choose text limits from domain rules, not from guesses. A display name, legal name, filename, URL, and free-form description have different needs. Ensure the encoding supports the languages and symbols the application accepts.
- A database length constraint is an integrity rule, not a user-interface design.
- Character count and byte count may differ for Unicode text.
- Case-insensitive uniqueness depends on collation or normalized comparison, not only lowercasing in one screen.
- Do not store large documents in dozens of numbered columns; use a coherent text/document model.
Statuses, categories, and flags
A Boolean is correct only when the domain has two stable states. is_active may be appropriate; order_complete may be too weak if orders can be pending, paid, packed, shipped, cancelled, or refunded.
| Domain | Possible representation | Tradeoff |
|---|---|---|
| Stable two-state flag | Boolean or constrained 0/1 | Simple and efficient |
| Small stable status set | Constrained text or native enum | Readable; migrations needed for changes |
| Business-managed categories | Reference table plus foreign key | Supports metadata and controlled evolution |
| Independent properties | Several focused flags | Avoids one overloaded status value |
A repeatable selection checklist
- Write one sentence defining the value.
- List valid, invalid, minimum, maximum, and missing cases.
- State whether exactness is required.
- Specify unit, time zone, currency, encoding, or format where relevant.
- List required operations: arithmetic, ordering, searching, joins, grouping, or date arithmetic.
- Choose the narrowest clear type that safely covers future valid values.
- Add
NOT NULL,CHECK,UNIQUE, or foreign-key constraints. - Test boundary values and invalid inputs in every supported DBMS.
Lab: refactor a weak table
The following design accepts almost any text and therefore preserves little meaning:
CREATE TABLE weak_order ( id TEXT, customer_email TEXT, ordered_at TEXT, total TEXT, status TEXT, gift TEXT);Refactor it into a constrained SQLite design. The checks are intentionally understandable rather than a complete email or timestamp standard.
DROP TABLE IF EXISTS purchase_order;CREATE TABLE purchase_order ( order_id INTEGER PRIMARY KEY, external_reference TEXT UNIQUE, customer_email TEXT NOT NULL CHECK (instr(customer_email, '@') > 1), ordered_at TEXT NOT NULL CHECK (datetime(ordered_at) IS NOT NULL), currency_code TEXT NOT NULL CHECK (length(currency_code) = 3), total_minor INTEGER NOT NULL CHECK (total_minor >= 0), status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled')), is_gift INTEGER NOT NULL DEFAULT 0 CHECK (is_gift IN (0, 1))) STRICT;INSERT INTO purchase_order (external_reference, customer_email, ordered_at, currency_code, total_minor, status, is_gift)VALUES ('WEB-000127', 'learner@example.com', '2026-08-05T11:30:00Z', 'EUR', 2499, 'paid', 1);The improved table distinguishes internal identity from an external reference, preserves leading zeros, validates missingness, documents currency, stores exact minor units, constrains the status set, and enforces the Boolean-style flag.
Boundary tests
- Attempt a negative
total_minor. - Attempt an unknown status such as
shipping. - Attempt a null customer email.
- Insert a second row with the same external reference.
- Insert the largest realistic order amount for your application and justify the selected range.
Common mistakes
Using a smaller integer only to save bytes
A range failure is more expensive than a few bytes. Select a range with realistic growth and vendor behavior in mind.
Encoding multiple facts in one string
Values such as "EUR:12.99:PAID" are difficult to validate, search, join, and evolve. Store independent facts in independent columns.
Using sentinel values instead of NULL
Dates such as 1900-01-01 and numbers such as -1 blur “missing” with legitimate values. Model absence deliberately.
Making every column nullable “for flexibility”
Nullability should reflect the domain and lifecycle. Required facts should be NOT NULL; genuinely optional or not-yet-known facts may be nullable.
Checkpoint and practice
Design exercise
- Choose types and constraints for a product SKU, stock quantity, price, launch date, weight, lifecycle status, and active flag.
- Explain which fields are identifiers and which are quantities.
- Specify units and exactness requirements.
- Identify which decisions are portable and which depend on the target DBMS.
Summary and next lesson
Type selection is domain modeling. Preserve identity, exactness, units, temporal meaning, valid states, and missingness before optimizing storage. The next lesson focuses on missing information itself and the three-valued logic SQL uses when predicates involve NULL.