Friends of OpenJDK Today

SQL Best Practices Every Java Engineer Must Know

October 01, 2024

Author(s)

  • Muaath Bin Ali

    Muaath Ali is a principal software engineer with 15+ years of Java experience. He runs MezoCode blog, helping Java developers design flexible and robust systems.

In the world of software development, SQL optimization is a critical skill that every Java engineer must master. Efficient database interactions can significantly enhance the performance of your applications, leading to faster response times and a better user experience.

This comprehensive guide dives into the best practices for SQL query optimization, tailored specifically for Java engineers. From understanding the importance of indexes to mastering joins and leveraging connection pooling, this article covers all the essential techniques you need to write efficient and performant SQL queries.

1. Use Indexes

Indexes can significantly improve query performance by allowing the database to quickly locate and access the data.

Tips:

  • Create indexes on columns used in WHERE, JOIN, ORDER BY, and GROUP BY clauses.
  • Use covering indexes to include all columns needed by a query.

Avoid Practice:

SELECT * FROM users WHERE email = '[email protected]';

🟢 Good Practice:

CREATE INDEX idx_users_email ON users (email);
SELECT name, email FROM users WHERE email = '[email protected]';

This creates an index on the email column of the users table, speeding up searches based on email.

Leverage Function-Based Indexes

Function-based indexes can significantly improve query performance when you frequently search or sort by the result of a function or expression.

Tips:

  • Create function-based indexes for frequently used expressions in WHERE, ORDER BY, or JOIN conditions.
  • Use function-based indexes to optimize queries that involve case-insensitive searches or date/time manipulations.

Avoid Practice:

-- no function-based index applied.
SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

🟢 Good Practice:

CREATE INDEX idx_upper_last_name ON employees (UPPER(last_name));
SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

This creates a function-based index on the uppercase version of the last_name column, speeding up case-insensitive searches.

In PostgreSQL, these are called expression indexes. Here's an example:

CREATE INDEX idx_lower_email ON users (LOWER(email)); 
SELECT * FROM users WHERE LOWER(email) = '[email protected]';

This creates an expression index on the lowercase version of the email column, optimizing case-insensitive email searches.

Function-based or expression indexes are handy when:

  • You frequently search on transformed column values (e.g., UPPER, LOWER, substring operations).
  • It would help if you index computed values or expressions.
  • You want to optimize queries involving date/time manipulations.

Remember that function-based indexes can significantly improve query performance but also increase storage requirements and slow down data modification operations. Use them judiciously based on your specific query patterns and performance needs.

2. Avoid Using SELECT *

Using SELECT * retrieves all columns from the table, which can be inefficient and lead to unnecessary data transfer.

Tips:

  • Specify only the columns you need in your SELECT statement.

Avoid Practice:

SELECT * FROM users;

🟢 Good Practice:

SELECT name, email FROM users;

This query retrieves only the name and email columns, reducing the amount of data transferred.

3. Use Proper Joins

Improper joins can lead to performance issues. Use the correct type of join for your query.

Tips:

  • Use INNER JOIN for matching rows in both tables.
  • Use LEFT JOIN to include all rows from the left table and matching rows from the right table.

Avoid Practice:

SELECT u.name, o.order_date
FROM users u, orders o
WHERE u.id = o.user_id;

🟢 Good Practice:

SELECT u.name, o.order_date
FROM users u
JOIN orders o ON u.id = o.user_id;

This query uses an INNER JOIN to combine data from the users and orders tables.

4. Use WHERE Clauses to Filter Data

Filtering data as early as possible in your query can help reduce the amount of data processed.

Tips:

  • Use WHERE clauses to filter data efficiently.

Avoid Practice:

SELECT name, email FROM users;

🟢 Good Practice:

SELECT name, email FROM users WHERE active = true;

This query retrieves only active users, reducing the amount of data processed.

5. Limit the Number of Rows Returned

When you don't need all rows, use the LIMIT clause to restrict the number of rows returned.

Tips:

  • Use the LIMIT clause to fetch a subset of rows.

Avoid Practice:

SELECT name, email FROM users WHERE active = true;

🟢 Good Practice:

SELECT name, email FROM users WHERE active = true LIMIT 10;

This query retrieves the first 10 active users, reducing the amount of data processed and transferred.

6. Use EXISTS Instead of IN

Using EXISTS can be more efficient than using IN, especially for large datasets.

Tips:

  • Use EXISTS for subqueries to check for the existence of rows.

7. Avoid Functions in WHERE Clauses

Using functions in `WHERE` clauses can prevent the use of indexes, leading to slower queries.

Tips:

  • Avoid using functions on indexed columns in `WHERE` clauses.

⛔ Avoid Practice:

SELECT name, email FROM users WHERE DATE_PART('year', created_at) = 2023;

🟢 Good Practice:

SELECT name, email FROM users WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';

This query filters on the `created_at` column without using a function, allowing the use of an index.

8. Use JOINs Instead of Subqueries

JOINs are often more efficient than subqueries, especially for large datasets.

Tips:

  • Use `JOIN` instead of subqueries when possible.

