SpikeMe
← Comparisons
Go

GORM vs sqlx

updated on July 14, 2026

GORM vs sqlx

GORM and sqlx compared as a full ORM (associations, migrations, hooks) vs a thin extension over database/sql (explicit SQL, struct scanning), weighing control against convenience, overhead, N+1 risk, and learning curve in Go.

FactGORMsqlx
Current version1.31.21.4.0
LicenseMITMIT
CriterionGORMsqlx
Abstractionfull ORM with a fluent API over the databasethin extension over database/sql, hand-written SQL
How you write querieschained methods (Where, Preload, Joins)literal SQL with automatic struct scanning
Associationshas one/has many/many2many declared per structno associations — you write the JOINs by hand
MigrationsAutoMigrate creates and alters tables from structsno migrations — use SQL or a dedicated tool
Hooks and callbacksBeforeCreate/AfterUpdate and the like in the lifecyclenone — the logic stays explicit in your code
N+1 riskpossible if you forget Preload; mitigated by eager loadingunlikely — every query is visible and intentional
Curve and overheadsteeper curve, reflection and abstraction cost some overheadshort curve if you know SQL, minimal overhead

Context

GORM and sqlx sit at opposite ends of the data-access spectrum in Go, and choosing between them is choosing how much abstraction you want between your code and the SQL that runs against the database. GORM is a full ORM: you describe your tables as Go structs, declare associations with tags, and the library generates the SQL, handles migrations, fires lifecycle hooks, and resolves relationship loading for you. sqlx does the opposite by design — it is a thin extension over the standard library's database/sql. It does not hide the SQL; it only removes the tedium of mapping columns to struct fields. You write the query, it scans the result straight into your type.

The central tension is control versus convenience. With GORM, a db.Create(&user) can insert the user and its associations through a chain of callbacks you never see — great for speed, less obvious when you need to debug performance. With sqlx, every query that hits the database is written in your code, visible and intentional; the cost is that everything is your responsibility, from migrations to JOINs. Neither is "better": they optimize different variables. GORM optimizes the speed of writing CRUD and navigating relationships. sqlx optimizes transparency and minimal overhead over Go's standard stack.

When to choose GORM

GORM is the right choice when CRUD volume is high and the relationships between entities are the heart of the domain. Declaring associations once on the struct and then loading them with Preload saves real repetitive SQL work, and AutoMigrate keeps the schema in sync with the models without you writing DDL by hand. Hooks like BeforeCreate centralize lifecycle rules (generate a UUID, normalize a field) on the model, instead of scattering them across every insertion point.

package main

import (
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string `gorm:"uniqueIndex"`
	Posts []Post // has many
}

type Post struct {
	ID     uint `gorm:"primaryKey"`
	Title  string
	UserID uint
}

func main() {
	db, err := gorm.Open(postgres.Open("postgres://localhost/app"), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	db.AutoMigrate(&User{}, &Post{}) // creates/alters tables from the structs

	db.Create(&User{Name: "Ada", Email: "ada@example.com"})

	var users []User
	// Preload avoids N+1: one query for users, another for all posts
	db.Preload("Posts").Where("name = ?", "Ada").Find(&users)
}

The example shows the gain and the risk in the same place. The gain: declared association, automatic migration, and relationship loading in two queries with a single Preload. The risk: if you iterate users and access user.Posts without the Preload, GORM can fire one query per user — the classic N+1 problem, hidden precisely because the abstraction makes fetching relationships cheap to write. GORM has the tools to avoid it (Preload, Joins), but you have to remember to use them. Add the reflection overhead and the longer curve to master conventions and the fluent API, and the profile is clear: GORM trades a bit of transparency and performance for a lot of development speed in relationship-rich domains.

When to choose sqlx

sqlx is the right choice when you want the SQL to be explicit and the overhead to be minimal. If the team already thinks in SQL, sqlx adds almost no new concepts: you use the same database/sql, with the bonus that Get and Select scan rows straight into structs via db tags. Every query that touches the database is in your code, so reviewing performance is reading what is written — there is no implicit query to hunt down. That makes N+1 unlikely by construction: relationships only load when you write the JOIN or the second query.

package main

import (
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
)

type User struct {
	ID    int    `db:"id"`
	Name  string `db:"name"`
	Email string `db:"email"`
}

func main() {
	db, err := sqlx.Connect("postgres", "postgres://localhost/app")
	if err != nil {
		panic(err)
	}

	_, err = db.Exec(
		`INSERT INTO users (name, email) VALUES ($1, $2)`,
		"Ada", "ada@example.com",
	)
	if err != nil {
		panic(err)
	}

	var users []User
	// Explicit SQL; Select scans each row into User via db tags
	err = db.Select(&users, `SELECT id, name, email FROM users WHERE name = $1`, "Ada")
	if err != nil {
		panic(err)
	}
}

The difference in philosophy is obvious: there is no AutoMigrate (the users table must already exist, created by SQL or a separate migration tool), no associations, and no hooks. In exchange, you get full predictability — the SQL you read is the SQL that runs, the overhead over database/sql is negligible, and the mental model is short if you already know SQL. The cost shows up when the domain has many relationships: writing JOINs, mapping nested results, and versioning migrations by hand becomes repetitive work an ORM would automate. sqlx assumes that explicit work is a fair price for transparency.

Verdict

The decision turns on one question about the shape of your domain and the culture of your team. If the application is relationship-rich, CRUD is heavy, and you want development speed with declarative associations, automatic migrations, and hooks, GORM pays off — provided the team internalizes the habit of using Preload/Joins to avoid N+1 and accepts some abstraction overhead. It is the most productive path for domains dense in related entities.

If the team thinks naturally in SQL, values predictability, and wants the smallest possible overhead over the standard library, sqlx is the more honest base. Every query is visible, the mental model is short, and there is no magic to debug when performance matters — at the cost of writing migrations, JOINs, and mappings by hand. A rule of thumb: the more your product's bottleneck is schema evolution and relationship navigation, the more GORM helps; the more it is fine control over queries and latency, the more sqlx rewards. Choose by the nature of the work, not by the reputation of ORMs.

GORM

Choose GORM if…

Choose GORM when CRUD productivity, declarative associations, automatic migrations, and lifecycle hooks are worth more than full control over every SQL statement.

sqlx

Choose sqlx if…

Choose sqlx when you want explicit, predictable SQL, minimal overhead over database/sql, and automatic struct scanning without hiding a single query.

The numbers above came from the registry, with source and date. Still, this comparison is generic: the right answer depends on versions, your team and what already exists in your project.

Generate the spike for YOUR stack

Share

XLinkedIn