SQLite je oblíbená relační databáze, kterou vkládáte do své aplikace. Existuje však mnoho pastí a nástrah, kterým byste se měli vyhnout. Tento článek popisuje několik úskalí (a jak se jim vyhnout), jako je použití ORM, jak získat zpět místo na disku, dbát na maximální počet proměnných dotazu, datové typy sloupců a jak zacházet s velkými celými čísly.
Úvod
SQLite je populární systém relačních databází (DB) . Má velmi podobnou sadu funkcí jako jeho větší bratři, jako je MySQL , což jsou systémy na bázi klient/server. SQLite je však vložený databáze . Může být součástí vašeho programu jako statická (nebo dynamická) knihovna. Toto zjednodušuje nasazení , protože není nutný žádný samostatný serverový proces. Knihovny vazeb a obalů vám umožní přistupovat k SQLite ve většině programovacích jazyků .
S SQLite jsem intenzivně pracoval při vývoji BSync v rámci své disertační práce. Tento článek je (náhodný) seznam pastí a nástrah, na které jsem během vývoje narazil . Doufám, že pro vás budou užitečné a vyvarujete se chyb, které jsem kdysi dělal já.
Pasti a nástrahy
Používejte knihovny ORM opatrně
Knihovny Object-Relational Mapping (ORM) abstrahují detaily z konkrétních databázových strojů a jejich syntaxi (jako jsou specifické příkazy SQL) do vysoce-úrovňového objektově orientovaného API. Existuje mnoho knihoven třetích stran (viz Wikipedie). Knihovny ORM mají několik výhod:
- Při vývoji šetří čas , protože rychle mapují váš kód/třídy do DB struktur,
- Jsou často multiplatformní , tj. umožnit náhradu konkrétní technologie DB (např. SQLite s MySQL),
- Nabízejí pomocný kód pro migraci schématu .
Mají však také několik vážných nevýhod měli byste si být vědomi:
- Umožňují práci s databázemi objevit snadné . Nicméně ve skutečnosti mají DB motory složité detaily, které prostě musíte znát . Jakmile se něco pokazí, např. když knihovna ORM vyvolá výjimky, kterým nerozumíte, nebo když se výkon za běhu sníží,čas vývoje, který jste ušetřili používáním ORM, bude rychle spotřebován úsilím potřebným k odladění problému . Pokud například nevíte, jaké indexy budete mít potíže s odstraňováním překážek výkonu způsobených ORM, když automaticky nevytváří všechny požadované indexy. V podstatě:neexistuje žádný oběd zdarma.
- Vzhledem k abstrakci konkrétního dodavatele DB jsou funkce specifické pro daného dodavatele buď těžko dostupné, nebo nejsou dostupné vůbec .
- Existuje určitá výpočetní režie ve srovnání s přímým psaním a prováděním SQL dotazů. Řekl bych však, že tento bod je v praxi sporný, protože je běžné, že po přechodu na vyšší úroveň abstrakce ztratíte výkon.
Používání knihovny ORM je nakonec věcí osobních preferencí. Pokud tak učiníte, buďte připraveni na to, že se budete muset dozvědět o zvláštnostech relačních databází (a výhradách specifických pro dodavatele), jakmile dojde k neočekávanému chování nebo omezení výkonu.
Zahrnout tabulku migrací od začátku
Pokud ne pomocí knihovny ORM se budete muset postarat o migraci schématu DB . To zahrnuje napsání migračního kódu, který změní vaše schémata tabulek a nějakým způsobem transformuje uložená data. Doporučuji vytvořit tabulku nazvanou „migrace“ nebo „verze“ s jedním řádkem a sloupcem, ve kterých je jednoduše uložena verze schématu, např. pomocí monotónně rostoucího celého čísla. To umožňuje vaší migrační funkci zjistit, které migrace je ještě třeba použít. Kdykoli byl krok migrace úspěšně dokončen, váš kód migračních nástrojů zvýší toto počítadlo prostřednictvím UPDATE
SQL příkaz.
Automaticky vytvořený řádkový sloupec
Kdykoli vytvoříte tabulku, SQLite automaticky vytvoří INTEGER
sloupec s názvem rowid
pro vás – pokud jste nezadali WITHOUT ROWID
klauzule (ale je pravděpodobné, že jste o této klauzuli nevěděli). rowid
řádek je sloupec primárního klíče. Pokud také sami určíte takový sloupec primárního klíče (např. pomocí syntaxe some_column INTEGER PRIMARY KEY
) tento sloupec bude jednoduše alias pro rowid
. Zde najdete další informace, které popisují totéž poněkud tajemnými slovy. Všimněte si, že SELECT * FROM table
prohlášení nebude zahrnout rowid
ve výchozím nastavení – musíte požádat o rowid
sloupec explicitně.
Ověřte, že PRAGMA
opravdu funguje
Mimo jiné PRAGMA
příkazy slouží ke konfiguraci nastavení databáze nebo k vyvolání různých funkcionalití (oficiální dokumenty). Existují však nezdokumentované vedlejší účinky, kdy někdy nastavení proměnné ve skutečnosti nemá žádný účinek . Jinými slovy, nefunguje a tiše selže.
Pokud například vydáte následující výpisy v daném pořadí, poslední prohlášení nebude mít nějaký účinek. Proměnná auto_vacuum
má stále hodnotu 0
(NONE
), bez dobrého důvodu.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Hodnotu proměnné můžete přečíst spuštěním PRAGMA variableName
a vynechání rovnítka a hodnoty.
Chcete-li opravit výše uvedený příklad, použijte jiné pořadí. Použití řazení řádků 3, 1, 2 bude fungovat podle očekávání.
Možná budete chtít zahrnout takové kontroly do své výroby kód, protože tyto vedlejší účinky mohou záviset na konkrétní verzi SQLite a na tom, jak byla vytvořena. Knihovna použitá v produkci se může lišit od té, kterou jste použili během vývoje.
Nárokování místa na disku pro velké databáze
Ve výchozím nastavení velikost databázového souboru SQLite monotónně roste . Smazáním řádků označíte pouze konkrétní stránky jako volné , takže je lze použít k INSERT
data v budoucnu. Chcete-li skutečně získat zpět místo na disku a zrychlit výkon, existují dvě možnosti:
- Spusťte příkaz
VACUUM
prohlášení . Má to však několik vedlejších účinků:- Zamkne celou databázi. Během
VACUUM
nelze provádět žádné souběžné operace operace. - Trvá to dlouho (u větších databází), protože se interně obnovuje DB v samostatném dočasném souboru a nakonec odstraní původní databázi a nahradí ji tímto dočasným souborem.
- Dočasný soubor spotřebuje další místo na disku, zatímco operace běží. Není tedy dobrý nápad spouštět
VACUUM
v případě, že máte málo místa na disku. Stále byste to mohli udělat, ale museli byste pravidelně kontrolovat, zda(freeDiskSpace - currentDbFileSize) > 0
.
- Zamkne celou databázi. Během
- Použijte
PRAGMA auto_vacuum = INCREMENTAL
při vytváření DB. Udělejte toPRAGMA
první výpis po vytvoření souboru! To umožňuje určité vnitřní vedení a pomáhá databázi získat zpět místo, kdykoli zavolátePRAGMA incremental_vacuum(N)
. Toto volání získá zpět ažN
stránky. Oficiální dokumenty poskytují další podrobnosti a také další možné hodnoty proauto_vacuum
.- Poznámka:Můžete určit, kolik volného místa na disku (v bajtech) by se získalo voláním
PRAGMA incremental_vacuum(N)
:vynásobte hodnotu vrácenouPRAGMA freelist_count
sPRAGMA page_size
.
- Poznámka:Můžete určit, kolik volného místa na disku (v bajtech) by se získalo voláním
Lepší možnost závisí na vašem kontextu. Pro velmi velké databázové soubory doporučuji možnost 2 , protože možnost 1 by vaše uživatele obtěžovala minutami nebo hodinami čekání na vyčištění databáze. Možnost 1 je vhodná pro menší databáze . Jeho další výhodou je výkon z DB zlepšía (což neplatí pro možnost 2), protože rekreace eliminuje vedlejší účinky fragmentace dat.
Dbejte na maximální počet proměnných v dotazech
Ve výchozím nastavení je maximální počet proměnných („parametry hostitele“), které můžete v dotazu použít, pevně zakódován na 999 (viz zde část Maximální počet parametrů hostitele v jednom příkazu SQL ). Tento limit se může lišit, protože se jedná o dobu kompilace parametr, jehož výchozí hodnotu jste vy (nebo kdokoli jiný kompiloval SQLite) mohli změnit.
To je v praxi problematické, protože není neobvyklé, že vaše aplikace poskytuje DB engine (libovolně velký) seznam. Například pokud chcete hromadně-DELETE
(nebo SELECT
) řádky založené například na seznamu ID. Prohlášení jako
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
vyvolá chybu a nedokončí se.
Chcete-li tento problém vyřešit, zvažte následující kroky:
- Analyzujte své seznamy a rozdělte je na menší seznamy,
- Pokud bylo rozdělení nutné, nezapomeňte použít
BEGIN TRANSACTION
aCOMMIT
napodobit atomicitu, kterou by měl jeden příkaz . - Ujistěte se, že zvažte také další
?
proměnné, které můžete použít ve svém dotazu a které nesouvisejí se seznamem příchozí pošty (např.?
proměnné používané vORDER BY
podmínka), takže celkem počet proměnných nepřekračuje limit.
Alternativním řešením je použití dočasných tabulek. Cílem je vytvořit dočasnou tabulku, vložit proměnné dotazu jako řádky a pak tuto dočasnou tabulku použít v dílčím dotazu, např.
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)
Pozor na afinitu typu SQLite
Sloupce SQLite nejsou přesně zapsány a konverze nemusí nutně probíhat tak, jak byste očekávali. Typy, které poskytujete, jsou pouze nápovědy . SQLite bude často ukládat data jakýchkoli zadejte jeho originál typu a převést data na typ sloupce pouze v případě, že je převod bezeztrátový. Můžete například jednoduše vložit "hello"
řetězec do INTEGER
sloupec. SQLite si nebude stěžovat ani vás neupozorní na neshody typu. Naopak, nemůžete očekávat, že data vrácená SELECT
příkaz INTEGER
sloupec je vždy INTEGER
. Tyto tipy typu jsou v SQLite-speak označovány jako „typová afinita“, viz zde. Nezapomeňte si pozorně prostudovat tuto část příručky SQLite, abyste lépe porozuměli významu typů sloupců, které zadáváte při vytváření nových tabulek.
Pozor na velká celá čísla
SQLite podporuje podepsané 64bitová celá čísla , které může ukládat nebo s nimi provádět výpočty. Jinými slovy, pouze čísla od -2^63
na (2^63) - 1
jsou podporovány, protože k reprezentaci znaménka je potřeba jeden bit!
To znamená, že pokud očekáváte práci s většími čísly, např. 128bitová celá čísla (se znaménkem) nebo 64bitová celá čísla bez znaménka, musíte převést data na text před vloženímem .
Hrůza začíná, když to ignorujete a jednoduše vložíte větší čísla (jako celá čísla). SQLite si nebude stěžovat a ukládat zaoblené číslo! Pokud například vložíte 2^63 (což je již mimo podporovaný rozsah), SELECT
ed hodnota bude 9223372036854776000, nikoli 2^63=9223372036854775808. V závislosti na používaném programovacím jazyce a knihovně vazeb se však chování může lišit! Například vazba sqlite3 Pythonu kontroluje takovéto přetečení celých čísel!
Nepoužívejte REPLACE()
pro cesty k souborům
Představte si, že ukládáte relativní nebo absolutní cesty k souboru v TEXT
sloupec v SQLite, např. pro sledování souborů ve skutečném systému souborů. Zde je příklad tří řádků:
foo/test.txt
foo/bar/
foo/bar/x.y
Předpokládejme, že chcete přejmenovat adresář „foo“ na „xyz“. Jaký SQL příkaz byste použili? Tento?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
To jsem dělal, dokud se nezačaly dít divné věci. Problém s REPLACE()
je, že nahradí vše výskytů. Pokud existoval řádek s cestou „foo/bar/foo/“, pak REPLACE(column_name, 'foo/', 'xyz/')
způsobí zmatek, protože výsledek nebude „xyz/bar/foo/“, ale „xyz/bar/xyz/“.
Lepší řešení je něco jako
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
odráží délku staré cesty (v tomto případě ‚foo/‘). Všimněte si, že jsem použil GLOB
místo LIKE
aktualizovat pouze ty řádky, které začínají s „foo/“.
Závěr
SQLite je fantastický databázový stroj, kde většina příkazů funguje podle očekávání. Konkrétní složitosti, jako jsou ty, které jsem právě představil, však stále vyžadují pozornost vývojáře. Kromě tohoto článku si také přečtěte oficiální dokumentaci k upozorněním SQLite.
Setkali jste se v minulosti s jinými výhradami? Pokud ano, dejte mi vědět v komentářích.