Finalmente tive tempo para sanar, com dados, uma das minhas e, imagino que de várias outras pessoas, maiores dúvidas quando se trata de Go e banco de dados. Qual a diferença, ao nível de consumo de recurso e performance, entre utilizar GORM vs escrever SQL na unha.
Para ficar mais fácil a leitura, separei o post em tópicos. Iniciarei explicando como fiz o setup, as funções comuns e realizei a execução dos benchmarks. Depois, separo o código do benchmark, assim como o resultado, em ações de CRUD.
Setup
Primeiramente, criei os packages entities, orm e std. Dentro do package entities, criei uma struct para ser utilizada em todos os benchmarks.
package entities
type Category struct {
ID int64 `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name"`
Description string `gorm:"column:description"`
}
Depois, em cada um dos packages de banco, implementei uma função chamada setup, com a finalidade de, iniciar uma nova conexão com o banco de dados e criar a tabela categories.
No caso do package orm, a função ficou da seguinte forma.
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
_ "github.com/lib/pq"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func setup() *gorm.DB {
db, err := gorm.Open(postgres.New(postgres.Config{
DriverName: "postgres",
DSN: "host=localhost port=5432 user=postgres password=1234 dbname=benchmark sslmode=disable",
}), &gorm.Config{})
if err != nil {
panic(err)
}
err = db.AutoMigrate(&entities.Category{})
if err != nil {
panic(err)
}
return db
}
Já no package std, ficou assim:
package std
import (
"database/sql"
_ "github.com/lib/pq"
)
func setup() *sql.DB {
db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres password=1234 dbname=benchmark sslmode=disable")
if err != nil {
panic(err)
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS categories (id SERIAL, name TEXT, description TEXT, PRIMARY KEY (id))")
if err != nil {
panic(err)
}
return db
}
Embora possa parecer estranho criar a tabela toda as vezes que a função setup for chamada, fiz isso pois, a execução dos benchmarks foram realizadas individualmente e limpas. Em outra palavras, entre uma execução e outra, sempre fiz um drop da tabela.
Mas pode ficar tranquilo, utilizei a função b.ResetTimer()para que a criação da tabela não impactasse no resultado dos benchmarks.
Funções Comuns
Tirando o benchmark de insert, para todos os outros é preciso que existam dados na tabela.
Como uma nova tabela sempre é criada, precisei implementar uma função para alimentar a tabela categories antes da execução do benchmark.
Afim de re-utilizar o máximo de código possível, além de isolar totalmente os packages, implementei uma função de insert em lote em cada um dos package.
package orm
import (
"fmt"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func InsertBenchORM(db *gorm.DB, size int) []entities.Category {
categories := make([]entities.Category, size)
for i := 0; i < size; i++ {
category := entities.Category{
Name: fmt.Sprintf("Category %d", i),
Description: fmt.Sprintf("Description %d", i),
}
cat, err := InsertORM(db, category)
if err != nil {
panic(err)
}
categories[i] = cat
}
return categories
}
package std
import (
"database/sql"
"fmt"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
)
func InsertBenchStd(db *sql.DB, size int) []entities.Category {
categories := make([]entities.Category, size)
for i := 0; i < size; i++ {
category := entities.Category{
Name: fmt.Sprintf("Category %d", i),
Description: fmt.Sprintf("Description %d", i),
}
cat, err := InsertStd(db, category)
if err != nil {
panic(err)
}
categories[i] = cat
}
return categories
}
Reparem que, exceto pelo tipo do parâmetro db e o nome da função de insert, o restante das funções são praticamente idênticas.
Execução
Como dito anteriormente, antes da execução de cada um dos benchmarks, a tabela categories era removida, para que todos os benchmarks fossem executados em uma tabela recém criada.
Outro ponto importante, é que todos os benchmarks foram executados individualmente, ou seja, passei o nome completo do benchmark a ser realizado na flag -bench.
Por fim, todas as execuções foram realizadas com as flags -cpu 8 -benchmem -benchtime 5s -count 5.
CREATE
Antes de ver os resultados, vamos analisar a diferença na implementação das funções de insert.
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func InsertORM(db *gorm.DB, data entities.Category) (entities.Category, error) {
err := db.Create(&data).Error
return data, err
}
Quando comparamos com a função abaixo, logo vemos que precisamos escrever muito mais código para obter o mesmo resultado, ou seja, ter um novo registro no banco de dados.
No entanto, será que compensa escrever esse monte de código a mais?
package std
import (
"database/sql"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
)
func InsertStd(db *sql.DB, data entities.Category) (entities.Category, error) {
err := db.QueryRow("INSERT INTO categories (name, description) VALUES ($1, $2) RETURNING id", data.Name, data.Description).Scan(&data.ID)
if err != nil {
return data, err
}
return data, nil
}
Como a função de benchmark tem a mesma estrutura para ambos os casos, abaixo mostro uma função genérica a marcação FUNCAO_DE_INSERT no lugar da chamada da função de insert.
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"testing"
)
// BnechmarkInsertORM para ORM
// BenchmarkInsertStd para SQL
func BenchmarkInsert(b *testing.B) {
db := setup()
category := entities.Category{
Name: "Category 1",
Description: "Description 1",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// InsertORM para ORM
// InsertStd para SQL
_, err := FUNCAO_DE_INSERT(db, category)
b.StopTimer()
if err != nil {
b.Error(err)
}
b.StartTimer()
}
}
Após executar os benchmarks, obtive os seguintes resultados.

Embora a quantidade de operações executadas pela função sem ORM tenha sido somente 15% maior – o que pode ser uma limitação da engine do banco -, a quantidade de bytes por operação foi 85% menor, além de precisar realizar 61 alocações a menos.
READ
No caso da leitura, implementei um benchmark para obter 1 registro (get) e outro para buscar todos os registros da tabela (list).
get
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func GetORM(db *gorm.DB, id int64) (*entities.Category, error) {
var category entities.Category
err := db.First(&category, id).Error
if err != nil {
return nil, err
}
return &category, nil
}
Assim como no caso do insert, é necessário escrever um pouco mais de código para que a função sem ORM consiga alcançar o mesmo resultado.
package std
import (
"database/sql"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
)
func GetStd(db *sql.DB, id int64) (*entities.Category, error) {
var category entities.Category
err := db.QueryRow("SELECT id, name, description FROM categories WHERE id = $1", id).
Scan(&category.ID, &category.Name, &category.Description)
if err != nil {
return nil, err
}
return &category, nil
}
Para o benchmark, escrevi a seguinte função:
import "testing"
// BenchmarkGetORM para ORM
// BenchmarkGetStd para SQL
func BenchmarkGet(b *testing.B) {
db := setup()
// InsertBenchORM para ORM
// InsertBenchStd para SQL
categories := INSERT_EM_LOTE(db, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// GetORM para ORM
// GetStd para SQL
_, err := FUNCAO_DE_GET(db, categories[i].ID)
b.StopTimer()
if err != nil {
b.Fatal(err)
} b.StartTimer()
}
}
Os seguintes resultados foram obtidos com a execução do benchmark.

Muito semelhante ao que vimos no caso do insert, a função sem ORM conseguiu realizar cerca de 15% mais operações – que realmente me faz acreditar ser uma limitação da engine -, consumindo 76% menos bytes e realizando 39 alocações a menos por operação.
list
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func ListORM(db *gorm.DB) ([]entities.Category, error) {
var categories []entities.Category
err := db.Find(&categories).Error
return categories, err
}
Sem sombra de dúvidas, escrever uma função para retornar uma lista de registro do banco de dados, é onde está a maior diferença na quantidade de código necessário para alcançar o mesmo resultado.
Me pergunto se isso irá se refletir no resultado do benchmark…
package std
import (
"database/sql"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
)
func ListStd(db *sql.DB) ([]entities.Category, error) {
var categories []entities.Category
rows, err := db.Query("SELECT id, name, description FROM categories")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var category entities.Category
err := rows.Scan(&category.ID, &category.Name, &category.Description)
if err != nil {
return nil, err
}
categories = append(categories, category)
}
err = rows.Err()
if err != nil {
return nil, err
}
return categories, nil
}
Para o benchmark, escrevi uma função bem simples.
import "testing"
// BenchmarkListORM para ORM
// BenchmarkListStd para SQL
func BenchmarkListStd(b *testing.B) {
db := setup()
// InsertBenchORM para ORM
// InsertBenchStd para SQL
_ = INSERT_EM_LOTE(db, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// ListORM para ORM
// ListStd para SQL
_, err := FUNCAO_DE_LIST(db)
b.StopTimer()
if err != nil {
b.Fatal(err)
}
b.StartTimer()
}
}
Com o benchmark executado, os seguintes resultados foram obtidos.

