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- 1
Which PostgreSQL data type stores variable-length character strings with no explicit length limit?
- 2
What does
SERIALdo in a PostgreSQL column definition? - 3
Which operator extracts a JSONB value as
jsonb(preserving type)? - 4
Which operator extracts a JSONB value as plain
text? - 5
What is the correct syntax to prevent duplicate inserts and instead do nothing on conflict in PostgreSQL?
- 6
Which PostgreSQL command shows the execution plan AND actually runs the query, reporting real timing?
- 7
Which constraint ensures a column value is unique across all rows but still allows multiple
NULLvalues? - 8
What does
ROLLBACKdo inside a PostgreSQL transaction block? - 9
Which data type is best for storing timezone-aware timestamps in PostgreSQL?
- 10
How do you create a non-unique index on the
emailcolumn of theuserstable? - 11
Which window function assigns sequential integers with no gaps and no ties — every row gets a unique number?
- 12
What is the purpose of the
PARTITION BYclause in a window function? - 13
In a recursive CTE, which SQL set operator connects the base case to the recursive case?
- 14
Which JSONB operator checks whether the left-hand jsonb value contains the right-hand jsonb value?
- 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
Which PostgreSQL feature restricts which rows a role can access in a table, without changing column-level privileges?
- 17
What type of column automatically recomputes and persists its value whenever the source columns change?
- 18
Which
EXPLAINoption also reports how many disk pages were read from the cache vs. fetched from disk? - 19
What is the key difference between a partial index and a regular index?
- 20
Which full-text search function is safest for accepting user-supplied search input without risking a syntax error?
Coding exercises
8 tasksCreate a Users Table
EasyCreate 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
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
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()
);Create a Table with a Foreign Key
EasyCreate 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
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
);Create an Index and Use EXPLAIN ANALYZE
Medium1. 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
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;Store and Query JSONB Metadata
Medium1. 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
-- 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';Upsert with ON CONFLICT and Transactions
HardWrap 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
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;Window Functions: Ranking and Running Totals
MediumGiven 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
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
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;Recursive CTE: Employee Hierarchy
HardGiven 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'.
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
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;Full-Text Search with a Generated tsvector Column
HardGiven 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.
-- 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
-- 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;