Skip to content

Expert: design, speed and engines · Schema design and normalisation · topic 45 of 62 · 35 min

Data types, naming and good habits

Explanation

Good schemas pick types that keep data exact: money as DECIMAL or whole paise (never FLOAT), times stored in UTC, and allowed values enforced with CHECK, enums or lookup tables. Consistent names (snake_case, one plural or singular rule), created_at and updated_at columns, soft deletes and a deliberate choice between integer and UUID keys make a schema easy to live with for years.

Money: exact, always

Module 1 showed that 0.1 + 0.2 is 0.30000000000000004 in floating point. Here is why that matters in design. FLOAT, REAL and DOUBLE store numbers in binary, and most decimal fractions have no exact binary form, just as 1/3 has no exact decimal form. Small errors pile up and comparisons fail.

sql
SELECT 0.1 + 0.2 = 0.3;          -- 0   (false!)
SELECT 0.1 + 0.2 + 0.3;          -- 0.6000000000000001
SELECT ROUND(2.675, 2);          -- 2.67, not 2.68: 2.675 is really 2.67499999...
SELECT (10 + 20) = 30;           -- 1   (paise as integers are exact)
Floating point fails equality checks and rounds surprisingly. Integers are exact.
  • PostgreSQL, MySQL, SQL Server: use DECIMAL(12, 2) or NUMERIC(12, 2). The first number is the total digits, the second the digits after the point.
  • SQLite (no true DECIMAL type) and many payment systems: store whole paise as INTEGER. ₹1,234.50 is 123450. Format it as rupees only when you show it.
  • Store the currency too (a currency column holding 'INR') if the app may ever handle more than one.

Dates, times and time zones

A timestamp without a time zone is ambiguous: "2025-03-28 18:45" in Mumbai and in London are five and a half hours apart. The safe habit is to store every moment in UTC (Coordinated Universal Time) and convert to the user's zone only for display. India Standard Time is UTC+5:30, with no daylight saving.

  • PostgreSQL: use TIMESTAMPTZ (timestamp with time zone). It stores the moment in UTC and shows it in the session's time zone.
  • MySQL: TIMESTAMP is converted to UTC for storage; DATETIME is stored as typed, with no zone.
  • SQLite: store UTC text 'YYYY-MM-DD HH:MM:SS'. CURRENT_TIMESTAMP and datetime('now') already return UTC.
  • A pure calendar date with no time (a date of birth, a train's journey date) is a DATE; do not turn it into a timestamp.
sql
SELECT datetime('2025-03-28 18:45:00', '+5 hours', '+30 minutes');
-- 2025-03-29 00:15:00   (the same moment shown in IST: already the next day)
Converting UTC to IST changes the date for evening times, which is why reports must say which zone they use.

Text lengths

In PostgreSQL, TEXT and VARCHAR(n) perform the same; VARCHAR(n) is a rule, not a speed trick. Use a length limit only when the business has one: a 6-digit PIN code, a 10-character PAN, a 2-letter state code. SQLite ignores VARCHAR lengths, so use CHECK (length(pincode) = 6) when you need the rule there. Store codes with leading zeros, like PIN codes and phone numbers, as text, never as integers.

Allowed values: CHECK, enum or lookup table

OptionHowGood for
CHECK constraintstatus TEXT NOT NULL CHECK (status IN ('Placed', 'Shipped', 'Delivered', 'Cancelled'))A short list that rarely changes; works in every engine
Enum typePostgreSQL CREATE TYPE order_status AS ENUM (...); MySQL ENUM('Placed', ...)Fixed lists; adding a value is easy, removing one is hard
Lookup tableorder_statuses(code PK, label, sort_order) plus a foreign keyLists that change, need extra details, or are edited by admins

Naming conventions

  • Use snake_case: order_items, created_at. Unquoted names in PostgreSQL are folded to lower case, so OrderDate silently becomes orderdate.
  • Plural (customers) or singular (customer) table names: both are common. Pick one rule and use it everywhere.
  • Name foreign keys after the table they point to: customer_id points to customers.id.
  • Avoid reserved words (order, user, group) as names; they need quoting forever.
  • Units in names: amount_paise, duration_seconds, weight_grams, so nobody guesses.

Audit columns and soft deletes

Add created_at and updated_at to most tables; they answer "when did this change?" during every support call. created_at can use DEFAULT CURRENT_TIMESTAMP. updated_at must be set on each update: MySQL has ON UPDATE CURRENT_TIMESTAMP, while PostgreSQL and SQLite need the app or a trigger (module 11).

A soft delete marks a row as deleted (deleted_at = the time) instead of removing it, so it can be restored and audited. The cost: every query must add WHERE deleted_at IS NULL, and UNIQUE rules must ignore deleted rows (a partial index, module 10). For personal data, privacy law may require a real delete, so check the rules.

Integer or UUID keys?

Integer ids are small, fast and readable, but they reveal how many rows you have and must come from one database. A UUID is a 128-bit random-looking value like 3f1c9a2e-... that any server can create without asking the database, which helps with offline apps and merging data. It is larger and, if fully random (version 4), scatters index inserts. Time-ordered UUIDs (version 7) reduce that; PostgreSQL has a uuid type and gen_random_uuid(), and PostgreSQL 18 added uuidv7(). Many teams use an integer key inside and a UUID in public URLs.

Common mistakes

  • FLOAT for money. Fix: DECIMAL or integer paise.
  • Storing local times without a zone. Fix: store UTC; convert on display.
  • Phone numbers or PIN codes as INTEGER, losing leading zeros. Fix: TEXT with a CHECK on length.
  • Mixed naming: OrderDate, order_date and orderdate in one schema. Fix: write a one-page naming guide.
  • Soft-delete columns that queries forget to filter. Fix: a view that hides deleted rows (module 10).

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Data types, naming and good habits · 1/7

Types, naming, habits

Small type and naming choices decide whether a schema stays healthy for years. Let us start with money.

Prefer a video by a teacher? Find free videos on “database design best practices data types money decimal utc timestamps naming conventions” on YouTube ↗

In real life: A payments ledger

Payment systems in India move amounts in paise. A typical ledger table stores amount_paise as a BIGINT, the currency code, a UTC created_at, and a status limited by a CHECK or enum. The app shows ₹1,234.50 on screen, but every addition, refund and reconciliation is done on exact integers, so totals always match the bank statement to the paisa.

sql
CREATE TABLE ledger (
  id           INTEGER PRIMARY KEY,
  amount_paise INTEGER NOT NULL CHECK (amount_paise > 0),
  currency     TEXT NOT NULL DEFAULT 'INR' CHECK (length(currency) = 3),
  status       TEXT NOT NULL CHECK (status IN ('Pending', 'Success', 'Failed', 'Refunded')),
  created_at   TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP   -- UTC
);
Exact money, a known currency, a closed list of statuses and a UTC time stamp.

Practice (0/4 done)

Task 1 · Code · SQL

See the float problem for yourself in one SELECT (no table needed). Return three columns: float_sum (0.1 + 0.2), float_equal (whether 0.1 + 0.2 = 0.3) and paise_equal (whether 10 + 20 = 30).

Task 2 · Code · SQL

payments stores money as whole paise in amount_paise. Return id, payer, amount_paise and rupees, where rupees is the amount formatted with exactly two decimals as text, for example '1234.50'. Use printf('%.2f', amount_paise / 100.0).

Task 3 · Code · SQL

1) Create a table shipments with id (INTEGER PRIMARY KEY) and status (TEXT NOT NULL with CHECK (status IN ('Placed', 'Shipped', 'Delivered', 'Cancelled'))). 2) Try inserting (4, 'Pending') and read the error. 3) Insert the valid rows (1, 'Placed'), (2, 'Shipped'), (3, 'Delivered'). 4) Return id and status of every shipment.

