Hi, I'm ThadeusB.

I code, I play, I love, I innovate

AI Writes Better SQL Than Your ORM

I touched on this in my raw SQL post but I keep bumping into it and I think it deserves its own space. The short version: AI coding agents are significantly more reliable when they're writing raw SQL than when they're writing ORM code. And the reasons go deeper than "there's more SQL on the internet."

The Prisma thing

I've been dealing with Prisma on a new project. After two years of aiosql where SQL just lives in .sql files, going back to an ORM has been... not fun.

Here's what keeps happening. I ask an AI agent to add a new table or modify a schema. Instead of updating the Prisma schema and running prisma migrate dev to let Prisma generate the migration, the agent just writes the migration SQL file directly. Which sounds fine except Prisma tracks its own migration state. The hand-written migration doesn't match what Prisma thinks the schema should look like, and now your preview deployments on Vercel are broken.

So you add rules to your context. "Always use prisma migrate dev to generate migrations. Never write migration files manually." That helps. Sometimes. Until the agent decides it knows better, or the context window gets long enough that it forgets, or the task is complex enough that it takes a shortcut.

It's a constant battle of making sure the agent actually uses the ORM's workflow instead of just writing what it thinks is correct. And the frustrating part? What it writes is often correct SQL. It just doesn't play nice with the framework that's supposed to be managing that SQL for you.

Why this keeps happening

The easy explanation is training data. SQL has been around since the 1970s, there's 50 years of examples, every database speaks the same fundamental language. More data means better generation. That's true, but it's not the interesting part.

SQL is declarative. You describe what you want and the database figures out how to do it. A SELECT with a JOIN and a GROUP BY has clear, well-defined semantics. You can read it, understand it, run it, and verify it independently of any application code. The meaning is right there in the statement.

ORMs are trickier. The schema definitions themselves are often pretty declarative. A Prisma schema or a set of SQLAlchemy models are readable, structured, fine. The AI can understand those. But ORMs aren't just schemas. They come with multi-step workflows, internal state tracking, CLI-driven migration systems, generated code, and opinions about what order things happen in. Edit this file, run this command, don't touch generated files, keep migration state aligned. The query API part also depends on runtime configuration, session state, relationship loading strategies, and framework version. When an AI writes User.objects.filter(orders__created_at__gte=cutoff).annotate(count=Count('id')), the SQL that actually executes depends on your Django settings, your model definitions, your database backend, and whether you've got any custom managers in play.

LLMs are really good at declarative mapping. "Describe what you want, I'll write it in a standard language with clear rules." That's SQL. They're much worse at following multi-step procedural workflows and simulating runtime API behavior across dozens of possible configurations. That's ORMs.

Or put it another way. SQL is a language with semantics. ORMs are workflow engines whose correctness depends on process compliance. LLMs are good at producing linguistically coherent output in well-defined languages. They're bad at following multi-step procedures with invisible internal state. SQL plays to their strength. ORM workflows play to their weakness.

The migration nightmare

Migrations are where this really falls apart.

With raw SQL migrations (like Alembic with op.execute()), the migration is the SQL. The agent writes CREATE TABLE, ALTER TABLE, CREATE INDEX. There's nothing to get wrong except the SQL itself, and as we've established, the AI is good at SQL.

With ORM-managed migrations? The agent has to understand the migration framework. Prisma wants you to edit schema.prisma and then run a command. Django wants you to edit models and run makemigrations. SQLAlchemy autogenerate compares model metadata to the database. Each framework has its own workflow, its own state tracking, its own opinions about what you're allowed to do.

The context you need to give the AI just to get clean migrations from an ORM is wild. "Use this command not that command. Don't edit migration files directly. Make sure you run generate after schema changes. Don't forget to handle this edge case in this framework version." That's a lot of framework-specific knowledge just to add a column.