Embora eu não saiba explicar o motivo da diferença entre a quantidade operações no primeiro teste vs os outros quatro (vou perguntar ao time do Go), a execução sem ORM consumiu cerca de 16% menos bytes e realizou uma média de 39.808 alocações de memória a menos por operação.
UPDATE
Continuando a linha dos outros tópicos, vamos começar comparando as funções de update com e sem ORM.
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func UpdateORM(db *gorm.DB, data entities.Category) error {
return db.Save(&data).Error
}
Embora a diferença em número de linhas não seja tão grande, a quantidade de código ainda é um pouco maior.
package std
import (
"database/sql"
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
)
func UpdateStd(db *sql.DB, data entities.Category) error {
_, err := db.Exec("UPDATE categories SET name = $1, description = $2 WHERE id = $3", data.Name, data.Description, data.ID)
return err
}
Para o benchmark, além do reset do timer logo após a inserção em lote, parei o timer logo na sequência. Isso por que o início do looping contém parte do setup também.
A fim de buscar maior precisão no benchmark, controlei o timer ao redor da FUNCAO_DE_UPDATE.
import (
"fmt"
"testing"
)
// BenchmarkUpdateORM para ORM
// BenchmarkUpdateStd para SQL
func BenchmarkUpdate(b *testing.B) {
db := setup()
// InsertBenchORM para ORM
// InsertBenchStd para SQL
categories := INSERT_EM_LOTE(db, b.N)
b.ResetTimer()
b.StopTimer()
for i := 0; i < b.N; i++ {
category := categories[i]
category.Name = fmt.Sprintf("Update Category %d", i)
b.StartTimer()
// UpdateORM para ORM
// UpdateStd para SQL
_ = FUNCAO_DE_UPDATE(db, category)
b.StopTimer()
}
}
Após execução, o seguinte resultado foi obtido.

