SQLite
 sql >> Base de Dados >  >> RDS >> SQLite

Armadilhas e armadilhas do SQLite


SQLite é um banco de dados relacional popular que você incorpora em seu aplicativo. No entanto, existem muitas armadilhas e armadilhas que você deve evitar. Este artigo discute várias armadilhas (e como evitá-las), como o uso de ORMs, como recuperar espaço em disco, cuidar do número máximo de variáveis ​​de consulta, tipos de dados de coluna e como lidar com números inteiros grandes.

Introdução


SQLite é um sistema de banco de dados relacional (DB) popular . Ele tem um conjunto de recursos muito semelhante aos seus irmãos maiores, como o MySQL , que são sistemas baseados em cliente/servidor. No entanto, SQLite é um incorporado banco de dados . Ele pode ser incluído em seu programa como uma biblioteca estática (ou dinâmica). Isso simplifica a implantação , porque nenhum processo de servidor separado é necessário. Bindings e bibliotecas wrapper permitem acessar o SQLite na maioria das linguagens de programação .

Eu tenho trabalhado extensivamente com SQLite enquanto desenvolvia o BSync como parte da minha dissertação de doutorado. Este artigo é uma lista (aleatória) de armadilhas e armadilhas que encontrei durante o desenvolvimento . Espero que você os ache úteis e evite cometer os mesmos erros que cometi uma vez.

Armadilhas e armadilhas

Use bibliotecas ORM com cuidado


As bibliotecas de mapeamento relacional de objeto (ORM) abstraem os detalhes de mecanismos de banco de dados concretos e sua sintaxe (como instruções SQL específicas) para uma API orientada a objetos de alto nível. Existem muitas bibliotecas de terceiros por aí (veja Wikipedia). As bibliotecas ORM têm algumas vantagens:
  • Eles economizam tempo durante o desenvolvimento , porque eles mapeiam rapidamente seu código/classes para estruturas de banco de dados,
  • Eles são geralmente multiplataforma , ou seja, permitir a substituição da tecnologia de banco de dados concreto (por exemplo, SQLite com MySQL),
  • Eles oferecem código auxiliar para migração de esquema .

No entanto, eles também têm várias desvantagens graves você deve estar ciente de:
  • Eles fazem o trabalho com bancos de dados aparecer fácil . No entanto, na realidade, os mecanismos de banco de dados têm detalhes complexos que você simplesmente precisa saber . Quando algo der errado, por exemplo, quando a biblioteca ORM gera exceções que você não entende ou quando o desempenho do tempo de execução diminui, o tempo de desenvolvimento que você economizou usando o ORM será rapidamente consumido pelos esforços necessários para depurar o problema . Por exemplo, se você não souber quais índices são, você teria dificuldade em solucionar gargalos de desempenho causados ​​pelo ORM, quando ele não criasse automaticamente todos os índices necessários. Em essência:não existe almoço grátis.
  • Devido à abstração do fornecedor de banco de dados concreto, funcionalidade específica do fornecedor é difícil de acessar, ou não acessível .
  • alguma sobrecarga computacional comparado a escrever e executar consultas SQL diretamente. No entanto, eu diria que esse ponto é discutível na prática, pois é comum que você perca desempenho quando muda para um nível mais alto de abstração.

No final, usar uma biblioteca ORM é uma questão de preferência pessoal. Se você fizer isso, esteja preparado para aprender sobre as peculiaridades dos bancos de dados relacionais (e advertências específicas do fornecedor), assim que ocorrerem comportamentos inesperados ou gargalos de desempenho.

Incluir uma tabela de migrações desde o início


Se você não usando uma biblioteca ORM, você terá que cuidar da migração de esquema do banco de dados . Isso envolve escrever código de migração que altera seus esquemas de tabela e transforma os dados armazenados de alguma forma. Eu recomendo que você crie uma tabela chamada “migrações” ou “versão”, com uma única linha e coluna, que simplesmente armazena a versão do esquema, por exemplo usando um inteiro monotonicamente crescente. Isso permite que sua função de migração detecte quais migrações ainda precisam ser aplicadas. Sempre que uma etapa de migração foi concluída com êxito, seu código de ferramentas de migração incrementa esse contador por meio de um UPDATE instrução SQL.

Coluna rowid criada automaticamente


