- Adicionar
STRICT ao final da definição de uma tabela permite bloquear cedo erros de tipo, como texto arbitrário em uma coluna de inteiros, aumentando a integridade dos dados
- Ao inserir ou atualizar, os tipos são verificados, mas valores que podem ser convertidos sem perda, como
'123', são permitidos; os tipos de coluna ficam limitados a INT, INTEGER, REAL, TEXT, BLOB, ANY
- Para colunas que precisam armazenar vários tipos, é possível especificar
ANY, combinando validação rigorosa e armazenamento flexível em uma mesma tabela
- Não é possível transformar diretamente uma tabela existente em STRICT; é necessário criar uma nova tabela e copiar os dados, limpando ou convertendo dados antigos inválidos
- Tabelas STRICT são compatíveis apenas com SQLite 3.37.0 ou superior e, embora haja um custo teórico de verificação, testes informais não identificaram diferenças claras de desempenho ou tamanho de arquivo
Criação de tabelas STRICT
- As tabelas STRICT do SQLite aplicam uma verificação de tipos mais rigorosa, semelhante à de outros motores SQL
- Basta acrescentar
STRICT ao final da definição em CREATE TABLE
CREATE TABLE people (name TEXT) STRICT;
Verificação de tipos em inserções e atualizações
- Tabelas SQLite comuns podem armazenar texto como
'garbage' até mesmo em colunas INTEGER, mas tabelas STRICT tratam incompatibilidades de tipo como erro
CREATE TABLE people_nonstrict (age INTEGER);
INSERT INTO people_nonstrict (age) VALUES ('garbage');
-- 정상 처리됨
CREATE TABLE people_strict (age INTEGER) STRICT;
INSERT INTO people_strict (age) VALUES ('garbage');
-- 오류: cannot store TEXT value in INTEGER column
- A mesma validação também se aplica a
UPDATE, impedindo tipos incorretos ao alterar valores após o armazenamento
- No entanto, se o valor puder ser convertido sem perda, ele também é permitido em tabelas STRICT
- A string
'123' pode ser convertida integralmente para o inteiro 123, então as duas inserções abaixo são tratadas da mesma forma
INSERT INTO people_strict (age) VALUES ('123');
INSERT INTO people_strict (age) VALUES (123);
Restrições de tipo na definição da tabela
- Em tabelas comuns, é possível usar na declaração de colunas até nomes de tipos que o SQLite não oferece suporte
- Tabelas STRICT geram erro já na criação quando se usa um nome de tipo não suportado
CREATE TABLE tbl (name GARBAGE) STRICT;
CREATE TABLE tbl (name DATETIME) STRICT;
CREATE TABLE tbl (name JSON) STRICT;
CREATE TABLE tbl (name UUID) STRICT;
CREATE TABLE tbl (name BLOBB) STRICT;
- Os únicos tipos de coluna permitidos são
INT, INTEGER, REAL, TEXT, BLOB, ANY
- Como todas as colunas precisam ter um tipo especificado, não é possível omitir o tipo como em
CREATE TABLE tbl (name)
Mantendo flexibilidade com ANY
- Se for necessário armazenar dados cujo tipo não é constante, é possível usar uma coluna
ANY
- Mesmo dentro de uma tabela STRICT, colunas
ANY podem armazenar todos os tipos, incluindo inteiros, texto, números reais e BLOBs
CREATE TABLE tbl (value ANY) STRICT;
INSERT INTO tbl (value) VALUES (123);
INSERT INTO tbl (value) VALUES ('text');
INSERT INTO tbl (value) VALUES (12.34);
INSERT INTO tbl (value) VALUES (X'8647');
Migração de tabelas existentes
- Como não há uma forma de converter diretamente uma tabela não estrita existente em uma tabela STRICT via
ALTER, é mais simples criá-la estrita desde o início
- Para converter uma tabela existente, é preciso seguir o processo de criar uma nova tabela STRICT → copiar os dados → substituir a tabela antiga
CREATE TABLE new_people (name TEXT) STRICT;
INSERT INTO new_people SELECT * FROM people;
DROP TABLE people;
ALTER TABLE new_people RENAME TO people;
- Se os dados existentes tiverem tipos incorretos, como texto em uma coluna de inteiros, a cópia pode gerar erro
- Antes da migração, talvez seja necessário limpar os dados ou convertê-los com
CAST
- Também é possível aplicar STRICT apenas a novas tabelas, mas isso faz com que a intensidade da validação varie entre tabelas, o que pode tornar o comportamento mais difícil de prever do que se todas fossem flexíveis
Quando tipos flexíveis são adequados
- A equipe de desenvolvimento do SQLite documenta separadamente as vantagens dos tipos flexíveis e apresenta casos em que o comportamento padrão é útil
- Usos em que tabelas não estritas podem ser razoáveis incluem:
- Um armazenamento chave-valor puro que guarda vários tipos diretamente
- Um espaço para armazenar outros atributos de tipos diferentes
- Casos em que é preciso importar CSVs desorganizados diretamente e preservar até valores inválidos sem perdê-los
- Porém, tipos inesperados podem causar bugs sutis; em tabelas comuns, costuma ser melhor gerar erro imediatamente do que permitir silenciosamente
- Há também um comentário no código-fonte do SQLite que chama tabelas não estritas de
legacy, mas é difícil tratá-lo como uma base mais confiável do que a documentação oficial
Compatibilidade de versão
- Tabelas STRICT foram introduzidas no SQLite 3.37.0, lançado em novembro de 2021
- Versões anteriores do SQLite não podem usar tabelas STRICT
- Versões antigas também não conseguem ler bancos de dados que contenham tabelas STRICT
- Se você criar uma tabela STRICT no SQLite mais recente e depois abrir o mesmo banco de dados com o SQLite 3.36.0, ocorrerá um erro
Desempenho e espaço de armazenamento
- Como tabelas STRICT fazem uma verificação adicional do tipo de dado em inserções e atualizações, elas podem ser mais lentas em teoria
- Em um experimento informal que inseriu milhões de linhas em uma tabela com 100 colunas, não houve diferença clara de desempenho em vários computadores
- O tamanho do arquivo de banco de dados em disco também foi igual, mas, por não ser um benchmark rigoroso, podem existir diferenças que não foram detectadas
- Também é possível que o desempenho melhore por impedir que valores incompatíveis com a afinidade da coluna sejam salvos por engano, mas isso não foi verificado separadamente
Pontos a considerar ao adotar
- Tabelas STRICT não resolvem todos os problemas de dados, mas reduzem erros relacionados a tipos e reforçam a integridade dos dados
- Na maioria dos casos, a adoção é simples: basta adicionar
STRICT à definição da tabela
- Em tabelas comuns, os benefícios da validação rigorosa de tipos tendem a superar o esforço de migração, a incompatibilidade com versões antigas e a flexibilidade limitada
1 comentários
Comentários do Hacker News
Como o SQLite não tem um recurso para usar
ALTERe mudar uma tabela para o modo estrito, sendo necessário copiar os dados de uma tabela não estrita para uma tabela estrita, foi adicionada ao sqlite-utils 4.1 uma função de conversão nos dois sentidosNa CLI, pode ser usado com
uvx sqlite-utils transform data.db mytable --strict; em Python, comdb.table("mytable").transform(strict=True)As notas de lançamento estão em https://sqlite-utils.datasette.io/en/stable/changelog.html#v..., a documentação da API Python em https://sqlite-utils.datasette.io/en/stable/python-api.html#... e a documentação da CLI em https://sqlite-utils.datasette.io/en/stable/cli.html#transfo...
https://sqlite.org/flextypegood.html explica por que a tipagem flexível é o padrão e por que isso provavelmente não vai mudar, mas isso não bate com a experiência prática
Ao contrário da explicação de que, mesmo que o nome de um cliente seja colocado no
Customer.creditScoreinteiro, isso pode ser facilmente detectado e corrigido, na prática uma linha corrompida é difícil de recuperar e os dados podem até ter desaparecido por completoTambém é muito difícil concordar com a lógica de que a checagem rígida de tipos só impede erros fáceis de encontrar e, por isso, acabaria dificultando a detecção e a correção de bugs
No Postgres, a prática é adicionar o máximo possível de verificações e proteções, para não precisar sair procurando depois erros que nem deveriam ter acontecido
Isso parece menos um princípio de engenharia rigoroso e mais um resultado da época em que o SQLite foi criado e das convicções fortes do autor, passando a impressão de manter a posição a qualquer custo depois de muitas críticas
O tipo de flexibilidade desejado pode ser oferecido com tipos como JSON ou HSTORE no Postgres, mas quase sempre é melhor deixar isso como uma escolha quando necessário, em vez de impor tipagem ilimitada como padrão
Seria bom se STRICT fosse o padrão
Tirando esse ponto, o desenvolvedor do SQLite é alguém incrível que criou uma ferramenta excelente
PRAGMA foreign_keys = ON;O problema maior é que não existe uma pragma equivalente para tabelas estritas, então é preciso adicionar o
STRICT, que não é padrão, a todoCREATE TABLEUma pragma global para STRICT chegou a ser considerada, mas não foi implementada; materiais relacionados podem ser vistos em https://sqlite.org/foreignkeys.html e https://sqlite.org/forum/forumpost/1b9d073a37ca5998
A ideia é evitar que um software para SQLite 3.53, ao ser atualizado para 3.54, passe de repente a criar tabelas estritas com
CREATE TABLE, gere erros e quebre tudoIsso faz pensar se houve um tempo em que o SQLite só suportava a sintaxe de chaves estrangeiras, sem implementar de fato a funcionalidade
Já houve caso de código distribuído para milhares de dispositivos armazenar as strings
'1'e'0'em uma coluna booleana, e limpar isso depois não foi nada agradávelTambém não existe um tipo de timestamp, então é preciso guardar isso em uma coluna de texto, e até as funções padrão de data e hora geram
yyyy-mm-dd HH:MM:SS, que assume UTC implicitamente, em vez do formato ISOyyyy-mm-ddTHH:MM:SSZO SQLite é realmente um projeto excelente, mas algumas decisões de design são desconcertantes
Vindo de um ambiente de SQL corporativo, eu nunca levei o SQLite muito a sério porque os tipos de campo não eram, por padrão, realmente impostos, e até me surpreendi quando ele virou a base dos metadados de apps de smartphone
Isso me lembra aquela velha história de rede em que alguém escolhe UDP por causa da baixa latência e da simplicidade, e depois acaba recriando manualmente na aplicação a maior parte dos mecanismos de confiabilidade do TCP
Mas não parece que implementar checagem de tipos manualmente em cima de um SQLite não estrito traga vantagens parecidas
O PostgreSQL também vem com padrões voltados para sistemas modestos, então é preciso configurar para obter o melhor desempenho
Além disso, o TCP é baseado em fluxo, então muitos aplicativos sofrem com bloqueio HOL; e, se você criar manualmente uma camada de confiabilidade sobre UDP, depois de muito trabalho ainda pode acabar com algo melhor que TCP
Eles vão dizer que são barulhentos e têm dirigibilidade ruim, mas isso acontece porque abordam um produto pensado para outro uso como se apenas o próprio caso de uso deles importasse
Tabelas estritas têm a desvantagem de que alguns tipos, como Date, não podem ser usados, mas ainda assim deveriam ser o padrão
Se vários aplicativos compartilham o banco de dados, deveria ser possível confiar no tipo declarado; se um deles gravar uma string numa coluna numérica, estraga tudo para os outros
Por outro lado, no principal caso de uso do SQLite, como banco de dados embarcado, normalmente só existe um único aplicativo usando-o, então também há a vantagem de poder evoluir o esquema em vez de criar um banco novo e copiar tudo
Nesse caso, o código da aplicação sabe o que esperar de cada coluna, inclusive tipos mistos
INTEGER,TEXT,BLOB,REAL,NUMERIC: https://sqlite.org/datatype3.htmlEXPLAINem uma consultaSELECTarbitrária para recuperar o nome de tipo definido apenas na formaPortanto, mesmo usando nomes como
DATEouDATETIME, não dá nem para inferi-losDatenão é um tipo real; isso apenas cria uma coluna com afinidade numéricaO modo estrito restringe a notação dos tipos de coluna, impedindo o uso de nomes mais significativos e fazendo com que o código que mapeia tipos do banco para tipos da aplicação não possa aproveitar esses nomes, então ele acaba até atrapalhando um sistema de tipos mais rigoroso na camada da aplicação
Há mais detalhes em https://hn.algolia.com/?query=chrismorgan+strict+sqlite&type..., e, se você trabalha com banco de dados usando crates como sqlx em Rust, eu diria que é melhor evitar o modo estrito
Eu quero tipos estritos em banco de dados e RPC, mas o SQLite tem um propósito um pouco diferente; se você realmente usá-lo, talvez passe a entender melhor https://sqlite.org/flextypegood.html
Por exemplo, o objetivo de fazer com que um script qualquer, não escrito para SQLite, acabe funcionando por acaso é uma característica que normalmente nem entra nas considerações de outros SGBDs
A principal vantagem da tipagem flexível é que ela permite evoluir o esquema com facilidade
Quando os requisitos de um banco embarcado mudam, em vez de criar um banco novo e migrar os dados, você pode alterar o esquema no próprio lugar; e, se só um aplicativo lê e escreve, não há surpresa se uma string aparecer numa coluna inteira
Em contrapartida, se vários aplicativos compartilham o mesmo banco e são atualizados em ritmos diferentes, o esquema é um contrato, então um único tipo incorreto pode quebrar outro aplicativo
Se uma tabela criada por um aplicativo for usada por outro, os tipos de dados combinados precisam ser seguidos com rigor
Verificar de novo em tempo de execução não traz ganho real, e um código que a análise estática confirmou que insere uma string não vai de repente sofrer mutação e passar a inserir um inteiro
O SQLite é diferente do Postgres, onde vários aplicativos compartilham dados e é preciso confiar em terceiros
Em um ambiente como o Postgres, a validação em tempo de execução é essencial, mas o SQLite foi concebido, por padrão, para um aplicativo e um banco de dados; portanto, basta confiar no próprio código, que pode ser avaliado em tempo de compilação
Em situações atípicas em que vários aplicativos compartilham um único arquivo, é possível ativar tabelas estritas
Tipagem dinâmica pode servir para usos como um armazenamento simples de chave e valor, mas é preciso perguntar o que está mais de acordo com o princípio da menor surpresa:
INTEGERaceitar'hello world'sem erro, ou só permitir esse tipo de valor quando algo como a palavra-chaveNONSTRICTou o tipoANYfor explicitadoA maioria dos usuários de SQLite provavelmente não esperaria que a primeira opção fosse possível
Isso também bate com a explicação de que ele quer que código feito para outros SGBDs acabe funcionando por acaso no SQLite
Ainda assim, é muito surpreendente que o usuário não tenha omitido o tipo, tenha escrito
INTEGERexplicitamente e ainda assim consiga inserir textoJá houve caso de UUIDs começando com algo como
08123…serem interpretados como octal, fazendo com que parte deles fosse convertida erroneamente para númeroFoi confuso e irritante, mas resolveu-se aplicando
STRICTe recriando a tabela inteiraO SQLite não tem tipo octal
Uso
CREATE TABLE ... STRICT WITHOUT ROWIDcomo padrão e não vejo motivo para, deliberadamente, criar de outra forma