⛔ Avoid Practice:

SELECT name, (
  -- Subquery to get order date for each user
  SELECT order_date 
  FROM orders 
  WHERE user_id = users.id
) AS order_date 
FROM users;

🟢 Good Practice:

SELECT u.name, o.order_date FROM users u JOIN orders o ON u.id = o.user_id;

This query uses a `JOIN` instead of a subquery, improving performance.

9. Optimize Group By and Order By Clauses

Using `GROUP BY` and `ORDER BY` clauses can be resource-intensive. Optimize them to improve performance.

Tips:

  • Use indexes on columns used in `GROUP BY` and `ORDER BY` clauses.
  • Reduce the number of columns specified in these clauses.

⛔ Avoid Practice:

SELECT user_id, COUNT(*), MAX(order_date) FROM orders GROUP BY user_id, order_date ORDER BY order_date;

🟢 Good Practice:

SELECT user_id, COUNT(*) FROM orders GROUP BY user_id ORDER BY user_id;

This query groups and orders by indexed columns, improving performance.

10. Use Appropriate Data Types

Choosing the correct data types for your columns can have a significant impact on performance and storage efficiency.

Tips:

  • Use appropriate data types for your columns.
  • Avoid using `TEXT` or `BLOB` unless necessary.

⛔ Avoid Practice:

-- Using TEXT for name and email which may be inefficient
 CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT,
  email TEXT,
  created_at TIMESTAMP
);

🟢 Good Practice:

-- Using more appropriate data types for better performance and storage efficiency


 CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR 100,
  email VARCHAR 100,
  created_at TIMESTAMP
);

This schema uses appropriate data types, improving performance and storage efficiency.

11. Analyze Query Execution Plans

Use tools like `EXPLAIN` to analyze your query execution plans and identify performance issues.

Tips:

  • Use `EXPLAIN` to understand how your queries are executed.
  • Identify and optimize slow parts of your queries.

⛔ Avoid Practice:

SELECT name, email FROM users WHERE active = true;

🟢 Good Practice:

EXPLAIN SELECT name, email FROM users WHERE active = true;

This command provides an execution plan for the query, helping identify potential performance issues.

12. Use Connection Pooling

For Java applications, using connection pooling can reduce the overhead of establishing database connections and improve performance.

Tips:

  • Use a connection pooling library like HikariCP or Apache DBCP.
  • Configure the pool size based on your application's needs and the database's capabilities.

⛔ Avoid Practice:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password");
// Use connection here
conn.close();

🟢 Good Practice:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydatabase");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10);
HikariDataSource dataSource = new HikariDataSource(config);

This sets up a connection pool with a maximum of 10 connections, reducing connection overhead.

13. Use Batch Processing

When performing multiple insert, update, or delete operations, using batch processing can significantly improve performance.

Tips:

  • Batch inserts/updates to reduce database round-trips.
  • Use prepared statements for batch operations.

Avoid Practice:

Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
for (User user : userList) {
  stmt.executeUpdate("INSERT INTO users (name, email) VALUES ('" + user.getName() + "', '" + user.getEmail() + "')");
}
stmt.close();
conn.close();

🟢 Good Practice:

Connection conn = dataSource.getConnection(); 
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name, email) VALUES (?, ?)"); 
for (User user : userList) { 
   pstmt.setString(1, 
   user.getName()); 
   pstmt.setString(2, 
   user.getEmail()); 
   pstmt.addBatch(); 
} 
pstmt.executeBatch(); 
pstmt.close(); 
conn.close();

This Java code uses batch processing to insert multiple users efficiently.

14. Optimize Joins

Properly optimizing joins can have a significant impact on query performance, especially for large datasets.

Tips:

  • Ensure that columns used in join conditions are indexed.
  • Start with the smallest table when joining multiple tables.

Avoid Practice:

SELECT u.name, o.order_date FROM orders o JOIN users u ON u.id = o.user_id WHERE u.active = true;

🟢 Good Practice:

SELECT u.name, o.order_date FROM users u JOIN orders o ON u.id = o.user_id WHERE u.active = true;

This query joins the `users` table with the `orders` table on an indexed column, improving performance.

15. Optimize Subqueries

Subqueries can often be replaced with joins or other more efficient query constructs.

Tips:

  • Use joins instead of subqueries whenever possible.
  • Use Common Table Expressions (CTEs) for complex queries to improve readability and sometimes performance.

Avoid Practice:

SELECT name, email FROM users WHERE id 
IN SELECT user_id FROM orders WHERE order_date > '2023-01-01';

🟢 Good Practice:

WITH RecentOrders AS 
SELECT user_id FROM orders WHERE order_date > '2023-01-01'

SELECT u.name, u.email FROM users u JOIN RecentOrders ro ON u.id = ro.user_id;

This query uses a CTE to improve readability and performance.

Related Articles

View All

Author(s)

  • Muaath Bin Ali

    Muaath Ali is a principal software engineer with 15+ years of Java experience. He runs MezoCode blog, helping Java developers design flexible and robust systems.

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard