SQLite
 sql >> база данни >  >> RDS >> SQLite

SQLite капани и клопки

SQLite е популярна релационна база данни, която вграждате във вашето приложение. Има обаче много капани и капани, които трябва да избягвате. Тази статия обсъжда няколко клопки (и как да ги избегнете), като например използването на ORM, как да възстановите дисково пространство, като вземете предвид максималния брой променливи на заявката, типове данни в колони и как да боравите с големи цели числа.

Въведение

SQLite е популярна система за релационна база данни (DB) . Има много подобен набор от функции като по-големите му братя, като MySQL , които са клиент/сървър-базирани системи. Въпреки това, SQLite е вграден база даннита . Тя може да бъде включена във вашата програма като статична (или динамична) библиотека. Това опростява внедряването , тъй като не е необходим отделен сървърен процес. Библиотеките за свързвания и обвивки ви позволяват достъп до SQLite в повечето езици за програмиране .

Работих със SQLite широко, докато разработвах BSync като част от моята докторска дисертация. Тази статия е (случаен) списък с капани и клопки, на които попаднах по време на разработка . Надявам се, че ще ги намерите за полезни и ще избегнете същите грешки, които направих някога.

Капани и клопки

Използвайте ORM библиотеки с повишено внимание

Библиотеките за обектно-релационно картографиране (ORM) абстрахират детайлите от конкретни машини за бази данни и техния синтаксис (като специфични SQL изрази) до високо ниво, обектно-ориентиран API. Има много библиотеки на трети страни (вижте Wikipedia). ORM библиотеките имат няколко предимства:

  • Те спестяват време по време на разработка , защото те бързо съпоставят вашия код/класове със структури на DB,
  • Те често са междуплатформени т.е. разрешаване на замяна на конкретната DB технология (напр. SQLite с MySQL),
  • Предлагат помощния код за миграция на схеми .

Ноте имат и няколко сериозни недостатъка трябва да сте наясно с:

  • Те правят работата с бази данни поява лесното . Въпреки това,в действителност, DB двигателите имат сложни детайли, които просто трябва да знаете . След като нещо се обърка, напр. когато библиотеката ORM хвърля изключения, които не разбирате, или когато производителността по време на изпълнение се влоши,времето за разработка, което сте спестили с помощта на ORM, бързо ще бъде изядено от усилията, необходими за отстраняване на грешки . Например, ако не знаете какви индекси са, ще имате трудности при отстраняването на проблеми с производителността, причинени от ORM, когато той не създаде автоматично всички необходими индекси. По същество:безплатен обяд няма.
  • Поради абстракцията на конкретния доставчик на DB, специфичната за доставчика функционалност е или трудна за достъп, или изобщо не е достъпна .
  • Има изчислителни разходи в сравнение с директното писане и изпълнение на SQL заявки. Въпреки това бих казал, че този въпрос е спорен на практика, тъй като е обичайно да губите производителност, след като преминете към по-високо ниво на абстракция.

В крайна сметка използването на ORM библиотека е въпрос на лично предпочитание. Ако го направите, просто бъдете подготвени, че ще трябва да научите за странностите на релационните бази данни (и специфичните за доставчиците предупреждения), след като възникнат неочаквано поведение или тесни места в производителността.

Включете таблица за миграции от самото начало

Ако сте не като използвате ORM библиотека, ще трябва да се погрижите за мигрирането на схеми на DB . Това включва писане на код за миграция, който променя вашите схеми на таблици и трансформира съхранените данни по някакъв начин. Препоръчвам ви да създадете таблица, наречена „миграции“ или „версия“, с един ред и колона, която просто съхранява версията на схемата, напр. използвайки монотонно нарастващо цяло число. Това позволява на вашата миграционна функция да открие кои миграции все още трябва да бъдат приложени. Всеки път, когато стъпка на мигриране е завършена успешно, вашият инструментален код за миграция увеличава този брояч чрез UPDATE SQL израз.

Автоматично създадена колона rowid

Винаги, когато създавате таблица, SQLite автоматично ще създаде INTEGER колона с име rowid за васте – освен ако не сте предоставили WITHOUT ROWID клауза (но има вероятност да не сте знаели за тази клауза). rowid ред е колона с първичен ключ. Ако сами посочите и такава колона с първичен ключ (например с помощта на синтаксиса some_column INTEGER PRIMARY KEY ) тази колона ще бъде просто псевдоним за rowid . Вижте тук за допълнителна информация, която описва същото нещо с доста загадъчни думи. Имайте предвид, че SELECT * FROM table изявлението не включва rowid по подразбиране – трябва да поискате rowid колона изрично.

