Reading Data with SELECT
Reading data is the most common database operation. This lesson covers the SELECT queries that power the Bulletin API’s feed, post detail, and user profile pages.
Basic SELECT
Section titled “Basic SELECT”-- All columns, all rowsSELECT * FROM posts;
-- Specific columnsSELECT id, title, created_at FROM posts;
-- With better-sqlite3const posts = db.prepare('SELECT id, title, created_at FROM posts').all()Prefer selecting specific columns over * — it’s more explicit and prevents accidentally leaking sensitive data like password_hash.
WHERE clause
Section titled “WHERE clause”Filter rows by condition:
SELECT * FROM posts WHERE user_id = 3;SELECT * FROM users WHERE username = 'alice';SELECT * FROM posts WHERE upvotes > 10;const userPosts = db.prepare('SELECT * FROM posts WHERE user_id = ?').all(userId)const user = db.prepare('SELECT id, username, created_at FROM users WHERE username = ?').get(username)ORDER BY
Section titled “ORDER BY”Sort results:
SELECT * FROM posts ORDER BY created_at DESC; -- newest firstSELECT * FROM posts ORDER BY upvotes DESC; -- most upvoted firstSELECT * FROM posts ORDER BY created_at ASC; -- oldest firstconst sort = req.query.sort === 'top' ? 'upvotes DESC' : 'created_at DESC'// Note: don't use req.query values directly in SQL strings// This is safe because 'upvotes DESC' and 'created_at DESC' are hardcodedconst posts = db.prepare(`SELECT * FROM posts ORDER BY ${sort}`).all()LIMIT and OFFSET (pagination)
Section titled “LIMIT and OFFSET (pagination)”SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 0; -- page 1SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 20; -- page 2const limit = 20const page = parseInt(req.query.page as string || '1', 10)const offset = (page - 1) * limit
const posts = db.prepare( 'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset)JOIN — combining tables
Section titled “JOIN — combining tables”The Bulletin feed needs the author’s username alongside each post:
SELECT posts.id, posts.title, posts.body, posts.upvotes, posts.created_at, users.username AS author_usernameFROM postsJOIN users ON posts.user_id = users.idORDER BY posts.created_at DESC;const posts = db.prepare(` SELECT posts.id, posts.title, posts.body, posts.upvotes, posts.created_at, users.username AS author_username FROM posts JOIN users ON posts.user_id = users.id ORDER BY posts.created_at DESC LIMIT ? OFFSET ?`).all(limit, offset)Count rows without fetching all the data:
SELECT COUNT(*) as total FROM posts WHERE user_id = 3;const result = db.prepare( 'SELECT COUNT(*) as total FROM posts WHERE user_id = ?').get(userId) as { total: number }
console.log(result.total) // 42Checking if a row exists
Section titled “Checking if a row exists”For things like “has this user upvoted this post”:
const upvote = db.prepare( 'SELECT 1 FROM post_upvotes WHERE user_id = ? AND post_id = ?').get(userId, postId)
const hasUpvoted = upvote !== undefinedExercise
Section titled “Exercise”- Write a query that returns all posts by a specific user, newest first.
- Write a query that returns a single post with the author’s username (using JOIN).
- Write a query that returns all comments for a post, joined with the commenter’s username.
- Test these queries in the REPL or a test script.
SELECT columns FROM table— basic read.WHERE condition— filter rows.ORDER BY column ASC|DESC— sort results.LIMIT n OFFSET m— paginate results.JOIN table ON condition— combine rows from multiple tables.COUNT(*)— count rows efficiently.- Always parameterize user-supplied values; hardcoded sort strings are safe in template literals.