Sempre que você criar uma tabela, o SQLite criará automaticamente um INTEGER coluna chamada rowid para você – a menos que você tenha fornecido o WITHOUT ROWID cláusula (mas é provável que você não saiba sobre esta cláusula). O rowid linha é uma coluna de chave primária. Se você também especificar uma coluna de chave primária (por exemplo, usando a sintaxe some_column INTEGER PRIMARY KEY ) esta coluna será simplesmente um alias para rowid . Veja aqui para mais informações, que descrevem a mesma coisa em palavras bastante enigmáticas. Observe que um SELECT * FROM table declaração não inclua rowid por padrão - você precisa pedir o rowid coluna explicitamente.

Verifique se PRAGMA realmente funciona


Entre outras coisas, PRAGMA instruções são usadas para definir as configurações do banco de dados ou para invocar várias funcionalidades (documentos oficiais). No entanto, há efeitos colaterais não documentados em que, às vezes, definir uma variável não tem efeito . Em outras palavras, não funciona e falha silenciosamente.

Por exemplo, se você emitir as seguintes declarações na ordem indicada, a última declaração não ter qualquer efeito. Variável auto_vacuum ainda tem valor 0 (NONE ), sem motivo.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)

Você pode ler o valor de uma variável executando PRAGMA variableName e omitindo o sinal de igual e o valor.

Para corrigir o exemplo acima, use uma ordem diferente. Usar a ordem de linha 3, 1, 2 funcionará conforme o esperado.

Você pode até querer incluir essas verificações em sua produção código, porque esses efeitos colaterais podem depender da versão concreta do SQLite e de como ela foi construída. A biblioteca usada na produção pode ser diferente daquela usada durante o desenvolvimento.

Reivindicando espaço em disco para bancos de dados grandes


Por padrão, o tamanho de um arquivo de banco de dados SQLite está aumentando monotonicamente . A exclusão de linhas marca apenas páginas específicas como gratuitas , para que possam ser usados ​​para INSERT dados no futuro. Para realmente recuperar espaço em disco e acelerar o desempenho, há duas opções:
  1. Execute o VACUUM declaração . No entanto, isso tem vários efeitos colaterais:
    • Ele bloqueia todo o banco de dados. Nenhuma operação simultânea pode ocorrer durante o VACUUM operação.
    • Demora muito tempo (para bancos de dados maiores), porque internamente recria o banco de dados em um arquivo temporário separado e, finalmente, exclui o banco de dados original, substituindo-o por esse arquivo temporário.
    • O arquivo temporário consome adicionais espaço em disco enquanto a operação está em execução. Portanto, não é uma boa ideia executar VACUUM caso você esteja com pouco espaço em disco. Você ainda pode fazer isso, mas teria que verificar regularmente se (freeDiskSpace - currentDbFileSize) > 0 .
  2. Use PRAGMA auto_vacuum = INCREMENTAL ao criar o banco de dados. Faça este PRAGMA o primeiro declaração depois de criar o arquivo! Isso permite alguma manutenção interna, ajudando o banco de dados a recuperar espaço sempre que você chamar PRAGMA incremental_vacuum(N) . Esta chamada recupera até N Páginas. Os documentos oficiais fornecem mais detalhes e também outros valores possíveis para auto_vacuum .
    • Observação:você pode determinar quanto espaço livre em disco (em bytes) seria ganho ao chamar PRAGMA incremental_vacuum(N) :multiplique o valor retornado por PRAGMA freelist_count com PRAGMA page_size .

A melhor opção depende do seu contexto. Para arquivos de banco de dados muito grandes, recomendo a opção 2 , porque a opção 1 incomodaria seus usuários com minutos ou horas de espera pela limpeza do banco de dados. A opção 1 é adequada para bancos de dados menores . Sua vantagem adicional é que o desempenho do banco de dados melhorará (o que não é o caso da opção 2), porque a recriação elimina os efeitos colaterais da fragmentação de dados.

Lembre-se do número máximo de variáveis ​​nas consultas


Por padrão, o número máximo de variáveis ​​("parâmetros de host") que você pode usar em uma consulta é codificado em 999 (veja aqui a seção Número máximo de parâmetros de host em uma única instrução SQL ). Este limite pode variar, pois é um tempo de compilação parâmetro, cujo valor padrão você (ou quem mais compilou o SQLite) pode ter alterado.

Isso é problemático na prática, porque não é incomum que seu aplicativo forneça uma lista (arbitrariamente grande) para o mecanismo de banco de dados. Por exemplo, se você quiser massa-DELETE (ou SELECT ) com base em, digamos, uma lista de IDs. Uma afirmação como
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)Code language: SQL (Structured Query Language) (sql)

