Tables, Rows, and Schemas
The building blocks
Everything in SQL starts with three concepts:
- Tables: Collections of related data (like a spreadsheet)
- Rows: Individual records in a table (like a row in a spreadsheet)
- Columns: The attributes of each row (like columns in a spreadsheet)
Think of a table as a structured container. Each row represents one entity (a user, a blog post, a transaction), and each column represents a property of that entity.
Tables
A table is a named collection of rows with a fixed structure. The structure is defined by its columns (also called fields or attributes).
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
This creates a table called users with four columns. Every row in this table will have these four columns (though some may be NULL).
Rows
A row (also called a record or tuple) is one complete entry in a table. Each row contains one value for each column.
id | email | name | created_at
---|----------------------|-------|------------
1 | alice@wolfcodes.dev | Alice | 2024-01-15
2 | bob@wolfcodes.dev | Bob | 2024-01-16
Columns and data types
Each column has a data type that defines what kind of data it can store. Here are the most common ones you'll use:
Exact type names vary by database (SQLite uses TEXT for everything, PostgreSQL has SERIAL, MySQL has AUTO_INCREMENT). The concepts are the same.
INTEGER
Whole numbers: 1, 42, -100
age INTEGER
TEXT (or VARCHAR)
Text strings: 'hello', 'user@wolfcodes.dev'
email TEXT -- no limit on length
name VARCHAR(100) -- limits length to 100 characters
BOOLEAN
True or false: TRUE, FALSE
is_active BOOLEAN
DATE / TIMESTAMP
Dates and times: '2024-01-15', '2024-01-15 14:30:00'
DATE stores only a date (year, month, day) without time information. Use it when you only care about the day, not the specific time.
TIMESTAMP stores both date and time (year, month, day, hour, minute, second). Use it when you need to know exactly when something happened, like when a record was created or updated.
created_at TIMESTAMP -- Knows the exact time: 2024-01-15 14:30:00
birth_date DATE -- Only the date matters: 2024-01-15
JSON / JSONB
Structured data (PostgreSQL): '{"key": "value"}'
JSON stores the exact text representation of JSON data. It preserves whitespace and key order, but must be parsed every time you query it.
JSONB (JSON Binary) stores JSON data in a binary format optimized for querying. It's faster to query and supports indexing, but doesn't preserve whitespace or key order.
In most cases, use JSONB because it's faster and more flexible. Use JSON only if you need to preserve the exact formatting or key order of the original JSON.
metadata JSONB -- Preferred: faster queries, supports indexing
config JSON -- Use only if you need exact formatting preserved
NULL vs empty values
NULL is special: it means "no value" or "unknown". It's different from:
- Empty string:
''(a string with zero length) - Zero:
0(a number) - False:
FALSE(a boolean)
-- NULL means "we don't know"
SELECT * FROM users WHERE middle_name IS NULL;
-- Empty string means "we know it's empty"
SELECT * FROM users WHERE middle_name = '';
Important: NULL comparisons use IS NULL or IS NOT NULL, not = NULL:
-- ❌ Wrong
WHERE email = NULL
-- ✅ Correct
WHERE email IS NULL
Primary keys PRIMARY KEY
A primary key uniquely identifies each row in a table. It's like a unique ID that can never be duplicated.
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- This is the primary key
email TEXT NOT NULL UNIQUE
);
Primary keys:
- Must be unique (no two rows can have the same value)
- Cannot be
NULL - Are typically used to reference rows from other tables
Common patterns:
- Auto-incrementing integers:
id SERIAL PRIMARY KEY(PostgreSQL) orid INTEGER PRIMARY KEY AUTOINCREMENT(SQLite) - UUIDs:
id UUID PRIMARY KEY DEFAULT gen_random_uuid() - Natural keys: Using an existing unique field like
email
Simple table creation
Here's a complete example:
CREATE TABLE expenses (
id SERIAL PRIMARY KEY,
amount DECIMAL(10, 2) NOT NULL,
description TEXT,
category TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
This creates an expenses table where:
- Each expense has a unique
id amountis required (NOT NULL) and can have 2 decimal placesdescriptionandcategoryare optional (can be NULL)created_atdefaults to the current time if not specified
Schemas
A schema is a namespace that contains tables, views, and other database objects. In most databases, you have a default schema (often called public in PostgreSQL).
When you create a table, it goes into your current schema:
CREATE TABLE users (...); -- Creates public.users (in PostgreSQL)
Schemas help organize related tables, especially in larger applications.
Common mistake: forgetting null
Remember that columns can be NULL unless you specify NOT NULL. This can cause surprises:
-- This allows NULL emails
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT -- Can be NULL!
);
-- Later, this query might not work as expected
SELECT * FROM users WHERE email = 'test@wolfcodes.dev';
-- Won't match rows where email IS NULL
Always think: "Should this column ever be NULL?" If not, add NOT NULL.