Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 20 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot

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 TABLE and INSERT statements for the tables it needs, then runs its queries. Every run in the browser, and every run of sqlite3 :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 INTEGER columns, 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

The mart shop: four tables and their rows SQL · mart.sql
-- 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

  • 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.

Entity-relationship diagram of mart: customers, orders, order_items and products, with their keys and one-to-many links.customerscustomer_idINTEGERPKnameTEXTcityTEXTordersorder_idINTEGERPKcustomer_idINTEGERFKstatusTEXTorder_itemsorder_idINTEGERPK, FKproduct_idINTEGERPK, FKquantityINTEGERproductsproduct_idINTEGERPKnameTEXTcategoryTEXTprice_paiseINTEGER

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.

  1. 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.
  2. 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.
  3. 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

Every table and index in the database SQL · list_tables.sql
-- 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_items in an index it creates itself, called sqlite_autoindex_order_items_1. The other tables have none, because a single INTEGER PRIMARY KEY column is the row’s own number in SQLite (its rowid) and needs no separate index.
  • The sql column holds the statement that created each object, a copy of what you wrote with only small details tidied, such as the capitals of CREATE TABLE. Line breaks are kept, as the result shows. It is what the .schema command of the sqlite3 program 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:

The columns of orders and the links of order_items SQL · table_columns.sql
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

Two catalog queries that fail SQL · catalog_mistakes.sql
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 reads orders as the name of a column, which does not exist here. The function wants the table’s name as text: 'orders'.
  • information_schema is 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'; and SELECT * FROM pragma_table_info('orders');. In the sqlite3 program: .tables and .schema orders.
  • PostgreSQL: SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'; and SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'orders';. Tables you create without naming a schema go into the schema called public. In psql: \dt and \d orders.
  • MySQL: the same two queries, but table_schema is the name of the database, such as WHERE table_schema = 'practice'. MySQL also has statements of its own for this: SHOW TABLES; and DESCRIBE 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_schema and pragma_table_info('…') describe a SQLite database; PostgreSQL and MySQL use information_schema, plus \d in psql and SHOW TABLES or DESCRIBE in 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 as INTEGER or TEXT.

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.

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.

  1. Question 1 of 5 In mart, order 103 has customer_id 1. What does that 1 tell you?

    Choose one answer.

    Show the answer to question 1

    Answer: The order was placed by the customer whose customer_id is 1, Asha Kulkarni

    orders.customer_id is a foreign key: its value is the key of a row in customers. Look up customer_id 1 in customers and you find who placed the order. Asha in fact has two orders, 101 and 103.

  2. Question 2 of 5 In the diagram, the line between customers and orders has two bars at the customers end and a circle with a crow's foot at the orders end. What does it say?

    Choose one answer.

    Show the answer to question 2

    Answer: Each order belongs to exactly one customer, and a customer has zero or many orders

    Read each end of the line as the answer to "how many of these?". From an order, there is exactly one customer (the two bars). From a customer, there are zero or many orders (the circle and the crow's foot): Tenzin Dorjee has none, and Asha Kulkarni has two.

  3. Question 3 of 5 You add a fifth table to the database of list_tables.sql: CREATE TABLE payments (payment_id INTEGER PRIMARY KEY, order_id INTEGER NOT NULL, amount_paise INTEGER NOT NULL);. How many rows does SELECT type, name, tbl_name FROM sqlite_schema; return now?

    Type a number.

    Show the answer to question 3

    Answer: 6

    Five rows are tables, and one is the index SQLite made for the two-column primary key of order_items. The new table's key is a single INTEGER PRIMARY KEY column, which is the row's own number in SQLite, so it needs no index of its own: 5 tables plus 1 index make 6.

  4. Question 4 of 5 Which of these list the tables of a database?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • \dt in psql
    • SHOW TABLES; in MySQL
    • SELECT name FROM sqlite_schema WHERE type = 'table'; in SQLite
    • .tables in the sqlite3 program

    Each engine has its own catalog. SQLite keeps it in sqlite_schema, and the sqlite3 program adds .tables. PostgreSQL and MySQL have the standard information_schema views, plus \dt in psql and the SHOW TABLES statement in MySQL. SQLite has no information_schema, so the query on information_schema.tables fails there with "no such table".

  5. Question 5 of 5 Why does SELECT * FROM pragma_table_info(orders); fail, while pragma_table_info('orders') works?

    Choose one answer.

    Show the answer to question 5

    Answer: Without quotes, orders is read as the name of a column, but the function needs the table's name as text

    The argument of pragma_table_info() is a value: the name of the table, as text. Without single quotes, SQLite looks for a column called orders, finds none and stops with "no such column: orders", the same mistake as text without quotes in the first lesson.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.