Seguindo a linha dos outros benchmarks realizados até aqui, a função sem ORM conseguiu executar um pouco mais operações. Já no quesito consumo de recursos, a função sem ORM consumiu 93% menos bytes com 75 alocações de memória a menos por operação.
DELETE
Por fim, mas não menos importante, vamos comparar as funções para exclusão de registros.
package orm
import (
"github.com/aprendagolang/benchmark-orm-vs-std/entities"
"gorm.io/gorm"
)
func DeleteORM(db *gorm.DB, id int64) error {
return db.Delete(&entities.Category{ID: id}).Error
}
De todas as funções que escrevemos até agora, essa é que menos dá diferença na quantidade de código.
package std
import "database/sql"
func DeleteStd(db *sql.DB, id int64) error {
_, err := db.Exec("DELETE FROM categories WHERE ID = $1", id)
return err
}
Para o benchmark, fiz a seguinte função.
import "testing"
// BenchmarkDeleteORM para ORM
// BenchmarkDeleteStd para SQL
func BenchmarkDeleteStd(b *testing.B) {
db := setup()
// InsertBenchORM para ORM
// InsertBenchStd para SQL
categories := INSERT_EM_LOTE(db, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// DeleteORM para ORM
// DeleteStd para SQL
err := FUNCAO_DE_DELETE(db, categories[i].ID)
b.StopTimer()
if err != nil {
b.Error(err)
} b.StartTimer()
}
}
A execução nos deu o seguinte resultado.

Nesse último benchmark, a função sem ORM executou cerca de 12% mais operações, consumindo 94% menos bytes e 57 alocações de memória a menos por operação.
Conclusão
Embora a quantidade de operações realizadas a mais pelas funções sem ORM não tenha sido muito expressiva – talvez uma questão da engine estar rodando na minha máquina -, o consumo de recursos dessas funções foi extremamente menor.
Sei que algumas pessoas defendem o uso de ORM pelo ganho de produtividade. No entanto, utilizando o Copilot como fiz para escrever essas funções sem ORM, a diferença foi praticamente nenhuma. Por outro lado, o consumo de recursos foi gigante.
Em um próximo post, farei o benchmark utilizando consultas mais complexas, com JOINS e sub queries.
Se você gostou desse post, assine nossa newsletter para não perder nenhum post.
Ah! E se você quer aprender um pouco mais sobre testes e benchmark, confira nossa curso!
Até a próxima!
Faça parte da comunidade!
Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.
Livros Recomendados
Abaixo listei alguns dos melhores livros que já li sobre GO.





Parabéns!