For DevelopersJanuary 28, 2025

How to Identify and Optimize Long-Running Queries in Java

Discover proven methods to identify, analyze, and optimize long-running queries in Java, ensuring better system reliability.

In modern software systems, performance is paramount. Long-running queries in Java applications can degrade performance, lead to resource contention, and negatively impact user experience. Identifying and understanding these queries is crucial for optimizing database interactions and ensuring system reliability.

This article delves into techniques to identify and analyze long-running queries in Java, employing industry best practices and tools. From using APM integrations to leveraging database-specific features, this guide aims to equip you with the skills to diagnose and address performance bottlenecks effectively.

Join Index.dev’s talent network to work on challenging Java projects with global companies while building your remote tech career.

 

What Are Long-Running Queries?

Long-running queries are database operations that exceed expected execution time thresholds, typically caused by:

  • Suboptimal query plans
  • Missing or inappropriate indexes
  • Large result sets
  • Resource contention
  • Lock wait scenarios
  • Complex joins or subqueries
  • Unoptimized SQL
  • Excessive data retrieval

They can lead to timeouts, high memory usage, and degraded performance in Java applications.

 

Critical Impact Areas

  1. System Performance
    • Connection pool exhaustion
    • Thread blocking
    • Increased memory pressure
  2. Resource Utilization
    • CPU saturation
    • I/O bottlenecks
    • Memory consumption patterns
  3. User Experience
    • Response time degradation
    • Transaction timeouts
    • Service availability issues

 

Method 1: Using Advanced Query Timeout Management

Unlike simple timeout implementations, enterprise applications require sophisticated timeout handling with proper resource management and performance monitoring. The QueryTimeoutMonitor implementation leverages JDBC's Statement interface to provide comprehensive control over query execution timing while ensuring proper resource cleanup and performance tracking.

Problem Tackled

Sometimes, database queries run indefinitely due to inefficient SQL, database locks, or other unforeseen circumstances which lead to cascading failures. Traditional query timeout implementations often lead to resource leaks and lack proper monitoring capabilities, making it difficult to manage long-running queries effectively in production environments.

Solution Derived

Implement a comprehensive timeout management system that combines resource tracking, performance monitoring, and proper cleanup mechanisms using JDBC's advanced features.

Implementation

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class QueryTimeoutMonitor {
    private static final Logger logger = LoggerFactory.getLogger(QueryTimeoutMonitor.class);
    private static final int DEFAULT_TIMEOUT_SECONDS = 5;
    
    private final String jdbcUrl;
    private final String username;
    private final String password;
    private final MetricsCollector metricsCollector;
    
    public QueryTimeoutMonitor(String jdbcUrl, String username, String password) {
        this.jdbcUrl = jdbcUrl;
        this.username = username;
        this.password = password;
        this.metricsCollector = new MetricsCollector();
    }
    
    public ResultSet executeQueryWithTimeout(String query, int timeoutSeconds) throws SQLException {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        
        long startTime = System.nanoTime();
        try {
            conn = DatabaseConnectionPool.getConnection(jdbcUrl, username, password);
            stmt = conn.prepareStatement(query,
                    ResultSet.TYPE_FORWARD_ONLY,
                    ResultSet.CONCUR_READ_ONLY);
            
            stmt.setQueryTimeout(timeoutSeconds);
            stmt.setFetchSize(1000);
            
            rs = stmt.executeQuery();
            
            long executionTime = (System.nanoTime() - startTime) / 1_000_000;
            metricsCollector.recordQueryExecution(query, executionTime);
            
            return rs;
        } catch (SQLException e) {
            logger.error("Query execution failed after {}ms: {} - Error: {}", 
                (System.nanoTime() - startTime) / 1_000_000, query, e.getMessage());
            throw new QueryExecutionException("Query execution failed", e, query);
        } finally {
            if (rs != null && rs.isClosed()) {
                closeResources(conn, stmt, rs);
            }
        }
    }
    
    private void closeResources(Connection conn, PreparedStatement stmt, ResultSet rs) {
        try {
            if (rs != null) rs.close();
            if (stmt != null) stmt.close();
            if (conn != null) conn.close();
        } catch (SQLException e) {
            logger.error("Error closing resources: {}", e.getMessage());
        }
    }
}