Compare that to: "Write a SQL migration that adds a status column to the users table." Done. The AI knows how to do that. Core DDL patterns are well-standardized across databases. No framework opinions to navigate.

The context bloat problem

This is the part that really bugs me. To get an AI agent to work reliably with an ORM, you end up stuffing your context with framework-specific rules. How your session management works. What your base model class looks like. Which version of the API to use. What commands to run and in what order. Your relationship naming conventions. Your migration workflow.

That's a lot of tokens spent teaching the AI about your specific abstraction layer. Tokens that could be spent on, you know, the actual problem you're trying to solve.

With SQL-first tooling the context is way smaller. "Here's the database schema, write queries against it." There's still some setup involved (more on that below), but the floor is much lower than what ORMs demand.

The schema tradeoff

Now, to be fair. ORMs do give you one thing that raw SQL doesn't: a greppable, in-code data model. Your SQLAlchemy models or your Prisma schema file describe the whole database structure in a format the AI can easily find and read. That's genuinely useful context.

With raw SQL you have to solve that differently. A schema.sql dump that you keep updated. Or an MCP server connected to your dev database so the agent can inspect tables directly. Or well-defined Pydantic models at the API boundary that mirror your tables closely enough.

It's an extra step. But honestly the tradeoff is worth it. Giving the AI a schema file and letting it write real SQL against it produces better results than giving it an ORM model definition and hoping it navigates the framework correctly. The schema tells it what exists. The SQL tells it what to do. Clean separation. No framework behavior to guess about.

Feedback loops

There's another angle here that I think matters. SQL gives you fast, clean feedback.

Write a query. Run it. If the syntax is wrong you get a clear error pointing at the problem. If it's slow you run EXPLAIN ANALYZE and see exactly what the database is doing. You can copy it into DBeaver, tweak it, run it again. The feedback loop is tight and the errors are useful.

ORM errors? Stack traces through framework internals. "AttributeError in sqlalchemy.orm.relationships line 847." Great. Now I'm debugging the framework instead of my query. The AI hits the same wall. It generates ORM code, gets back a framework error that it doesn't fully understand, tries a different approach that also doesn't work, and you're three rounds deep before anyone has looked at the actual data problem.

AI performance improves with clean feedback loops. SQL has clean feedback loops. ORMs often don't.

The bigger pattern

I want to be careful about generalizing too much here because I'm writing from specific experience, not a grand theory. But I do think there's a pattern worth noticing.

AI tools amplify simple, stable, declarative systems. They struggle with context-sensitive abstraction layers. SQL vs ORMs is one example. But you see the same thing with Kubernetes YAML vs dynamic config code, plain JSON schemas vs meta-framework DSLs, direct API calls vs heavily wrapped SDK abstractions. The more layers between "what you wrote" and "what actually happens," the harder it is for the AI to get it right.

That doesn't mean abstraction is bad. It means the tradeoff calculation changed. Abstractions that save humans time might cost you that time right back when your AI tools can't navigate them reliably.

So what do you actually do

I'm not saying drop your ORM tomorrow. If you've got a working codebase and SQLAlchemy or Django ORM is doing its job, that's fine. But if you're starting something new and you plan to lean on AI agents (and at this point, who isn't), the SQL-first approach gives you a real advantage.

Your AI writes better queries. Your context stays focused on the actual problem. Your migrations are just SQL. Your debugging happens in real database tools. And you're not spending tokens teaching the agent about your framework's opinions.

One thing worth acknowledging: ORM migration engines do enforce schema discipline for you. If you go SQL-first, that discipline has to come from somewhere else. Reviewing migrations, snapshotting schema, keeping a schema.sql up to date. For teams already doing that, the practical loss is small. For teams that relied on the ORM to be the guardrail, it's a real adjustment.

aiosql is what I use. SQL in .sql files, Python calls methods, nothing in between. But even if you use something else, the principle is the same. The closer your code is to the actual SQL, the better your AI tools will work with it.