Guardar imagens no banco de dados ou em object storage?

Na maioria dos casos, guarde a imagem em object storage e só a chave dela no banco. Guardar no próprio banco só compensa com poucos arquivos pequenos.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Entender o que muda ao guardar imagens como BLOB, bytea ou GridFS
  • Comparar banco de dados e object storage em tamanho, cache, backup e acesso
  • Implementar o padrão de salvar o arquivo no storage e só a chave no banco
Nesta página

Quase todo app guarda imagens: avatares, fotos de produto, comprovantes. Colocar tudo no banco parece o caminho mais simples, porque ele já existe, já tem backup e já tem transação. Funciona, mas costuma sair caro.

A seguir estão os limites de cada banco, o efeito no desempenho e no backup e o padrão que a maioria dos projetos acaba adotando. Não existe vencedor universal, mas existe um ponto em que a conta muda.

1Imagem no bancotodo byte passa pelo bancoNavegadorAPIBancobytesbytes2Imagem no object storageo banco guarda só a chaveNavegadorAPIBancoCDNObjectstorageURLchavebytesbytes
Com a imagem no banco, todo byte passa pela API; no storage, a API entrega só a URL e a CDN entrega o arquivo.

Como se guarda uma imagem no banco de dados?#

Em bancos relacionais, a imagem vira uma coluna binária, e o conteúdo do arquivo viaja dentro do INSERT. Os tipos mudam por banco:

  • MySQL: BLOB (até 64 KB), MEDIUMBLOB (até 16 MB) e LONGBLOB (até 4 GB). O tamanho que de fato trafega também depende do max_allowed_packet, que precisa ser ajustado no servidor e no cliente.
  • PostgreSQL: bytea, com limite de 1 GB por valor. Valores grandes vão para uma tabela TOAST, fora da linha principal. Para ir além disso existem os large objects, com API própria.
  • MongoDB: um documento BSON aceita no máximo 16 MiB. Para arquivos maiores, a documentação indica o GridFS, que divide o arquivo em pedaços de 255 KiB por padrão e os grava em fs.chunks, com metadados em fs.files. Se todos os arquivos têm menos de 16 MiB, a própria documentação sugere guardar cada um em um documento com o tipo BinData.

Em todos os casos o arquivo é só mais um dado do banco. É isso que traz as vantagens e os problemas.

Quais são as vantagens de guardar no banco?#

  • Consistência transacional. Apagar a linha apaga a imagem. Não existe imagem órfã nem linha apontando para um arquivo que sumiu.
  • Um sistema só. Sem segundo serviço, sem segundo conjunto de credenciais, sem configurar CORS ou permissões de bucket.
  • Backup único. Um dump contém dados e imagens juntos, na mesma versão.
  • Controle de acesso reaproveitado. Quem lê a linha lê a imagem, pelas regras que você já tem.

Para um protótipo, uma ferramenta interna ou um punhado de ícones pequenos, isso pesa a favor do banco.

Quais são os problemas de guardar no banco?#

  • O banco incha. Um banco com 2 GB de dados passa a ter dezenas de GB quando as fotos entram nele. Restaurar, migrar e replicar passam a levar horas, e cada dump carrega todos os arquivos de novo.
  • Cada imagem passa pela conexão do banco. Para exibir uma foto, sua API lê os bytes do banco e os reenvia ao navegador. As conexões e a memória do banco, em geral o recurso mais limitado, ficam ocupadas servindo arquivo estático.
  • Sem CDN direta. Uma imagem servida pela API só chega a um cache de borda com trabalho extra. Num object storage, a URL já pode ser cacheada por uma CDN.
  • Memória disputada. Binários grandes competem com os dados quentes que o banco deveria manter em cache.
  • Limites duros. O bytea para em 1 GB e o BSON em 16 MiB, e PDFs e vídeos esbarram nisso rápido. Se você não escolher os campos, um ORM que carrega a linha inteira traz a coluna binária junto.
  • Custo de escala. Aumentar o banco só para caber arquivos é pagar por capacidade de consulta que os arquivos não usam.

O que é object storage e o que ele resolve?#

Object storage guarda arquivos como objetos em buckets, cada um com uma chave. Você envia o arquivo por HTTP e ele ganha uma URL. O object storage foi desenhado para este uso: muitos arquivos, tamanhos variados, leitura de qualquer lugar da internet.

Ele cobre o que o banco não cobre bem: URL pública cacheável por CDN, URLs assinadas para arquivos privados, regras de ciclo de vida como expiração automática, upload direto do navegador sem passar pela sua API e upload multipart para arquivos grandes. Em contrapartida, não há transação entre o storage e o banco, e manter os dois consistentes passa a ser trabalho seu.

Banco de dados ou object storage: qual escolher?#

Critério Imagem no banco Imagem em object storage
Tamanho do arquivo Limitado (16 MiB no BSON, 1 GB no bytea) Muito maior; arquivos de GB são normais
Tamanho do banco Cresce com cada arquivo Fica só com metadados
Servir ao navegador Passa pela API e pelo banco Direto do storage ou da CDN
Cache de borda Trabalho extra Natural
Consistência com a linha Transacional Manual
Backup Um só, mas pesado Separado do banco
Acesso privado Regras da sua API URL assinada ou link de compartilhamento
Complexidade inicial Menor Um serviço e credenciais a mais