Explanation

This implementation optimizes memory usage through TYPE_FORWARD_ONLY result sets and controlled fetch sizes. The MetricsCollector tracks query performance using System.nanoTime() for precise timing. Connection pooling minimizes overhead in high-throughput scenarios. Particularly useful for enterprise applications requiring detailed performance monitoring and resource management.

 

Method 2: Logging SQL Queries with Execution Time

Database performance monitoring requires comprehensive visibility into query execution patterns and timing. By implementing sophisticated logging mechanisms, developers can track and analyze query performance without significantly impacting system resources. For more information about advanced logging best practices, refer to Oracle's Java Logging Guide.

Problem Tackled

In large systems, identifying slow queries can be challenging without visibility into query execution times. Traditional logging approaches lack the granularity needed to identify problematic queries effectively, especially in production environments where performance patterns may vary significantly. However, by implementing logging techniques that capture execution times, developers can gain initial insights into query performance.

Solution Derived

Implement a robust query logging system that captures execution times, normalizes queries for pattern recognition, and provides detailed performance metrics while maintaining minimal overhead.

Implementation

public class QueryPerformanceLogger {
    private static final Logger logger = LoggerFactory.getLogger(QueryPerformanceLogger.class);
    private static final ConcurrentHashMap<String, QueryMetrics> queryMetrics = new ConcurrentHashMap<>();
    
    public static void logQueryPerformance(String query, Connection conn, long thresholdMs) 
            throws SQLException {
        String normalizedQuery = QueryNormalizer.normalize(query);
        Instant startTime = Instant.now();
        
        try (PreparedStatement stmt = conn.prepareStatement(query);
             ResultSet rs = stmt.executeQuery()) {
            
            long executionTime = Duration.between(startTime, Instant.now()).toMillis();
            
            QueryMetrics metrics = queryMetrics.computeIfAbsent(
                normalizedQuery, 
                k -> new QueryMetrics()
            );
            metrics.updateMetrics(executionTime);
            
            if (executionTime > thresholdMs) {
                handleSlowQuery(conn, query, executionTime, metrics);
            }
        }
    }
    
    private static void handleSlowQuery(Connection conn, String query, 
            long executionTime, QueryMetrics metrics) throws SQLException {
        String explainPlan = getQueryExplainPlan(conn, query);
        
        logger.warn("Slow Query Detected:\n" +
                   "Execution Time: {}ms\n" +
                   "Query: {}\n" +
                   "Avg Time: {}ms\n" +
                   "Execution Count: {}\n" +
                   "Explain Plan:\n{}",
                   executionTime,
                   query,
                   metrics.getAverageExecutionTime(),
                   metrics.getExecutionCount(),
                   explainPlan);
    }
}

Explanation

The implementation uses ConcurrentHashMap for thread-safe metrics collection and Duration for precise timing measurements. The QueryNormalizer helps identify similar query patterns, while automatic EXPLAIN plan generation provides insights into performance bottlenecks. This solution is particularly valuable for production monitoring and performance optimization.

Read More: How to Build a REST API using Java Spring Boot

 

Method 3: Database-Specific Performance Monitoring

Performance optimization often requires leveraging database-specific features and tools. By implementing a flexible monitoring system that adapts to different database engines, developers can utilize native performance features while maintaining application portability. The Java Database Connectivity API, outlined in Oracle's JDBC Guide, provides the foundation for this implementation.

Problem Tackled

Sometimes, query optimization needs a deeper understanding of the database itself. Generic monitoring approaches miss optimization opportunities specific to different database systems, leading to suboptimal performance in production environments.

Solution Derived

Create an extensible monitoring system that leverages database-specific features while maintaining a consistent interface for the application. 

Implementation

public class DatabasePerformanceMonitor {
    private final DatabaseType databaseType;
    private final Connection connection;
    private final PerformanceAnalyzer analyzer;
    
