Query_optimization
Chapter 31: Query Optimization Strategies
Section titled “Chapter 31: Query Optimization Strategies”Maximizing Database Performance
Section titled “Maximizing Database Performance”31.1 Query Optimization Overview
Section titled “31.1 Query Optimization Overview”Query optimization ensures your database queries run efficiently, minimizing resource usage and response time.
Query Optimization Layers ================================================================================
+------------------+ | Application | <-- Query structure, N+1 prevention +------------------+ | v +------------------+ | TypeORM | <-- QueryBuilder, relations, caching +------------------+ | v +------------------+ | Database | <-- Indexes, execution plan +------------------+ | v +------------------+ | Storage | <-- Disk I/O, memory +------------------+
Optimization Goals: - Reduce query execution time - Minimize memory usage - Reduce database load - Improve scalability
================================================================================31.2 Understanding Query Execution
Section titled “31.2 Understanding Query Execution”Analyzing Query Performance
Section titled “Analyzing Query Performance”// Enable query loggingawait dataSource.initialize({ // ... logging: true, // Log all queries logger: 'advanced-console', // Detailed logging});
// Analyze specific query with EXPLAINconst query = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.isActive = :active', { active: true }) .getSql();
// Run EXPLAIN ANALYZEconst analysis = await dataSource.query( `EXPLAIN ANALYZE ${query}`);
console.log(analysis);Query Execution Plan
Section titled “Query Execution Plan” PostgreSQL EXPLAIN Output ================================================================================
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
Result: +------------------------------------------------------------------+ | Index Scan using idx_user_email on users (cost=0.28..8.29 rows=1)| | Index Cond: (email = 'test@example.com'::text) | | Planning Time: 0.100 ms | | Execution Time: 0.025 ms | +------------------------------------------------------------------+
Key Metrics: - cost: Estimated computational cost - rows: Estimated rows returned - Planning Time: Time to create execution plan - Execution Time: Actual query execution time
================================================================================31.3 Index Optimization
Section titled “31.3 Index Optimization”Creating Indexes
Section titled “Creating Indexes”// Entity with indexes@Entity()@Index(['email']) // Single column index@Index(['firstName', 'lastName']) // Composite index@Index(['createdAt', 'status']) // Composite for common queriesexport class User { @PrimaryGeneratedColumn() id: number;
@Column() @Index() // Column-level index email: string;
@Column() firstName: string;
@Column() lastName: string;
@Column() status: string;
@CreateDateColumn() createdAt: Date;}
// Unique index@Entity()@Index(['email'], { unique: true })export class User { @Column() email: string;}
// Partial index (PostgreSQL)@Entity()@Index( ['status', 'createdAt'], { where: 'status = \'active\'' })export class User { @Column() status: string;
@CreateDateColumn() createdAt: Date;}Index Types
Section titled “Index Types” Index Types ================================================================================
+-------------------+------------------------------------------+ | Type | Use Case | +-------------------+------------------------------------------+ | B-Tree | Default, good for equality and range | | Hash | Equality comparisons only | | GiST | Geometric data, full-text search | | GIN | Array, JSON, full-text search | | BRIN | Large tables, ordered data | +-------------------+------------------------------------------+
When to Index: - Columns in WHERE clauses - Columns in JOIN conditions - Columns in ORDER BY - Columns in GROUP BY - Foreign key columns
When NOT to Index: - Small tables (< 1000 rows) - Columns with low selectivity - Columns that change frequently - Columns rarely used in queries
================================================================================Index via Migration
Section titled “Index via Migration”import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm';
export class AddUserIndexes1700000001 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // Single column index await queryRunner.createIndex( 'users', new TableIndex({ name: 'IDX_USER_EMAIL', columnNames: ['email'], isUnique: true, }), );
// Composite index await queryRunner.createIndex( 'users', new TableIndex({ name: 'IDX_USER_NAME_STATUS', columnNames: ['firstName', 'lastName', 'status'], }), );
// Partial index (PostgreSQL) await queryRunner.query(` CREATE INDEX IDX_USER_ACTIVE ON users (createdAt) WHERE status = 'active' `); }
public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropIndex('users', 'IDX_USER_ACTIVE'); await queryRunner.dropIndex('users', 'IDX_USER_NAME_STATUS'); await queryRunner.dropIndex('users', 'IDX_USER_EMAIL'); }}31.4 N+1 Problem Prevention
Section titled “31.4 N+1 Problem Prevention”Understanding N+1 Problem
Section titled “Understanding N+1 Problem” N+1 Problem Visualization ================================================================================
Problematic Code: const users = await userRepository.find(); // 1 query for (const user of users) { const posts = await postRepository.find({ // N queries! where: { author: { id: user.id } } }); }
Total Queries: 1 + N (where N = number of users)
Example with 100 users: - 1 query to get users - 100 queries to get posts for each user - Total: 101 queries!
================================================================================Solutions to N+1 Problem
Section titled “Solutions to N+1 Problem”// Solution 1: Eager Loading with relationsconst users = await userRepository.find({ relations: ['posts'],});
// Solution 2: QueryBuilder with joinconst users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .getMany();
// Solution 3: Use IN clause with mapconst users = await userRepository.find();const userIds = users.map(u => u.id);
const posts = await postRepository .createQueryBuilder('post') .where('post.authorId IN (:...userIds)', { userIds }) .getMany();
// Map posts to usersconst userPostsMap = new Map<number, Post[]>();posts.forEach(post => { if (!userPostsMap.has(post.authorId)) { userPostsMap.set(post.authorId, []); } userPostsMap.get(post.authorId)!.push(post);});
users.forEach(user => { user.posts = userPostsMap.get(user.id) || [];});
// Solution 4: Data Loader pattern (for GraphQL)import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds: number[]) => { const posts = await postRepository .createQueryBuilder('post') .where('post.authorId IN (:...userIds)', { userIds }) .getMany();
const postsMap = new Map<number, Post[]>(); posts.forEach(post => { if (!postsMap.has(post.authorId)) { postsMap.set(post.authorId, []); } postsMap.get(post.authorId)!.push(post); });
return userIds.map(id => postsMap.get(id) || []);});
// Usageconst posts = await postLoader.load(userId);31.5 Select Optimization
Section titled “31.5 Select Optimization”Select Only Required Columns
Section titled “Select Only Required Columns”// Bad: Select all columnsconst users = await userRepository.find();
// Good: Select only needed columnsconst users = await userRepository .createQueryBuilder('user') .select(['user.id', 'user.name', 'user.email']) .getMany();
// Using find optionsconst users = await userRepository.find({ select: ['id', 'name', 'email'],});
// Exclude sensitive columns@Entity()export class User { @Column({ select: false }) // Excluded by default password: string;}Limit Result Set
Section titled “Limit Result Set”// Paginationconst users = await userRepository.find({ skip: (page - 1) * limit, take: limit,});
// QueryBuilder paginationconst users = await userRepository .createQueryBuilder('user') .skip((page - 1) * limit) .take(limit) .getMany();
// Cursor-based pagination (more efficient for large datasets)const users = await userRepository .createQueryBuilder('user') .where('user.id > :cursor', { cursor: lastId }) .orderBy('user.id', 'ASC') .take(limit) .getMany();31.6 Join Optimization
Section titled “31.6 Join Optimization”Choose the Right Join Type
Section titled “Choose the Right Join Type” Join Types Comparison ================================================================================
+-------------------+------------------------------------------+ | Join Type | Use Case | +-------------------+------------------------------------------+ | INNER JOIN | Only matching rows from both tables | | LEFT JOIN | All from left, matching from right | | RIGHT JOIN | All from right, matching from left | | FULL JOIN | All rows from both tables | | CROSS JOIN | Cartesian product | +-------------------+------------------------------------------+
Performance Tips: - INNER JOIN is usually fastest - Use LEFT JOIN only when needed - Index join columns - Filter early in WHERE clause
================================================================================Optimized Join Examples
Section titled “Optimized Join Examples”// Inner join (faster when you only need matching records)const usersWithPosts = await userRepository .createQueryBuilder('user') .innerJoin('user.posts', 'post') .getMany();
// Left join with conditionconst usersWithPublishedPosts = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post', 'post.isPublished = :published', { published: true, }) .getMany();
// Join without selecting (for WHERE conditions only)const usersWithActivePosts = await userRepository .createQueryBuilder('user') .leftJoin('user.posts', 'post') .where('post.status = :status', { status: 'active' }) .select(['user.id', 'user.name']) .distinct(true) .getMany();
// Multiple joins optimizationconst results = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.profile', 'profile') .leftJoinAndSelect('user.posts', 'post') .leftJoin('post.comments', 'comment') // Don't select, just filter .where('comment.isApproved = :approved', { approved: true }) .getMany();31.7 Batch Operations
Section titled “31.7 Batch Operations”Batch Insert
Section titled “Batch Insert”// Bad: Insert one by onefor (const userData of usersData) { await userRepository.save(userData);}
// Good: Batch insertawait userRepository .createQueryBuilder() .insert() .values(usersData) .execute();
// Or using repositoryawait userRepository.save(usersData);
// Large batch: Insert in chunksconst chunkSize = 1000;for (let i = 0; i < usersData.length; i += chunkSize) { const chunk = usersData.slice(i, i + chunkSize); await userRepository.insert(chunk);}Batch Update
Section titled “Batch Update”// Bad: Update one by onefor (const user of users) { user.status = 'active'; await userRepository.save(user);}
// Good: Batch updateawait userRepository .createQueryBuilder() .update(User) .set({ status: 'active' }) .where('id IN (:...ids)', { ids: userIds }) .execute();
// Using CASE for different valuesawait userRepository .createQueryBuilder() .update(User) .set({ status: () => ` CASE WHEN id = 1 THEN 'active' WHEN id = 2 THEN 'inactive' ELSE 'pending' END `, }) .where('id IN (:...ids)', { ids: [1, 2, 3] }) .execute();Batch Delete
Section titled “Batch Delete”// Bad: Delete one by onefor (const id of userIds) { await userRepository.delete(id);}
// Good: Batch deleteawait userRepository .createQueryBuilder() .delete() .where('id IN (:...ids)', { ids: userIds }) .execute();
// Or using repositoryawait userRepository.delete(userIds);31.8 Query Caching
Section titled “31.8 Query Caching”Enable Query Caching
Section titled “Enable Query Caching”// Enable in DataSource configawait dataSource.initialize({ // ... cache: { type: 'redis', // or 'ioredis', 'database' options: { host: 'localhost', port: 6379, }, duration: 30000, // 30 seconds default },});
// Cache specific queryconst users = await userRepository .createQueryBuilder('user') .cache(true) // Use default cache duration .getMany();
// Cache with custom durationconst users = await userRepository .createQueryBuilder('user') .cache(60000) // 60 seconds .getMany();
// Cache with custom keyconst users = await userRepository .createQueryBuilder('user') .cache('users_active_list', 60000) .getMany();
// Clear cacheawait dataSource.queryResultCache.clear();await dataSource.queryResultCache.remove(['users_active_list']);Cache Service
Section titled “Cache Service”import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';
@Injectable()export class CacheService { constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
async getOrSet<T>( key: string, factory: () => Promise<T>, ttl: number = 30000, ): Promise<T> { const cached = await this.cacheManager.get<T>(key);
if (cached) { return cached; }
const value = await factory(); await this.cacheManager.set(key, value, ttl);
return value; }
async invalidate(pattern: string): Promise<void> { // For Redis-based cache await this.cacheManager.del(pattern); }}
// Usage in service@Injectable()export class UsersService { constructor( private cacheService: CacheService, private userRepository: Repository<User>, ) {}
async findOne(id: number): Promise<User> { return this.cacheService.getOrSet( `user:${id}`, () => this.userRepository.findOne({ where: { id } }), 60000, ); }
async update(id: number, data: UpdateUserDto): Promise<User> { const user = await this.userRepository.save({ id, ...data }); await this.cacheService.invalidate(`user:${id}`); return user; }}31.9 Connection Pool Optimization
Section titled “31.9 Connection Pool Optimization”Pool Configuration
Section titled “Pool Configuration”export default new DataSource({ type: 'postgres', // ... connection settings
// Connection pool settings poolSize: 20, // Number of connections in pool
// Advanced pool settings (for PostgreSQL) extra: { max: 20, // Maximum connections min: 5, // Minimum connections idleTimeoutMillis: 30000, // Close idle connections after 30s connectionTimeoutMillis: 2000, // Connection timeout acquireTimeoutMillis: 60000, // Acquire connection timeout },});Pool Sizing Guidelines
Section titled “Pool Sizing Guidelines” Connection Pool Sizing ================================================================================
Formula: Pool Size = (Core Count * 2) + Effective Spindle Count
For typical web application: - Development: 5-10 connections - Production: 20-50 connections - High-traffic: 50-100 connections
Considerations: - Database server limits - Memory per connection - Expected concurrent requests - Query execution time
Warning Signs: - Connection timeouts - "Too many connections" errors - Slow response times under load
================================================================================31.10 Query Optimization Checklist
Section titled “31.10 Query Optimization Checklist” Query Optimization Checklist ================================================================================
[ ] 1. Analyze slow queries with EXPLAIN ANALYZE [ ] 2. Add indexes on frequently queried columns [ ] 3. Use composite indexes for multi-column queries [ ] 4. Prevent N+1 queries with proper joins [ ] 5. Select only required columns [ ] 6. Implement pagination for large result sets [ ] 7. Use batch operations for bulk changes [ ] 8. Cache frequently accessed data [ ] 9. Configure connection pool appropriately [ ] 10. Monitor query performance regularly
================================================================================31.11 Summary
Section titled “31.11 Summary” Query Optimization Quick Reference +------------------------------------------------------------------+ | | | Technique | Benefit | | -------------------|------------------------------------------| | Indexing | Faster data lookup | | N+1 Prevention | Reduce query count | | Column Selection | Reduce data transfer | | Pagination | Limit result size | | Batch Operations | Reduce round trips | | Query Caching | Skip database entirely | | Connection Pool | Reuse connections | | | | Tools | Purpose | | -------------------|------------------------------------------| | EXPLAIN ANALYZE | Understand query plan | | Query Logging | Identify slow queries | | Performance Monitor| Track metrics over time | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 32: Indexing Strategies
Last Updated: February 2026