Task 4 · Code · SQL

logins stores created_at in UTC. Show each login in India Standard Time (UTC+5:30). Return id, user_name, created_at and created_ist, where created_ist is datetime(created_at, '+5 hours', '+30 minutes').

Quick quiz

Question 1

1.What is the safest way to store ₹1,234.50 in SQLite?

Question 2

2.Why store timestamps in UTC?

Question 3

3.Order statuses change often and admins want to add labels and a sort order. What fits best?

Question 4

4.Why store PIN codes and phone numbers as TEXT?

Question 5

5.What is the main cost of soft deletes?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the safest way to store ₹1,234.50 in SQLite?

Answer: INTEGER 123450 (paise)

Whole paise in an integer are exact; format as rupees only for display.

Why store timestamps in UTC?

Answer: Each moment has one unambiguous value, and display code converts it to any zone

Mixed local times cannot be compared safely. UTC is one shared clock.

Order statuses change often and admins want to add labels and a sort order. What fits best?

Answer: A lookup table with a foreign key

A lookup table can be edited like data and can hold extra details.

Why store PIN codes and phone numbers as TEXT?

Answer: You never do maths on them, and integers drop leading zeros

Codes are labels made of digits, not quantities.

What is the main cost of soft deletes?

Answer: Every query and unique rule must ignore rows where deleted_at is set

Forgotten filters show "deleted" rows. Views and partial indexes help.

A SQLite column is declared VARCHAR(3). What happens when you insert 'ABCDEFG'?Related

Answer: All 7 characters are stored

SQLite gives VARCHAR TEXT affinity and ignores the length. PostgreSQL would reject it.

From Tables, rows, columns and data types

Why do many apps use a soft delete (is_active = 0) for customers?Related

Answer: Old orders, invoices and audits still need the row

The row is hidden from normal use but kept for history.

From UPDATE and DELETE safely

Which column should NOT exist in the users table?Related

Answer: password (the plain password)

Plain passwords must never be stored. Only a hash of the password is kept.

From Designing the data model

What is the UTC offset of IST?Related

Answer: +5:30

India Standard Time is UTC+5:30 all year.

From Dates and times

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Schema design and normalisation” before its exam: Design project: an online store.
  2. Module 9 exam: Schema design and normalisation · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Indexes, query plans and views. 62 topics left in the course before the final exam.

← Previous: Denormalisation, OLTP and OLAP