    public DatabasePerformanceMonitor(DatabaseType databaseType, Connection connection) {
        this.databaseType = databaseType;
        this.connection = connection;
        this.analyzer = PerformanceAnalyzerFactory.create(databaseType);
    }
    
    public List<SlowQueryInfo> analyzeSlowQueries() throws SQLException {
        return analyzer.getSlowQueries(connection);
    }
    
    public QueryOptimizationReport optimizeQuery(String query) throws SQLException {
        return analyzer.suggestOptimizations(connection, query);
    }
}

interface PerformanceAnalyzer {
    List<SlowQueryInfo> getSlowQueries(Connection conn) throws SQLException;
    QueryOptimizationReport suggestOptimizations(Connection conn, String query) 
            throws SQLException;
}

class MySQLPerformanceAnalyzer implements PerformanceAnalyzer {
    @Override
    public List<SlowQueryInfo> getSlowQueries(Connection conn) throws SQLException {
        try (PreparedStatement stmt = conn.prepareStatement(
                "SELECT * FROM performance_schema.events_statements_summary_by_digest " +
                "WHERE avg_timer_wait > ?")) {
            stmt.setLong(1, getSlowQueryThreshold());
            return processSlowQueries(stmt.executeQuery());
        }
    }
    
    @Override
    public QueryOptimizationReport suggestOptimizations(Connection conn, String query) 
            throws SQLException {
        try (PreparedStatement stmt = conn.prepareStatement("EXPLAIN ANALYZE " + query)) {
            return analyzeExplainPlan(stmt.executeQuery());
        }
    }
}

Explanation

The implementation uses the Factory pattern to create database-specific analyzers. It leverages native performance monitoring features like MySQL's performance_schema and EXPLAIN ANALYZE. The QueryOptimizationReport provides actionable insights for query optimization.

For more on enabling slow query logs, see the MySQL slow query log documentation.

 

Method 4: Enterprise-Grade APM Integration for Query Performance Analysis

Unlike traditional debugging approaches, modern enterprise applications require sophisticated real-time performance monitoring that can handle complex microservice architectures and high-throughput environments. This implementation leverages APM tools with custom instrumentation to provide granular insights into query performance patterns.

Problem Tackled

Enterprise applications often suffer from performance bottlenecks that are difficult to diagnose due to complex interaction patterns between application layers and databases. Traditional logging and profiling methods may miss critical context about system state and resource utilization during query execution.

Solution Derived

Implement a comprehensive APM integration layer that combines custom instrumentation with professional monitoring tools, providing real-time visibility into query performance while maintaining minimal overhead.

Implementation

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EnterpriseQueryMonitor {
    private static final Logger logger = LoggerFactory.getLogger(EnterpriseQueryMonitor.class);
    private final MeterRegistry meterRegistry;
    private final QueryAnalyzer queryAnalyzer;
    private final Map<String, Timer> queryTimers;

    public EnterpriseQueryMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.queryAnalyzer = new QueryAnalyzer();
        this.queryTimers = new ConcurrentHashMap<>();
    }

    @Instrumented(metricName = "query.execution")
    public <T> T monitorQuery(String queryId, String sql, Supplier<T> queryExecution) {
        // Create or get existing timer for this query pattern
        Timer timer = queryTimers.computeIfAbsent(queryId, 
            id -> Timer.builder("query.execution")
                      .tag("query_id", id)
                      .tag("query_type", queryAnalyzer.determineQueryType(sql))
                      .register(meterRegistry));
        
        // Record query execution time and metadata
        Timer.Sample sample = Timer.start(meterRegistry);
        try {
            T result = queryExecution.get();
            sample.stop(timer);
            return result;
        } catch (Exception e) {
            timer.record(sample.stop(timer));
            logger.error("Query execution failed: {}", queryId, e);
            throw e;
        }
    }

    // Analyze query patterns and performance characteristics
    private static class QueryAnalyzer {
        public String determineQueryType(String sql) {
            // Implement query pattern recognition logic
            return sql.trim().toLowerCase()
                     .replaceAll("\\s+", " ")
                     .startsWith("select") ? "READ" : "WRITE";
        }
    }
}

Explanation

