SQLite er en populær relationel database, som du integrerer i din applikation. Der er dog mange fælder og faldgruber, du bør undgå. Denne artikel diskuterer adskillige faldgruber (og hvordan man undgår dem), såsom brugen af ORM'er, hvordan man genvinder diskplads, passe på det maksimale antal forespørgselsvariabler, kolonnedatatyper og hvordan man håndterer store heltal.
Introduktion
SQLite er et populært relationsdatabasesystem (DB) . Den har et meget lignende funktionssæt som dets større brødre, såsom MySQL , som er klient/server-baserede systemer. Men SQLite er en indlejret database . Det kan inkluderes i dit program som et statisk (eller dynamisk) bibliotek. Dette forenkler implementeringen , fordi ingen separat serverproces er nødvendig. Bindinger og wrapper-biblioteker giver dig adgang til SQLite på de fleste programmeringssprog .
Jeg har arbejdet meget med SQLite, mens jeg udviklede BSync som en del af min ph.d.-afhandling. Denne artikel er en (tilfældig) liste over fælder og faldgruber, jeg faldt over under udviklingen . Jeg håber, at du vil finde dem nyttige og undgå at begå de samme fejl, som jeg gjorde engang.
Fælder og faldgruber
Brug ORM-biblioteker med forsigtighed
Objekt-Relational Mapping (ORM)-biblioteker abstraherer detaljerne fra konkrete databasemotorer og deres syntaks (såsom specifikke SQL-sætninger) til en objektorienteret API på højt niveau. Der er mange tredjepartsbiblioteker derude (se Wikipedia). ORM-biblioteker har et par fordele:
- De sparer tid under udviklingen , fordi de hurtigt kortlægger din kode/klasser til DB-strukturer,
- De er ofte på tværs af platforme , dvs. tillade substitution af den konkrete DB-teknologi (f.eks. SQLite med MySQL),
- De tilbyder hjælperkode til skemamigrering .
Men de har også flere alvorlige ulemper du skal være opmærksom på:
- De får arbejdet med databaser til at vises let . Men i virkeligheden har DB-motorer indviklede detaljer, du bare skal kende . Når noget går galt, f.eks. når ORM-biblioteket kaster undtagelser, du ikke forstår, eller når køretidens ydeevne forringes, vil den udviklingstid, du har sparet ved at bruge ORM, hurtigt blive spist op af den indsats, der kræves for at fejlfinde problemet . For eksempel, hvis du ikke ved hvilke indekser er, ville du have svært ved at fejlfinde ydeevneflaskehalse forårsaget af ORM, når den ikke automatisk oprettede alle de nødvendige indekser. Kort sagt:der er ingen gratis frokost.
- På grund af abstraktionen af den konkrete DB-leverandør er leverandørspecifik funktionalitet enten svær at få adgang til, slet ikke tilgængelig .
- Der er nogle beregningsmæssige overhead sammenlignet med at skrive og udføre SQL-forespørgsler direkte. Jeg vil dog sige, at dette punkt er uklart i praksis, da det er almindeligt, at du mister ydeevne, når du skifter til et højere abstraktionsniveau.
I sidste ende er brugen af et ORM-bibliotek et spørgsmål om personlig præference. Hvis du gør det, skal du bare være forberedt på, at du bliver nødt til at lære om særegenhederne ved relationelle databaser (og leverandørspecifikke forbehold), når der opstår uventet adfærd eller flaskehalse i ydeevnen.
Medtag en migrationstabel fra starten
Hvis du ikke er det ved at bruge et ORM-bibliotek, skal du tage dig af DB'ens skemamigrering . Dette involverer at skrive migreringskode, der ændrer dine tabelskemaer og transformerer de lagrede data på en eller anden måde. Jeg anbefaler, at du opretter en tabel kaldet "migrationer" eller "version", med en enkelt række og kolonne, der blot gemmer skemaversionen, f.eks. ved at bruge et monotont stigende heltal. Dette lader din migreringsfunktion registrere, hvilke migreringer der stadig skal anvendes. Når et migreringstrin blev gennemført med succes, øger din migreringsværktøjskode denne tæller via en UPDATE
SQL-sætning.
Automatisk oprettet rowid-kolonne
Når du opretter en tabel, vil SQLite automatisk oprette en INTEGER
kolonne med navnet rowid
for dig – medmindre du har angivet WITHOUT ROWID
klausul (men chancerne er, at du ikke kendte til denne klausul). rowid
række er en primær nøglekolonne. Hvis du også selv angiver en sådan primær nøglekolonne (f.eks. ved at bruge syntaksen some_column INTEGER PRIMARY KEY
) denne kolonne vil blot være et alias for rowid
. Se her for yderligere information, som beskriver det samme med ret kryptiske ord. Bemærk, at en SELECT * FROM table
erklæring vil ikke inkludere rowid
som standard – du skal bede om rowid
kolonne eksplicit.
Bekræft at PRAGMA
det virker virkelig
Blandt andet PRAGMA
sætninger bruges til at konfigurere databaseindstillinger eller til at aktivere forskellige funktioner (officielle dokumenter). Men der er udokumenterede bivirkninger, hvor nogle gange indstilling af en variabel faktisk ikke har nogen effekt . Det virker med andre ord ikke og fejler lydløst.
Hvis du f.eks. udsteder følgende udsagn i den givne rækkefølge, er den sidste erklæring ikke have nogen effekt. Variabel auto_vacuum
har stadig værdien 0
(NONE
), uden god grund.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Du kan læse værdien af en variabel ved at udføre PRAGMA variableName
og udeladelse af lighedstegnet og værdien.
For at rette ovenstående eksempel skal du bruge en anden rækkefølge. Brug af rækkefølgen 3, 1, 2 vil fungere som forventet.
Du vil måske endda inkludere sådanne checks i din produktion kode, fordi disse bivirkninger kan afhænge af den konkrete SQLite-version og hvordan den blev bygget. Det bibliotek, der bruges i produktionen, kan afvige fra det, du brugte under udviklingen.
Gøre krav på diskplads til store databaser
Som standard er en SQLite-databasefils størrelse monotonisk voksende . Sletning af rækker markerer kun bestemte sider som gratis , så de kan bruges til at INSERT
data i fremtiden. For faktisk at genvinde diskplads og for at øge ydeevnen er der to muligheder:
- Udfør
VACUUM
erklæring . Dette har dog flere bivirkninger:- Den låser hele DB. Ingen samtidige operationer kan finde sted under
VACUUM
operation. - Det tager lang tid (for større databaser), fordi det internt genskaber DB i en separat, midlertidig fil, og til sidst sletter den originale database og erstatter den med den midlertidige fil.
- Den midlertidige fil bruger yderligere diskplads, mens handlingen kører. Det er således ikke en god idé at køre
VACUUM
hvis du mangler diskplads. Du kan stadig gøre det, men du skal jævnligt kontrollere, at(freeDiskSpace - currentDbFileSize) > 0
.
- Den låser hele DB. Ingen samtidige operationer kan finde sted under
- Brug
PRAGMA auto_vacuum = INCREMENTAL
når du opretter DB. Lav dennePRAGMA
den første erklæring efter oprettelse af filen! Dette muliggør en del intern husholdning, hvilket hjælper databasen med at genvinde plads, hver gang du kalderPRAGMA incremental_vacuum(N)
. Dette opkald genvinder op tilN
sider. De officielle dokumenter giver yderligere detaljer, og også andre mulige værdier forauto_vacuum
.- Bemærk:du kan bestemme, hvor meget ledig diskplads (i bytes) der vil blive opnået, når du kalder
PRAGMA incremental_vacuum(N)
:gange den returnerede værdi medPRAGMA freelist_count
medPRAGMA page_size
.
- Bemærk:du kan bestemme, hvor meget ledig diskplads (i bytes) der vil blive opnået, når du kalder
Den bedre mulighed afhænger af din kontekst. For meget store databasefiler anbefaler jeg mulighed 2 , fordi mulighed 1 ville irritere dine brugere med minutter eller timers ventetid på, at databasen rydder op. Mulighed 1 er velegnet til mindre databaser . Dens yderligere fordel er, at ydelsen af DB vil forbedre (hvilket ikke er tilfældet for mulighed 2), fordi rekreationen eliminerer bivirkninger af datafragmentering.
Vær opmærksom på det maksimale antal variabler i forespørgsler
Som standard er det maksimale antal variabler ("værtsparametre"), du kan bruge i en forespørgsel, hårdkodet til 999 (se her, afsnittet Maksimalt antal værtsparametre i en enkelt SQL-sætning ). Denne grænse kan variere, fordi det er en kompileringstid parameter, hvis standardværdi du (eller hvem der ellers kompilerede SQLite) kan have ændret.
Dette er problematisk i praksis, fordi det ikke er ualmindeligt, at din applikation giver en (vilkårligt stor) liste til DB-motoren. For eksempel hvis du vil masse-DELETE
(eller SELECT
) rækker baseret på f.eks. en liste over ID'er. Et udsagn som
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
vil give en fejl og vil ikke fuldføre.
For at løse dette skal du overveje følgende trin:
- Analyser dine lister og del dem op i mindre lister,
- Hvis en opdeling var nødvendig, sørg for at bruge
BEGIN TRANSACTION
ogCOMMIT
at efterligne den atomicitet et enkelt udsagn ville have haft . - Sørg for også at overveje andre
?
variabler, du kan bruge i din forespørgsel, som ikke er relateret til listen over indgående indstillinger (f.eks.?
variabler brugt i enORDER BY
betingelse), så den totale antallet af variabler ikke overstiger grænsen.
En alternativ løsning er brugen af midlertidige tabeller. Ideen er at oprette en midlertidig tabel, indsætte forespørgselsvariablerne som rækker og derefter bruge den midlertidige tabel i en underforespørgsel, f.eks.
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)
Pas på SQLites typeaffinitet
SQLite-kolonner er ikke strengt skrevet, og konverteringer sker ikke nødvendigvis, som du kunne forvente. De typer, du angiver, er kun tip . SQLite vil ofte gemme data fra hvilken som helst indtast dens originale type, og kun konvertere data til kolonnens type, hvis konverteringen er tabsfri. For eksempel kan du blot indsætte en "hello"
streng til en INTEGER
kolonne. SQLite vil ikke klage eller advare dig om typeuoverensstemmelser. Omvendt forventer du muligvis ikke, at data returneres af en SELECT
sætning af en INTEGER
kolonne er altid et INTEGER
. Disse typetip omtales som "typeaffinitet" i SQLite-speak, se her. Sørg for at studere denne del af SQLite-manualen nøje for bedre at forstå betydningen af de kolonnetyper, du angiver, når du opretter nye tabeller.
Pas på store heltal
SQLite understøtter signerede 64-bit heltal , som den kan gemme eller udføre beregninger med. Med andre ord, kun tal fra -2^63
til (2^63) - 1
understøttes, fordi en bit er nødvendig for at repræsentere tegnet!
Det betyder, at hvis du forventer at arbejde med større tal, f.eks. 128-bit (signerede) heltal eller usignerede 64-bit heltal, du skal konverter dataene til tekst før du indsætter den .
Rædselen starter, når du ignorerer dette og blot indsætter større tal (som heltal). SQLite vil ikke klage og gemme en afrundet nummer i stedet! Hvis du f.eks. indsætter 2^63 (som allerede er uden for det understøttede område), skal SELECT
ed-værdien vil være 9223372036854776000 og ikke 2^63=9223372036854775808. Afhængigt af programmeringssproget og bindingsbiblioteket du bruger, kan adfærden dog variere! For eksempel tjekker Pythons sqlite3-binding for sådanne heltalsoverløb!
Brug ikke REPLACE()
for filstier
Forestil dig, at du gemmer relative eller absolutte filstier i en TEXT
kolonne i SQLite, f.eks. at holde styr på filer på det faktiske filsystem. Her er et eksempel på tre rækker:
foo/test.txt
foo/bar/
foo/bar/x.y
Antag, at du vil omdøbe mappen "foo" til "xyz". Hvilken SQL-kommando ville du bruge? Denne?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
Dette er hvad jeg gjorde, indtil der begyndte at ske mærkelige ting. Problemet med REPLACE()
er, at det vil erstatte alle forekomster. Hvis der var en række med stien "foo/bar/foo/", så REPLACE(column_name, 'foo/', 'xyz/')
vil skabe kaos, da resultatet ikke bliver "xyz/bar/foo/", men "xyz/bar/xyz/".
En bedre løsning er noget i stil med
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
afspejler længden af den gamle sti ('foo/' i dette tilfælde). Bemærk, at jeg brugte GLOB
i stedet for LIKE
for kun at opdatere de rækker, der starter med 'foo/'.
Konklusion
SQLite er en fantastisk databasemotor, hvor de fleste kommandoer fungerer som forventet. Men specifikke forviklinger, som dem jeg lige har præsenteret, kræver stadig en udviklers opmærksomhed. Ud over denne artikel skal du sørge for også at læse den officielle SQLite-caveats-dokumentation.
Er du stødt på andre forbehold tidligere? Hvis ja, så lad mig det vide i kommentarerne.