Проверете, че PRAGMA наистина работи

Наред с други неща, PRAGMA изразите се използват за конфигуриране на настройките на базата данни или за извикване на различни функционални (официални документи). Въпреки това, има недокументирани странични ефекти, при които понякога задаване на променлива всъщност няма ефект . С други думи, не работи и се проваля безшумно.

Например, ако издадете следните изявления в дадения ред, последният изявление щене имат някакъв ефект. Променлива auto_vacuum все още има стойност 0 (NONE ), без основателна причина.

PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)

Можете да прочетете стойността на променлива, като изпълните PRAGMA variableName и пропускане на знака за равенство и стойността.

За да коригирате горния пример, използвайте различен ред. Използването на подреждане на редове 3, 1, 2 ще работи според очакванията.

Може дори да искате да включите такива проверки във вашата продукция код, тъй като тези странични ефекти може да зависят от конкретната версия на SQLite и как е изградена. Библиотеката, използвана в производството, може да се различава от тази, която сте използвали по време на разработката.

Искане на дисково пространство за големи бази данни

По подразбиране размерът на файла с база данни на SQLite монотонно нараства . Изтриването на редове маркира само определени страници като безплатни , така че да могат да се използват за INSERT данни в бъдеще. За да възстановите действително дисково пространство и да ускорите производителността, има две опции:

  1. Изпълнете VACUUM изявление . Това обаче има няколко странични ефекта:
    • Заключва цялата БД. Не могат да се извършват едновременни операции по време на VACUUM операция.
    • Отнема много време (за по-големи бази данни), защото вътрешно пресъздава DB в отделен временен файл и накрая изтрива оригиналната база данни, като я заменя с този временен файл.
    • Временният файл консумира допълнителен дисково пространство, докато операцията работи. Следователно не е добра идея да стартирате VACUUM в случай, че нямате достатъчно място на диска. Все пак бихте могли да го направите, но ще трябва редовно да проверявате това (freeDiskSpace - currentDbFileSize) > 0 .
  2. Използвайте PRAGMA auto_vacuum = INCREMENTAL при създаване DB. Направете тази PRAGMA първият изявление след създаване на файла! Това позволява някакво вътрешно поддържане на домакинството, като помага на базата данни да възстанови място всеки път, когато извикате PRAGMA incremental_vacuum(N) . Това извикване възстановява до N страници. Официалните документи предоставят допълнителни подробности, както и други възможни стойности за auto_vacuum .
    • Забележка:можете да определите колко свободно дисково пространство (в байтове) ще бъде спечелено при извикване на PRAGMA incremental_vacuum(N) :умножете върнатата стойност по PRAGMA freelist_count с PRAGMA page_size .

По-добрият вариант зависи от вашия контекст. За много големи файлове с база данни препоръчвам вариант 2 , защото опция 1 би дразнила потребителите ви с минути или часове чакане базата данни да се изчисти. Вариант 1 е подходящ за по-малки бази данни . Неговодопълнително предимство е, чепроизводителност на БД ще се подобрит (което не важи за вариант 2), тъй като повторното създаване елиминира страничните ефекти от фрагментацията на данните.

Имайте предвид максималния брой променливи в заявките

По подразбиране максималният брой променливи („параметри на хоста“), които можете да използвате в заявка, е твърдо кодиран на 999 (вижте тук, раздел Максимален брой параметри на хоста в единичен SQL оператор ). Това ограничение може да варира, тъй като е време за компилиране параметър, чиято стойност по подразбиране вие ​​(или който и да е друг компилирал SQLite) може да сте променили.

Това е проблематично на практика, защото не е необичайно вашето приложение да предоставя (произволно голям) списък на DB двигателя. Например, ако искате масово DELETE (или SELECT ) редове въз основа, да речем, на списък с идентификатори. Изявление като

DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)Code language: SQL (Structured Query Language) (sql)

ще изведе грешка и няма да завърши.

