'use strict'; const express = require('express'); const { Pool } = require('pg'); const app = express(); const PORT = process.env.PORT || 3000; const pool = new Pool({ host: process.env.PG_HOST || 'localhost', port: parseInt(process.env.PG_PORT || '5432', 10), user: process.env.PG_USER || 'demo', password: process.env.PG_PASSWORD || 'demo', database: process.env.PG_DATABASE || 'demo', }); // --- Database seeding --- async function seedDatabase() { const client = await pool.connect(); try { const tableCheck = await client.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'users' ) `); if (tableCheck.rows[0].exists) { const countResult = await client.query('SELECT COUNT(*) FROM users'); if (parseInt(countResult.rows[0].count, 10) > 0) { console.log('Database already seeded, skipping.'); return; } } console.log('Seeding database...'); await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(150) NOT NULL, created_at TIMESTAMP DEFAULT NOW() ) `); await client.query(` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), title VARCHAR(200) NOT NULL, body TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW() ) `); // Insert 50 users for (let i = 1; i <= 50; i++) { await client.query( 'INSERT INTO users (name, email) VALUES ($1, $2)', [`User ${i}`, `user${i}@example.com`] ); } // Insert 10 posts per user (500 posts total) for (let userId = 1; userId <= 50; userId++) { for (let p = 1; p <= 10; p++) { await client.query( 'INSERT INTO posts (user_id, title, body) VALUES ($1, $2, $3)', [userId, `Post ${p} by User ${userId}`, `Content of post ${p} by user ${userId}. Lorem ipsum dolor sit amet.`] ); } } console.log('Database seeded: 50 users, 500 posts.'); } finally { client.release(); } } // --- Routes --- app.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); // Fast route: single query, returns 10 users app.get('/fast', async (_req, res) => { const start = Date.now(); try { const result = await pool.query('SELECT * FROM users LIMIT 10'); const duration = Date.now() - start; res.json({ route: '/fast', description: 'Single query - SELECT users LIMIT 10', query_count: 1, duration_ms: duration, data: result.rows, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Slow route: N+1 query pattern app.get('/slow', async (_req, res) => { const start = Date.now(); try { const usersResult = await pool.query('SELECT * FROM users'); const users = usersResult.rows; let queryCount = 1; const usersWithPosts = []; for (const user of users) { const postsResult = await pool.query( 'SELECT * FROM posts WHERE user_id = $1', [user.id] ); queryCount++; usersWithPosts.push({ ...user, posts: postsResult.rows, }); } const duration = Date.now() - start; res.json({ route: '/slow', description: 'N+1 pattern - 1 query for users + 1 query per user for posts', query_count: queryCount, user_count: users.length, total_posts: usersWithPosts.reduce((sum, u) => sum + u.posts.length, 0), duration_ms: duration, data: usersWithPosts, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Fixed route: single JOIN query app.get('/fixed', async (_req, res) => { const start = Date.now(); try { const result = await pool.query(` SELECT u.id AS user_id, u.name, u.email, u.created_at AS user_created_at, p.id AS post_id, p.title, p.body, p.created_at AS post_created_at FROM users u LEFT JOIN posts p ON p.user_id = u.id ORDER BY u.id, p.id `); // Group results by user const usersMap = new Map(); for (const row of result.rows) { if (!usersMap.has(row.user_id)) { usersMap.set(row.user_id, { id: row.user_id, name: row.name, email: row.email, created_at: row.user_created_at, posts: [], }); } if (row.post_id) { usersMap.get(row.user_id).posts.push({ id: row.post_id, title: row.title, body: row.body, created_at: row.post_created_at, }); } } const usersWithPosts = Array.from(usersMap.values()); const duration = Date.now() - start; res.json({ route: '/fixed', description: 'Single JOIN query - the correct way', query_count: 1, user_count: usersWithPosts.length, total_posts: usersWithPosts.reduce((sum, u) => sum + u.posts.length, 0), duration_ms: duration, data: usersWithPosts, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // --- Startup --- async function main() { // Wait for PostgreSQL to be ready (with retries) let retries = 10; while (retries > 0) { try { await pool.query('SELECT 1'); console.log('Connected to PostgreSQL.'); break; } catch (err) { retries--; if (retries === 0) { console.error('Failed to connect to PostgreSQL after retries:', err.message); process.exit(1); } console.log(`Waiting for PostgreSQL... (${retries} retries left)`); await new Promise((resolve) => setTimeout(resolve, 3000)); } } await seedDatabase(); app.listen(PORT, () => { console.log(`Demo app listening on port ${PORT}`); }); } main().catch((err) => { console.error('Fatal error:', err); process.exit(1); });