CPCODELAB
Practice

Practice PostgreSQL

You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.

Quick quiz

20 questions
Score: 0 / 20
0/20 answered
  1. 1

    Which PostgreSQL data type stores variable-length character strings with no explicit length limit?

  2. 2

    What does SERIAL do in a PostgreSQL column definition?

  3. 3

    Which operator extracts a JSONB value as jsonb (preserving type)?

  4. 4

    Which operator extracts a JSONB value as plain text?

  5. 5

    What is the correct syntax to prevent duplicate inserts and instead do nothing on conflict in PostgreSQL?

  6. 6

    Which PostgreSQL command shows the execution plan AND actually runs the query, reporting real timing?

  7. 7

    Which constraint ensures a column value is unique across all rows but still allows multiple NULL values?

  8. 8

    What does ROLLBACK do inside a PostgreSQL transaction block?

  9. 9

    Which data type is best for storing timezone-aware timestamps in PostgreSQL?

  10. 10

    How do you create a non-unique index on the email column of the users table?

  11. 11

    Which window function assigns sequential integers with no gaps and no ties — every row gets a unique number?

  12. 12

    What is the purpose of the PARTITION BY clause in a window function?

  13. 13

    In a recursive CTE, which SQL set operator connects the base case to the recursive case?

  14. 14

    Which JSONB operator checks whether the left-hand jsonb value contains the right-hand jsonb value?

  15. 15

    Which GIN operator class should you choose when your jsonb queries use only the @> containment operator and you want the smallest possible index?

  16. 16

    Which PostgreSQL feature restricts which rows a role can access in a table, without changing column-level privileges?

  17. 17

    What type of column automatically recomputes and persists its value whenever the source columns change?

  18. 18

    Which EXPLAIN option also reports how many disk pages were read from the cache vs. fetched from disk?

  19. 19

    What is the key difference between a partial index and a regular index?

  20. 20

    Which full-text search function is safest for accepting user-supplied search input without risking a syntax error?

🛠️

Coding exercises

8 tasks
1

Create a Users Table

Easy

Create a users table with the following columns: - id — auto-incrementing primary key - username — unique, not null, up to 50 chars - email — unique, not null - is_active — boolean, defaults to true - created_at — timezone-aware timestamp, defaults to the current time

Starter
sql
CREATE TABLE users (
  -- your columns here
);
💡 Show hint

Use SERIAL for the auto-incrementing id, VARCHAR(50) for username, TEXT for email, BOOLEAN for is_active, and TIMESTAMPTZ with DEFAULT NOW() for created_at.

✅ Show solution
sql
CREATE TABLE users (
  id          SERIAL PRIMARY KEY,
  username    VARCHAR(50)  UNIQUE NOT NULL,
  email       TEXT         UNIQUE NOT NULL,
  is_active   BOOLEAN      NOT NULL DEFAULT true,
  created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);
2

Create a Table with a Foreign Key

Easy

Create a posts table that references the users table from the previous exercise: - id — auto-incrementing primary key - user_id — foreign key referencing users(id), deletes cascade - title — not null text - body — text, nullable - published_at — timezone-aware timestamp, nullable

💡 Show hint

Use REFERENCES users(id) ON DELETE CASCADE for the foreign key constraint.

✅ Show solution
sql
CREATE TABLE posts (
  id            SERIAL PRIMARY KEY,
  user_id       INTEGER      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  title         TEXT         NOT NULL,
  body          TEXT,
  published_at  TIMESTAMPTZ
);
3

Create an Index and Use EXPLAIN ANALYZE

Medium

1. Create a non-unique index on posts.user_id to speed up lookups by user. 2. Create a non-unique index on posts.published_at to speed up date-range queries. 3. Run EXPLAIN ANALYZE on a query that fetches all posts for user_id = 1 ordered by published_at DESC. Write all three statements.

💡 Show hint

Use CREATE INDEX ON posts (column_name); for each index, then prefix your SELECT with EXPLAIN ANALYZE.

✅ Show solution
sql
CREATE INDEX ON posts (user_id);
CREATE INDEX ON posts (published_at);

EXPLAIN ANALYZE
SELECT *
FROM   posts
WHERE  user_id = 1
ORDER  BY published_at DESC;
4

Store and Query JSONB Metadata

Medium

1. Add a metadata column of type JSONB to the posts table (allow null). 2. Update the post with id = 1 to set metadata to {"tags": ["postgres", "sql"], "views": 42}. 3. Write a query that selects the title and the views value (as text) from all posts where the metadata contains the key "views". Use the JSONB operators -> and ->>.

💡 Show hint

Use ALTER TABLE to add the column. Use @> or ? to test key existence, and ->> to extract views as text.

✅ Show solution
sql
-- Step 1: add the column
ALTER TABLE posts ADD COLUMN metadata JSONB;

