Um jeito de enxergar o que acabou de acontecer
Quando você executa um INSERT, UPDATE ou DELETE, o SQLite informa quantas linhas foram afetadas — mas não diz quais linhas, nem como ficaram os valores finais. A gambiarra clássica é mandar um SELECT logo depois. Isso significa duas idas ao banco, dois comandos e uma brechinha de concorrência em que alguém pode alterar a linha entre uma operação e outra.
A cláusula RETURNING no SQLite resolve isso. Você anexa ela ao comando de escrita, lista as colunas que quer de volta e o SQLite devolve as linhas afetadas como se você tivesse acabado de rodar um SELECT em cima delas:
Uma instrução, uma ida ao banco, e você já recebe de volta o id gerado e o valor padrão de created_at que o próprio banco preencheu para você.
A cláusula RETURNING foi adicionada no SQLite 3.35.0 (março de 2021). Se a sua instrução for rejeitada com erro de sintaxe, dê uma olhada em SELECT sqlite_version(); — versões mais antigas não reconhecem essa palavra-chave.
Recuperando o ID gerado com INSERT RETURNING
O motivo mais comum para usar RETURNING no SQLite é justamente pegar a chave primária autogerada logo após o insert:
Antes da cláusula RETURNING, o jeito era fazer o INSERT e depois chamar last_insert_rowid() (ou o equivalente do seu driver) na mesma conexão. Isso ainda funciona, mas depende do estado da conexão — algo fácil de quebrar quando você usa pool de conexões ou múltiplas threads. Já o RETURNING id é explícito, fica preso ao próprio comando e se comporta da mesma maneira independentemente de quem está gerenciando a conexão.
Se a sua tabela não declara um INTEGER PRIMARY KEY explícito, dá pra recuperar o identificador implícito da linha do mesmo jeito:
Toda tabela comum no SQLite tem um rowid, e o RETURNING devolve ele pra você sem complicação.
Várias colunas e expressões
A cláusula RETURNING aceita o mesmo formato da lista de colunas de um SELECT. Você pode listar colunas, usar *, montar expressões e dar apelidos a elas:
RETURNING * é útil quando você quer trazer tudo de uma vez — inclusive os valores padrão que o banco preencheu — sem precisar listar cada coluna:
Você verá o novo id, o name que você passou e o timestamp que o SQLite calculou.
RETURNING com UPDATE
Em um UPDATE, a cláusula RETURNING devolve os valores já atualizados — ou seja, a linha como ela ficou depois das suas alterações:
Você recebe de volta o novo saldo da Ada, que é 125 — não o antigo 100. É justamente isso que torna o RETURNING perfeito para contadores atômicos e operações de débito/crédito: você não precisa ler, calcular, gravar e ler de novo.
Se o WHERE casar com várias linhas, você recebe uma linha de retorno para cada linha afetada:
Três linhas inseridas, três linhas devolvidas. A ordem não é garantida — se você precisar de uma ordem específica, ordene o resultado no lado do cliente.
RETURNING com DELETE
No DELETE, o RETURNING devolve as linhas como elas estavam logo antes da exclusão. Isso é útil para arquivamento, trilhas de auditoria ou simplesmente para confirmar o que foi removido:
Você recebe de volta as duas sessões expiradas com todos os campos intactos, mesmo que elas não existam mais na tabela. Se a ideia for movê-las para outro lugar, esse é o cenário perfeito para uma tabela de arquivo — basta ler o resultado e inserir em outro lugar dentro da mesma transação.
RETURNING com UPSERT
A cláusula RETURNING também funciona com INSERT ... ON CONFLICT ... DO UPDATE. A linha retornada reflete qual caminho foi executado — o insert novo ou o update do conflito:
Execute esse comando duas vezes. Na primeira, ele faz o INSERT e devolve ('visits', 1). Na segunda, o conflito é acionado, o valor é incrementado e você recebe ('visits', 2). Em ambos os casos, um comando entra e uma linha sai — sem precisar perguntar "foi insert ou update?" antes de seguir adiante.
Esse é o padrão mais limpo do SQLite para "me devolva o valor atual, criando se preciso", sem ficar indo e voltando ao banco.
Alguns detalhes que vale saber
Algumas pegadinhas que costumam pegar a galera de surpresa:
- O
RETURNINGsempre enxerga a linha depois da alteração no caso deINSERTeUPDATE, e antes da alteração noDELETE. Não existe sintaxe para pedir o outro lado. - A ordem das linhas retornadas não é garantida. Se isso importa, adicione um
ORDER BYno lado do cliente. - Você não pode usar
RETURNINGdentro de uma subquery. É uma cláusula de nível superior no comando de escrita, não uma expressão. - O
RETURNINGnão devolve os dados modificados pelos triggersBEFORE— ele retorna os valores que realmente foram gravados. Os triggersAFTERrodam entre a escrita e a devolução da linha. - Colunas geradas e valores
DEFAULTaparecem no resultado. É justamente por isso queRETURNING *é uma forma rápida de conferir o que o banco preencheu para você.
A seguir: importando dados de CSV
A cláusula RETURNING é ótima quando você está gravando uma linha ou um punhado delas e quer ver o resultado na hora. Quando o cenário é carregar milhares de linhas a partir de um arquivo, o caminho são as ferramentas de importação de CSV do SQLite — assunto da próxima página.
Perguntas frequentes
O SQLite suporta a cláusula RETURNING?
Sim, a partir da versão 3.35.0 (lançada em março de 2021). Dá pra acrescentar RETURNING em comandos INSERT, UPDATE e DELETE para receber de volta as linhas afetadas. Se você estiver numa versão mais antiga, o parser vai recusar — confira com SELECT sqlite_version();.
Como pegar o ID de uma linha recém-inserida no SQLite?
Use INSERT ... RETURNING id (ou RETURNING rowid se a tabela não tiver chave primária explícita). O valor gerado volta junto com o próprio comando, então você não precisa fazer uma segunda chamada com last_insert_rowid().
Dá para retornar mais de uma coluna com RETURNING?
Dá sim. É só listar as colunas separadas por vírgula, igualzinho num SELECT: RETURNING id, name, created_at. Você também pode usar RETURNING * para trazer todas as colunas, ou escrever expressões como RETURNING id, price * quantity AS total.
RETURNING funciona com UPSERT e ON CONFLICT?
Funciona. O INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING ... devolve a linha tanto se ela foi inserida agora quanto se foi atualizada pela resolução de conflito. É a forma mais limpa de fazer um upsert e já ler o estado final em uma única ida ao banco.