Syed Jafer K

Its all about Trade-Offs

Have you been told “Use plain SQL, its faster than ORM” ?

You might have heard this advice before,

Just use SQL; no need for ORMs.

At first glance, that sounds elegant. SQL is expressive, close to the metal, and avoids the “magic” of an ORM.

But as systems grow, accidental complexity sneaks in, the kind that comes not from business logic, but from all the supporting structure you build around “just SQL.”

The Simple Case SQL Feels Easy Peasy

Let’s say you want to fetch all users,


import sqlite3

conn = sqlite3.connect("users.db")
cursor = conn.cursor()

cursor.execute("SELECT username, email FROM users")
rows = cursor.fetchall()

for row in rows:
    print(row)

Simple and clear just SQL, no magic.

Now, you add a filter


email = "contact.syedjafer@gmail.com"

cursor.execute(f"SELECT username, email FROM users WHERE email = '{email}'")
rows = cursor.fetchall()

Still fine, right?


Well… not really.

SQL Injection Appears

Let’s imagine a malicious user enters this input,


email = "'; DROP TABLE users; --"

That innocent-looking string would destroy your table.


So you switch to parameterized queries,


email = "'; DROP TABLE users; --"

cursor.execute("SELECT username, email FROM users WHERE email = ?", (email,))
rows = cursor.fetchall()

Now you’re safe


But as soon as you add more optional filters like username, created_at, age, etc. your SQL builder starts to look… less simple.

The Complexity Creeps In

You now need to dynamically build WHERE clauses,


filters = {"username": "syed", "age": 28}
query = "SELECT username, email, age FROM users WHERE 1=1"
params = []

for key, value in filters.items():
    query += f" AND {key} = ?"
    params.append(value)

cursor.execute(query, params)
rows = cursor.fetchall()

You just wrote your own mini ORM.

Tomorrow, you’ll add support for LIKE, >, <, BETWEEN, and pagination.


You’ll refactor it into a helper class.

You’ll test it.


And slowly… you’ll realize, It’s no longer “just SQL.”

Accidental Complexity Explained

In other words plain SQL is perfect until your usecase stops being trivial.


When filters, sorting, joins, and pagination start piling up, you end up re-implementing features ORMs already solved years ago.

That’s called accidental complexity complexity born out of solving infrastructure problems, not business problems

Take is , Use SQL for complex queries and ORM for the rest.


from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, Session

engine = create_engine("sqlite:///users.db")
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)
    age = Column(Integer)

# Querying with ORM
with Session(engine) as session:
    users = session.query(User).filter(User.email == "syed@example.com").all()
    for user in users:
        print(user.username, user.email)

No manual query strings.


Automatic escaping.


Refactor-friendly.

Of course, you pay with abstraction overhead the ORM hides the SQL layer from you.


But when your system grows, this tradeoff often becomes worth it.

When to Use Plain SQLWhen to Use ORM
Small scripts or one-off toolsLarge apps with many models
Static queriesComplex, dynamic filtering
You need total control over performanceYou want fast iteration and less boilerplate
Database skills are strongYou’re working in a team with mixed expertise
Schema changes are rareSchema evolves frequently



SQL is fine. ORMs are fine.

The question is, What’s the right tool for your current context?

So next time someone says “Just use SQL“, smile and ask , “Sure, but for how long?”