За да коригирате това, помислете за следните стъпки:

  • Анализирайте списъците си и ги разделете на по-малки списъци,
  • Ако е било необходимо разделяне, уверете се, че използвате BEGIN TRANSACTION и COMMIT за да подражава на атомарността, която би имало едно изявление .
  • Уверете се, че сте обмислили и други ? променливи, които може да използвате в заявката си, които не са свързани с входящия списък (напр. ? променливи, използвани в ORDER BY условие), така че общо броят на променливите не надвишава ограничението.

Алтернативно решение е използването на временни таблици. Идеята е да създадете временна таблица, да вмъкнете променливите на заявката като редове и след това да използвате тази временна таблица в подзаявка, напр.

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)

Пазете се от афинитета на типа на SQLite

Колоните на SQLite не са строго въведени и реализациите не се случват непременно, както бихте очаквали. Типовете, които предоставяте, са само подсказки . SQLite често съхранява данни за всякакви въведете неговия оригинал тип и преобразувайте данните към типа на колоната само в случай, че преобразуването е без загуби. Например, можете просто да вмъкнете "hello" низ в INTEGER колона. SQLite няма да се оплаче или да ви предупреди за несъответствия на типовете. Обратно, може да не очаквате тези данни, върнати от SELECT изявление на INTEGER колоната винаги е INTEGER . Тези подсказки за тип се наричат ​​„тип афинитет“ в SQLite-speak, вижте тук. Уверете се, че сте проучили внимателно тази част от ръководството за SQLite, за да разберете по-добре значението на типовете колони, които посочвате, когато създавате нови таблици.

Пазете се от големи цели числа

SQLite поддържа подписани 64-битови цели числа , който може да съхранява или да извършва изчисления с него. С други думи, само числа от -2^63 до (2^63) - 1 се поддържат, защото е необходим един бит за представяне на знака!

Това означава, че ако очаквате да работите с по-големи числа, напр. 128-битови (знакови) цели числа или 64-битови цели числа без знак, трябва да конвертирайте данните в текст преди да го поставите .

Ужасът започва, когато игнорирате това и просто вмъкнете по-големи числа (като цели числа). SQLite няма да се оплаква и съхранява закръглено номер вместо това! Например, ако вмъкнете 2^63 (което вече е извън поддържания диапазон), SELECT ed стойността ще бъде 9223372036854776000, а не 2^63=9223372036854775808. Въпреки това, в зависимост от езика за програмиране и библиотеката за свързване, които използвате, поведението може да се различава! Например, свързването sqlite3 на Python проверява за такива препълвания на цяло число!

Не използвайте REPLACE() за пътеки на файлове

Представете си, че съхранявате относителни или абсолютни файлови пътеки в TEXT колона в SQLite, напр. за да следите файловете в действителната файлова система. Ето пример за три реда:

foo/test.txt
foo/bar/
foo/bar/x.y

Да предположим, че искате да преименувате директорията “foo” на “xyz”. Каква SQL команда бихте използвали? Този?

REPLACE(path_column, old_path, new_path) Code language: SQL (Structured Query Language) (sql)

Това правех, докато не започнаха да се случват странни неща. Проблемът с REPLACE() е, че ще замени всички събития. Ако имаше ред с път „foo/bar/foo/“, тогава REPLACE(column_name, 'foo/', 'xyz/') ще предизвика хаос, тъй като резултатът няма да бъде „xyz/bar/foo/“, а „xyz/bar/xyz/“.

По-добро решение е нещо като

UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"Code language: SQL (Structured Query Language) (sql)

4 отразява дължината на стария път („foo/“ в този случай). Имайте предвид, че използвах GLOB вместо LIKE за да актуализирате само онези редове, които започват с „foo/“.

Заключение

SQLite е фантастичен двигател за база данни, където повечето команди работят според очакванията. Въпреки това, специфични тънкости, като тези, които току-що представих, все още изискват внимание на разработчика. В допълнение към тази статия, не забравяйте да прочетете и официалната документация за предупреждения на SQLite.

Сблъсквали ли сте се с други предупреждения в миналото? Ако е така, уведомете ме в коментарите.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Android ListView с помощта на SQLite

  2. не може да се копира база данни с помощта на клас SQLiteAssetHelper

  3. Добавете месеци към дата в SQLite

  4. Как да филтрирате във връзка едно към много с android room db

  5. Намерете стойности, които не съдържат числа в SQLite