SQLite je pro peníze nevhodná
Jednoduchou databázi SQLite mám hodně rád – malá, rychlá, prakticky nic nepotřebuje a funguje. Vím sice dobře, že ač podporuje řadu datových typů, reálně používá jen 4, do kterých vše konvertuje. To samo o sobě ještě problém není (pouze výkonnostní, s čímž se počítá).
To, že do databáze může v jeden okamžik zapisovat jen jeden proces, je jasné – celá databáze je v jednom souboru a asynchronní zápisy by poškodily integritu. Proto se vytváří zámek na celý soubor. Velké databáze tímto přirozeně netrpí, protože datové soubory obhospodařuje koordinovaným způsobem jeden démon.
SQLite se používá jako výchozí databáze ve frameworku Ruby on Rails, a to z velmi prostého důvodu: není potřeba nic konfigurovat, hned to funguje, člověk může hned začít vyvíjet či testovat. Na reálný provoz je nepoužitelná, to je jasné (z výše uvedených důvodů). Zde jsem také narazil na docela zásadní problém, pro který se zmenšuje i množina aplikací, pro jejichž vývoj se dá tato databáze použít (kromě těch, které používají vlastnosti specifické pro určitou databázi).
Je známo, že reálná čísla, jak jsou běžně reprezentována v počítačích, mají omezenou přesnost, a proto může docházet k tomu, že po pár matematických operacích dostanete jiný výsledek, než jste očekávali (což třeba v testech zjistíte při kontrole rovnosti mezi správným a spočteným výsledkem). Jsou lidé, kteří to nevědí a používají je i pro reprezentaci peněžních částek ve svých aplikacích. Zvláště tam, kde se hodně provádí nějaké převody či obchody, je to vcelku sebevražedná záležitost.
Jedním řešením je používat celá čísla, což se ale nehodí vždy (máte zde implicitní „zaokroulování“ směrem dolů). Druhým jsou speciální reprezentace reálných čísel s libovolnou přesností. V Ruby tuto funkcionalitu poskytuje třída BigDecimal ze standardní knihovny. V databázích je na toto rovněž myšleno (příslušný datový typ se obvykle jmenuje decimal
, v migracích v Railsech se jmenuje stejně).
A konečně už se dostávám k jádru problému. SQLite zde opět konvertuje číslo na jinou reprezentaci. Zatímco velké databáze mají striktně uživatelem definováno, jaký rozsah a přesnost budou uchovávaná čísla mít, SQLite na toto nebere zřetel. Uloží vám (možná v dobré víře) klidně i číslo s přesností vyšší. U peněz je to však chování zcela nežádoucí! Pokud například převádíte z jednoho účtu na druhý nějaké částky, tak chcete, aby byly ostře rozděleny, například s přesností na dvě desetinná místa. Pokud vám SQLite dovolí uložit více, a máte pak zlomky haléřů, nemůžete si být 100% jisti, kolik tedy na tom účtu je – zda máte částku zaokrouhlit nahoru či dolů, aby to náhodou někde nechybělo nebo naopak nepřebývalo.
Problém může znít naprosto triviálně – vždyť stačí před uložením zaokrouhlit a databázi předat zaokrouhlené číslo. Bohužel ne. V mém případě došlo k tomu, že jsem ukládal číslo 8,571136085, ale uložilo se mi 8,571136084999999… (poznámka: už to první číslo má devět desetinných míst místo osmi – to jak se během výpočtu ukládaly různé údaje do databáze a došlo opět ke zmiňované chybě).
Při záměně databáze za MySQL již všechny testy proběhnou na jedničku a ladicí výstupy ukazují korektní čísla (vč. 8,57113609). Docela dlouho jsem (neúspěšně) hledal chybu ve svém kódu a ověřoval veškeré výpočetní kroky. Že mě ale může takto podvádět SQLite, to jsem si nemyslel. Pozor na ni.
Zaškatulkováno v kategoriích: Programování a Ruby on Rails | 12. března 2014