SQL (PostgreSQL, MySQL, SQLite) Module 1 – Start here: databases and your first query
Meet the practice database and its tables
Meet mart, the small invented shop database these lessons query, read its ER diagram of keys and links, and list the tables and columns of any database.
What you will learn
- Describe the mart practice tables and how each example builds them in a fresh database
- Read an ER diagram with primary keys, foreign keys and one-to-many links
- List a database's tables and columns with catalog queries in SQLite, PostgreSQL and MySQL
Before you start
On this page
To learn queries you need data small enough to check every answer by eye. This track uses small invented datasets made for its lessons. This lesson introduces mart, a tiny online grocery that this lesson and the next one query, shows how to read its diagram, and teaches catalog queries: questions you ask a database about itself, which work on any database you are given.
How the practice data works
- Invented. Every customer, product and order is made up for this track. The names are drawn from many languages and regions of India, and they do not refer to any real person or business.
- Small. Each table has a handful of rows, so you can see why a query returns what it does.
- Built by the example itself. An example starts with the
CREATE TABLEandINSERTstatements for the tables it needs, then runs its queries. Every run in the browser, and every run ofsqlite3 :memory:, starts from an empty database, so those first lines are what makes the data exist. Read them once; after that, the query at the end is what matters. - Money in paise. Prices are whole numbers of paise in
INTEGERcolumns, so totals stay exact.
Lessons that need other kinds of data later in the track build it the same way, in the example itself.
The mart tables
-- mart: a small, invented online grocery. Four tables, then every row of each.
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price_paise INTEGER NOT NULL -- 100 paise = 1 rupee
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers (customer_id),
status TEXT NOT NULL -- 'placed', 'delivered' or 'cancelled'
);
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,
PRIMARY KEY (order_id, product_id) -- a product appears once per order
);
INSERT INTO customers (customer_id, name, city) VALUES
(1, 'Asha Kulkarni', 'Pune'),
(2, 'Vikram Nair', 'Kochi'),
(3, 'Meera Rathore', 'Jaipur'),
(4, 'Tenzin Dorjee', 'Gangtok');
INSERT INTO products (product_id, name, category, price_paise) VALUES
(1, 'Basmati rice 1 kg', 'Grains', 14999),
(2, 'Toor dal 500 g', 'Pulses', 8950),
(3, 'Masala chai 250 g', 'Beverages', 12000),
(4, 'Coconut oil 1 L', 'Oils', 21500),
(5, 'Ragi flour 1 kg', 'Grains', 6800);
INSERT INTO orders (order_id, customer_id, status) VALUES
(101, 1, 'delivered'),
(102, 2, 'delivered'),
(103, 1, 'placed'),
(104, 3, 'cancelled');
INSERT INTO order_items (order_id, product_id, quantity) VALUES
(101, 1, 2), (101, 2, 1),
(102, 4, 1),
(103, 3, 2), (103, 5, 1),
(104, 2, 3);
SELECT * FROM customers;
SELECT * FROM products;
SELECT * FROM orders;
SELECT * FROM order_items; Output
┌─────────────┬───────────────┬─────────┐ │ customer_id │ name │ city │ ├─────────────┼───────────────┼─────────┤ │ 1 │ Asha Kulkarni │ Pune │ │ 2 │ Vikram Nair │ Kochi │ │ 3 │ Meera Rathore │ Jaipur │ │ 4 │ Tenzin Dorjee │ Gangtok │ └─────────────┴───────────────┴─────────┘ ┌────────────┬───────────────────┬───────────┬─────────────┐ │ product_id │ name │ category │ price_paise │ ├────────────┼───────────────────┼───────────┼─────────────┤ │ 1 │ Basmati rice 1 kg │ Grains │ 14999 │ │ 2 │ Toor dal 500 g │ Pulses │ 8950 │ │ 3 │ Masala chai 250 g │ Beverages │ 12000 │ │ 4 │ Coconut oil 1 L │ Oils │ 21500 │ │ 5 │ Ragi flour 1 kg │ Grains │ 6800 │ └────────────┴───────────────────┴───────────┴─────────────┘ ┌──────────┬─────────────┬───────────┐ │ order_id │ customer_id │ status │ ├──────────┼─────────────┼───────────┤ │ 101 │ 1 │ delivered │ │ 102 │ 2 │ delivered │ │ 103 │ 1 │ placed │ │ 104 │ 3 │ cancelled │ └──────────┴─────────────┴───────────┘ ┌──────────┬────────────┬──────────┐ │ order_id │ product_id │ quantity │ ├──────────┼────────────┼──────────┤ │ 101 │ 1 │ 2 │ │ 101 │ 2 │ 1 │ │ 102 │ 4 │ 1 │ │ 103 │ 3 │ 2 │ │ 103 │ 5 │ 1 │ │ 104 │ 2 │ 3 │ └──────────┴────────────┴──────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < mart.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
- customers: who buys. One row per customer.
- products: what the shop sells, with its category and price in paise.
- orders: one row per order: who placed it (
customer_id) and where it stands (status). - order_items: one row per product in an order: which order, which product and how many.
The tables point to each other by their keys. Follow order 103 by hand: its customer_id is 1, and customer 1 is
Asha Kulkarni. In order_items, the rows with order_id 103 say it holds 2 of product 3 (masala chai) and 1 of
product 5 (ragi flour). The module on joins teaches SQL to follow these links for you. Notice also that Tenzin Dorjee
has no orders at all: “customers who never ordered” is a question later lessons answer.
Keys and the ER diagram
A primary key is a column, or a set of columns, whose value picks out exactly one row: no two rows of the table
may share it. customer_id, product_id and order_id are the primary keys of their tables. The primary key of
order_items is two columns together, (order_id, product_id): an order can hold many products and a product can be
in many orders, but one product appears only once in each order.
A foreign key is a column whose values are the keys of rows in another table. orders.customer_id holds a
customer_id from customers, and REFERENCES customers (customer_id) in the CREATE TABLE statement says so.
An entity-relationship (ER) diagram draws this: a box per table with its columns and types, PK and FK marks for the keys, and a line for each foreign key. The ends of a line say how many: two bars mean exactly one, and a circle with a crow’s foot means zero or many.
The four tables of the mart practice shop
Text description of the diagram
The diagram shows four tables, one under the other, each with its columns and their types. PK marks a primary key column and FK a foreign key column.
- customers: customer_id INTEGER (PK), name TEXT, city TEXT.
- orders: order_id INTEGER (PK), customer_id INTEGER (FK), status TEXT.
- order_items: order_id INTEGER (PK and FK), product_id INTEGER (PK and FK), quantity INTEGER.
- products: product_id INTEGER (PK), name TEXT, category TEXT, price_paise INTEGER.
Three lines join the tables. Each has two bars at one end, meaning exactly one, and a circle with a crow's foot at the other, meaning zero or many.
- customers to orders: each order belongs to exactly one customer, and a customer has zero or many orders. The link is orders.customer_id, which refers to customers.customer_id.
- orders to order_items: each order item belongs to exactly one order, and an order has zero or many items. The link is order_items.order_id.
- products to order_items: each order item is for exactly one product, and a product appears in zero or many order items. The link is order_items.product_id.
Read the top line from both ends: each order belongs to exactly one customer, and a customer has zero or many
orders. This is a one-to-many relationship. Orders and products are different: an order has
many products, and a product appears in many orders. A table in between, here order_items, turns that
many-to-many relationship into two one-to-many links.
Note
SQLite records foreign keys, but it checks them only on connections that switch the checks on with
PRAGMA foreign_keys = ON. Until then, nothing stops an order from naming a customer who does not exist. The module
on creating tables and constraints shows the difference.
Ask the database about itself
Every database keeps a description of its own tables, called its catalog, and you can query it like any other table. That is how you find your way around a database somebody hands you.
SQLite: the sqlite_schema table
-- The mart tables again, without rows: catalog queries only need the definitions.
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE products (product_id INTEGER PRIMARY KEY, name TEXT NOT NULL,
category TEXT NOT NULL, price_paise INTEGER NOT NULL);
CREATE TABLE orders (order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers (customer_id),
status TEXT 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,
PRIMARY KEY (order_id, product_id));
-- One row for every table, index, view and trigger in the database.
SELECT type, name, tbl_name FROM sqlite_schema;
-- The statement that created a table, as SQLite stored it.
SELECT sql FROM sqlite_schema WHERE name = 'customers'; Output
┌───────┬────────────────────────────────┬─────────────┐ │ type │ name │ tbl_name │ ├───────┼────────────────────────────────┼─────────────┤ │ table │ customers │ customers │ │ table │ products │ products │ │ table │ orders │ orders │ │ table │ order_items │ order_items │ │ index │ sqlite_autoindex_order_items_1 │ order_items │ └───────┴────────────────────────────────┴─────────────┘ ┌────────────────────────────────────┐ │ sql │ ├────────────────────────────────────┤ │ CREATE TABLE customers ( │ │ customer_id INTEGER PRIMARY KEY, │ │ name TEXT NOT NULL, │ │ city TEXT NOT NULL │ │ ) │ └────────────────────────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < list_tables.sql
sqlite_schema has one row for each table, index, view and trigger. type says which kind it is, name is its
name, and tbl_name the table it belongs to. Two rows are worth a closer look:
- The index row. SQLite keeps the two-column primary key of
order_itemsin an index it creates itself, calledsqlite_autoindex_order_items_1. The other tables have none, because a singleINTEGER PRIMARY KEYcolumn is the row’s own number in SQLite (its rowid) and needs no separate index. - The
sqlcolumn holds the statement that created each object, a copy of what you wrote with only small details tidied, such as the capitals ofCREATE TABLE. Line breaks are kept, as the result shows. It is what the.schemacommand of thesqlite3program prints. For the automatic index it is empty (NULL).
Older programs and answers online use the name sqlite_master; it is the same table.
Columns and keys: pragma functions
Many of SQLite’s PRAGMA statements, the ones that only report something, have twins that you can use in FROM
like a table, named with a pragma_ prefix:
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL);
CREATE TABLE products (product_id INTEGER PRIMARY KEY, name TEXT NOT NULL,
category TEXT NOT NULL, price_paise INTEGER NOT NULL);
CREATE TABLE orders (order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers (customer_id),
status TEXT 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,
PRIMARY KEY (order_id, product_id));
-- Every column of orders: its position, name, declared type, whether it is
-- NOT NULL, its default value and its place in the primary key (0 = not in it).
SELECT * FROM pragma_table_info('orders');
-- The foreign keys of order_items. "table", "from" and "to" are words SQL
-- uses itself, so these column names need double quotes.
SELECT "from" AS column_name, "table" AS refers_to, "to" AS key_column
FROM pragma_foreign_key_list('order_items'); Output
┌─────┬─────────────┬─────────┬─────────┬────────────┬────┐ │ cid │ name │ type │ notnull │ dflt_value │ pk │ ├─────┼─────────────┼─────────┼─────────┼────────────┼────┤ │ 0 │ order_id │ INTEGER │ 0 │ │ 1 │ │ 1 │ customer_id │ INTEGER │ 1 │ │ 0 │ │ 2 │ status │ TEXT │ 1 │ │ 0 │ └─────┴─────────────┴─────────┴─────────┴────────────┴────┘ ┌─────────────┬───────────┬────────────┐ │ column_name │ refers_to │ key_column │ ├─────────────┼───────────┼────────────┤ │ product_id │ products │ product_id │ │ order_id │ orders │ order_id │ └─────────────┴───────────┴────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < table_columns.sql
pragma_table_info('orders') lists the columns: their position (cid), name, declared type, whether they are
NOT NULL (1) or not (0), their default value and their place in the primary key (pk, 0 when they are not part of
it). One value surprises people: order_id shows notnull 0, because it was not declared NOT NULL. It still never
holds NULL: an INTEGER PRIMARY KEY is the row’s own number, and inserting NULL into it makes SQLite fill in a new
number instead, usually one more than the largest already in the table.
pragma_foreign_key_list('order_items') lists the table’s foreign keys: the column, the table it refers to and the
key column there. In the sqlite3 program, .tables and .schema orders show much the same.
Two common mistakes
CREATE TABLE orders (order_id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, status TEXT NOT NULL);
-- The table's name is text here, so it needs single quotes.
SELECT * FROM pragma_table_info(orders);
-- A PostgreSQL and MySQL habit: SQLite has no information_schema.
SELECT table_name FROM information_schema.tables; Output (exit status 1)
Printed as an error (standard error)
Parse error near line 4: no such column: orders Parse error near line 7: no such table: information_schema.tables
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < catalog_mistakes.sql
pragma_table_info(orders)without quotes readsordersas the name of a column, which does not exist here. The function wants the table’s name as text:'orders'.information_schemais how PostgreSQL and MySQL describe themselves, and answers online often use it. SQLite does not have it, so the query stops with “no such table”.
PostgreSQL and MySQL: information_schema
The SQL standard defines information_schema, a set of views that describe the tables and columns of a database,
and both PostgreSQL and MySQL have it. Here is how to list the tables, and then the columns of orders, in each of
the three engines:
- SQLite:
SELECT name FROM sqlite_schema WHERE type = 'table';andSELECT * FROM pragma_table_info('orders');. In thesqlite3program:.tablesand.schema orders. - PostgreSQL:
SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';andSELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'orders';. Tables you create without naming a schema go into the schema calledpublic. In psql:\dtand\d orders. - MySQL: the same two queries, but
table_schemais the name of the database, such asWHERE table_schema = 'practice'. MySQL also has statements of its own for this:SHOW TABLES;andDESCRIBE orders;.
PostgreSQL also has system catalogs of its own, with details that information_schema leaves out; the standard views are the part that the two server engines have in common.
Key takeaways
- The practice data is invented and small, and every example creates the tables it uses, because each run starts from an empty database.
- A primary key picks out one row; a foreign key holds the key of a row in another table.
- In an ER diagram, two bars mean exactly one and a crow’s foot means many. A table in between turns a many-to-many relationship into two one-to-many links.
sqlite_schemaandpragma_table_info('…')describe a SQLite database; PostgreSQL and MySQL useinformation_schema, plus\din psql andSHOW TABLESorDESCRIBEin MySQL.
Exercise
Exercise · Easy · SQL
List the columns of a table and their types
Somebody hands you a SQLite database with a table called orders, and you want to know what is in it. Write a query that lists every column of orders with the type it was declared with, in two columns:
name: the column's name;type: its declared type, such asINTEGERorTEXT.
For the orders table of the mart shop, the result is:
┌─────────────┬─────────┐
│ name │ type │
├─────────────┼─────────┤
│ order_id │ INTEGER │
│ customer_id │ INTEGER │
│ status │ TEXT │
└─────────────┴─────────┘
The order of the rows does not matter. The sample tests also try an orders table with other columns, next to a second table whose columns must not appear. SQLite lets a column be declared without a type; its type is then empty text.
Starter code · query.sql
-- One row for each column of orders, with two columns: name and type.
-- This query lists the database's tables; make it list the columns of orders.
SELECT name FROM sqlite_schema WHERE type = 'table'; The sample tests · sample_tests.yaml
# Sample tests (public): each test creates its own tables in a fresh database, runs the learner's query and compares
# the result with the expected rows (the order of rows does not matter; column names must match).
tests:
- name: lists the columns of the mart orders table
setup: |
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
status TEXT NOT NULL
);
expected:
columns: [name, type]
rows:
- [order_id, INTEGER]
- [customer_id, INTEGER]
- [status, TEXT]
- name: lists only orders, including a column declared without a type
setup: |
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
total_paise INTEGER NOT NULL,
note
);
expected:
columns: [name, type]
rows:
- [order_id, INTEGER]
- [customer_id, INTEGER]
- [total_paise, INTEGER]
- [note, ''] A hint
sqlite_schema has a row for each table, not for each column. The columns of one table come from pragma_table_info('orders'), which can stand in FROM like a table and already has columns called name and type.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- SQLite: The Schema Table (SQLite)
- SQLite: Pragma statements (PRAGMA functions, table_info, foreign_key_list) (SQLite)
- SQLite: CREATE TABLE (PRIMARY KEY constraints and rowid tables) (SQLite)
- SQLite: Autoincrement and ROWID (SQLite)
- SQLite: Foreign Key Support (SQLite)
- Command Line Shell For SQLite (SQLite)
- PostgreSQL 18 Documentation: The Information Schema (The PostgreSQL Global Development Group)
- PostgreSQL 18 Documentation: Schemas (The PostgreSQL Global Development Group)
- PostgreSQL 18 Documentation: psql (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: INFORMATION_SCHEMA Tables (Oracle Corporation)
- MySQL 8.4 Reference Manual: The INFORMATION_SCHEMA TABLES Table (Oracle Corporation)
- MySQL 8.4 Reference Manual: SHOW TABLES Statement (Oracle Corporation)
- MySQL 8.4 Reference Manual: DESCRIBE Statement (Oracle Corporation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress