Introduction
Today, I will be talking about some of the common and high impact SQL anti-patterns I have seen from experience that can make queries and pipelines difficult to maintain, or have slower than expected performance. These issues can compound, causing erosion in trust in data, and slower query development in general.
Mishandling Excessive Case When Statements
When working with larger enterprise software, it is common to have large CASE WHEN statements translating application status codes into plain English. For example, status code 1 could mean the item is out of stock. Now imagine there are hundreds of status codes that need to be categorized after talking to a stakeholder who requested a dashboard to track item statuses in your warehouse.
Because you need to get this dashboard developed quickly, you decide to place this large CASE WHEN logic only within a view for that specific dashboard. This is an anti-pattern because now other developers will either have to copy and paste your logic or skip it entirely and use the raw status codes. Over time, this creates a huge mess. I know because I have seen it happen.
When handling large CASE WHEN statements, it is better to create a dimension table or view, ideally sourced from the landed table where the original status column is populated. This way, the logic is accessible and consistent for everyone.
Using Functions on Indexed Columns
In SQL server especially, it’s pretty easy to work against the index if you are not careful.
For example: WHERE UPPER(name) = ‘ABC’
Instead you should:
query WHERE name = ‘abc’
create an indexed UPPER(name) column
This allows the database to leverage indexes properly and prevents unnecessary full table scans.
Using SELECT * In Views
When developing views, it is tempting to slap a SELECT * at the very end and call it a day. This is not ideal because:
Schema evolution can break your view, which can have downstream effects
You are bringing in unnecessary columns potentially
Overusing DISTINCT to “Fix” Duplicates
One of the most common mistakes I’ve seen is developers using SELECT DISTINCT as a quick way to eliminate duplicates that appear after a bad join. It’s an easy fix, but it hides a deeper problem. Usually, the duplicates exist because the join condition is incomplete or the relationship between tables isn’t truly one-to-one. Using DISTINCT might make the results look right temporarily, but it masks the root cause. This becomes a nightmare when someone later builds metrics off the same dataset and gets inconsistent counts. The correct approach is to fix the join logic itself, ensuring each relationship is properly defined before aggregation or reporting.
Excessive View Layer Stacking
In larger data environments, it’s easy to fall into the trap of layering views on top of views. At first, this seems modular and organized. But over time, as more teams build their own transformations on top of existing views, the dependency chain becomes unmanageable. Performance slows down because the database has to expand multiple layers of logic each time, and debugging turns into an archaeological dig through nested queries. The fix is to flatten transformations periodically and materialize heavy logic into clean, well-defined base views or tables.
Nested Subqueries That Do Too Much
Nested subqueries are useful when you need to isolate logic or filter data when starting out on a query, but they can quickly turn into an anti-pattern when overused or deeply nested. I’ve seen queries with three or four layers of subqueries, each one filtering or aggregating the results of the previous one, totaling over 5000 lines of code. As you can imagine, this makes debugging queries pretty difficult. You should consider using CTEs instead, as they tend to be more readable.
Conclusion
SQL is one of those languages that looks simple on the surface but grows in complexity as teams and systems scale. Most anti-patterns don’t start as bad ideas. They come from speed, deadlines, or small shortcuts that accumulate over time. The best teams treat SQL like production code: shared, versioned, reviewed, and optimized. A few minutes spent designing for clarity upfront can save hours of rework and confusion later.