lançará um erro e não será concluído.

Para corrigir isso, considere as seguintes etapas:
  • Analise suas listas e divida-as em listas menores,
  • Se uma divisão for necessária, certifique-se de usar BEGIN TRANSACTION e COMMIT para emular a atomicidade que uma única declaração teria .
  • Lembre-se de considerar também outros ? variáveis ​​que você pode usar em sua consulta que não estão relacionadas à lista de entrada (por exemplo, ? variáveis ​​usadas em um ORDER BY condição), para que o total o número de variáveis ​​não excede o limite.

Uma solução alternativa é o uso de tabelas temporárias. A ideia é criar uma tabela temporária, inserir as variáveis ​​de consulta como linhas e, em seguida, usar essa tabela temporária em uma subconsulta, por exemplo,
DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)Code language: SQL (Structured Query Language) (sql)

Cuidado com a afinidade de tipo do SQLite


As colunas SQLite não são estritamente tipadas, e as conversões não ocorrem necessariamente como esperado. Os tipos que você fornece são apenas dicas . O SQLite geralmente armazena dados de qualquer digite seu original type, e apenas converta os dados para o tipo da coluna caso a conversão seja sem perdas. Por exemplo, você pode simplesmente inserir um "hello" string em um INTEGER coluna. O SQLite não reclamará ou avisará sobre incompatibilidades de tipo. Por outro lado, você não pode esperar que os dados sejam retornados por um SELECT declaração de um INTEGER coluna é sempre um INTEGER . Essas dicas de tipo são chamadas de “afinidade de tipo” na linguagem SQLite, veja aqui. Certifique-se de estudar atentamente esta parte do manual SQLite, para entender melhor o significado dos tipos de coluna que você especifica ao criar novas tabelas.

Cuidado com números inteiros grandes


SQLite suporta assinado inteiros de 64 bits , com o qual ele pode armazenar ou fazer cálculos. Em outras palavras, apenas números de -2^63 para (2^63) - 1 são suportados, porque um bit é necessário para representar o sinal!

Isso significa que, se você espera trabalhar com números maiores, por exemplo, inteiros de 128 bits (assinados) ou inteiros não assinados de 64 bits, você deve converter os dados para texto antes de inseri-lo .

O horror começa quando você ignora isso e simplesmente insere números maiores (como números inteiros). SQLite não vai reclamar e armazenar um arredondado número em vez disso! Por exemplo, se você inserir 2^63 (que já está fora do intervalo suportado), o SELECT valor ed será 9223372036854776000, e não 2^63=9223372036854775808. Dependendo da linguagem de programação e da biblioteca de vinculação que você usa, o comportamento pode ser diferente! Por exemplo, a ligação sqlite3 do Python verifica esses estouros de inteiros!

Não use REPLACE() para caminhos de arquivo


Imagine que você armazena caminhos de arquivos relativos ou absolutos em um TEXT coluna no SQLite, por exemplo para acompanhar os arquivos no sistema de arquivos real. Aqui está um exemplo de três linhas:
foo/test.txt
foo/bar/
foo/bar/x.y

Suponha que você queira renomear o diretório “foo” para “xyz”. Qual comando SQL você usaria? Este?
REPLACE(path_column, old_path, new_path) Code language: SQL (Structured Query Language) (sql)

Foi o que fiz, até que coisas estranhas começaram a acontecer. O problema com REPLACE() é que ele substituirá todos ocorrências. Se houver uma linha com o caminho “foo/bar/foo/”, então REPLACE(column_name, 'foo/', 'xyz/') causará estragos, pois o resultado não será “xyz/bar/foo/”, mas “xyz/bar/xyz/”.

Uma solução melhor é algo como
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"Code language: SQL (Structured Query Language) (sql)

O 4 reflete o comprimento do caminho antigo ('foo/' neste caso). Observe que usei GLOB em vez de LIKE para atualizar apenas as linhas que iniciam com 'foo/'.

Conclusão


SQLite é um fantástico mecanismo de banco de dados, onde a maioria dos comandos funciona conforme o esperado. No entanto, complexidades específicas, como as que acabei de apresentar, ainda exigem a atenção de um desenvolvedor. Além deste artigo, certifique-se de ler também a documentação oficial de advertências do SQLite.

Você já encontrou outras advertências no passado? Se sim, deixe-me saber nos comentários.