SQLite est une base de données relationnelle populaire que vous intégrez dans votre application. Cependant, il existe de nombreux pièges et pièges à éviter. Cet article traite de plusieurs pièges (et comment les éviter), tels que l'utilisation d'ORM, comment récupérer de l'espace disque, respecter le nombre maximum de variables de requête, les types de données de colonne et comment gérer les grands nombres entiers.
Présentation
SQLite est un système de base de données relationnelle (DB) populaire . Il a un ensemble de fonctionnalités très similaire à ses grands frères, tels que MySQL , qui sont des systèmes client/serveur. Cependant, SQLite est un embarqué base de données . Il peut être inclus dans votre programme en tant que bibliothèque statique (ou dynamique). Cela simplifie le déploiement , car aucun processus serveur distinct n'est nécessaire. Les liaisons et les bibliothèques wrapper vous permettent d'accéder à SQLite dans la plupart des langages de programmation .
J'ai beaucoup travaillé avec SQLite lors du développement de BSync dans le cadre de ma thèse de doctorat. Cet article est une liste (aléatoire) de pièges et de pièges sur lesquels je suis tombé pendant le développement . J'espère que vous les trouverez utiles et que vous éviterez de commettre les mêmes erreurs que moi.
Pièges et pièges
Utilisez les bibliothèques ORM avec prudence
Les bibliothèques de mappage objet-relationnel (ORM) résument les détails des moteurs de base de données concrets et leur syntaxe (telles que des instructions SQL spécifiques) dans une API orientée objet de haut niveau. Il existe de nombreuses bibliothèques tierces (voir Wikipedia). Les bibliothèques ORM présentent quelques avantages :
- Ils gagnent du temps lors du développement , car ils mappent rapidement votre code/classes aux structures de base de données,
- Ils sont souvent multiplateformes , c'est-à-dire permettre la substitution de la technologie de base de données concrète (par exemple, SQLite avec MySQL),
- Ils proposent un code d'assistance pour la migration de schéma .
Cependant, ils présentent également plusieurs inconvénients graves vous devez être conscient de :
- Ils font apparaître le travail avec les bases de données facile . Cependant, en réalité, les moteurs de base de données ont des détails complexes que vous devez simplement connaître . Une fois que quelque chose ne va pas, par ex. lorsque la bibliothèque ORM lève des exceptions que vous ne comprenez pas, ou lorsque les performances d'exécution se dégradent, le temps de développement que vous avez économisé en utilisant ORM sera rapidement absorbé par les efforts requis pour déboguer le problème . Par exemple, si vous ne savez pas quels indices sont, vous auriez du mal à résoudre les goulots d'étranglement de performances causés par l'ORM, quand il n'a pas créé automatiquement tous les index requis. Essentiellement :il n'y a pas de déjeuner gratuit.
- En raison de l'abstraction du fournisseur de base de données concret, les fonctionnalités spécifiques au fournisseur sont soit difficiles d'accès, soit pas du tout accessibles .
- Il y a une surcharge de calcul par rapport à l'écriture et à l'exécution directes de requêtes SQL. Cependant, je dirais que ce point est sans objet dans la pratique, car il est courant que vous perdiez des performances une fois que vous passez à un niveau d'abstraction supérieur.
En fin de compte, l'utilisation d'une bibliothèque ORM est une question de préférence personnelle. Si c'est le cas, préparez-vous simplement à découvrir les particularités des bases de données relationnelles (et les mises en garde spécifiques aux fournisseurs), en cas de comportement inattendu ou de goulots d'étranglement des performances.
Inclure un tableau des migrations dès le début
Si vous n'êtes pas en utilisant une bibliothèque ORM, vous devrez vous occuper de la migration du schéma de la base de données . Cela implique d'écrire du code de migration qui modifie vos schémas de table et transforme les données stockées d'une manière ou d'une autre. Je vous recommande de créer une table appelée "migrations" ou "version", avec une seule ligne et colonne, qui stocke simplement la version du schéma, par ex. en utilisant un nombre entier monotone croissant. Cela permet à votre fonction de migration de détecter les migrations qui doivent encore être appliquées. Chaque fois qu'une étape de migration a été effectuée avec succès, votre code d'outil de migration incrémente ce compteur via un UPDATE
Instruction SQL.
Colonne rowid créée automatiquement
Chaque fois que vous créez une table, SQLite créera automatiquement un INTEGER
colonne nommée rowid
pour vous – sauf si vous avez fourni le WITHOUT ROWID
clause (mais il y a de fortes chances que vous ne connaissiez pas cette clause). Le rowid
row est une colonne de clé primaire. Si vous spécifiez également vous-même une telle colonne de clé primaire (par exemple, en utilisant la syntaxe some_column INTEGER PRIMARY KEY
) cette colonne sera simplement un alias pour rowid
. Voir ici pour plus d'informations, qui décrivent la même chose avec des mots plutôt cryptés. Notez qu'une table SELECT * FROM table
déclaration ne sera pas inclure rowid
par défaut - vous devez demander le rowid
colonne explicitement.
Vérifiez que PRAGMA
ça marche vraiment
Entre autres, PRAGMA
les instructions sont utilisées pour configurer les paramètres de la base de données ou pour invoquer diverses fonctionnalités (documents officiels). Cependant, il existe des effets secondaires non documentés où parfois la définition d'une variable n'a en fait aucun effet . En d'autres termes, cela ne fonctionne pas et échoue silencieusement.
Par exemple, si vous émettez les instructions suivantes dans l'ordre indiqué, le dernier déclaration ne sera pas avoir aucun effet. Variable auto_vacuum
a toujours la valeur 0
(NONE
), sans raison valable.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Vous pouvez lire la valeur d'une variable en exécutant PRAGMA variableName
et en omettant le signe égal et la valeur.
Pour corriger l'exemple ci-dessus, utilisez un ordre différent. L'utilisation de l'ordre des lignes 3, 1, 2 fonctionnera comme prévu.
Vous pouvez même inclure de telles vérifications dans votre production code, car ces effets secondaires peuvent dépendre de la version concrète de SQLite et de la façon dont elle a été construite. La bibliothèque utilisée en production peut différer de celle que vous avez utilisée lors du développement.
Réclamer de l'espace disque pour les grandes bases de données
Par défaut, la taille d'un fichier de base de données SQLite est croissante de façon monotone . La suppression de lignes ne marque que des pages spécifiques comme libre , afin qu'ils puissent être utilisés pour INSERT
données à l'avenir. Pour réellement récupérer de l'espace disque et accélérer les performances, il existe deux options :
- Exécuter le
VACUUM
déclaration . Cependant, cela a plusieurs effets secondaires :- Il verrouille l'intégralité de la base de données. Aucune opération simultanée ne peut avoir lieu pendant le
VACUUM
opération. - Cela prend beaucoup de temps (pour les grandes bases de données), car il recrée en interne la base de données dans un fichier temporaire séparé, et supprime finalement la base de données d'origine, en la remplaçant par ce fichier temporaire.
- Le fichier temporaire consomme supplémentaire espace disque pendant l'exécution de l'opération. Ainsi, ce n'est pas une bonne idée d'exécuter
VACUUM
au cas où vous manquez d'espace disque. Vous pouvez toujours le faire, mais vous devrez vérifier régulièrement que(freeDiskSpace - currentDbFileSize) > 0
.
- Il verrouille l'intégralité de la base de données. Aucune opération simultanée ne peut avoir lieu pendant le
- Utilisez
PRAGMA auto_vacuum = INCREMENTAL
lors de la création la BD. Faites cePRAGMA
le premier déclaration après la création du fichier ! Cela permet une certaine maintenance interne, aidant la base de données à récupérer de l'espace chaque fois que vous appelezPRAGMA incremental_vacuum(N)
. Cet appel récupère jusqu'àN
pages. Les documents officiels fournissent plus de détails, ainsi que d'autres valeurs possibles pourauto_vacuum
.- Remarque :vous pouvez déterminer la quantité d'espace disque libre (en octets) qui serait gagnée lors de l'appel de
PRAGMA incremental_vacuum(N)
:multiplier la valeur renvoyée parPRAGMA freelist_count
avecPRAGMA page_size
.
- Remarque :vous pouvez déterminer la quantité d'espace disque libre (en octets) qui serait gagnée lors de l'appel de
La meilleure option dépend de votre contexte. Pour les fichiers de base de données très volumineux, je recommande l'option 2 , car l'option 1 ennuierait vos utilisateurs avec des minutes ou des heures d'attente pour que la base de données soit nettoyée. L'option 1 convient aux petites bases de données . Son avantage supplémentaire est que les performances de la base de données s'améliorera (ce qui n'est pas le cas pour l'option 2), car la recréation élimine les effets secondaires de la fragmentation des données.
Attention au nombre maximum de variables dans les requêtes
Par défaut, le nombre maximum de variables ("paramètres d'hôte") que vous pouvez utiliser dans une requête est codé en dur à 999 (voir ici, section Nombre maximum de paramètres d'hôte dans une seule instruction SQL ). Cette limite peut varier, car il s'agit d'un temps de compilation paramètre, dont vous (ou quiconque d'autre compilé SQLite) avez peut-être modifié la valeur par défaut.
Ceci est problématique en pratique, car il n'est pas rare que votre application fournisse une liste (arbitrairement grande) au moteur de base de données. Par exemple, si vous voulez masse-DELETE
(ou SELECT
) lignes basées sur, par exemple, une liste d'ID. Une déclaration telle que
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
lancera une erreur et ne se terminera pas.
Pour résoudre ce problème, procédez comme suit :
- Analysez vos listes et divisez-les en listes plus petites,
- Si une division était nécessaire, assurez-vous d'utiliser
BEGIN TRANSACTION
etCOMMIT
pour imiter l'atomicité qu'une seule instruction aurait eu . - Assurez-vous de prendre également en compte d'autres
?
les variables que vous pourriez utiliser dans votre requête qui ne sont pas liées à la liste entrante (par exemple?
variables utilisées dans unORDER BY
condition), de sorte que le total le nombre de variables ne dépasse pas la limite.
Une solution alternative est l'utilisation de tables temporaires. L'idée est de créer une table temporaire, d'insérer les variables de requête sous forme de lignes, puis d'utiliser cette table temporaire dans une sous-requête, par exemple
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)
Méfiez-vous de l'affinité de type de SQLite
Les colonnes SQLite ne sont pas strictement typées et les conversions ne se produisent pas nécessairement comme on pourrait s'y attendre. Les types que vous fournissez ne sont que des indices . SQLite stockera souvent les données de tout tapez son original type et ne convertit les données dans le type de la colonne que si la conversion est sans perte. Par exemple, vous pouvez simplement insérer un "hello"
chaîne dans un INTEGER
colonne. SQLite ne se plaindra pas et ne vous avertira pas des incompatibilités de type. Inversement, vous ne pouvez pas vous attendre à ce que les données renvoyées par un SELECT
instruction d'un INTEGER
colonne est toujours un INTEGER
. Ces indications de type sont appelées "affinité de type" en langage SQLite, voir ici. Assurez-vous d'étudier attentivement cette partie du manuel SQLite, afin de mieux comprendre la signification des types de colonnes que vous spécifiez lors de la création de nouvelles tables.
Attention aux grands entiers
SQLite prend en charge signé Entiers 64 bits , qu'il peut stocker ou utiliser pour effectuer des calculs. En d'autres termes, seuls les nombres de -2^63
à (2^63) - 1
sont pris en charge, car un bit est nécessaire pour représenter le signe !
Cela signifie que si vous prévoyez de travailler avec des nombres plus grands, par ex. Entiers 128 bits (signés) ou entiers 64 bits non signés, vous devez convertir les données en texte avant de l'insérer .
L'horreur commence lorsque vous ignorez cela et que vous insérez simplement des nombres plus grands (sous forme d'entiers). SQLite ne se plaindra pas et stockera un arrondi numéro à la place ! Par exemple, si vous insérez 2^63 (qui est déjà en dehors de la plage prise en charge), le SELECT
ed sera 9223372036854776000, et non 2^63=9223372036854775808. Selon le langage de programmation et la bibliothèque de liaison que vous utilisez, le comportement peut cependant différer ! Par exemple, la liaison sqlite3 de Python vérifie ces débordements d'entiers !
Ne pas utiliser REPLACE()
pour les chemins de fichiers
Imaginez que vous stockiez des chemins de fichiers relatifs ou absolus dans un TEXT
colonne dans SQLite, par ex. pour garder une trace des fichiers sur le système de fichiers réel. Voici un exemple de trois lignes :
foo/test.txt
foo/bar/
foo/bar/x.y
Supposons que vous souhaitiez renommer le répertoire "foo" en "xyz". Quelle commande SQL utiliseriez-vous ? Celui-ci ?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
C'est ce que j'ai fait, jusqu'à ce que des choses étranges commencent à se produire. Le problème avec REPLACE()
c'est qu'il remplacera tous occurrences. S'il y avait une ligne avec le chemin "foo/bar/foo/", alors REPLACE(column_name, 'foo/', 'xyz/')
fera des ravages, car le résultat ne sera pas "xyz/bar/foo/", mais "xyz/bar/xyz/".
Une meilleure solution est quelque chose comme
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
Le 4
reflète la longueur de l'ancien chemin ("foo/" dans ce cas). Notez que j'ai utilisé GLOB
au lieu de LIKE
pour mettre à jour uniquement les lignes qui débutent avec 'foo/'.
Conclusion
SQLite est un moteur de base de données fantastique, où la plupart des commandes fonctionnent comme prévu. Cependant, des complexités spécifiques, comme celles que je viens de présenter, nécessitent toujours l'attention d'un développeur. En plus de cet article, assurez-vous de lire également la documentation officielle sur les mises en garde de SQLite.
Avez-vous rencontré d'autres mises en garde dans le passé ? Si oui, faites-le moi savoir dans les commentaires.