Course: DATA 013
Format: Asynchronous
Semester: Spring 2026
Focus: core SQL concepts with portable examples across multiple database systems.
What is expected before starting this course.
Any modern programming language background is sufficient.
We learn SQL as the language that talks to a database system and, through it, to the data.
SQL stays the same; the DBMS may change.
If SQL works here, it will generally work in:
Reference (informal but practical): SQL Standard Keywords Reference
All official course activity runs through Canvas.
The syllabus is the authoritative course reference.
| Week | Topic | Assignment (Canvas) | Database | Comment |
|---|---|---|---|---|
| 1 (Jan 12, 14, 16) | CREATE, DATABASE, SCHEMA, TABLE | Lab 1, Homework 1 (2w) | SQLite | |
| 2 (Jan 21, 23) | INSERT, VALUES, INTO, UPDATE, SET, DELETE | Holiday on Monday (Jan 19) | ||
| 3 (Jan 26, 28, 30) | SELECT, FROM, WHERE, GROUP, BY, HAVING | Lab 2, Homework 2 (2w) | ||
| 4 (Feb 2, 4, 6) | ORDER, DISTINCT, AS, ALL, LIMIT, OFFSET | PostgreSQL | ||
| 5 (Feb 9, 11, 13) | JOIN, INNER, LEFT, RIGHT, FULL, OUTER | Lab 3, Homework 3 (2w) | ||
| 6 (Feb 18, 20) | EXISTS, IN, ANY, BETWEEN, LIKE, ESCAPE | Holiday on Monday (Feb 16) | ||
| 7 (Feb 23, 25, 27) | CROSS, ON, USING, UNION, INTERSECT, EXCEPT | MariaDB | ||
| 8 (Mar 2, 4, 6) | NULL, IS, NOT, DEFAULT, CASE, WHEN | |||
| 9 (–) | Spring Break | |||
| 10 (Mar 16, 18, 20) | SCHEMA, COLUMN, SEQUENCE, REFERENCES, DATABASE, INDEX | Lab 4, Homework 4 (2w), Project (4w) | Oracle | |
| 11 (Mar 23, 25, 27) | CONSTRAINT, PRIMARY, FOREIGN, KEY, UNIQUE, CHECK | |||
| 12 (Mar 30, Apr 1, 3) | TRANSACTION, BEGIN, COMMIT, ROLLBACK, SAVEPOINT, RELEASE | Lab 5, Homework 5 (2w) | ||
| 13 (Apr 6, 8, 10) | LOCK, GRANT, REVOKE, USER, ROLE | ClickHouse | ||
| 14 (Apr 13, 15, 17) | SQL Databases Review | Lab 6, Homework 6 (2w) | ||
| 15 (Apr 20, 22, 24) | Semester Project Review | |||
| 16 (Apr 27) | Semester SQL Query Review | Classes End (April 28) |
CREATE · SELECT · DROP · ALTER · RENAME · TABLE · COLUMN · TYPES
Structure first: we define containers, then organization, then tables and columns.
Goal: create, inspect, rename, and drop a database safely.
CREATE DATABASE flower_shop;
-- PostgreSQL
\l
-- MySQL and MariaDB
SHOW DATABASES;
-- SQLite
-- database is a file; list attached
PRAGMA database_list;
In many teams, rename is avoided in production.
-- PostgreSQL (typical)
ALTER DATABASE flower_shop RENAME TO flower_store;
-- MySQL and MariaDB
-- often: create new database, migrate data
-- SQLite
-- rename the file (outside SQL)
DROP DATABASE flower_shop;
-- Good
flower_shop
sales_data
inventory
-- Risky
my database
db1
select
FLOWERS_2026
| Error pattern | Cause | Mitigation |
|---|---|---|
| Already exists | Name collision | Inspect first, then choose unique name |
| Permission denied | Missing privileges | Use correct account, request access, document roles |
| Connected to wrong environment | Wrong host or database | Verify connection string and current database before running DDL |
| Rename not supported | Vendor limitation | Choose stable name early, use migrate strategy if needed |
| Operation | SQLite | PostgreSQL | MySQL | MariaDB | Oracle | SQL Server |
|---|---|---|---|---|---|---|
| CREATE DATABASE | Concept via file | Yes | Yes | Yes | Yes | Yes |
| List databases | PRAGMA database_list | \l | SHOW DATABASES | SHOW DATABASES | SELECT from data dictionary | sys.databases |
| Rename database | Rename file | ALTER DATABASE RENAME | Not standard | Not standard | Admin workflow | ALTER DATABASE MODIFY NAME |
| DROP DATABASE | Delete file | Yes | Yes | Yes | Yes | Yes |
Portable habit: plan names early; treat rename as exceptional.
Goal: organize objects using schemas when the system supports them.
CREATE SCHEMA inventory;
CREATE SCHEMA sales;
-- Standard information schema (many systems)
SELECT schema_name
FROM information_schema.schemata
ORDER BY schema_name;
-- PostgreSQL
\dn
-- PostgreSQL
ALTER SCHEMA inventory RENAME TO stock;
-- MySQL and MariaDB
-- schema is the database name in practice
-- SQLite
-- schemas are limited; prefer clear table names
CASCADECASCADE drops dependentsDROP SCHEMA inventory;
-- If objects exist (system dependent)
DROP SCHEMA inventory CASCADE;
-- Good
inventory
sales
customers
-- Risky
SalesSchema
sales-data
my schema
select
| Error pattern | Cause | Mitigation |
|---|---|---|
| Schema not found | Wrong database or search path | Confirm current database; use qualified names |
| Already exists | Duplicate name | Inspect schemas first; follow naming standard |
| Cannot drop, objects exist | Tables and views inside | Drop objects first; use CASCADE only in demos |
| Feature not supported | Vendor differences | Use database namespaces or clear table prefixes |
| Operation | SQLite | PostgreSQL | MySQL | MariaDB | Oracle | SQL Server |
|---|---|---|---|---|---|---|
| CREATE SCHEMA | Limited | Yes | Yes (often equals database) | Yes (often equals database) | Yes (user centric) | Yes |
| List schemas | Limited | \dn or information schema | information schema | information schema | Data dictionary | sys.schemas |
| Rename schema | No | ALTER SCHEMA RENAME | Not standard | Not standard | Admin workflow | ALTER SCHEMA |
| DROP SCHEMA | Limited | Yes | Yes (drop database) | Yes (drop database) | Yes | Yes |
Portable habit: always use qualified names in examples.
Goal: create tables, inspect them, rename them, and define columns with common types.
This is a portable baseline; vendors add extra types.
CREATE TABLE flowers (
flower_id INTEGER,
name TEXT,
color TEXT,
price DECIMAL,
in_stock INTEGER
);
-- SQLite: list tables
SELECT name
FROM sqlite_master
WHERE type='table'
ORDER BY name;
-- SQLite: inspect columns
PRAGMA table_info(flowers);
-- PostgreSQL: describe
-- \d flowers
ALTER TABLE flowers RENAME TO flower;
-- Some systems also support:
-- RENAME TABLE flowers TO flower;
DROP TABLE flowers;
| Meaning | Common type | Examples | Notes |
|---|---|---|---|
| Identifier | INTEGER | flower_id, customer_id | Often paired with primary key later |
| Short text | TEXT or VARCHAR | name, color | VARCHAR is length bounded in many systems |
| Money | INTEGER cents or DECIMAL | price_cents, unit_price | Prefer integer cents to avoid rounding issues |
| Boolean flag | BOOLEAN or INTEGER | in_stock | SQLite commonly uses 0 and 1 |
| Date and time | TEXT or TIMESTAMP | created_at, order_date | SQLite stores as text by convention; others have native types |
-- Good
flowers
order_item
customer
-- Risky
Order
MyTable
flower data
select
| Error pattern | Cause | Mitigation |
|---|---|---|
| Syntax error near keyword | Reserved word used as name | Rename object or quote carefully; prefer safe names |
| No such table | Wrong schema or typo | Use qualified names; inspect tables list |
| Type mismatch or constraint failure | Bad data inserted later | Validate inputs; add constraints gradually |
| Cannot drop due to dependencies | Foreign keys and views reference it | Drop dependents first; avoid CASCADE in real systems |
| Operation | SQLite | PostgreSQL | MySQL | MariaDB | Oracle | SQL Server |
|---|---|---|---|---|---|---|
| CREATE TABLE | Yes | Yes | Yes | Yes | Yes | Yes |
| Inspect columns | PRAGMA table_info | \d or information schema | DESCRIBE or information schema | DESCRIBE or information schema | Data dictionary | sp_help or sys.columns |
| Rename table | ALTER TABLE RENAME | ALTER TABLE RENAME | RENAME TABLE or ALTER TABLE | RENAME TABLE or ALTER TABLE | RENAME (or ALTER TABLE) | sp_rename or ALTER SCHEMA |
| DROP TABLE | Yes | Yes | Yes | Yes | Yes | Yes |
| Common types | INTEGER, TEXT, REAL | INT, TEXT, NUMERIC | INT, VARCHAR, DECIMAL | INT, VARCHAR, DECIMAL | NUMBER, VARCHAR2, DATE | INT, NVARCHAR, DECIMAL |
Portable habit: use simple types first, then specialize per vendor.
-- Drop only if present
DROP TABLE IF EXISTS flowers;
DROP SCHEMA IF EXISTS inventory;
DROP DATABASE IF EXISTS flower_shop;
Not all databases support IF EXISTS in every statement. Check vendor support.
INSERT · INTO · VALUES · SELECT · UPDATE · SET · DELETE · WHERE
Focus: safe row changes on one table at a time. Verification is done with SELECT.
flowers-- Example schema (for reference only)
-- flowers(
-- flower_id INTEGER PRIMARY KEY AUTOINCREMENT,
-- name TEXT NOT NULL,
-- color TEXT NOT NULL,
-- price REAL NOT NULL,
-- stock INTEGER NOT NULL
-- );
Every change is paired with SELECT: preview and confirm.
-- Preview
SELECT flower_id, name, price, stock
FROM flowers
WHERE color = 'Red';
-- Confirm (same query after change)
SELECT flower_id, name, price, stock
FROM flowers
WHERE color = 'Red';
Goal: add new rows using INSERT, INTO, VALUES, and INSERT FROM SELECT.
-- SQLite style:
-- flower_id INTEGER PRIMARY KEY AUTOINCREMENT
-- Insert without flower_id:
INSERT INTO flowers (name, color, price, stock)
VALUES ('Lily', 'White', 4.50, 40);
INSERT INTO flowers (flower_id, name, color, price, stock)
VALUES (102, 'Tulip', 'Yellow', 2.49, 80);
INSERT INTO flowers (name, color, price, stock)
VALUES ('Lily', 'White', 4.50, 40);
INSERT INTO flowers (name, color, price, stock)
VALUES
('Daisy', 'White', 1.50, 60),
('Orchid', 'Purple', 5.99, 20),
('Carnation', 'Pink', 2.25, 55);
SELECT flower_id, name, color, price, stock
FROM flowers
ORDER BY flower_id;
-- Copy all white flowers as "Ivory" variants
INSERT INTO flowers (name, color, price, stock)
SELECT name || ' (Ivory)', 'Ivory', price, stock
FROM flowers
WHERE color = 'White';
flower_id for primary key-- Good
flower_id
created_at
price_cents
-- Risky
id
date
order
| Error pattern | Cause | Mitigation |
|---|---|---|
| Column count mismatch | Values do not align | List columns; match order and count |
| Constraint failure | NOT NULL or UNIQUE | Insert required fields; validate inputs |
| Type mismatch | Wrong value type | Use correct types; cast if needed |
| Primary key conflict | Duplicate id | Use auto increment or verify ids first |
Goal: modify existing rows using UPDATE and SET safely.
-- Preview
SELECT flower_id, name, price
FROM flowers
WHERE flower_id = 101;
-- Update (same filter)
UPDATE flowers
SET price = 4.25
WHERE flower_id = 101;
UPDATE flowers
SET price = 4.25
WHERE flower_id = 101;
UPDATE flowers
SET price = 4.25,
color = 'Deep Red'
WHERE flower_id = 101;
-- Preview
SELECT flower_id, name, price
FROM flowers
WHERE color = 'Red';
-- Change
UPDATE flowers
SET price = price * 1.10
WHERE color = 'Red';
-- Receive new inventory shipment
UPDATE flowers
SET stock = stock + 25
WHERE flower_id = 103;
-- Correct an overcount
UPDATE flowers
SET stock = stock - 5
WHERE flower_id = 103;
SELECT flower_id, name, color, price, stock
FROM flowers
WHERE flower_id = 103;
_id for keys-- Good
flower_id
price
stock
-- Risky
val
num
data
| Error pattern | Cause | Mitigation |
|---|---|---|
| Updated too many rows | Missing WHERE | SELECT first; use WHERE always |
| No rows updated | Wrong filter | Confirm key values before updating |
| Unexpected price math | Wrong multiplier | Preview with SELECT; test on one row first |
| Invalid value | Type or constraint issue | Validate inputs; keep domain rules simple |
Goal: remove rows safely using DELETE and WHERE.
-- Preview
SELECT *
FROM flowers
WHERE flower_id = 102;
-- Delete (same filter)
DELETE FROM flowers
WHERE flower_id = 102;
SELECT flower_id, name, color, price
FROM flowers
WHERE flower_id = 102;
DELETE FROM flowers
WHERE flower_id = 102;
-- Preview candidates
SELECT flower_id, name, stock
FROM flowers
WHERE stock = 0;
-- Delete after review
DELETE FROM flowers
WHERE stock = 0;
SELECT flower_id, name, color, price, stock
FROM flowers
ORDER BY flower_id;
is_active or deleted_atThis is a common real world compromise between safety and simplicity.
-- Concept if column exists:
-- UPDATE flowers
-- SET is_active = 0
-- WHERE flower_id = 102;
-- Query habit:
-- WHERE is_active = 1
-- Good filters
WHERE flower_id = 102
WHERE stock = 0
-- Risky filters
WHERE name = 'Rose'
| Error pattern | Cause | Mitigation |
|---|---|---|
| Deleted too many rows | Missing WHERE | SELECT first; use WHERE always |
| Deleted the wrong rows | Wrong filter | Preview with SELECT and confirm keys |
| Cannot recover | No backups / no soft delete | Use backups; consider soft delete |
| Deleted nothing | Filter did not match | Verify row exists before deleting |
-- Safe habit (repeat it)
SELECT ... WHERE ...; -- preview
INSERT / UPDATE / DELETE ...;
SELECT ... WHERE ...; -- confirm
SELECT · FROM · WHERE · GROUP · BY · HAVING
bicycle_orders(
order_id INTEGER PRIMARY KEY,
bike_type TEXT,
quantity INTEGER
);
| order_id | bike_type | quantity |
|---|---|---|
| 1 | Road | 2 |
| 2 | Road | 1 |
| 3 | Road | 3 |
| 4 | Mountain | 4 |
| 5 | Mountain | 2 |
| 6 | Hybrid | 6 |
| 7 | Hybrid | 1 |
| 8 | BMX | 5 |
| 9 | BMX | 2 |
| Clause | Role |
|---|---|
| FROM | Select table |
| WHERE | Filter rows |
| GROUP BY | Create groups |
| HAVING | Filter groups |
| SELECT | Display result |
SELECT order_id, bike_type, quantity
FROM bicycle_orders;
SELECT *
FROM bicycle_orders;
SELECT *
FROM bicycle_orders
WHERE quantity > 3;
SELECT *
FROM bicycle_orders
WHERE bike_type = 'Road';
SELECT *
FROM bicycle_orders
WHERE bike_type = 'BMX'
AND quantity >= 2;
| bike_type | quantity |
|---|---|
| Road | 2 |
| Road | 1 |
| Road | 3 |
| bike_type | values |
|---|---|
| Road | 2, 1, 3 |
| Mountain | 4, 2 |
| Hybrid | 6, 1 |
| BMX | 5, 2 |
SELECT bike_type, COUNT(*) AS total_orders
FROM bicycle_orders
GROUP BY bike_type;
SELECT bike_type, SUM(quantity) AS total_quantity
FROM bicycle_orders
GROUP BY bike_type;
| bike_type | total_quantity |
|---|---|
| Road | 6 |
| Mountain | 6 |
| Hybrid | 7 |
| BMX | 7 |
SELECT bike_type, SUM(quantity) AS total_quantity
FROM bicycle_orders
GROUP BY bike_type
HAVING SUM(quantity) > 6;
| bike_type | total_quantity |
|---|---|
| Hybrid | 7 |
| BMX | 7 |
| WHERE | HAVING |
|---|---|
| Row filter | Group filter |
| No aggregates | Uses aggregates |
| Before grouping | After grouping |
SELECT bike_type, SUM(quantity) AS total_quantity
FROM bicycle_orders
WHERE quantity >= 2
GROUP BY bike_type
HAVING SUM(quantity) > 5;
-- Incorrect
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
WHERE SUM(quantity) > 5
GROUP BY bike_type;
-- Correct
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
GROUP BY bike_type
HAVING SUM(quantity) > 5;
-- Incorrect
SELECT bike_type, SUM(quantity)
FROM bicycle_orders;
-- Correct
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
GROUP BY bike_type;
SELECT bike_type, COUNT(*)
FROM bicycle_orders
GROUP BY bike_type;
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
GROUP BY bike_type;
Road → 2 + 1 + 3 = 6
Mountain → 4 + 2 = 6
Hybrid → 6 + 1 = 7
BMX → 5 + 2 = 7
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
WHERE quantity >= 2
GROUP BY bike_type
HAVING SUM(quantity) > 5;
-- You describe the result,
-- not the algorithm.
SELECT bike_type, COUNT(*)
FROM bicycle_orders
GROUP BY bike_type;
-- Prefer this:
SELECT bike_type, SUM(quantity)
FROM bicycle_orders
GROUP BY bike_type;
-- Clear structure
-- Predictable formatting
-- Minimal surprises
SELECT → WHERE → GROUP BY → HAVING
A complete mental model
ORDER · DISTINCT · AS · ALL · LIMIT · OFFSET
-- This module adds "shape controls" to SELECT:
-- DISTINCT, AS, ORDER BY, LIMIT, OFFSET, ALL
-- Recommended order:
-- 1) customers
-- 2) products
-- 3) orders (FK -> customers)
-- 4) order_items (FK -> orders/products)
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL
);
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
order_date DATE NOT NULL,
total_amount NUMERIC(10,2) NOT NULL
);
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(order_id),
product_id INTEGER NOT NULL REFERENCES products(product_id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, product_id)
);
INSERT INTO customers(name, city) VALUES
('Ava', 'Stockton'),
('Tom','Sacramento'),
('Mia', 'Stockton');
INSERT INTO products(name, category, price) VALUES
('USB-C Cable', 'Accessories', 9.99),
('Laptop Stand','Accessories', 29.99),
('Notebook', 'Stationery', 3.49);
INSERT INTO orders(customer_id, order_date, total_amount) VALUES
(1, DATE '2026-02-01', 39.98),
(2, DATE '2026-02-02', 3.49),
(1, DATE '2026-02-03', 9.99);
| Topic | SQLite | PostgreSQL |
|---|---|---|
| Auto increment id | INTEGER PRIMARY KEY AUTOINCREMENT |
SERIAL (demo) or GENERATED AS IDENTITY |
| Money type | Often stored as REAL or cents as INTEGER |
NUMERIC(10,2) common |
| Date type | Stored as TEXT/INTEGER/REAL by convention | DATE is native |
SELECT DISTINCT city
FROM customers
ORDER BY city;
SELECT DISTINCT city, name
FROM customers
ORDER BY city, name;
SELECT DISTINCT category
FROM products
ORDER BY category;
| Item | SQLite | PostgreSQL |
|---|---|---|
| DISTINCT meaning | Removes duplicate result rows | Removes duplicate result rows |
| Interaction with ORDER BY | Works; keep ORDER BY explicit | Works; keep ORDER BY explicit |
| Good habit | Use for unique labels, not as a “fix” | Use for unique labels, not as a “fix” |
SELECT
order_id,
total_amount AS order_total
FROM orders;
SELECT
order_id,
total_amount,
(total_amount * 0.08) AS est_tax,
(total_amount * 1.08) AS total_with_tax
FROM orders
ORDER BY order_id;
SELECT
order_id,
total_amount AS total
FROM orders
ORDER BY total DESC;
| Topic | SQLite | PostgreSQL |
|---|---|---|
| Basic alias | expr AS alias |
expr AS alias |
| Quoting alias with spaces | AS "Order Total" |
AS "Order Total" |
| Best practice | snake_case aliases | snake_case aliases |
SELECT name, price
FROM products
ORDER BY price;
SELECT order_id, total_amount
FROM orders
ORDER BY total_amount DESC;
SELECT city, name
FROM customers
ORDER BY city ASC, name ASC;
-- Top 3 most expensive products
SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 3;
| Goal | SQLite | PostgreSQL |
|---|---|---|
| Put NULLs last | Workaround: ORDER BY (col IS NULL), col |
Supports NULLS LAST / NULLS FIRST |
| Put NULLs first | Workaround: ORDER BY (col IS NOT NULL), col |
ORDER BY col NULLS FIRST |
SELECT *
FROM customers
LIMIT 2;
SELECT order_id, order_date, total_amount
FROM orders
ORDER BY order_date DESC
LIMIT 10;
LIMIT ALL means “no limit”-- PostgreSQL
SELECT *
FROM products
LIMIT ALL; -- same as no LIMIT
| Pattern | SQLite | PostgreSQL |
|---|---|---|
| Return first N rows | LIMIT 10 |
LIMIT 10 |
| No limit keyword | Omit LIMIT | LIMIT ALL or omit LIMIT |
SELECT order_id, order_date
FROM orders
ORDER BY order_date DESC
OFFSET 10;
SELECT order_id, order_date, total_amount
FROM orders
ORDER BY order_date DESC
LIMIT 10 OFFSET 10;
-- Suppose last page ended at order_date = '2026-02-01'
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_date < DATE '2026-02-01'
ORDER BY order_date DESC
LIMIT 10;
| Topic | SQLite | PostgreSQL |
|---|---|---|
| Syntax | LIMIT n OFFSET m |
LIMIT n OFFSET m |
| Performance note | Large OFFSET may still scan many rows | Large OFFSET may still scan many rows |
SELECT name, price
FROM products
WHERE price > ALL (
SELECT price
FROM products
WHERE category = 'Accessories'
)
ORDER BY price;
SELECT order_id, total_amount
FROM orders
WHERE total_amount > ALL (
SELECT total_amount
FROM orders
WHERE customer_id = 1
)
ORDER BY total_amount DESC;
SELECT name, price
FROM products
WHERE price > (
SELECT MAX(price)
FROM products
WHERE category = 'Accessories'
)
ORDER BY price;
| Feature | SQLite | PostgreSQL |
|---|---|---|
ALL with subqueries |
Not commonly supported | Supported |
| Portable alternative | MAX(), MIN(), or a subquery with aggregates |
ALL or MAX()/MIN() |
SELECT DISTINCT category
FROM products
ORDER BY category;
SELECT
order_id,
total_amount,
(total_amount * 1.08) AS total_with_tax
FROM orders
ORDER BY total_with_tax DESC
LIMIT 10;
SELECT DISTINCT city
FROM customers
ORDER BY city;
SELECT order_id, total_amount AS total
FROM orders
ORDER BY total DESC
LIMIT 5;
| Clause | Purpose | Retail example |
|---|---|---|
DISTINCT |
Remove duplicates | Unique cities / categories |
AS |
Rename for clarity | total_amount AS total |
ORDER BY |
Sort results | Most recent orders first |
LIMIT |
Return first N rows | Top 10 by value |
OFFSET |
Skip rows | Pagination |
ALL |
Compare to every value in a set | Price above all in a category |
SELECT DISTINCT category AS cat
FROM products
ORDER BY cat
LIMIT 10 OFFSET 0;
JOIN · INNER · LEFT · RIGHT · FULL · OUTER
-- The habit:
-- FROM + JOIN ... ON ...
-- Choose join type based on "who must appear"
-- Always verify row counts
-- Recommended order:
-- 1) riders
-- 2) bikes
-- 3) stations
-- 4) rentals (FK -> riders/bikes/stations)
-- 5) returns (optional FK -> rentals)
CREATE TABLE riders (
rider_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL,
email TEXT UNIQUE
);
CREATE TABLE bikes (
bike_id SERIAL PRIMARY KEY,
bike_type TEXT NOT NULL, -- road, city, e-bike
status TEXT NOT NULL -- available, maintenance
);
CREATE TABLE stations (
station_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
neighborhood TEXT NOT NULL
);
CREATE TABLE rentals (
rental_id SERIAL PRIMARY KEY,
rider_id INTEGER NOT NULL REFERENCES riders(rider_id),
bike_id INTEGER NOT NULL REFERENCES bikes(bike_id),
pickup_station_id INTEGER NOT NULL REFERENCES stations(station_id),
pickup_ts TIMESTAMP NOT NULL,
dropoff_station_id INTEGER REFERENCES stations(station_id),
dropoff_ts TIMESTAMP
);
INSERT INTO riders(name, city, email) VALUES
('Ava', 'Stockton', 'ava@example.com'),
('Tom', 'Sacramento', 'tom@example.com'),
('Mia', 'Stockton', 'mia@example.com'),
('Liam', 'Lodi', 'liam@example.com'); -- no rentals
INSERT INTO bikes(bike_type, status) VALUES
('city', 'available'),
('road', 'available'),
('e-bike','maintenance'),
('city', 'available'); -- never rented
INSERT INTO stations(name, neighborhood) VALUES
('Downtown Dock', 'Downtown'),
('Campus Rack', 'University'),
('River Trail', 'Waterfront');
INSERT INTO rentals(rider_id, bike_id, pickup_station_id, pickup_ts, dropoff_station_id, dropoff_ts) VALUES
(1, 1, 1, TIMESTAMP '2026-02-10 09:00', 2, TIMESTAMP '2026-02-10 09:25'),
(2, 2, 2, TIMESTAMP '2026-02-10 10:00', 1, TIMESTAMP '2026-02-10 10:18'),
(1, 2, 1, TIMESTAMP '2026-02-11 08:10', NULL, NULL); -- active ride
JOIN without a keyword means INNER JOINON with the key matchSELECT
r.rental_id,
rd.name AS rider_name,
r.pickup_ts
FROM rentals r
JOIN riders rd
ON r.rider_id = rd.rider_id
ORDER BY r.rental_id;
| Join type | Keep rows from left table? | Keep rows from right table? | Typical question |
|---|---|---|---|
INNER JOIN |
Only matched | Only matched | “Show rentals with rider info” |
LEFT JOIN |
All left rows | Matched when possible | “Show all riders, even with no rentals” |
RIGHT JOIN |
Matched when possible | All right rows | “Show all bikes, even never rented” (if bikes is right) |
FULL OUTER JOIN |
All left rows | All right rows | “Show everything; highlight gaps” |
SELECT
r.rental_id,
rd.name AS rider,
rd.city,
r.pickup_ts,
r.dropoff_ts
FROM rentals r
INNER JOIN riders rd
ON r.rider_id = rd.rider_id
ORDER BY r.pickup_ts;
SELECT
r.rental_id,
rd.name AS rider,
b.bike_type AS bike,
s.name AS pickup_station,
r.pickup_ts
FROM rentals r
JOIN riders rd ON r.rider_id = rd.rider_id
JOIN bikes b ON r.bike_id = b.bike_id
JOIN stations s ON r.pickup_station_id = s.station_id
ORDER BY r.rental_id;
IS NULLSELECT
r.rental_id,
rd.name AS rider,
b.bike_type,
r.pickup_ts
FROM rentals r
JOIN riders rd ON r.rider_id = rd.rider_id
JOIN bikes b ON r.bike_id = b.bike_id
WHERE r.dropoff_ts IS NULL
ORDER BY r.pickup_ts;
SELECT
rd.rider_id,
rd.name,
r.rental_id,
r.pickup_ts
FROM riders rd
LEFT JOIN rentals r
ON r.rider_id = rd.rider_id
ORDER BY rd.rider_id, r.pickup_ts;
SELECT
rd.rider_id,
rd.name,
rd.city
FROM riders rd
LEFT JOIN rentals r
ON r.rider_id = rd.rider_id
WHERE r.rental_id IS NULL
ORDER BY rd.rider_id;
SELECT
b.bike_id,
b.bike_type,
r.rental_id,
r.pickup_ts
FROM rentals r
RIGHT JOIN bikes b
ON r.bike_id = b.bike_id
ORDER BY b.bike_id, r.pickup_ts;
SELECT
b.bike_id,
b.bike_type,
r.rental_id,
r.pickup_ts
FROM bikes b
LEFT JOIN rentals r
ON r.bike_id = b.bike_id
ORDER BY b.bike_id, r.pickup_ts;
SELECT
rd.rider_id,
rd.name,
r.rental_id,
r.pickup_ts
FROM riders rd
FULL OUTER JOIN rentals r
ON r.rider_id = rd.rider_id
ORDER BY rd.rider_id NULLS LAST, r.rental_id NULLS LAST;
OUTER is optional syntaxLEFT OUTER JOIN = LEFT JOINFULL OUTER JOIN is common for clarity-- Equivalent pairs:
-- LEFT JOIN == LEFT OUTER JOIN
-- RIGHT JOIN == RIGHT OUTER JOIN
-- FULL OUTER JOIN == FULL JOIN (commonly written with OUTER)
| Feature | SQLite | PostgreSQL |
|---|---|---|
| INNER JOIN | Supported | Supported |
| LEFT JOIN | Supported | Supported |
| RIGHT JOIN | Not supported in many SQLite versions | Supported |
| FULL OUTER JOIN | Not supported directly | Supported |
| Portable workaround | Use LEFT JOIN + UNION (carefully) | Use RIGHT/FULL directly |
UNION-- Conceptual pattern (portable-ish):
-- 1) left side keeps all riders
-- 2) second query keeps rentals with no rider match
SELECT rd.rider_id, rd.name, r.rental_id
FROM riders rd
LEFT JOIN rentals r ON r.rider_id = rd.rider_id
UNION
SELECT rd.rider_id, rd.name, r.rental_id
FROM rentals r
LEFT JOIN riders rd ON r.rider_id = rd.rider_id
WHERE rd.rider_id IS NULL;
-- Bad idea (cross join effect):
-- SELECT ... FROM rentals r JOIN riders rd;
-- Good:
SELECT COUNT(*) FROM rentals;
SELECT COUNT(*) FROM rentals r JOIN riders rd
ON r.rider_id = rd.rider_id;
-- Fact table baseline:
SELECT COUNT(*) AS rental_rows FROM rentals;
-- Expected: same as rentals (many-to-one)
SELECT COUNT(*) AS joined_rows
FROM rentals r
JOIN riders rd ON r.rider_id = rd.rider_id;
SELECT
r.rental_id,
rd.name AS rider,
b.bike_type,
r.pickup_ts
FROM rentals r
JOIN riders rd ON r.rider_id = rd.rider_id
JOIN bikes b ON r.bike_id = b.bike_id
ORDER BY r.pickup_ts;
SELECT rd.rider_id, rd.name
FROM riders rd
LEFT JOIN rentals r ON r.rider_id = rd.rider_id
WHERE r.rental_id IS NULL
ORDER BY rd.rider_id;
SELECT b.bike_id, b.bike_type, b.status
FROM bikes b
LEFT JOIN rentals r ON r.bike_id = b.bike_id
WHERE r.rental_id IS NULL
ORDER BY b.bike_id;
SELECT
r.rental_id,
sp.name AS pickup_station,
sd.name AS dropoff_station,
r.pickup_ts,
r.dropoff_ts
FROM rentals r
JOIN stations sp ON r.pickup_station_id = sp.station_id
LEFT JOIN stations sd ON r.dropoff_station_id = sd.station_id
ORDER BY r.rental_id;
| Need | Join to use | Bicycle example |
|---|---|---|
| Only matched rows | INNER JOIN |
Rentals + riders |
| All riders, rentals if any | LEFT JOIN |
Riders with optional rentals |
| All bikes, rentals if any | RIGHT JOIN (or swap + LEFT) |
Bikes with optional rentals |
| Everything from both sides | FULL OUTER JOIN |
Audit missing links |
ON can multiply rows-- A clean, common pattern:
SELECT rd.name, b.bike_type, r.pickup_ts
FROM rentals r
JOIN riders rd ON r.rider_id = rd.rider_id
JOIN bikes b ON r.bike_id = b.bike_id
ORDER BY r.pickup_ts;
EXISTS · IN · ANY · BETWEEN · LIKE · ESCAPE
IN, EXISTS, ANYBETWEENLIKE and ESCAPE-- Habit:
-- 1) Start with a base SELECT
-- 2) Add one WHERE filter
-- 3) Validate row counts (COUNT(*))
Run these once so every example works.
CREATE TABLE riders (
rider_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL,
email TEXT
);
CREATE TABLE bikes (
bike_id SERIAL PRIMARY KEY,
bike_type TEXT NOT NULL,
status TEXT NOT NULL
);
CREATE TABLE rentals (
rental_id SERIAL PRIMARY KEY,
rider_id INTEGER REFERENCES riders(rider_id),
bike_id INTEGER REFERENCES bikes(bike_id),
pickup_ts TIMESTAMP NOT NULL,
dropoff_ts TIMESTAMP
);
INSERT INTO riders(name, city, email) VALUES
('Ava', 'Stockton', 'ava@example.com'),
('Tom', 'Sacramento', 'tom@example.com'),
('Mia', 'Stockton', 'mia@test.com'),
('Liam', 'Lodi', 'liam@sample.org'); -- intentionally different domain
INSERT INTO bikes(bike_type, status) VALUES
('city', 'available'),
('road', 'available'),
('e-bike','maintenance'),
('city', 'available'); -- will show “never rented” later
INSERT INTO rentals(rider_id, bike_id, pickup_ts, dropoff_ts) VALUES
(1, 1, TIMESTAMP '2026-02-10 09:00', TIMESTAMP '2026-02-10 09:25'),
(2, 2, TIMESTAMP '2026-02-11 10:00', TIMESTAMP '2026-02-11 10:18'),
(1, 2, TIMESTAMP '2026-02-12 08:10', NULL); -- active ride
SELECT rider_id, name, city, email
FROM riders
ORDER BY rider_id;
IN checks membership in a list (or subquery result)OR chains-- Pattern:
WHERE value IN (v1, v2, v3)
SELECT name, city
FROM riders
WHERE city IN ('Stockton', 'Lodi')
ORDER BY name;
-- Before:
SELECT name, city
FROM riders
WHERE city = 'Stockton'
OR city = 'Lodi'
OR city = 'Sacramento';
-- After:
SELECT name, city
FROM riders
WHERE city IN ('Stockton','Lodi','Sacramento');
SELECT name
FROM riders
WHERE rider_id IN (
SELECT rider_id
FROM rentals
)
ORDER BY name;
| Situation | Use | Why |
|---|---|---|
| Short fixed list | IN |
Clean and readable |
| Existence question | EXISTS |
More explicit meaning |
| NULL involved | Be careful | Three-valued logic |
SELECT r.name
FROM riders r
WHERE EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
)
ORDER BY r.name;
-- Before:
SELECT name
FROM riders
WHERE rider_id IN (SELECT rider_id FROM rentals);
-- After:
SELECT r.name
FROM riders r
WHERE EXISTS (
SELECT 1 FROM rentals rt
WHERE rt.rider_id = r.rider_id
);
SELECT r.rider_id, r.name
FROM riders r
WHERE NOT EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
)
ORDER BY r.rider_id;
| Tip | Why |
|---|---|
Use SELECT 1 |
You only need existence |
| Keep correlation in WHERE | Readable logic |
| Pair with NOT EXISTS | Find missing links |
-- Pattern:
value OP ANY (subquery)
SELECT r.name
FROM riders r
WHERE r.rider_id = ANY (
SELECT rider_id
FROM rentals
);
SELECT r.rider_id, r.name
FROM riders r
WHERE r.rider_id > ANY (
SELECT rt.rider_id
FROM rentals rt
)
ORDER BY r.rider_id;
| Need | Use | Reason |
|---|---|---|
| Equality membership | IN |
Most readable |
| Flexible comparisons | ANY |
Supports <, >, = |
-- Pattern:
WHERE value BETWEEN low AND high
SELECT rental_id, pickup_ts
FROM rentals
WHERE pickup_ts BETWEEN
TIMESTAMP '2026-02-10 00:00'
AND
TIMESTAMP '2026-02-11 23:59'
ORDER BY pickup_ts;
-- Before:
SELECT rental_id
FROM rentals
WHERE pickup_ts >= TIMESTAMP '2026-02-10 00:00'
AND pickup_ts <= TIMESTAMP '2026-02-11 23:59';
-- After:
SELECT rental_id
FROM rentals
WHERE pickup_ts BETWEEN
TIMESTAMP '2026-02-10 00:00'
AND
TIMESTAMP '2026-02-11 23:59';
SELECT bike_id, bike_type, status
FROM bikes
WHERE bike_id BETWEEN 1 AND 3
ORDER BY bike_id;
| Symbol | Meaning | Example |
|---|---|---|
% |
0 or more characters | 'S%' |
_ |
Exactly one character | '____' |
-- Common patterns:
'prefix%'
'%contains%'
'%suffix'
'____' -- exact length
'S%'SELECT name, city
FROM riders
WHERE city LIKE 'S%'
ORDER BY city, name;
'%example%'SELECT rider_id, name, email
FROM riders
WHERE email LIKE '%example%'
ORDER BY rider_id;
ILIKE is PostgreSQL-onlySELECT name, city
FROM riders
WHERE city ILIKE 's%';
% and _ are wildcards in LIKE\% means literal percent sign-- Pattern:
LIKE '%\%%' ESCAPE '\'
SELECT *
FROM riders
WHERE email LIKE '%\%%' ESCAPE '\';
-- Before (not literal):
SELECT * FROM riders
WHERE email LIKE '%%%'; -- just means "anything"
-- After (literal % somewhere):
SELECT * FROM riders
WHERE email LIKE '%\%%' ESCAPE '\';
SELECT name, city, email
FROM riders
WHERE city IN ('Stockton', 'Sacramento')
AND email LIKE '%@example.com'
ORDER BY name;
SELECT r.name
FROM riders r
WHERE EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
AND rt.pickup_ts BETWEEN
TIMESTAMP '2026-02-10 00:00'
AND
TIMESTAMP '2026-02-12 23:59'
)
ORDER BY r.name;
= NULL is not correctIS NULL / IS NOT NULL-- Active rentals (no dropoff yet):
SELECT rental_id, rider_id, pickup_ts
FROM rentals
WHERE dropoff_ts IS NULL;
SELECT COUNT(*) AS rider_rows
FROM riders;
SELECT COUNT(*) AS filtered_rows
FROM riders
WHERE city IN ('Stockton','Lodi');
| Feature | SQLite | PostgreSQL |
|---|---|---|
| IN | Supported | Supported |
| EXISTS | Supported | Supported |
| ANY | Limited / uncommon | Supported |
| BETWEEN | Supported | Supported |
| LIKE | Supported | Supported |
| ESCAPE | Varies by build / usage | Supported |
| ILIKE | No | Supported |
-- Portable core:
-- IN / EXISTS / BETWEEN / LIKE
SELECT r.rider_id, r.name
FROM riders r
WHERE NOT EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
)
ORDER BY r.rider_id;
-- Simple approach (works here because rentals.bike_id is not NULL in our inserts):
SELECT b.bike_id, b.bike_type, b.status
FROM bikes b
WHERE b.bike_id NOT IN (
SELECT rt.bike_id
FROM rentals rt
)
ORDER BY b.bike_id;
SELECT r.name
FROM riders r
WHERE EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
AND rt.dropoff_ts IS NULL
)
ORDER BY r.name;
| Need | Use | Bicycle example |
|---|---|---|
| Membership in a set | IN |
Riders in certain cities |
| Does a related row exist? | EXISTS |
Riders with rentals |
| Flexible set comparison | ANY |
Threshold comparisons |
| Range filter | BETWEEN |
Rentals in a date window |
| Pattern match | LIKE |
Emails containing “example” |
| Literal wildcard | ESCAPE |
Find literal % in text |
-- Clean, common pattern:
SELECT r.name
FROM riders r
WHERE EXISTS (
SELECT 1
FROM rentals rt
WHERE rt.rider_id = r.rider_id
)
ORDER BY r.name;
CROSS · ON · USING · UNION · INTERSECT · EXCEPT
ON and USINGCROSS JOIN (Cartesian product)UNION, INTERSECT, EXCEPT-- Habit:
-- 1) Start with a small SELECT
-- 2) Add one JOIN condition
-- 3) Validate row counts (COUNT(*))
-- 4) Only then add SET operators
Run these once so every example works.
CREATE TABLE riders (
rider_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
email VARCHAR(255)
);
CREATE TABLE bikes (
bike_id INT AUTO_INCREMENT PRIMARY KEY,
bike_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL
);
CREATE TABLE rentals (
rental_id INT AUTO_INCREMENT PRIMARY KEY,
rider_id INT,
bike_id INT,
pickup_ts DATETIME NOT NULL,
dropoff_ts DATETIME,
FOREIGN KEY (rider_id) REFERENCES riders(rider_id),
FOREIGN KEY (bike_id) REFERENCES bikes(bike_id)
);
INSERT INTO riders(name, city, email) VALUES
('Ava', 'Stockton', 'ava@example.com'),
('Tom', 'Sacramento', 'tom@example.com'),
('Mia', 'Stockton', 'mia@test.com'),
('Liam', 'Lodi', 'liam@sample.org');
INSERT INTO bikes(bike_type, status) VALUES
('city', 'available'),
('road', 'available'),
('e-bike', 'maintenance'),
('city', 'available');
INSERT INTO rentals(rider_id, bike_id, pickup_ts, dropoff_ts) VALUES
(1, 1, '2026-02-10 09:00:00', '2026-02-10 09:25:00'),
(2, 2, '2026-02-11 10:00:00', '2026-02-11 10:18:00'),
(1, 2, '2026-02-12 08:10:00', NULL);
SELECT rider_id, name, city, email
FROM riders
ORDER BY rider_id;
CROSS JOIN = every row with every row (Cartesian product)-- Pattern:
FROM A
CROSS JOIN B
SELECT r.name, b.bike_type
FROM riders r
CROSS JOIN bikes b
ORDER BY r.name, b.bike_type;
SELECT
(SELECT COUNT(*) FROM riders) AS rider_rows,
(SELECT COUNT(*) FROM bikes) AS bike_rows,
(SELECT COUNT(*) FROM riders) * (SELECT COUNT(*) FROM bikes) AS expected_pairs;
ON defines the matching rule between two tables-- Pattern:
FROM A
JOIN B
ON A.key = B.key
SELECT rt.rental_id, r.name, rt.pickup_ts, rt.dropoff_ts
FROM rentals rt
JOIN riders r
ON rt.rider_id = r.rider_id
ORDER BY rt.rental_id;
SELECT rt.rental_id, b.bike_type, b.status, rt.pickup_ts
FROM rentals rt
JOIN bikes b
ON rt.bike_id = b.bike_id
ORDER BY rt.rental_id;
SELECT rt.rental_id, r.name, b.bike_type, rt.pickup_ts, rt.dropoff_ts
FROM rentals rt
JOIN riders r
ON rt.rider_id = r.rider_id
JOIN bikes b
ON rt.bike_id = b.bike_id
ORDER BY rt.rental_id;
JOIN ... ON for clarity-- Before:
SELECT rt.rental_id, r.name
FROM rentals rt
CROSS JOIN riders r
WHERE rt.rider_id = r.rider_id;
-- After:
SELECT rt.rental_id, r.name
FROM rentals rt
JOIN riders r
ON rt.rider_id = r.rider_id;
USING (col) is shorthand when both tables share the same column name-- Pattern:
FROM A
JOIN B USING (shared_key)
rider_idUSING naturalCREATE TABLE rider_prefs (
rider_id INT PRIMARY KEY,
prefers VARCHAR(50) NOT NULL,
FOREIGN KEY (rider_id) REFERENCES riders(rider_id)
);
INSERT INTO rider_prefs(rider_id, prefers) VALUES
(1, 'e-bike'),
(2, 'road');
USING (rider_id) avoids repeating r.rider_id = p.rider_idrider_idSELECT rider_id, name, prefers
FROM riders
JOIN rider_prefs USING (rider_id)
ORDER BY rider_id;
-- Before:
SELECT r.rider_id, r.name, p.prefers
FROM riders r
JOIN rider_prefs p
ON r.rider_id = p.rider_id;
-- After:
SELECT rider_id, name, prefers
FROM riders
JOIN rider_prefs USING (rider_id);
| Need | Use | Reason |
|---|---|---|
| Different column names | ON |
Most flexible |
| Same column name(s) | USING |
Shorter + cleaner output |
| All combinations | CROSS JOIN |
Intentional Cartesian product |
-- Shape rule:
SELECT col1, col2
...
UNION / INTERSECT / EXCEPT
SELECT col1, col2
...
UNION combines and removes duplicatesUNION ALL keeps duplicates-- Pattern:
queryA
UNION
queryB
SELECT city AS label
FROM riders
UNION
SELECT status AS label
FROM bikes
ORDER BY label;
SELECT label, COUNT(*) AS occurrences
FROM (
SELECT city AS label FROM riders
UNION ALL
SELECT status AS label FROM bikes
) x
GROUP BY label
ORDER BY occurrences DESC, label;
INTERSECT returns only rows present in both results-- Pattern:
queryA
INTERSECT
queryB
SELECT rider_id
FROM riders
INTERSECT
SELECT rider_id
FROM rentals
ORDER BY rider_id;
SELECT r.rider_id, r.name
FROM riders r
JOIN (
SELECT rider_id FROM riders
INTERSECT
SELECT rider_id FROM rentals
) x USING (rider_id)
ORDER BY r.rider_id;
EXCEPT returns rows in the first result not in the second-- Pattern:
queryA
EXCEPT
queryB
SELECT rider_id
FROM riders
EXCEPT
SELECT rider_id
FROM rentals
ORDER BY rider_id;
SELECT r.rider_id, r.name
FROM riders r
JOIN (
SELECT rider_id FROM riders
EXCEPT
SELECT rider_id FROM rentals
) x USING (rider_id)
ORDER BY r.rider_id;
| Operator | Keeps | Typical question |
|---|---|---|
UNION |
All distinct rows from both | “Show everything from A or B” |
INTERSECT |
Only rows in both | “What’s common to A and B?” |
EXCEPT |
Rows in A but not B | “What’s in A missing from B?” |
ORDER BY after the final query-- Correct:
SELECT city AS label FROM riders
UNION
SELECT status AS label FROM bikes
ORDER BY label;
SELECT b.bike_id, b.bike_type, b.status
FROM bikes b
JOIN (
SELECT bike_id FROM bikes
EXCEPT
SELECT bike_id FROM rentals
) x USING (bike_id)
ORDER BY b.bike_id;
SELECT b.bike_id, b.bike_type
FROM bikes b
JOIN (
SELECT bike_id FROM bikes
INTERSECT
SELECT bike_id FROM rentals
) x USING (bike_id)
ORDER BY b.bike_id;
SELECT r.rider_id, r.name, x.source
FROM riders r
JOIN (
SELECT rider_id, 'city:Stockton' AS source
FROM riders
WHERE city = 'Stockton'
UNION ALL
SELECT rider_id, 'has:rental' AS source
FROM rentals
) x USING (rider_id)
ORDER BY r.rider_id, x.source;
| Feature | SQLite | MariaDB |
|---|---|---|
| CROSS JOIN | Supported | Supported |
| JOIN ... ON | Supported | Supported |
| JOIN ... USING | Supported | Supported |
| UNION / UNION ALL | Supported | Supported |
| INTERSECT | Supported | Supported |
| EXCEPT | Supported | Supported |
JOIN ... ON, JOIN ... USING, CROSS JOINUNION, INTERSECT, EXCEPT-- Portable core:
-- JOIN ... ON / USING
-- UNION / INTERSECT / EXCEPT
-- + COUNT(*) checks
SELECT rt.rental_id, r.name, b.bike_type, rt.pickup_ts
FROM rentals rt
JOIN riders r
ON rt.rider_id = r.rider_id
JOIN bikes b
ON rt.bike_id = b.bike_id
ORDER BY rt.rental_id;
SELECT r.rider_id, r.name
FROM riders r
JOIN (
SELECT rider_id FROM riders
EXCEPT
SELECT rider_id FROM rentals
) x USING (rider_id)
ORDER BY r.rider_id;
SELECT r.rider_id, r.name
FROM riders r
JOIN (
SELECT rider_id FROM riders WHERE city = 'Stockton'
INTERSECT
SELECT rider_id FROM rentals
) x USING (rider_id)
ORDER BY r.rider_id;
| Need | Use | Bike example |
|---|---|---|
| All combinations | CROSS JOIN |
All rider × bike pairs |
| Match rows by rule | JOIN ... ON |
Rentals ↔ Riders (rider_id) |
| Match on shared column | JOIN ... USING |
Riders ↔ Prefs (rider_id) |
| Combine distinct rows | UNION |
Labels from two sources |
| Common rows | INTERSECT |
Riders who appear in rentals |
| Missing rows | EXCEPT |
Riders who never rented |
-- Clean, common pattern:
SELECT rt.rental_id, r.name, b.bike_type
FROM rentals rt
JOIN riders r ON rt.rider_id = r.rider_id
JOIN bikes b ON rt.bike_id = b.bike_id
ORDER BY rt.rental_id;
Handling missing values in a flower dataset
-- Query habit
-- 1 inspect table
-- 2 detect NULL
-- 3 filter safely
-- 4 classify data
| Column | Description |
|---|---|
| flower_id | unique flower record |
| flower_name | type of flower |
| color | flower color |
| price | price per flower |
| bloom_season | season flower blooms |
CREATE TABLE flowers (
flower_id INT PRIMARY KEY,
flower_name VARCHAR(50),
color VARCHAR(30),
price DECIMAL(5,2),
bloom_season VARCHAR(30)
);
INSERT INTO flowers
(flower_id, flower_name, color, price, bloom_season)
VALUES
(1, 'Rose', 'Red', 4.50, 'Spring'),
(2, 'Tulip', 'Yellow', 3.20, 'Spring'),
(3, 'Orchid', 'Purple', 7.80, NULL),
(4, 'Lily', 'White', 5.00, 'Summer'),
(5, 'Sunflower', 'Yellow', 2.80, NULL);
| flower_id | flower | color | price | bloom_season |
|---|---|---|---|---|
| 1 | Rose | Red | 4.50 | Spring |
| 3 | Orchid | Purple | 7.80 | NULL |
| 5 | Sunflower | Yellow | 2.80 | NULL |
SELECT *
FROM flowers
WHERE bloom_season IS NULL;
| flower | bloom_season |
|---|---|
| Orchid | NULL |
| Sunflower | NULL |
-- incorrect
WHERE bloom_season = NULL
WHERE bloom_season IS NULL
SELECT *
FROM flowers
WHERE bloom_season IS NOT NULL;
| flower | season |
|---|---|
| Rose | Spring |
| Lily | Summer |
SELECT *
FROM flowers
WHERE NOT color = 'Yellow';
| flower | color |
|---|---|
| Rose | Red |
| Orchid | Purple |
CREATE TABLE flowers_default (
flower_id INT PRIMARY KEY,
flower_name VARCHAR(50),
color VARCHAR(30),
price DECIMAL(5,2),
bloom_season VARCHAR(30) DEFAULT 'Unknown'
);
CASE
WHEN condition THEN value
ELSE value
END
SELECT
flower_name,
CASE
WHEN bloom_season IS NULL THEN 'Unknown Season'
ELSE 'Season Known'
END AS season_status
FROM flowers;
| flower | status |
|---|---|
| Rose | Season Known |
| Orchid | Unknown Season |
SELECT
flower_name,
CASE
WHEN price > 6 THEN 'Premium'
WHEN price > 3 THEN 'Standard'
ELSE 'Budget'
END AS price_category
FROM flowers;
| flower | price_category |
|---|---|
| Orchid | Premium |
| Rose | Standard |
| Sunflower | Budget |
| Concept | Purpose |
|---|---|
| NULL | missing value |
| IS NULL | find missing |
| NOT | reverse logic |
| DEFAULT | automatic value |
| CASE | conditional logic |
SELECT *
FROM flowers
WHERE bloom_season IS NULL;
Reproducible SQL structure examples in a flower database system
-- execution order
-- CREATE
-- INSERT
-- UPDATE
-- DELETE
-- SELECT
-- INDEX
| phase | purpose |
|---|---|
| build | create objects |
| load | insert rows |
| inspect | query results |
CREATE DATABASE flower_shop;
| status |
|---|
| CREATE DATABASE |
CREATE SCHEMA inventory;
CREATE SCHEMA sales;
| schema_name |
|---|
| inventory |
| sales |
CREATE SEQUENCE inventory.flower_seq
START 1
INCREMENT 1;
| sequence_name |
|---|
| flower_seq |
CREATE TABLE sales.customers (
customer_id INTEGER PRIMARY KEY,
customer_name VARCHAR(60) NOT NULL,
city VARCHAR(40),
email VARCHAR(80) UNIQUE
);
| column | type |
|---|---|
| customer_id | INTEGER |
| customer_name | VARCHAR |
| VARCHAR |
CREATE TABLE inventory.flowers (
flower_id INTEGER PRIMARY KEY,
flower_name VARCHAR(50) NOT NULL,
color VARCHAR(30),
price DECIMAL(6,2) NOT NULL,
stock INTEGER DEFAULT 0
);
| column | rule |
|---|---|
| flower_name | NOT NULL |
| price | NOT NULL |
| stock | DEFAULT 0 |
CREATE TABLE sales.orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES sales.customers(customer_id),
order_date DATE,
status VARCHAR(20)
);
| column | reference |
|---|---|
| customer_id | customers(customer_id) |
CREATE TABLE sales.order_items (
item_id INTEGER PRIMARY KEY,
order_id INTEGER REFERENCES sales.orders(order_id),
flower_id INTEGER REFERENCES inventory.flowers(flower_id),
quantity INTEGER NOT NULL
);
| column | reference |
|---|---|
| order_id | orders(order_id) |
| flower_id | flowers(flower_id) |
INSERT INTO sales.customers
(customer_id, customer_name, city, email)
VALUES
(1, 'Alice', 'Stockton', 'alice@demo.com'),
(2, 'Brian', 'Lodi', 'brian@demo.com'),
(3, 'Carla', 'Stockton', 'carla@demo.com');
SELECT customer_id, customer_name, city
FROM sales.customers;
| customer_id | customer_name | city |
|---|---|---|
| 1 | Alice | Stockton |
| 2 | Brian | Lodi |
| 3 | Carla | Stockton |
INSERT INTO inventory.flowers
(flower_id, flower_name, color, price, stock)
VALUES
(NEXTVAL('inventory.flower_seq'),'Rose','Red',4.50,20),
(NEXTVAL('inventory.flower_seq'),'Tulip','Yellow',3.20,30),
(NEXTVAL('inventory.flower_seq'),'Lily','White',5.00,15);
SELECT flower_id, flower_name, price
FROM inventory.flowers;
| flower_id | flower_name | price |
|---|---|---|
| 1 | Rose | 4.50 |
| 2 | Tulip | 3.20 |
| 3 | Lily | 5.00 |
INSERT INTO sales.orders
(order_id, customer_id, order_date, status)
VALUES
(101, 1, '2026-04-01', 'OPEN'),
(102, 2, '2026-04-02', 'PAID'),
(103, 1, '2026-04-03', 'PAID');
SELECT order_id, customer_id, status
FROM sales.orders;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | OPEN |
| 102 | 2 | PAID |
| 103 | 1 | PAID |
INSERT INTO sales.order_items
(item_id, order_id, flower_id, quantity)
VALUES
(1, 101, 1, 6),
(2, 101, 2, 4),
(3, 102, 3, 2),
(4, 103, 1, 5);
SELECT item_id, order_id, flower_id, quantity
FROM sales.order_items;
| item_id | order_id | flower_id | quantity |
|---|---|---|---|
| 1 | 101 | 1 | 6 |
| 2 | 101 | 2 | 4 |
| 3 | 102 | 3 | 2 |
SELECT flower_id, flower_name, color, price
FROM inventory.flowers;
| flower_id | flower_name | color | price |
|---|---|---|---|
| 1 | Rose | Red | 4.50 |
| 2 | Tulip | Yellow | 3.20 |
| 3 | Lily | White | 5.00 |
SELECT customer_id, customer_name
FROM sales.customers;
| customer_id | customer_name |
|---|---|
| 1 | Alice |
| 2 | Brian |
| 3 | Carla |
UPDATE inventory.flowers
SET price = 4.80
WHERE flower_name = 'Rose';
SELECT flower_name, price
FROM inventory.flowers
WHERE flower_name = 'Rose';
| flower_name | price |
|---|---|
| Rose | 4.80 |
SELECT flower_id, flower_name, price
FROM inventory.flowers
WHERE price >= 4.80;
| flower_id | flower_name | price |
|---|---|---|
| 1 | Rose | 4.80 |
| 3 | Lily | 5.00 |
UPDATE inventory.flowers
SET stock = stock - 5
WHERE flower_id = 1;
SELECT flower_id, flower_name, stock
FROM inventory.flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 15 |
SELECT flower_name, stock
FROM inventory.flowers
WHERE stock >= 15;
| flower_name | stock |
|---|---|
| Rose | 15 |
| Tulip | 30 |
| Lily | 15 |
UPDATE sales.orders
SET status = 'PAID'
WHERE order_id = 101;
SELECT order_id, status
FROM sales.orders
WHERE order_id = 101;
| order_id | status |
|---|---|
| 101 | PAID |
SELECT order_id, customer_id, status
FROM sales.orders
WHERE customer_id = 1;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | PAID |
| 103 | 1 | PAID |
DELETE FROM sales.order_items
WHERE item_id = 3;
SELECT item_id, order_id, flower_id
FROM sales.order_items;
| item_id | order_id | flower_id |
|---|---|---|
| 1 | 101 | 1 |
| 2 | 101 | 2 |
| 4 | 103 | 1 |
SELECT order_id, flower_id, quantity
FROM sales.order_items
ORDER BY item_id;
| order_id | flower_id | quantity |
|---|---|---|
| 101 | 1 | 6 |
| 101 | 2 | 4 |
| 103 | 1 | 5 |
DELETE FROM sales.customers
WHERE customer_id = 1;
| result |
|---|
| foreign key violation |
SELECT customer_id, customer_name, city
FROM sales.customers
WHERE city = 'Stockton';
| customer_id | customer_name | city |
|---|---|---|
| 1 | Alice | Stockton |
| 3 | Carla | Stockton |
SELECT flower_name, price
FROM inventory.flowers
WHERE price >= 4.80;
| flower_name | price |
|---|---|
| Rose | 4.80 |
| Lily | 5.00 |
SELECT order_id, customer_id, status
FROM sales.orders
WHERE customer_id = 1;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | PAID |
| 103 | 1 | PAID |
SELECT o.order_id, c.customer_name, o.status
FROM sales.orders o
JOIN sales.customers c
ON o.customer_id = c.customer_id;
| order_id | customer_name | status |
|---|---|---|
| 101 | Alice | PAID |
| 102 | Brian | PAID |
| 103 | Alice | PAID |
SELECT oi.order_id, f.flower_name, oi.quantity
FROM sales.order_items oi
JOIN inventory.flowers f
ON oi.flower_id = f.flower_id;
| order_id | flower_name | quantity |
|---|---|---|
| 101 | Rose | 6 |
| 101 | Tulip | 4 |
| 103 | Rose | 5 |
CREATE INDEX idx_flower_name
ON inventory.flowers(flower_name);
| index_name | column |
|---|---|
| idx_flower_name | flower_name |
SELECT flower_id, flower_name, price
FROM inventory.flowers
WHERE flower_name = 'Rose';
| flower_id | flower_name | price |
|---|---|---|
| 1 | Rose | 4.80 |
CREATE INDEX idx_orders_customer
ON sales.orders(customer_id);
| index_name | column |
|---|---|
| idx_orders_customer | customer_id |
SELECT order_id, customer_id
FROM sales.orders
WHERE customer_id = 1;
| order_id | customer_id |
|---|---|
| 101 | 1 |
| 103 | 1 |
SELECT NEXTVAL('inventory.flower_seq');
| nextval |
|---|
| 4 |
INSERT INTO inventory.flowers
(flower_id, flower_name, color, price, stock)
VALUES
(NEXTVAL('inventory.flower_seq'),'Orchid','Purple',7.80,8);
SELECT flower_id, flower_name, price
FROM inventory.flowers
WHERE flower_name = 'Orchid';
| flower_id | flower_name | price |
|---|---|---|
| 5 | Orchid | 7.80 |
SELECT flower_name, color, stock
FROM inventory.flowers
WHERE flower_name = 'Orchid';
| flower_name | color | stock |
|---|---|---|
| Orchid | Purple | 8 |
DELETE FROM inventory.flowers
WHERE flower_name = 'Orchid';
SELECT flower_id, flower_name
FROM inventory.flowers;
| flower_id | flower_name |
|---|---|
| 1 | Rose |
| 2 | Tulip |
| 3 | Lily |
SELECT flower_name, color, stock
FROM inventory.flowers
ORDER BY flower_name;
| flower_name | color | stock |
|---|---|---|
| Lily | White | 15 |
| Rose | Red | 15 |
| Tulip | Yellow | 30 |
| Concept | Purpose |
|---|---|
| DATABASE | top container |
| SCHEMA | object grouping |
| COLUMN | table attribute |
| SEQUENCE | generated id |
| REFERENCES | table relationship |
| INDEX | query speed |
| CREATE | add object or row |
| READ | query data |
| UPDATE | modify row |
| DELETE | remove row |
Reproducible constraint examples in a flower database system
-- execution order
-- CREATE TABLE
-- INSERT valid rows
-- INSERT invalid rows
-- UPDATE rows
-- DELETE rows
-- SELECT results
| focus | goal |
|---|---|
| constraint | protect data |
| key | identify rows |
| check | validate values |
-- common constraint types
PRIMARY KEY
FOREIGN KEY
UNIQUE
CHECK
| type | purpose |
|---|---|
| PRIMARY KEY | identify row |
| UNIQUE | avoid duplicates |
| CHECK | enforce condition |
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
customer_name VARCHAR(60) NOT NULL,
city VARCHAR(40),
email VARCHAR(80) UNIQUE
);
| column | constraint |
|---|---|
| customer_id | PRIMARY KEY |
| UNIQUE |
CREATE TABLE flowers (
flower_id INTEGER PRIMARY KEY,
flower_name VARCHAR(50) UNIQUE,
color VARCHAR(30),
price DECIMAL(6,2) CHECK (price > 0),
stock INTEGER CHECK (stock >= 0)
);
| column | constraint |
|---|---|
| flower_id | PRIMARY KEY |
| flower_name | UNIQUE |
| price | CHECK |
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
status VARCHAR(20) CHECK (status IN ('OPEN','PAID','CANCELLED')),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
| column | constraint |
|---|---|
| order_id | PRIMARY KEY |
| customer_id | FOREIGN KEY |
| status | CHECK |
CREATE TABLE order_items (
item_id INTEGER PRIMARY KEY,
order_id INTEGER,
flower_id INTEGER,
quantity INTEGER CHECK (quantity > 0),
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (flower_id) REFERENCES flowers(flower_id)
);
| column | constraint |
|---|---|
| item_id | PRIMARY KEY |
| order_id | FOREIGN KEY |
| quantity | CHECK |
SELECT customer_id, customer_name
FROM customers;
| customer_id | customer_name |
|---|---|
| 1 | Alice |
| 2 | Brian |
| 3 | Carla |
INSERT INTO customers
(customer_id, customer_name, city, email)
VALUES
(1, 'Alice', 'Stockton', 'alice@demo.com'),
(2, 'Brian', 'Lodi', 'brian@demo.com'),
(3, 'Carla', 'Manteca', 'carla@demo.com');
SELECT customer_id, customer_name, email
FROM customers;
| customer_id | customer_name | |
|---|---|---|
| 1 | Alice | alice@demo.com |
| 2 | Brian | brian@demo.com |
| 3 | Carla | carla@demo.com |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(1, 'Rose', 'Red', 4.50, 20),
(2, 'Tulip', 'Yellow', 3.20, 30),
(3, 'Lily', 'White', 5.00, 15);
SELECT flower_id, flower_name, price, stock
FROM flowers;
| flower_id | flower_name | price | stock |
|---|---|---|---|
| 1 | Rose | 4.50 | 20 |
| 2 | Tulip | 3.20 | 30 |
| 3 | Lily | 5.00 | 15 |
INSERT INTO orders
(order_id, customer_id, status)
VALUES
(101, 1, 'OPEN'),
(102, 2, 'PAID'),
(103, 1, 'PAID');
SELECT order_id, customer_id, status
FROM orders;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | OPEN |
| 102 | 2 | PAID |
| 103 | 1 | PAID |
INSERT INTO order_items
(item_id, order_id, flower_id, quantity)
VALUES
(1, 101, 1, 6),
(2, 101, 2, 4),
(3, 102, 3, 2);
SELECT item_id, order_id, flower_id, quantity
FROM order_items;
| item_id | order_id | flower_id | quantity |
|---|---|---|---|
| 1 | 101 | 1 | 6 |
| 2 | 101 | 2 | 4 |
| 3 | 102 | 3 | 2 |
INSERT INTO customers
(customer_id, customer_name, city, email)
VALUES
(1, 'Diana', 'Lodi', 'diana@demo.com');
| result |
|---|
| primary key violation |
SELECT customer_id, customer_name
FROM customers
ORDER BY customer_id;
| customer_id | customer_name |
|---|---|
| 1 | Alice |
| 2 | Brian |
| 3 | Carla |
SELECT customer_name, email
FROM customers;
| customer_name | |
|---|---|
| Alice | alice@demo.com |
| Brian | brian@demo.com |
| Carla | carla@demo.com |
INSERT INTO customers
(customer_id, customer_name, city, email)
VALUES
(4, 'Diana', 'Lodi', 'alice@demo.com');
| result |
|---|
| unique constraint violation |
SELECT customer_id, email
FROM customers
ORDER BY customer_id;
| customer_id | |
|---|---|
| 1 | alice@demo.com |
| 2 | brian@demo.com |
| 3 | carla@demo.com |
SELECT flower_name, price, stock
FROM flowers;
| flower_name | price | stock |
|---|---|---|
| Rose | 4.50 | 20 |
| Tulip | 3.20 | 30 |
| Lily | 5.00 | 15 |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(4, 'Orchid', 'Purple', -2.00, 8);
| result |
|---|
| check constraint violation |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(4, 'Orchid', 'Purple', 7.80, -1);
| result |
|---|
| check constraint violation |
INSERT INTO orders
(order_id, customer_id, status)
VALUES
(104, 2, 'SHIPPED');
| result |
|---|
| check constraint violation |
SELECT order_id, customer_id
FROM orders;
| order_id | customer_id |
|---|---|
| 101 | 1 |
| 102 | 2 |
| 103 | 1 |
INSERT INTO orders
(order_id, customer_id, status)
VALUES
(104, 99, 'OPEN');
| result |
|---|
| foreign key violation |
INSERT INTO order_items
(item_id, order_id, flower_id, quantity)
VALUES
(4, 999, 1, 3);
| result |
|---|
| foreign key violation |
DELETE FROM customers
WHERE customer_id = 1;
| result |
|---|
| foreign key violation |
UPDATE flowers
SET stock = 18
WHERE flower_id = 1;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 18 |
UPDATE flowers
SET stock = -4
WHERE flower_id = 2;
| result |
|---|
| check constraint violation |
UPDATE orders
SET customer_id = 99
WHERE order_id = 102;
| result |
|---|
| foreign key violation |
UPDATE customers
SET email = 'alice@demo.com'
WHERE customer_id = 2;
| result |
|---|
| unique constraint violation |
DELETE FROM order_items
WHERE item_id = 3;
SELECT item_id, order_id, flower_id
FROM order_items;
| item_id | order_id | flower_id |
|---|---|---|
| 1 | 101 | 1 |
| 2 | 101 | 2 |
DELETE FROM orders
WHERE order_id = 102;
SELECT order_id, customer_id, status
FROM orders;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | OPEN |
| 103 | 1 | PAID |
-- examples
customer_id INTEGER PRIMARY KEY
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id)
| phrase | role |
|---|---|
| PRIMARY KEY | identify row |
| FOREIGN KEY | link tables |
SELECT flower_id, flower_name, price, stock
FROM flowers
ORDER BY flower_id;
| flower_id | flower_name | price | stock |
|---|---|---|---|
| 1 | Rose | 4.50 | 18 |
| 2 | Tulip | 3.20 | 30 |
| 3 | Lily | 5.00 | 15 |
SELECT o.order_id, c.customer_name, o.status
FROM orders o
JOIN customers c
ON o.customer_id = c.customer_id;
| order_id | customer_name | status |
|---|---|---|
| 101 | Alice | OPEN |
| 103 | Alice | PAID |
SELECT flower_name, price
FROM flowers
WHERE price > 3.00;
| flower_name | price |
|---|---|
| Rose | 4.50 |
| Tulip | 3.20 |
| Lily | 5.00 |
SELECT customer_id, customer_name
FROM customers
WHERE email = 'brian@demo.com';
| customer_id | customer_name |
|---|---|
| 2 | Brian |
| Constraint | Main Purpose |
|---|---|
| PRIMARY KEY | unique row identity |
| FOREIGN KEY | valid table relationship |
| UNIQUE | prevent duplicates |
| CHECK | enforce value rule |
| KEY | part of relational structure |
| CONSTRAINT | protect data quality |
SELECT customer_id, city
FROM customers
WHERE city = 'Lodi';
| customer_id | city |
|---|---|
| 2 | Lodi |
SELECT order_id, status
FROM orders
ORDER BY order_id;
| order_id | status |
|---|---|
| 101 | OPEN |
| 103 | PAID |
Reproducible transaction control examples in a flower database system
-- execution order
-- BEGIN
-- INSERT or UPDATE
-- SAVEPOINT
-- more changes
-- ROLLBACK or COMMIT
| phase | purpose |
|---|---|
| begin | start unit of work |
| rollback | undo changes |
| commit | save changes |
-- transaction control words
BEGIN
COMMIT
ROLLBACK
SAVEPOINT
RELEASE
| term | purpose |
|---|---|
| BEGIN | start transaction |
| COMMIT | make permanent |
| ROLLBACK | undo work |
CREATE TABLE flowers (
flower_id INTEGER PRIMARY KEY,
flower_name VARCHAR(50) UNIQUE,
color VARCHAR(30),
price DECIMAL(6,2),
stock INTEGER
);
| column | role |
|---|---|
| flower_id | primary key |
| flower_name | flower label |
| stock | inventory count |
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
customer_name VARCHAR(60),
city VARCHAR(40)
);
| column | role |
|---|---|
| customer_id | primary key |
| customer_name | buyer name |
| city | location |
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
status VARCHAR(20),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
| column | role |
|---|---|
| order_id | primary key |
| customer_id | customer link |
| status | order state |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(1, 'Rose', 'Red', 4.50, 20),
(2, 'Tulip', 'Yellow', 3.20, 30),
(3, 'Lily', 'White', 5.00, 15);
SELECT flower_id, flower_name, stock
FROM flowers;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 20 |
| 2 | Tulip | 30 |
| 3 | Lily | 15 |
INSERT INTO customers
(customer_id, customer_name, city)
VALUES
(1, 'Alice', 'Stockton'),
(2, 'Brian', 'Lodi');
SELECT customer_id, customer_name, city
FROM customers;
| customer_id | customer_name | city |
|---|---|---|
| 1 | Alice | Stockton |
| 2 | Brian | Lodi |
INSERT INTO orders
(order_id, customer_id, status)
VALUES
(101, 1, 'OPEN'),
(102, 2, 'PAID');
SELECT order_id, customer_id, status
FROM orders;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | OPEN |
| 102 | 2 | PAID |
BEGIN;
| transaction_state |
|---|
| active |
BEGIN;
UPDATE flowers
SET stock = 12
WHERE flower_id = 1;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 12 |
ROLLBACK;
| transaction_state |
|---|
| cancelled |
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 20 |
COMMIT;
| transaction_state |
|---|
| saved |
BEGIN;
UPDATE flowers
SET stock = 25
WHERE flower_id = 2;
COMMIT;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 2;
| flower_id | flower_name | stock |
|---|---|---|
| 2 | Tulip | 25 |
SELECT flower_name, stock
FROM flowers
ORDER BY flower_id;
| flower_name | stock |
|---|---|
| Rose | 20 |
| Tulip | 25 |
| Lily | 15 |
SAVEPOINT sp_stock;
| savepoint |
|---|
| sp_stock |
BEGIN;
UPDATE flowers
SET stock = 18
WHERE flower_id = 1;
SAVEPOINT sp_stock;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 18 |
UPDATE flowers
SET stock = 10
WHERE flower_id = 3;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 3;
| flower_id | flower_name | stock |
|---|---|---|
| 3 | Lily | 10 |
ROLLBACK TO SAVEPOINT sp_stock;
| rollback_scope |
|---|
| after sp_stock only |
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id IN (1, 3)
ORDER BY flower_id;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 18 |
| 3 | Lily | 15 |
COMMIT;
| transaction_state |
|---|
| saved after partial rollback |
SELECT flower_name, stock
FROM flowers
ORDER BY flower_id;
| flower_name | stock |
|---|---|
| Rose | 18 |
| Tulip | 25 |
| Lily | 15 |
RELEASE SAVEPOINT sp_order;
| savepoint_state |
|---|
| released |
BEGIN;
INSERT INTO orders
(order_id, customer_id, status)
VALUES
(103, 2, 'OPEN');
SAVEPOINT sp_order;
SELECT order_id, customer_id, status
FROM orders
WHERE order_id = 103;
| order_id | customer_id | status |
|---|---|---|
| 103 | 2 | OPEN |
RELEASE SAVEPOINT sp_order;
| savepoint | state |
|---|---|
| sp_order | removed |
COMMIT;
SELECT order_id, customer_id, status
FROM orders
ORDER BY order_id;
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | OPEN |
| 102 | 2 | PAID |
| 103 | 2 | OPEN |
BEGIN;
UPDATE flowers
SET price = 6.00
WHERE flower_id = 1;
UPDATE flowers
SET price = 2.90
WHERE flower_id = 2;
ROLLBACK;
SELECT flower_id, flower_name, price
FROM flowers
ORDER BY flower_id;
| flower_id | flower_name | price |
|---|---|---|
| 1 | Rose | 4.50 |
| 2 | Tulip | 3.20 |
| 3 | Lily | 5.00 |
BEGIN;
UPDATE flowers
SET stock = 17
WHERE flower_id = 1;
UPDATE orders
SET status = 'PAID'
WHERE order_id = 101;
COMMIT;
SELECT order_id, status
FROM orders
WHERE order_id = 101;
| order_id | status |
|---|---|
| 101 | PAID |
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 17 |
BEGIN;
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(4, 'Orchid', 'Purple', 7.80, 8);
SAVEPOINT sp_insert;
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(5, 'Daisy', 'White', 2.50, 22);
SELECT flower_id, flower_name
FROM flowers
WHERE flower_id IN (4,5)
ORDER BY flower_id;
| flower_id | flower_name |
|---|---|
| 4 | Orchid |
| 5 | Daisy |
ROLLBACK TO SAVEPOINT sp_insert;
SELECT flower_id, flower_name
FROM flowers
WHERE flower_id IN (4,5)
ORDER BY flower_id;
| flower_id | flower_name |
|---|---|
| 4 | Orchid |
COMMIT;
SELECT flower_id, flower_name
FROM flowers
WHERE flower_id = 4;
| flower_id | flower_name |
|---|---|
| 4 | Orchid |
RELEASE SAVEPOINT name;
COMMIT;
| command | effect |
|---|---|
| RELEASE | remove savepoint |
| COMMIT | save transaction |
ROLLBACK;
ROLLBACK TO SAVEPOINT sp1;
| command | scope |
|---|---|
| ROLLBACK | entire transaction |
| ROLLBACK TO | after savepoint only |
SELECT order_id, status
FROM orders
ORDER BY order_id;
| order_id | status |
|---|---|
| 101 | PAID |
| 102 | PAID |
| 103 | OPEN |
SELECT flower_id, flower_name, stock
FROM flowers
ORDER BY flower_id;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 17 |
| 2 | Tulip | 25 |
| 3 | Lily | 15 |
| 4 | Orchid | 8 |
| Command | Purpose |
|---|---|
| TRANSACTION | group related work |
| BEGIN | start transaction |
| COMMIT | make changes permanent |
| ROLLBACK | undo entire transaction |
| SAVEPOINT | mark checkpoint |
| RELEASE | remove savepoint |
SELECT flower_name, stock
FROM flowers
WHERE stock >= 15
ORDER BY flower_id;
| flower_name | stock |
|---|---|
| Rose | 17 |
| Tulip | 25 |
| Lily | 15 |
Reproducible access control examples in a flower database system
-- execution order
-- CREATE USER
-- CREATE ROLE
-- GRANT
-- REVOKE
-- LOCK
-- SELECT result
| topic | purpose |
|---|---|
| USER | account identity |
| ROLE | permission group |
| LOCK | control concurrent access |
-- security words
USER
ROLE
GRANT
REVOKE
LOCK
| term | meaning |
|---|---|
| USER | login identity |
| ROLE | permission set |
| GRANT | give access |
CREATE TABLE flowers (
flower_id INTEGER PRIMARY KEY,
flower_name VARCHAR(50),
color VARCHAR(30),
price DECIMAL(6,2),
stock INTEGER
);
| column | use |
|---|---|
| flower_id | primary key |
| flower_name | product name |
| stock | inventory value |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(1, 'Rose', 'Red', 4.50, 20),
(2, 'Tulip', 'Yellow', 3.20, 30),
(3, 'Lily', 'White', 5.00, 15);
SELECT flower_id, flower_name, stock
FROM flowers;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 20 |
| 2 | Tulip | 30 |
| 3 | Lily | 15 |
-- user examples
manager_user
analyst_user
cashier_user
| user | purpose |
|---|---|
| manager_user | manage data |
| analyst_user | read reports |
| cashier_user | limited operations |
CREATE USER manager_user
IDENTIFIED BY 'demo123';
| user_name |
|---|
| manager_user |
CREATE USER analyst_user
IDENTIFIED BY 'demo123';
| user_name |
|---|
| analyst_user |
-- role examples
inventory_reader
inventory_editor
sales_manager
| role | purpose |
|---|---|
| inventory_reader | read stock |
| inventory_editor | change stock |
CREATE ROLE inventory_reader;
| role_name |
|---|
| inventory_reader |
CREATE ROLE inventory_editor;
| role_name |
|---|
| inventory_editor |
GRANT SELECT ON flowers TO analyst_user;
| action | object |
|---|---|
| SELECT | flowers |
GRANT SELECT
ON flowers
TO inventory_reader;
| grantee | permission |
|---|---|
| inventory_reader | SELECT |
GRANT UPDATE
ON flowers
TO inventory_editor;
| grantee | permission |
|---|---|
| inventory_editor | UPDATE |
GRANT inventory_reader
TO analyst_user;
| user | role |
|---|---|
| analyst_user | inventory_reader |
GRANT inventory_editor
TO manager_user;
| user | role |
|---|---|
| manager_user | inventory_editor |
SELECT flower_id, flower_name, stock
FROM flowers;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 20 |
| 2 | Tulip | 30 |
| 3 | Lily | 15 |
UPDATE flowers
SET stock = 18
WHERE flower_id = 1;
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 18 |
REVOKE UPDATE
ON flowers
FROM inventory_editor;
| action | effect |
|---|---|
| REVOKE | remove permission |
REVOKE UPDATE
ON flowers
FROM inventory_editor;
| grantee | removed_permission |
|---|---|
| inventory_editor | UPDATE |
UPDATE flowers
SET stock = 16
WHERE flower_id = 1;
| result |
|---|
| permission denied |
REVOKE inventory_reader
FROM analyst_user;
| user | removed_role |
|---|---|
| analyst_user | inventory_reader |
SELECT flower_id, flower_name
FROM flowers;
| result |
|---|
| permission denied |
GRANT SELECT
ON flowers
TO analyst_user;
| user | permission |
|---|---|
| analyst_user | SELECT |
SELECT flower_name, price
FROM flowers
ORDER BY flower_id;
| flower_name | price |
|---|---|
| Rose | 4.50 |
| Tulip | 3.20 |
| Lily | 5.00 |
LOCK TABLE flowers
IN EXCLUSIVE MODE;
| lock_type | target |
|---|---|
| EXCLUSIVE | flowers |
BEGIN;
LOCK TABLE flowers
IN EXCLUSIVE MODE;
UPDATE flowers
SET stock = 17
WHERE flower_id = 1;
| session | state |
|---|---|
| A | lock acquired |
UPDATE flowers
SET stock = 40
WHERE flower_id = 2;
| session | result |
|---|---|
| B | blocked by lock |
COMMIT;
| lock_state |
|---|
| released after commit |
SELECT flower_id, flower_name, stock
FROM flowers
WHERE flower_id = 1;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 17 |
GRANT SELECT, INSERT, UPDATE
ON flowers
TO manager_user;
| user | permissions |
|---|---|
| manager_user | SELECT, INSERT, UPDATE |
REVOKE INSERT
ON flowers
FROM manager_user;
| user | removed_permission |
|---|---|
| manager_user | INSERT |
INSERT INTO flowers
(flower_id, flower_name, color, price, stock)
VALUES
(4, 'Orchid', 'Purple', 7.80, 8);
| result |
|---|
| permission denied |
GRANT inventory_reader TO analyst_user;
GRANT inventory_reader TO cashier_user;
| role | assigned_users |
|---|---|
| inventory_reader | 2 users |
-- identity vs permission
USER = analyst_user
ROLE = inventory_reader
| concept | question answered |
|---|---|
| USER | who? |
| ROLE | what rights? |
LOCK TABLE flowers
IN EXCLUSIVE MODE;
| question | answered by |
|---|---|
| who may update? | GRANT |
| who must wait now? | LOCK |
SELECT flower_id, flower_name, stock
FROM flowers
ORDER BY flower_id;
| flower_id | flower_name | stock |
|---|---|---|
| 1 | Rose | 17 |
| 2 | Tulip | 30 |
| 3 | Lily | 15 |
| Concept | Purpose |
|---|---|
| LOCK | control concurrent access |
| GRANT | give permission |
| REVOKE | remove permission |
| USER | database account |
| ROLE | permission group |
SELECT flower_name, price
FROM flowers
WHERE price > 3.00;
| flower_name | price |
|---|---|
| Rose | 4.50 |
| Tulip | 3.20 |
| Lily | 5.00 |
Table of Contents | Office Hours: Mon, Wed, 2:00 PM - 3:00 PM, CTC 117 | sberhe@pacific.edu | Syllabus | Join Zoom | OneCompiler (SQL Editor)