-- Step 2: set metadata for post 1
UPDATE posts
SET    metadata = '{"tags": ["postgres", "sql"], "views": 42}'::jsonb
WHERE  id = 1;

-- Step 3: query posts that have a 'views' key
SELECT title,
       metadata ->> 'views' AS views
FROM   posts
WHERE  metadata ? 'views';
5

Upsert with ON CONFLICT and Transactions

Hard

Wrap the following operations in a single transaction: 1. Insert a new user with username = 'alice' and email = 'alice@example.com'. If a user with that email already exists, update username to the new value and set is_active = true. 2. Insert a post with title = 'Hello World' for the user whose email = 'alice@example.com'. 3. If any step fails, roll back everything. Write the complete transaction block.

💡 Show hint

Use BEGIN / COMMIT. For the upsert, use INSERT INTO users ... ON CONFLICT (email) DO UPDATE SET .... Use a subquery with SELECT id FROM users WHERE email = ... for the post insert.

✅ Show solution
sql
BEGIN;

-- Upsert the user
INSERT INTO users (username, email, is_active)
VALUES ('alice', 'alice@example.com', true)
ON CONFLICT (email)
DO UPDATE SET
  username  = EXCLUDED.username,
  is_active = true;

-- Insert the post referencing the upserted user
INSERT INTO posts (user_id, title)
SELECT id, 'Hello World'
FROM   users
WHERE  email = 'alice@example.com';

COMMIT;
6

Window Functions: Ranking and Running Totals

Medium

Given a table orders(id, customer_id, total, created_at), write a single query that returns: - customer_id - order_id (the id column) - total - rank_by_total — the rank of each order within its customer, ordered by total DESC (ties share the same rank) - running_total — cumulative sum of total for that customer, ordered by created_at ASC

Starter
sql
SELECT
  customer_id,
  id AS order_id,
  total,
  -- rank_by_total here
  -- running_total here
FROM orders
ORDER BY customer_id, created_at;
💡 Show hint

Use RANK() OVER (PARTITION BY customer_id ORDER BY total DESC) and SUM(total) OVER (PARTITION BY customer_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW).

✅ Show solution
sql
SELECT
  customer_id,
  id AS order_id,
  total,
  RANK() OVER (PARTITION BY customer_id ORDER BY total DESC) AS rank_by_total,
  SUM(total) OVER (
    PARTITION BY customer_id
    ORDER BY created_at
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total
FROM orders
ORDER BY customer_id, created_at;
7

Recursive CTE: Employee Hierarchy

Hard

Given a table employees(id, name, manager_id) where manager_id is NULL for the CEO, write a recursive CTE that returns every employee under manager id = 1 (inclusive), along with their depth (0 for the root) and a path string like 'Alice > Bob > Carol'.

Starter
sql
WITH RECURSIVE hierarchy AS (
  -- base case
  
  UNION ALL
  
  -- recursive case
)
SELECT depth, path FROM hierarchy ORDER BY path;
💡 Show hint

Base case: SELECT id, name, manager_id, 0 AS depth, name AS path FROM employees WHERE id = 1. Recursive case: JOIN employees on manager_id = hierarchy.id and build the path with ||.

✅ Show solution
sql
WITH RECURSIVE hierarchy AS (
  SELECT id, name, manager_id, 0 AS depth, name::text AS path
  FROM employees
  WHERE id = 1

  UNION ALL

  SELECT e.id, e.name, e.manager_id, h.depth + 1, h.path || ' > ' || e.name
  FROM employees e
  JOIN hierarchy h ON h.id = e.manager_id
)
SELECT depth, path FROM hierarchy ORDER BY path;
8

Full-Text Search with a Generated tsvector Column

Hard

Given a posts(id, title, body) table: 1. Add a stored generated column fts of type tsvector that indexes both title and body using the 'english' configuration. 2. Create a GIN index on the new column. 3. Write a query that returns id and title for all posts matching the phrase 'postgres index', ordered by ts_rank descending.

Starter
sql
-- Step 1: add generated column

-- Step 2: create GIN index

-- Step 3: search query
💡 Show hint

Use GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''))) STORED. Use plainto_tsquery('english', 'postgres index') for the search query and ts_rank(fts, query) for ranking.

✅ Show solution
sql
-- Step 1: add generated column
ALTER TABLE posts
  ADD COLUMN fts tsvector
    GENERATED ALWAYS AS (
      to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
    ) STORED;

-- Step 2: create GIN index
CREATE INDEX idx_posts_fts ON posts USING GIN (fts);

-- Step 3: search query
SELECT id, title,
       ts_rank(fts, query) AS rank
FROM posts,
     plainto_tsquery('english', 'postgres index') AS query
WHERE fts @@ query
ORDER BY rank DESC;