Skip to content
aviral gupta

// B1.4 · ~32 min · Beginner

Build: the library database

After this build you have the library database the rest of the course uses: books, members and loans, created with suitable types, loaded, and checked.

Lesson 4 of 4 in B1 Getting started

End of the module

You will be able to

  • Choose a type for each column of books, members and loans, and create the three tables
  • Load the rows with multi-row INSERT statements, writing NULL where a value is unknown
  • Check the load with SELECT * … ORDER BY and count(*), and make the script safe to run again
  1. Warm-up · Activity 1 of 7

    Warm-up: which type suits loaned_on, the day a book was lent?

  2. Predict · Activity 2 of 7

    Predict before you read on: three of these five loans have no return date. What does the last statement return?

    CREATE TABLE loans (id integer, loaned_on date, returned_on date);
    INSERT INTO loans VALUES
      (1, '2026-09-01', '2026-09-10'),
      (2, '2026-09-12', NULL),
      (3, '2026-09-05', '2026-09-20'),
      (4, '2026-09-15', NULL),
      (5, '2026-09-18', NULL);
    SELECT count(*) FROM loans;
  3. Practice · Activity 3 of 7

    Match each column to the type that fits it.

  4. Practice · Activity 4 of 7

    Linus has no email address. Fill in the value that says “unknown”.

    INSERT INTO members VALUES (3, 'Linus', ____, 'Berlin', '2026-02-10');
    INSERT INTO members VALUES (3, 'Linus', , 'Berlin', '2026-02-10');
  5. Practice · Activity 5 of 7

    Put the parts of a re-runnable build script in the order they must run.

    1. 1.INSERT INTO books VALUES (1, 'Dune', …), (2, 'Emma', …);
    2. 2.CREATE TABLE books (id integer, title text, …);
    3. 3.SELECT count(*) FROM books;
    4. 4.DROP TABLE IF EXISTS books;
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The loans table is empty. This INSERT has three rows, and the second has a day that does not exist. How many rows does loans hold afterwards?

    INSERT INTO loans VALUES
      (1, 1, 1, '2026-09-01', '2026-09-10'),
      (2, 3, 1, '2026-09-31', NULL),
      (3, 2, 2, '2026-09-05', '2026-09-20');
  7. Apply · Activity 7 of 7

    Mini-task. Start from the books script of the worked example. Add two books you like in one more INSERT with two rows, ids 7 and 8, and end with SELECT count(*) FROM books;. Run it: the count must be 8. Then leave out one comma between your two rows and read the error.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

The books table, built and checked

The first table of the library, the way the whole build works: drop the old table, create it with a type per column, load all rows in one INSERT, then look at them and count them. DROP TABLE IF EXISTS prints only a notice when there is no table yet, as on every run here. On your computer, save it as main.sql and run psql -X -q -v ON_ERROR_STOP=1 -f main.sql, as often as you like.

main.sql

-- The library, step 1: books.
DROP TABLE IF EXISTS books;

CREATE TABLE books (
  id integer,
  title text,
  author text,
  year integer,
  genre text,
  copies integer
);