Escolha o banco quando os arquivos são poucos, pequenos (alguns KB), raramente lidos e precisam mudar junto com a linha. Escolha object storage quando as imagens vêm de uploads de usuários, passam de algumas centenas de KB, aparecem em páginas públicas ou o volume tende a crescer. Na dúvida, comece pelo storage: tirar GBs de imagens de dentro de um banco depois dá muito mais trabalho do que começar fora dele.

Como fazer o padrão "arquivo no storage, chave no banco"?#

Envie o arquivo ao storage, grave só a chave e os metadados no banco e monte a URL na hora da leitura. O exemplo usa Python, boto3 e psycopg 3 (pip install boto3 "psycopg[binary]"), com as credenciais em variáveis de ambiente:

python
import os
import uuid

import boto3
import psycopg

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ.get("S3_ENDPOINT"),  # omita no AWS S3
    aws_access_key_id=os.environ["S3_ACCESS_KEY"],
    aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)
BUCKET = os.environ["S3_BUCKET"]


def save_avatar(conn: psycopg.Connection, user_id: int, data: bytes) -> str:
    key = f"avatars/{user_id}/{uuid.uuid4()}.webp"
    s3.put_object(Bucket=BUCKET, Key=key, Body=data, ContentType="image/webp")
    try:
        conn.execute(
            "UPDATE users SET avatar_key = %s, avatar_size = %s WHERE id = %s",
            (key, len(data), user_id),
        )
        conn.commit()
    except Exception:
        s3.delete_object(Bucket=BUCKET, Key=key)  # não deixa objeto órfão
        raise
    return key


def avatar_url(key: str) -> str:
    # arquivo privado: URL assinada por 10 minutos
    return s3.generate_presigned_url(
        "get_object",
        Params={"Bucket": BUCKET, "Key": key},
        ExpiresIn=600,
    )

Três detalhes importam. O primeiro é a ordem: grave o arquivo antes da linha e apague o objeto se o banco falhar. Assim, o pior caso é um objeto sobrando, nunca uma linha apontando para o vazio. O segundo é usar uma chave nova a cada upload (aqui, um UUID), porque trocar o conteúdo sob a mesma chave deixa caches antigos servindo a imagem velha. O terceiro é guardar a chave, não a URL, e decidir na leitura se a imagem é pública ou privada (veja arquivo público ou privado).

Para limpar os órfãos que escaparam, rode de tempos em tempos um job que compare as chaves do bucket com as do banco, ou envie uploads temporários para uma pasta com regra de expiração.

Como fazer isso na Square Cloud#

O Blob Storage da Square Cloud é um object storage com API compatível com S3, então o código acima funciona com boto3, aws-cli, rclone ou os SDKs da AWS, apontando S3_ENDPOINT para https://s3-blob.squarecloud.app (região auto) com as chaves da aba "Acesso S3" do painel. Também dá para usar a API REST ou o painel. Cada arquivo pode ter até 10 GB, e o multipart só é obrigatório acima de 100 MB.

Arquivos públicos, como avatares e fotos de produto, saem por uma CDN global na edge, que guarda cópias em cache. Para os privados, a URL pré-assinada SigV4 que o seu código gera pode valer até 7 dias, e o painel oferece o link temporário (até 24 horas) e o link de compartilhamento (até 30 dias, revogável). Não há taxa de saída nem de entrada, e uma cota vem incluída em todo plano.

Para os dados, há hospedagem de MongoDB, PostgreSQL, MySQL e Redis a partir do plano Standard, no mesmo datacenter das aplicações. Com as imagens no Blob, o banco guarda só a chave e os metadados.

Conheça o Blob Storage

Blob Storage

Object storage compatível com S3, sem taxa de saída

Saiba mais

Perguntas frequentes

Posso guardar imagens no MySQL ou no PostgreSQL?

Pode. O MySQL tem BLOB, MEDIUMBLOB (até 16 MB) e LONGBLOB (até 4 GB), e o PostgreSQL guarda binários em bytea, com limite de 1 GB por valor. Funciona, mas o banco cresce, os backups engordam e cada imagem passa pela conexão do banco.

O que é o GridFS do MongoDB?

É a especificação do MongoDB para guardar arquivos maiores que o limite de 16 MiB por documento. Ele divide o arquivo em pedaços de 255 KiB por padrão e os grava em duas coleções, fs.files e fs.chunks.

Devo guardar a URL ou a chave do objeto no banco?

Prefira a chave, por exemplo avatars/42/foto.webp. A URL depende do domínio, da CDN e, em arquivos privados, de uma assinatura que expira. Com a chave você monta a URL certa a cada requisição.

Ícones pequenos, de poucos KB, podem ficar no banco?

Podem. Para poucos arquivos minúsculos que mudam junto com a linha, guardar no banco simplifica o código e dá consistência transacional. O custo aparece quando o volume e o tamanho crescem.

Próximo artigoO que é um bucket e como organizar arquivos neleUm bucket é o contêiner de mais alto nível do object storage: guarda objetos e define regras de acesso. Veja nomes, prefixos e como organizar arquivos nele.