DEV Community

Mohammad Waseem
Mohammad Waseem

Posted on

Optimizing Slow Database Queries in Legacy Codebases with TypeScript

In the landscape of legacy systems, performance bottlenecks often manifest as slow, inefficient database queries that degrade overall application responsiveness. As a security researcher transitioning into a senior developer role, I encountered a common challenge: optimizing these sluggish queries while maintaining stability and security.

Understanding the Legacy Code Context

Legacy codebases are often characterized by their outdated practices, lack of proper documentation, and tightly coupled components. Before jumping into optimization, it's crucial to understand the existing data access patterns. In this scenario, the codebase was primarily using raw SQL queries embedded within TypeScript services, with little consideration for query performance.

Profiling and Identifying Bottlenecks

The first step involves profiling the application to pinpoint the slow queries. Tools like the node-mysql2 driver with query logging or application performance monitoring (APM) tools such as New Relic or DataDog can help. For example, enabling query logging in mysql2:

import mysql from 'mysql2/promise';

const connection = await mysql.createConnection({host: 'localhost', user: 'root', database: 'legacy_db'});

// Log all queries
connection.on('enqueue', (sequence) => {
  if (sequence.sql) {
    console.log('Executing query:', sequence.sql);
  }
});
Enter fullscreen mode Exit fullscreen mode

Once identified, focus on the most frequent or time-consuming queries.

Refactoring and Optimizing Queries

One effective approach is rewriting queries for efficiency. It’s often helpful to analyze query plans and identify missing indexes or suboptimal joins. Here's an example of improving a query in TypeScript:

// Original query
const [rows] = await connection.execute(
  `SELECT * FROM transactions WHERE user_id = ? AND status = 'completed'`,
  [userId]
);
Enter fullscreen mode Exit fullscreen mode

Optimized version by adding an index and rewriting with explicit fields:

-- Adding index for faster lookups
CREATE INDEX idx_user_id_status ON transactions(user_id, status);

-- Revised query for better performance
const [rows] = await connection.execute(
  `SELECT id, amount, date FROM transactions WHERE user_id = ? AND status = 'completed'`,
  [userId]
);
Enter fullscreen mode Exit fullscreen mode

In code, this would be:

const [rows] = await connection.execute<Transaction[]>(
  `SELECT id, amount, date FROM transactions WHERE user_id = ? AND status = 'completed'`,
  [userId]
);
Enter fullscreen mode Exit fullscreen mode

Leveraging TypeScript for Safety and Efficiency

TypeScript provides an extra layer of safety and clarity. Define interfaces for your data models:

interface Transaction {
  id: number;
  amount: number;
  date: string;
}
Enter fullscreen mode Exit fullscreen mode

And use parameterized queries to prevent SQL injection:

const userIdParam = 123;
const [transactions] = await connection.execute<Transaction[]>(
  `SELECT id, amount, date FROM transactions WHERE user_id = ?`,
  [userIdParam]
);
Enter fullscreen mode Exit fullscreen mode

This helps ensure only sanitized inputs are processed.

Implementing Query Caching and Pagination

For frequently accessed data, consider caching results at the application layer or using Redis. Additionally, applying pagination can reduce data load:

const pageSize = 50;
const pageNumber = 2;

const [rows] = await connection.execute(
  `SELECT id, amount, date FROM transactions WHERE user_id = ? LIMIT ? OFFSET ?`,
  [userId, pageSize, pageSize * (pageNumber - 1)]
);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Optimizing slow queries in legacy TypeScript code requires a systematic approach: profiling, analyzing, and iteratively refactoring. Incorporating index optimization, parameterized queries, and caching strategies significantly improves performance while maintaining security. As a security researcher turned developer, understanding both the data security implications and performance considerations enables crafting resilient, efficient solutions for legacy systems.

Continual monitoring and incremental improvements are vital. Remember, small, data-driven optimizations can lead to substantial performance gains in complex, legacy environments.


🛠️ QA Tip

To test this safely without using real user data, I use TempoMail USA.

Top comments (0)