INSERT INTO books VALUES
  (1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
  (2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
  (3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
  (4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
  (5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
  (6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);

SELECT * FROM books ORDER BY id;

SELECT count(*) AS books FROM books;

Run it with

psql -X -q -v ON_ERROR_STOP=1 -f main.sql

Output

 id |    title    |      author       | year |  genre  | copies
----+-------------+-------------------+------+---------+--------
  1 | Dune        | Frank Herbert     | 1965 | sci-fi  |      2
  2 | Emma        | Jane Austen       | 1815 | classic |      1
  3 | Kindred     | Octavia E. Butler | 1979 | sci-fi  |      1
  4 | Beloved     | Toni Morrison     | 1987 | fiction |      1
  5 | The Hobbit  | J. R. R. Tolkien  | 1937 | fantasy |      3
  6 | Neuromancer | William Gibson    | 1984 | sci-fi  |      1
(6 rows)

 books
-------
     6
(1 row)
  • One INSERT adds all six rows: a comma between the rows, one semicolon at the end.
  • id, year and copies are integer, so psql right-aligns them; text is left-aligned.
  • ORDER BY id shows the rows in id order; without it, SQL promises no order.
  • count(*) counts the rows: 6, as many as the INSERT added.
  • The notice from DROP TABLE IF EXISTS is not part of the result; psql prints it separately, as a message.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.

Exercises

Exercise 1 of 2

Step 2: the members

Create the table members with the columns id, name, email, city and joined, each with a suitable type, and load the four members listed in the starter in one INSERT. Linus has no email and Margaret no city: write NULL. End with SELECT * FROM members ORDER BY id;.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.

Hints
  1. Hint 1

    id is integer, joined is date, and the other three columns are text.

  2. Hint 2

    One INSERT INTO members VALUES with four rows in brackets, separated by commas. Dates go in quotes: '2025-01-15'.

  3. Hint 3

    Linus's row: (3, 'Linus', NULL, 'Berlin', '2026-02-10'). NULL has no quotes.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

CREATE TABLE members (
  id integer,
  name text,
  email text,
  city text,
  joined date
);

INSERT INTO members VALUES
  (1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
  (2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
  (3, 'Linus', NULL, 'Berlin', '2026-02-10'),
  (4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');

SELECT * FROM members ORDER BY id;
Run it on your computer

Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.sql

-- Create the table members (id, name, email, city, joined)
-- and add these four members in one INSERT:
--   1  Ada       ada@example.com       Berlin  2025-01-15
--   2  Grace     grace@example.com     Munich  2025-03-02
--   3  Linus     (no email)            Berlin  2026-02-10
--   4  Margaret  margaret@example.com  (no city)  2026-05-20
-- Then list them, ordered by id.

test.sql

-- test: members has the columns id (integer), name, email, city (text) and joined (date)
SELECT count(*) = 5 FROM information_schema.columns
WHERE table_name = 'members'
  AND ((column_name = 'id' AND data_type = 'integer')
    OR (column_name IN ('name', 'email', 'city') AND data_type = 'text')
    OR (column_name = 'joined' AND data_type = 'date'));

-- test: The table holds four members
SELECT count(*) = 4 FROM members;

-- test: Linus has no email (NULL, not the text 'NULL')
SELECT EXISTS (SELECT 1 FROM members WHERE id = 3 AND name = 'Linus' AND email IS NULL AND city = 'Berlin' AND joined = '2026-02-10');

-- test: Margaret has no city (NULL)
SELECT EXISTS (SELECT 1 FROM members WHERE id = 4 AND name = 'Margaret' AND email = 'margaret@example.com' AND city IS NULL AND joined = '2026-05-20');

-- test: The script lists the four members, ordered by id
-- output:
--  id |   name   |        email         |  city  |   joined
-- ----+----------+----------------------+--------+------------
--   1 | Ada      | ada@example.com      | Berlin | 2025-01-15
--   2 | Grace    | grace@example.com    | Munich | 2025-03-02
--   3 | Linus    |                      | Berlin | 2026-02-10
--   4 | Margaret | margaret@example.com |        | 2026-05-20
-- (4 rows)

psql runs the files on your own PostgreSQL server. Use an empty database you can throw away: add -d and its name to the command.

Run the program:

psql -X -q -v ON_ERROR_STOP=1 -f main.sql

There is no command for the checks on your computer yet. They are in test.sql: each check query prints t when it passes.

Exercise 2 of 2

Step 3: the loans, and a count of everything

setup.sql has created books and members. Create the table loans (id, book_id, member_id, loaned_on, returned_on) and load the five loans from the starter; the open ones have no return date. Then count the rows of all three tables, one query each, labelled books, members and loans with AS.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads PostgreSQL for your browser (up to 10.1 MB) and keeps it cached. Every run starts from an empty database. Your code stays on your device.

Hints
  1. Hint 1

    The three ids are integer; loaned_on and returned_on are date.

  2. Hint 2

    An open loan has NULL as its return date: (2, 3, 1, '2026-09-12', NULL).

  3. Hint 3

    SELECT count(*) AS books FROM books; then the same for members and loans.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

CREATE TABLE loans (
  id integer,
  book_id integer,
  member_id integer,
  loaned_on date,
  returned_on date
);

INSERT INTO loans VALUES
  (1, 1, 1, '2026-09-01', '2026-09-10'),
  (2, 3, 1, '2026-09-12', NULL),
  (3, 2, 2, '2026-09-05', '2026-09-20'),
  (4, 1, 3, '2026-09-15', NULL),
  (5, 6, 2, '2026-09-18', NULL);

SELECT count(*) AS books FROM books;
SELECT count(*) AS members FROM members;
SELECT count(*) AS loans FROM loans;
Run it on your computer

Install PostgreSQL 18 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.sql

-- setup.sql has created books and members.
-- Create loans (id, book_id, member_id, loaned_on, returned_on)
-- and add these five loans in one INSERT:
--   1  book 1  member 1  2026-09-01  2026-09-10
--   2  book 3  member 1  2026-09-12  (open)
--   3  book 2  member 2  2026-09-05  2026-09-20
--   4  book 1  member 3  2026-09-15  (open)
--   5  book 6  member 2  2026-09-18  (open)
-- Then count the rows of books, members and loans.

test.sql

-- test: loans has integer ids and two date columns
SELECT count(*) = 5 FROM information_schema.columns
WHERE table_name = 'loans'
  AND ((column_name IN ('id', 'book_id', 'member_id') AND data_type = 'integer')
    OR (column_name IN ('loaned_on', 'returned_on') AND data_type = 'date'));

-- test: The table holds five loans
SELECT count(*) = 5 FROM loans;

-- test: Loans 2, 4 and 5 are open (returned_on is NULL)
SELECT (SELECT count(*) FROM loans WHERE returned_on IS NULL AND id IN (2, 4, 5)) = 3
   AND (SELECT count(*) FROM loans WHERE returned_on IS NULL) = 3;

-- test: Loan 4 is book 1 lent to member 3 on 2026-09-15
SELECT EXISTS (SELECT 1 FROM loans WHERE id = 4 AND book_id = 1 AND member_id = 3 AND loaned_on = '2026-09-15');

-- test: The script prints the three counts: 6, 4 and 5
-- output:
--  books
-- -------
--      6
-- (1 row)
--
--  members
-- ---------
--        4
-- (1 row)
--
--  loans
-- -------
--      5
-- (1 row)

setup.sql

CREATE TABLE books (
  id integer,
  title text,
  author text,
  year integer,
  genre text,
  copies integer
);

INSERT INTO books VALUES
  (1, 'Dune', 'Frank Herbert', 1965, 'sci-fi', 2),
  (2, 'Emma', 'Jane Austen', 1815, 'classic', 1),
  (3, 'Kindred', 'Octavia E. Butler', 1979, 'sci-fi', 1),
  (4, 'Beloved', 'Toni Morrison', 1987, 'fiction', 1),
  (5, 'The Hobbit', 'J. R. R. Tolkien', 1937, 'fantasy', 3),
  (6, 'Neuromancer', 'William Gibson', 1984, 'sci-fi', 1);

CREATE TABLE members (
  id integer,
  name text,
  email text,
  city text,
  joined date
);

INSERT INTO members VALUES
  (1, 'Ada', 'ada@example.com', 'Berlin', '2025-01-15'),
  (2, 'Grace', 'grace@example.com', 'Munich', '2025-03-02'),
  (3, 'Linus', NULL, 'Berlin', '2026-02-10'),
  (4, 'Margaret', 'margaret@example.com', NULL, '2026-05-20');

psql runs the files on your own PostgreSQL server. Use an empty database you can throw away: add -d and its name to the command.

Run the program:

psql -X -q -v ON_ERROR_STOP=1 -f setup.sql -f main.sql

There is no command for the checks on your computer yet. They are in test.sql: each check query prints t when it passes.

Common mistakes

Running the script a second time

CREATE TABLE books (
  id integer,
  title text
);
INSERT INTO books VALUES (1, 'Dune'), (2, 'Emma');

What psql prints

ERROR:  relation "books" already exists

Why, and the fix

On your own database, the first run created books, and it is still there. A second CREATE TABLE books fails, and ON_ERROR_STOP ends the run. Start the script with DROP TABLE IF EXISTS books; so each run removes the old table first, then creates it anew.

'NULL' in quotes

CREATE TABLE loans (id integer, loaned_on date, returned_on date);
INSERT INTO loans VALUES
  (1, '2026-09-01', '2026-09-10'),
  (2, '2026-09-12', 'NULL');

What psql prints

ERROR:  invalid input syntax for type date: "NULL"

Why, and the fix

In quotes, 'NULL' is text, the four letters N, U, L, L, and a date column cannot read it as a day. Write NULL without quotes to say the value is unknown. Watch out in text columns: there 'NULL' is accepted and stored as a word, which later looks like a value.

A missing comma between rows

CREATE TABLE books (id integer, title text);
INSERT INTO books VALUES
  (1, 'Dune')
  (2, 'Emma');

What psql prints

ERROR:  syntax error at or near "("

Why, and the fix

The rows of a multi-row INSERT are separated by commas. Without one, PostgreSQL finishes reading (1, 'Dune') and does not expect another bracket, so it stops at the ( of the next row. Look at the end of the row before the one the error points to, and add the comma there.

PostgreSQL in the browser: PGlite 0.5.8 (PostgreSQL 18.3), Apache-2.0 and PostgreSQL License. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Three tables, one fact in each place

The library keeps three kinds of facts: books (title, author, year, genre, copies), members (name, email, city, the day they joined) and loans (which book went to which member, when, and when it came back). Each gets its own table, with an id column that numbers its rows. A loan does not repeat the title or the name: it stores book_id and member_id, the ids of the book and the member. Types follow the data: integer for ids, years and copies, text for names and titles, date for days. A column typed date accepts only real dates, so a mistake such as 2026-09-31 is refused when you insert it, not discovered months later.

Load many rows at once, and NULL for the unknown

One INSERT can add many rows: INSERT INTO books VALUES (…), (…), (…); with a comma between the rows and one semicolon at the end. Text and dates go in single quotes, dates in the unambiguous form '2026-09-01'. When a value is unknown, write NULL without quotes: Linus has no email, Margaret no city, and a loan that is still open has no return date. 'NULL' in quotes is the four letters N, U, L, L: a date column refuses it, and a text column would store it as a word. One INSERT is one statement: if one of its rows is wrong, none of them is added.

Check what you built, and make the script re-runnable

After loading, look: SELECT * FROM members ORDER BY id; shows every row in a fixed order, with NULL as an empty cell, and SELECT count(*) FROM loans; counts the rows (6 books, 4 members, 5 loans). On your computer, tables stay until you drop them, so a second run of the script stops at ERROR: relation "books" already exists. Start the script with DROP TABLE IF EXISTS books; for each table: it removes the old table, or, when there is none, only prints a notice. Save the script as library.sql and run it with psql -X -q -v ON_ERROR_STOP=1 -f library.sql whenever you want a fresh library.

Sources

Last reviewed September 30, 2026