SQLite is een populaire, relationele database die u in uw toepassing insluit. Er zijn echter veel valkuilen en valkuilen die u moet vermijden. Dit artikel bespreekt verschillende valkuilen (en hoe deze te vermijden), zoals het gebruik van ORM's, het terugwinnen van schijfruimte, het letten op het maximale aantal queryvariabelen, kolomgegevenstypen en het omgaan met grote gehele getallen.
Inleiding
SQLite is een populair relationeel databasesysteem (DB) . Het heeft een zeer vergelijkbare functieset als zijn grotere broers, zoals MySQL , die client/server-gebaseerde systemen zijn. SQLite is echter een embedded database . Het kan in uw programma worden opgenomen als een statische (of dynamische) bibliotheek. Dit vereenvoudigt de implementatie , omdat er geen apart serverproces nodig is. Bindingen en wrapperbibliotheken geven u toegang tot SQLite in de meeste programmeertalen .
Ik heb uitgebreid met SQLite gewerkt tijdens het ontwikkelen van BSync als onderdeel van mijn proefschrift. Dit artikel is een (willekeurige) lijst van valkuilen en valkuilen die ik tegenkwam tijdens de ontwikkeling . Ik hoop dat je ze nuttig zult vinden en dat je niet dezelfde fouten zult maken als ik ooit deed.
Valvallen en valkuilen
Gebruik ORM-bibliotheken met de nodige voorzichtigheid
Object-Relational Mapping (ORM)-bibliotheken abstraheren de details van concrete database-engines en hun syntaxis (zoals specifieke SQL-instructies) tot een objectgeoriënteerde API op hoog niveau. Er zijn veel bibliotheken van derden (zie Wikipedia). ORM-bibliotheken hebben een aantal voordelen:
- Ze besparen tijd tijdens de ontwikkeling , omdat ze uw code/klassen snel toewijzen aan DB-structuren,
- Ze zijn vaak platformoverschrijdend , d.w.z. vervanging van de concrete DB-technologie toestaan (bijv. SQLite met MySQL),
- Ze bieden helpercode voor schemamigratie .
ze hebben echter ook een aantal ernstige nadelen je moet op de hoogte zijn van:
- Ze laten het werken met databases verschijnen gemakkelijk . In werkelijkheid hebben DB-engines echter ingewikkelde details die u gewoon moet weten . Als er iets mis gaat, b.v. wanneer de ORM-bibliotheek uitzonderingen genereert die u niet begrijpt, of wanneer de runtime-prestaties verslechteren, zal de ontwikkelingstijd die u hebt bespaard door ORM te gebruiken snel worden opgeslokt door de inspanningen die nodig zijn om het probleem op te lossen . Als u bijvoorbeeld niet weet welke indexen zijn, zou u moeite hebben met het oplossen van prestatieknelpunten veroorzaakt door de ORM, wanneer deze niet automatisch alle vereiste indices aanmaakte. In wezen:er is geen gratis lunch.
- Vanwege de abstractie van de concrete DB-leverancier, is leverancierspecifieke functionaliteit ofwel moeilijk toegankelijk, of helemaal niet toegankelijk .
- Er is wat rekenkundige overhead vergeleken met het rechtstreeks schrijven en uitvoeren van SQL-query's. Ik zou echter zeggen dat dit punt in de praktijk niet klopt, omdat het gebruikelijk is dat je prestaties verliest zodra je overschakelt naar een hoger abstractieniveau.
Uiteindelijk is het gebruik van een ORM-bibliotheek een kwestie van persoonlijke voorkeur. Als u dat doet, moet u er rekening mee houden dat u de eigenaardigheden van relationele databases (en leveranciersspecifieke waarschuwingen) moet leren kennen, zodra zich onverwacht gedrag of prestatieknelpunten voordoen.
Vanaf het begin een migratietabel opnemen
Als u niet bent als u een ORM-bibliotheek gebruikt, moet u zorgen voor de schemamigratie van de DB . Dit omvat het schrijven van migratiecode die uw tabelschema's wijzigt en de opgeslagen gegevens op de een of andere manier transformeert. Ik raad u aan een tabel te maken met de naam "migraties" of "versie", met een enkele rij en kolom, die eenvoudig de schemaversie opslaat, b.v. met behulp van een monotoon stijgend geheel getal. Hiermee kan uw migratiefunctie detecteren welke migraties nog moeten worden toegepast. Telkens wanneer een migratiestap met succes is voltooid, verhoogt uw migratietoolingcode deze teller via een UPDATE
SQL-instructie.
Automatisch gemaakte rowid-kolom
Telkens wanneer u een tabel aanmaakt, maakt SQLite automatisch een INTEGER
kolom met de naam rowid
voor jou – tenzij u de WITHOUT ROWID
. heeft opgegeven clausule (maar de kans is groot dat u niet op de hoogte was van deze clausule). De rowid
rij is een primaire sleutelkolom. Als u zelf ook zo'n primaire sleutelkolom opgeeft (bijvoorbeeld met behulp van de syntaxis some_column INTEGER PRIMARY KEY
) deze kolom is gewoon een alias voor rowid
. Zie hier voor meer informatie, die hetzelfde beschrijft in nogal cryptische bewoordingen. Merk op dat een SELECT * FROM table
verklaring zal niet inclusief rowid
standaard – je moet om de rowid
. vragen kolom expliciet.
Controleer of PRAGMA
het werkt echt
Onder andere PRAGMA
statements worden gebruikt om database-instellingen te configureren of om verschillende functionaliteit aan te roepen (officiële documenten). Er zijn echter niet-gedocumenteerde bijwerkingen waarbij het instellen van een variabele soms geen effect heeft . Met andere woorden, het werkt niet en faalt stil.
Als u bijvoorbeeld de volgende uitspraken in de gegeven volgorde geeft, wordt de laatste verklaring zal niet enig effect hebben. Variabele auto_vacuum
heeft nog steeds waarde 0
(NONE
), zonder goede reden.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
U kunt de waarde van een variabele lezen door PRAGMA variableName
. uit te voeren en het gelijkteken en de waarde weglaten.
Gebruik een andere volgorde om het bovenstaande voorbeeld op te lossen. Het gebruik van de rijvolgorde 3, 1, 2 werkt zoals verwacht.
Misschien wilt u dergelijke controles zelfs opnemen in uw productie code, omdat deze bijwerkingen kunnen afhangen van de concrete SQLite-versie en hoe deze is gebouwd. De bibliotheek die tijdens de productie wordt gebruikt, kan verschillen van de bibliotheek die u tijdens de ontwikkeling hebt gebruikt.
Schijfruimte claimen voor grote databases
Standaard is de grootte van een SQLite-databasebestand monotoon groeiend . Het verwijderen van rijen markeert alleen specifieke pagina's als gratis , zodat ze kunnen worden gebruikt om INSERT
gegevens in de toekomst. Om daadwerkelijk schijfruimte terug te winnen en de prestaties te versnellen, zijn er twee opties:
- Voer de
VACUUM
uit verklaring . Dit heeft echter verschillende bijwerkingen:- Het vergrendelt de hele DB. Er kunnen geen gelijktijdige bewerkingen plaatsvinden tijdens de
VACUUM
operatie. - Het duurt lang (voor grotere databases), omdat het intern recreëert de DB in een apart, tijdelijk bestand en verwijdert uiteindelijk de originele database en vervangt deze door dat tijdelijke bestand.
- Het tijdelijke bestand verbruikt extra schijfruimte terwijl de bewerking wordt uitgevoerd. Het is dus geen goed idee om
VACUUM
. uit te voeren voor het geval u weinig schijfruimte heeft. Je zou het nog steeds kunnen doen, maar je zou dat regelmatig moeten controleren(freeDiskSpace - currentDbFileSize) > 0
.
- Het vergrendelt de hele DB. Er kunnen geen gelijktijdige bewerkingen plaatsvinden tijdens de
- Gebruik
PRAGMA auto_vacuum = INCREMENTAL
bij het maken de database. Maak dezePRAGMA
de eerste statement na het aanmaken van het bestand! Dit maakt enige interne huishouding mogelijk, waardoor de database ruimte kan vrijmaken wanneer uPRAGMA incremental_vacuum(N)
aanroept. . Deze oproep claimt maximaalN
Pagina's. De officiële documenten bieden verdere details, en ook andere mogelijke waarden voorauto_vacuum
.- Opmerking:u kunt bepalen hoeveel vrije schijfruimte (in bytes) wordt gewonnen bij het aanroepen van
PRAGMA incremental_vacuum(N)
:vermenigvuldig de geretourneerde waarde metPRAGMA freelist_count
metPRAGMA page_size
.
- Opmerking:u kunt bepalen hoeveel vrije schijfruimte (in bytes) wordt gewonnen bij het aanroepen van
De betere optie hangt af van uw context. Voor zeer grote databasebestanden raad ik optie 2 aan , omdat optie 1 uw gebruikers zou irriteren met minuten of uren wachten op het opschonen van de database. Optie 1 is geschikt voor kleinere databases . Het bijkomende voordeel is dat de prestaties van de DB zal verbeteren (wat niet het geval is voor optie 2), omdat de recreatie de bijwerkingen van gegevensfragmentatie elimineert.
Let op het maximale aantal variabelen in query's
Standaard is het maximale aantal variabelen ("hostparameters") dat u in een query kunt gebruiken, hard gecodeerd tot 999 (zie hier, sectie Maximum aantal hostparameters in een enkele SQL-instructie ). Deze limiet kan variëren, omdat het een compileertijd . is parameter, waarvan u (of wie dan ook die SQLite heeft gecompileerd) de standaardwaarde heeft gewijzigd.
Dit is in de praktijk problematisch, omdat het niet ongebruikelijk is dat uw applicatie een (willekeurig grote) lijst aan de DB-engine levert. Als u bijvoorbeeld massa-DELETE
. wilt (of SELECT
) rijen op basis van bijvoorbeeld een lijst met ID's. Een verklaring zoals
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
geeft een foutmelding en wordt niet voltooid.
Overweeg de volgende stappen om dit op te lossen:
- Analyseer uw lijsten en deel ze op in kleinere lijsten,
- Als een splitsing nodig was, zorg dan dat je
BEGIN TRANSACTION
gebruikt enCOMMIT
om de atomiciteit na te bootsen die een enkele verklaring zou hebben gehad . - Zorg ervoor dat u ook rekening houdt met andere
?
variabelen die u in uw zoekopdracht zou kunnen gebruiken en die geen verband houden met de lijst met inkomende berichten (bijv.?
variabelen gebruikt in eenORDER BY
voorwaarde), zodat het totaal aantal variabelen overschrijdt de limiet niet.
Een alternatieve oplossing is het gebruik van tijdelijke tabellen. Het idee is om een tijdelijke tabel te maken, de queryvariabelen als rijen in te voegen en die tijdelijke tabel vervolgens in een subquery te gebruiken, bijvoorbeeld
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 op voor de typeaffiniteit van SQLite
SQLite-kolommen zijn niet strikt getypt en conversies gebeuren niet noodzakelijkerwijs zoals u zou verwachten. De typen die u opgeeft, zijn slechts hints . SQLite slaat vaak gegevens op van elke typ de originele . in type, en converteer alleen gegevens naar het type van de kolom als de conversie verliesvrij is. U kunt bijvoorbeeld eenvoudig een "hello"
. invoegen tekenreeks in een INTEGER
kolom. SQLite zal niet klagen of u waarschuwen voor typemismatches. Omgekeerd mag u niet verwachten dat gegevens worden geretourneerd door een SELECT
verklaring van een INTEGER
kolom is altijd een INTEGER
. Deze typehints worden in SQLite-taal "typeaffiniteit" genoemd, zie hier. Zorg ervoor dat u dit deel van de SQLite-handleiding nauwkeurig bestudeert om de betekenis van de kolomtypen die u opgeeft bij het maken van nieuwe tabellen beter te begrijpen.
Pas op voor grote gehele getallen
SQLite ondersteunt ondertekend 64-bits gehele getallen , die het kan opslaan, of waarmee het berekeningen kan doen. Met andere woorden, alleen cijfers van -2^63
naar (2^63) - 1
worden ondersteund, omdat er één bit nodig is om het teken weer te geven!
Dat betekent dat als u verwacht met grotere aantallen te werken, b.v. 128-bits (ondertekende) gehele getallen of niet-ondertekende 64-bits gehele getallen, u moet converteer de gegevens naar tekst voordat je het invoegt .
De horror begint wanneer je dit negeert en simpelweg grotere getallen invoegt (als gehele getallen). SQLite zal niet klagen en een afgeronde . opslaan nummer in plaats daarvan! Als u bijvoorbeeld 2^63 invoert (wat al buiten het ondersteunde bereik ligt), wordt de SELECT
ed-waarde is 9223372036854776000, en niet 2^63=9223372036854775808. Afhankelijk van de programmeertaal en bindingsbibliotheek die u gebruikt, kan het gedrag echter verschillen! De sqlite3-binding van Python controleert bijvoorbeeld op dergelijke integer-overflows!
Gebruik REPLACE()
niet voor bestandspaden
Stel je voor dat je relatieve of absolute bestandspaden opslaat in een TEXT
kolom in SQLite, b.v. om bestanden op het eigenlijke bestandssysteem bij te houden. Hier is een voorbeeld van drie rijen:
foo/test.txt
foo/bar/
foo/bar/x.y
Stel dat u de map "foo" wilt hernoemen naar "xyz". Welk SQL-commando zou je gebruiken? Deze?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
Dit is wat ik deed, totdat er rare dingen begonnen te gebeuren. Het probleem met REPLACE()
is dat het alles . zal vervangen voorvallen. Als er een rij was met het pad "foo/bar/foo/", dan REPLACE(column_name, 'foo/', 'xyz/')
zal grote schade aanrichten, aangezien het resultaat niet "xyz/bar/foo/" zal zijn, maar "xyz/bar/xyz/".
Een betere oplossing is zoiets als
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
De 4
geeft de lengte van het oude pad weer ('foo/' in dit geval). Merk op dat ik GLOB
. heb gebruikt in plaats van LIKE
om alleen die rijen bij te werken die starten met 'foo/'.
Conclusie
SQLite is een fantastische database-engine, waar de meeste commando's werken zoals verwacht. Specifieke fijne kneepjes, zoals die ik zojuist heb gepresenteerd, vereisen echter nog steeds de aandacht van een ontwikkelaar. Zorg ervoor dat u naast dit artikel ook de officiële documentatie met SQLite-waarschuwingen leest.
Ben je in het verleden andere waarschuwingen tegengekomen? Als dat zo is, laat het me dan weten in de reacties.