This implementation uses the Micrometer metrics facade for vendor-neutral monitoring integration. The EnterpriseQueryMonitor uses Timer and Sample for high-precision timing while keeping overhead minimal. The concurrent hash map ensures thread-safe timer management for high-throughput scenarios. The @Instrumented annotation enables seamless integration with APM tools that support aspect-oriented monitoring.

 

Method 5: Dynamic Query Analysis with Event-Driven Monitoring

Modern enterprise applications need real-time visibility into query performance patterns while maintaining separation of concerns. This implementation provides a flexible, event-driven approach to query monitoring that can adapt to different monitoring requirements and database systems.

Problem Tackled

In complex applications, there is a pertinent need to implement query monitoring solutions that can scale across microservices while maintaining low overhead and providing actionable insights. Traditional listeners often lack the context needed for effective analysis.

Solution Derived

Create an event-driven monitoring system that combines the Observer pattern with structured logging to provide comprehensive query analysis capabilities.

Implementation

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EnterpriseQueryMonitor {
    private static final Logger logger = LoggerFactory.getLogger(EnterpriseQueryMonitor.class);
    private final MeterRegistry meterRegistry;
    private final QueryAnalyzer queryAnalyzer;
    private final Map<String, Timer> queryTimers;

    public EnterpriseQueryMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.queryAnalyzer = new QueryAnalyzer();
        this.queryTimers = new ConcurrentHashMap<>();
    }

    @Instrumented(metricName = "query.execution")
    public <T> T monitorQuery(String queryId, String sql, Supplier<T> queryExecution) {
        // Create or get existing timer for this query pattern
        Timer timer = queryTimers.computeIfAbsent(queryId, 
            id -> Timer.builder("query.execution")
                      .tag("query_id", id)
                      .tag("query_type", queryAnalyzer.determineQueryType(sql))
                      .register(meterRegistry));
        
        // Record query execution time and metadata
        Timer.Sample sample = Timer.start(meterRegistry);
        try {
            T result = queryExecution.get();
            sample.stop(timer);
            return result;
        } catch (Exception e) {
            timer.record(sample.stop(timer));
            logger.error("Query execution failed: {}", queryId, e);
            throw e;
        }
    }

    // Analyze query patterns and performance characteristics
    private static class QueryAnalyzer {
        public String determineQueryType(String sql) {
            // Implement query pattern recognition logic
            return sql.trim().toLowerCase()
                     .replaceAll("\\s+", " ")
                     .startsWith("select") ? "READ" : "WRITE";
        }
    }
}

Explanation

This implementation leverages Java's Flow API for reactive streams processing. The QueryEventMonitor uses the publisher-subscriber pattern to decouple monitoring logic from query execution. The record class provides an immutable data structure for query events, while the SubmissionPublisher ensures thread-safe event publishing. This design is particularly effective for microservice architectures where query monitoring needs to scale horizontally.

Read More: 30 Essential Mobile Development Tools for 2025

 

Conclusion

Long-running queries can severely impact application performance. By leveraging these methods, developers can proactively monitor, identify, and optimize queries. Each method serves a unique purpose, whether it’s handling timeouts, logging query performance, or using advanced tools like APM solutions.

Join Index.dev, the talent network that connects top Java developers with leading tech companies. Access high-paying remote jobs in the UK, EU, and US!

Share

Pallavi PremkumarPallavi PremkumarTechnical Content Writer

Related Articles

For EmployersHow Enterprise Engineering Teams Are Structured (Data Study)
Tech HiringInsights
This listicle roundup explains how enterprise engineering teams are structured using real data. It covers leadership models, team size, role ratios, and how companies scale with small teams. It also shows how structure, ownership, and internal tools help improve speed, productivity, and delivery.
Eugene GarlaEugene GarlaVP of Talent
For EmployersHow AI-Native Software Is Changing Every Industry
Software DevelopmentArtificial Intelligence
AI-native apps are software products built with artificial intelligence at their core, where AI drives logic, user experience, and decision-making. From healthcare to finance, they learn, adapt, and improve continuously, delivering faster, smarter, and more secure experiences.
Eugene GarlaEugene GarlaVP of Talent