VillageSQL is a drop-in replacement for MySQL with extensions.
All examples in this guide work on VillageSQL. Install Now →
The Settings That Matter Everywhere
Whatever the framework, three things must be true:- The connection uses
utf8mb4, or emoji break in transit. See utf8mb4 and emoji. - Strict SQL mode is on (it is the MySQL 8.x default), so bad data errors instead of silently truncating.
- The pool size is deliberate. Every framework opens a pool per process; multiply by your process count before picking a number. See Connection pooling.
Django
- Use
mysqlclient, the maintained C driver Django’s docs recommend. - Django emulates some constraints in Python; add real database constraints too (
unique=Truebecomes a real unique index, but check constraints needCheckConstraintinMeta). select_related()(JOIN) andprefetch_related()(second query) are the N+1 tools; see below for when.
Rails
encoding: utf8mb4indatabase.ymlsets both the connection charset and the default forrails db:create.- ActiveRecord validations (
validates_uniqueness_of) race under concurrency; back every uniqueness validation with a real unique index, and let the 1062 duplicate-key error be the last line of defense. includesis the N+1 tool;strict_loadingmode turns lazy loading into an error so N+1s fail in development instead of shipping.
Prisma
- Prisma speaks utf8mb4 by default; the setting to watch is
connection_limitin the URL, which defaults low. prisma migrategenerates DDL from schema drift. Read the generated SQL before applying to production, exactly as you would a hand-written migration. See Schema migrations.- Relation queries use
include; the N+1 shape appears when you loop over results and access relations one by one.
The N+1 Problem, Once
The universal ORM performance bug: load 100 orders, then lazily load each order’s customer, and the ORM issues 101 queries. The fix is the same idea in every framework — declare the relations you need up front so the ORM fetches them in one or two queries (select_related / includes / include). The detection tool is also universal: turn on query logging in development and read what the ORM actually sends, or watch the slow query log in production.
When to Drop to SQL
ORMs earn their keep on CRUD and lose it on analytics. Window functions, CTEs, bulk upserts, and multi-table reporting queries are clearer in SQL than in a query-builder chain trying to express them. Every ORM has an escape hatch (raw() in Django, find_by_sql in Rails, $queryRaw in Prisma); using it for the 5 percent of queries that deserve it is good engineering, not defeat. It is also how VillageSQL extension functions reach an ORM application: SELECT UUID_V7() or ai_prompt(...) work through any of these escape hatches, since the ORM just passes the SQL through.
See also
- Connection pooling — sizing pools across processes
- utf8mb4 and emoji — the charset setting every config above sets
- Slow query log — catching what the ORM generates
- Schema migrations — applying ORM-generated DDL safely

