sql >> Database teknologi >  >> RDS >> SQLite

SQLite fælder og faldgruber

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:

  1. 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 .
  2. Brug PRAGMA auto_vacuum = INCREMENTAL når du opretter DB. Lav denne PRAGMA 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 kalder PRAGMA incremental_vacuum(N) . Dette opkald genvinder op til N sider. De officielle dokumenter giver yderligere detaljer, og også andre mulige værdier for auto_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 med PRAGMA freelist_count med PRAGMA page_size .

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 og COMMIT 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 en ORDER 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.


  1. Meddelelse af den generelle tilgængelighed af SQL Secure 4.0

  2. ODBC-skalarfunktioner for dato og klokkeslæt i SQL Server (T-SQL-eksempler)

  3. Neo4j browser

  4. Tillad null i unik kolonne