SQLite är en populär relationsdatabas som du bäddar in i din applikation. Det finns dock många fällor och fallgropar du bör undvika. Den här artikeln diskuterar flera fallgropar (och hur man undviker dem), såsom användningen av ORM, hur man tar tillbaka diskutrymme, beaktar det maximala antalet frågevariabler, kolumndatatyper och hur man hanterar stora heltal.
Introduktion
SQLite är ett populärt relationsdatabassystem (DB) . Den har en mycket liknande funktion som sina större bröder, som MySQL , som är klient/server-baserade system. Men SQLite är en inbäddad databas . Det kan inkluderas i ditt program som ett statiskt (eller dynamiskt) bibliotek. Detta förenklar implementeringen , eftersom ingen separat serverprocess är nödvändig. Bindningar och omslagsbibliotek ger dig åtkomst till SQLite i de flesta programmeringsspråk .
Jag har arbetat mycket med SQLite medan jag utvecklade BSync som en del av min doktorsavhandling. Den här artikeln är en (slumpmässig) lista över fällor och fallgropar som jag snubblade över under utvecklingen . Jag hoppas att du kommer att ha nytta av dem och undvika att göra samma misstag som jag en gång gjorde.
Fällor och fallgropar
Använd ORM-bibliotek med försiktighet
ORM-bibliotek (Object-Relational Mapping) abstraherar detaljerna från konkreta databasmotorer och deras syntax (som specifika SQL-satser) till ett objektorienterat API på hög nivå. Det finns många tredjepartsbibliotek där ute (se Wikipedia). ORM-bibliotek har några fördelar:
- De sparar tid under utvecklingen , eftersom de snabbt mappar din kod/klasser till DB-strukturer,
- De är ofta plattformsoberoende , d.v.s. tillåta substitution av den konkreta DB-tekniken (t.ex. SQLite med MySQL),
- De erbjuder hjälparkod för schemamigrering .
Men de har också flera allvarliga nackdelar du bör vara medveten om:
- De gör att arbete med databaser visas lätt . Men i verkligheten har DB-motorer intrikata detaljer som du bara måste känna till . När något går fel, t.ex. när ORM-biblioteket ger undantag som du inte förstår, eller när prestanda vid körning försämras, kommer utvecklingstiden du sparat genom att använda ORM snabbt att ätas upp av de ansträngningar som krävs för att felsöka problemet . Till exempel, om du inte vet vilka index är, skulle du ha svårt att felsöka prestandaflaskhalsar orsakade av ORM, när den inte automatiskt skapade alla nödvändiga index. I huvudsak:det finns ingen gratis lunch.
- På grund av abstraktionen av den konkreta DB-leverantören är leverantörsspecifik funktionalitet antingen svåråtkomlig, inte alls tillgänglig .
- Det finns visse beräkningskostnader jämfört med att skriva och köra SQL-frågor direkt. Jag skulle dock säga att den här punkten är omtvistad i praktiken, eftersom det är vanligt att du tappar prestanda när du väl byter till en högre abstraktionsnivå.
I slutändan är det en fråga om personlig preferens att använda ett ORM-bibliotek. Om du gör det, var bara beredd på att du måste lära dig om egenskaperna hos relationsdatabaser (och leverantörsspecifika varningar), när oväntat beteende eller prestandaflaskhalsar inträffar.
Inkludera en migreringstabell från början
Om du inte är det med ett ORM-bibliotek måste du ta hand om DB:s schemamigrering . Detta involverar att skriva migreringskod som ändrar dina tabellscheman och omvandlar lagrad data på något sätt. Jag rekommenderar att du skapar en tabell som heter "migrationer" eller "version", med en enda rad och kolumn, som helt enkelt lagrar schemaversionen, t.ex. med ett monotont ökande heltal. Detta låter din migreringsfunktion upptäcka vilka migreringar som fortfarande behöver tillämpas. När ett migreringssteg har slutförts framgångsrikt, ökar din migreringsverktygskod denna räknare via en UPDATE
SQL-sats.
Auto-skapad rowid-kolumn
När du skapar en tabell kommer SQLite automatiskt att skapa en INTEGER
kolumn med namnet rowid
för dig – såvida du inte angav WITHOUT ROWID
klausul (men chansen är stor att du inte kände till den här klausulen). rowid
rad är en primärnyckelkolumn. Om du också anger en sådan primärnyckelkolumn själv (t.ex. genom att använda syntaxen some_column INTEGER PRIMARY KEY
) denna kolumn blir helt enkelt ett alias för rowid
. Se här för ytterligare information, som beskriver samma sak i ganska kryptiska ord. Observera att en SELECT * FROM table
uttalande inte inkludera rowid
som standard – du måste be om rowid
kolumn uttryckligen.
Verifiera att PRAGMA
det fungerar verkligen
Bland annat PRAGMA
uttalanden används för att konfigurera databasinställningar eller för att anropa olika funktioner (officiella handlingar). Men det finns odokumenterade biverkningar där ibland att ställa in en variabel faktiskt inte har någon effekt . Med andra ord, det fungerar inte och misslyckas tyst.
Till exempel, om du utfärdar följande uttalanden i den givna ordningen, den sista uttalande inte ha någon effekt. Variabel auto_vacuum
har fortfarande värdet 0
(NONE
), utan god anledning.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Du kan läsa värdet på en variabel genom att köra PRAGMA variableName
och utelämna likhetstecknet och värdet.
För att fixa exemplet ovan, använd en annan ordning. Att använda radbeställningen 3, 1, 2 kommer att fungera som förväntat.
Du kanske till och med vill inkludera sådana kontroller i din produktion kod, eftersom dessa biverkningar kan bero på den konkreta SQLite-versionen och hur den byggdes. Biblioteket som används i produktionen kan skilja sig från det du använde under utvecklingen.
Anspråk på diskutrymme för stora databaser
Som standard är storleken på en SQLite-databasfil monotont växer . Att radera rader markerar bara specifika sidor som fria , så att de kan användas för att INSERT
data i framtiden. För att faktiskt återta diskutrymme och för att påskynda prestanda finns det två alternativ:
- Kör
VACUUM
uttalande . Detta har dock flera biverkningar:- Den låser hela DB. Inga samtidiga operationer kan ske under
VACUUM
operation. - Det tar lång tid (för större databaser), eftersom det internt återskapar DB i en separat, temporär fil och tar slutligen bort den ursprungliga databasen och ersätter den med den temporära filen.
- Den temporära filen förbrukar ytterligare diskutrymme medan operationen körs. Därför är det ingen bra idé att köra
VACUUM
om du har ont om diskutrymme. Du kan fortfarande göra det, men du måste regelbundet kontrollera att(freeDiskSpace - currentDbFileSize) > 0
.
- Den låser hela DB. Inga samtidiga operationer kan ske under
- Använd
PRAGMA auto_vacuum = INCREMENTAL
när du skapar DB. Gör dennaPRAGMA
den första uttalande efter att filen skapats! Detta möjliggör viss intern hushållning, vilket hjälper databasen att ta tillbaka utrymme när du ringerPRAGMA incremental_vacuum(N)
. Det här anropet återtar upp tillN
sidor. De officiella dokumenten ger ytterligare detaljer, och även andra möjliga värden förauto_vacuum
.- Obs! Du kan bestämma hur mycket ledigt diskutrymme (i byte) som skulle vinnas när du anropar
PRAGMA incremental_vacuum(N)
:multiplicera värdet som returneras medPRAGMA freelist_count
medPRAGMA page_size
.
- Obs! Du kan bestämma hur mycket ledigt diskutrymme (i byte) som skulle vinnas när du anropar
Det bättre alternativet beror på ditt sammanhang. För mycket stora databasfiler rekommenderar jag alternativ 2 , eftersom alternativ 1 skulle irritera dina användare med minuter eller timmars väntan på att databasen ska städas upp. Alternativ 1 är lämpligt för mindre databaser . Dess ytterligare fördel är att prestandan av DB kommer att förbättras (vilket inte är fallet för alternativ 2), eftersom rekreationen eliminerar biverkningar av datafragmentering.
Tänk på det maximala antalet variabler i frågor
Som standard är det maximala antalet variabler ("värdparametrar") du kan använda i en fråga hårdkodat till 999 (se här, avsnittet Maximalt antal värdparametrar i en enda SQL-sats ). Denna gräns kan variera, eftersom det är en kompileringstid parameter, vars standardvärde du (eller någon annan som kompilerat SQLite) kan ha ändrat.
Detta är problematiskt i praktiken, eftersom det inte är ovanligt att din applikation tillhandahåller en (godtyckligt stor) lista till DB-motorn. Till exempel om du vill mass-DELETE
(eller SELECT
) rader baserade på till exempel en lista med ID:n. Ett uttalande som
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
kommer att ge ett fel och kommer inte att slutföras.
För att åtgärda detta, överväg följande steg:
- Analysera dina listor och dela upp dem i mindre listor,
- Om en uppdelning var nödvändig se till att använda
BEGIN TRANSACTION
ochCOMMIT
att efterlikna den atomicitet som ett enskilt uttalande skulle ha haft . - Se till att även överväga andra
?
variabler som du kan använda i din fråga som inte är relaterade till inkommande lista (t.ex.?
variabler som används i enORDER BY
skick), så att totalt antalet variabler inte överstiger gränsen.
En alternativ lösning är att använda tillfälliga tabeller. Tanken är att skapa en temporär tabell, infoga frågevariablerna som rader och sedan använda den temporära tabellen i en underfråga, t.ex.
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)
Se upp för SQLites typaffinitet
SQLite-kolumner är inte strikt skrivna och omvandlingar sker inte nödvändigtvis som du kan förvänta dig. Typerna du tillhandahåller är bara tips . SQLite lagrar ofta data för vilken som helst skriv in dess original typ och endast konvertera data till kolumntypen om konverteringen är förlustfri. Till exempel kan du helt enkelt infoga en "hello"
sträng till en INTEGER
kolumn. SQLite kommer inte att klaga eller varna dig för typfel. Omvänt kanske du inte förväntar dig att data returneras av en SELECT
sats av en INTEGER
kolumnen är alltid ett INTEGER
. Dessa typtips kallas för "typaffinitet" i SQLite-speak, se här. Se till att studera denna del av SQLite-manualen noggrant för att bättre förstå innebörden av kolumntyperna du anger när du skapar nya tabeller.
Se upp för stora heltal
SQLite stöder signerade 64-bitars heltal , som den kan lagra eller göra beräkningar med. Med andra ord, endast nummer från -2^63
till (2^63) - 1
stöds, eftersom en bit behövs för att representera tecknet!
Det betyder att om du räknar med att arbeta med större antal, t.ex. 128-bitars (tecken) heltal eller osignerade 64-bitars heltal, du måste konvertera data till text innan du sätter in den .
Skräcken börjar när du ignorerar detta och helt enkelt sätter in större tal (som heltal). SQLite kommer inte att klaga och lagra en rundad nummer istället! Till exempel, om du infogar 2^63 (som redan är utanför det stödda intervallet), SELECT
ed-värdet kommer att vara 9223372036854776000 och inte 2^63=9223372036854775808. Beroende på vilket programmeringsspråk och bindningsbibliotek du använder kan beteendet dock skilja sig! Till exempel kontrollerar Pythons sqlite3-bindning efter sådana heltalsspill!
Använd inte REPLACE()
för filsökvägar
Föreställ dig att du lagrar relativa eller absoluta filsökvägar i en TEXT
kolumn i SQLite, t.ex. för att hålla reda på filer på det faktiska filsystemet. Här är ett exempel på tre rader:
foo/test.txt
foo/bar/
foo/bar/x.y
Anta att du vill byta namn på katalogen "foo" till "xyz". Vilket SQL-kommando skulle du använda? Den här?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
Detta är vad jag gjorde, tills konstiga saker började hända. Problemet med REPLACE()
är att den kommer att ersätta alla händelser. Om det fanns en rad med sökvägen "foo/bar/foo/", så REPLACE(column_name, 'foo/', 'xyz/')
kommer att orsaka förödelse, eftersom resultatet inte blir "xyz/bar/foo/", utan "xyz/bar/xyz/".
En bättre lösning är något liknande
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
återspeglar längden på den gamla banan ('foo/' i det här fallet). Observera att jag använde GLOB
istället för LIKE
för att bara uppdatera de rader som startar med 'foo/'.
Slutsats
SQLite är en fantastisk databasmotor, där de flesta kommandon fungerar som förväntat. Men specifika krångligheter, som de jag just presenterade, kräver fortfarande en utvecklares uppmärksamhet. Utöver den här artikeln, se till att du också läser den officiella dokumentationen för SQLite-förbehåll.
Har du stött på andra varningar tidigare? Om så är fallet, låt mig veta i kommentarerna.