SQLite — замечательная встраиваемая БД (часть 3)

    Первая часть — вводная.
    Вторая часть — быстрый старт.

    Третья часть — тонкости и особенности.



    Эта часть является сборной солянкой всевозможных особенностей SQLite. Я собрал здесь (на мой взгляд) наиболее важные темы, без понимания которых невозможно постичь SQLite нирвану.

    Поскольку, опять-таки, информации очень много, то формат статьи будет такой: небольшая вводная в интересную тему и ссылка на родной сайт, где подробности. Сайт, увы, на английском.

    Использование SQLite в многопоточных приложениях

    SQLite может быть собран в однопоточном варианте (параметр компиляции SQLITE_THREADSAFE = 0).

    В этом варианте его нельзя одновременно использовать из нескольких потоков, поскольку полностью отсутствует код синхронизации. Зачем? Для бешеной скорости.

    Проверить, есть ли многопоточность можно через вызов sqlite3_threadsafe(): если вернула 0, то это однопоточный SQLite.

    По умолчанию, SQLite собран с поддержкой потоков (sqlite3.dll).

    Есть два способа использования многопоточного SQLite: serialized и multi-thread.

    Serialized (надо указать флаг SQLITE_OPEN_FULLMUTEX при открытии соединения). В этом режиме потоки могут как угодно дергать вызовы SQLite, никаких ограничений. Но все вызовы блокируют друг друга и обрабатываются строго последовательно.

    Multi-thread (SQLITE_OPEN_NOMUTEX). В этом режиме нельзя использовать одно и то же соединение одновременно из нескольких потоков (но допускается одновременное использование разных соединений разными потоками). Обычно используется именно этот режим.

    Узнать больше

    Формат данных

    База данных SQLite может хранить (текстовые) данные в UTF-8 или UTF-16.

    Набор вызовов API состоит из вызовов, которые получают UTF-8 (sqlite3_XXX) и вызовов, которые получают UTF-16 (sqlite3_XXX16).

    Если тип данных интерфейса и соединения не совпадает, то выполняется конвертация «на лету».

    Всегда используйте UTF-8.

    Поддержка UNICODE

    По умолчанию — нету поддержки. Надо создать свой collation (способ сравнения) через sqlite3_create_collation .
    И определить свои встроенные функции like(), upper(), lower() через www.sqlite.org/c3ref/create_function.html.

    Есть такой проект «International Components for Unicode», ICU.

    И некоторые собирают SQLite DLL уже с ним.

    Как использовать ICU в SQLite.

    Наткнулся на статью на хабре.

    Типы данных и сравнение значений

    Как уже говорилось, SQLIte позволяет записать в любой столбец любое значение.

    Значение внутри БД может принадлежать к одному из следующих типов хранения (storage class):
    NULL,
    INTEGER (занимает 1,2,3,4,6 или 8 байт),
    REAL (число с плавающей точкой, 8 байт в формате IEEE),
    TEXT (строка в формате данных базы, обычно UTF-8),
    BLOB (двоичные данные, хранятся «как есть»).

    Порядок сортировки значений разных типов:
    NULL меньше всего (включая другой NULL);
    INTEGER и REAL меньше любого TEXT и BLOB, между собой сравниваются арифметически;
    TEXT меньше любого BLOB, между собой сравниваются на базе своих collation;
    BLOB-ы сравниваются между собой через memcmp().

    SQLite выполняет неявные преобразования типов «на лету» в нескольких местах:
    — при занесении значения в столбец (тип столбца задает рекомендацию по преобразованию);
    — при сравнении значений между собой.

    Столбец может иметь следующие рекомендации приведения типа: TEXT, NUMERIC, INTEGER, REAL, NONE.

    Значения BLOB и NULL всегда заносятся в любой столбец «как есть».

    В столбец TEXT значения TEXT заносятся «как есть», значения INTEGER и REAL становятся строками.
    В столбец NUMERIC, INTEGER числа записываются «как есть», а строки становятся числами, если _могут_ (то есть допустимо обратное преобразование «без потерь»).
    Для столбца REAL правила похожи на INTEGER(NUMERIC); отличие в том, что все числа представлены в формате с плавающей запятой.
    В столбец NONE значения заносятся «как есть» (этот тип используется по умолчанию, если не задан другой).

    При сравнении значений разного типа между собой может выполняться дополнительное преобразование типов.

    При сравнении числа со строкой, если строка может быть преобразована в число «без потерь», она становится числом.

    Узнать больше

    Отмечу здесь, что в SQLite в уникальном индексе может быть сколько угодно NULL значений (с этим согласен Oracle и не согласен MS SQL).

    База данных в памяти

    Если в вызове sqlite3_open() передать имя файла как ":memory:", то SQLite создаст соединение к новой (чистой) БД в памяти.

    Это соединение абсолютно неотличимо от соединения к БД в файле по логике использования: доступен тот же набор SQL команд.

    Увы, не существует возможности открыть два соединения к одной и той же БД в памяти.

    UPD: Уже, оказывается, можно открыть два соединения к одной БД в памяти.

    rc = sqlite3_open("file:memdb1?mode=memory&cache=shared", &db);
    


    ATTACH DATABASE 'file:memdb1?mode=memory&cache=shared' AS aux1;
    


    Узнать больше.

    Присоединение одновременно к нескольким БД

    Чтобы открыть соединение к БД используется вызов sqlite3_open().

    В любой момент времени мы можем к открытому соединению присоединить еще до 10 баз данных через SQL команду ATTACH DATABASE.

    sqlite3_open('foo.sqlite3', &db); // откроем соединение к БД в файле "foo.sqlite3"
    
    sqlite3_exec(&db, "ATTACH 'bar.sqlite3' AS bar", ... ); // присоединим "bar.sqlite3"
    


    Теперь все таблицы БД в файле db1.sqlite3 стали прозрачно доступны в нашем соединении.

    Для разрешения конфликтов имен следует использовать имя присоединения (основная база называется «main»):

    SELECT * FROM main.my_table UNION SELECT * FROM bar.my_table
    


    Ничего не мешает присоединить к БД новую базу в памяти и использовать ее для кэширования и пр.

    sqlite3_open('foo.sqlite3', &db); // откроем соединение к БД в файле "foo.sqlite3"
    
    sqlite3_exec(&db, "ATTACH ':memory:' AS mem", ... ); // присоединим новую БД в памяти
    


    Узнать больше

    Это очень полезная возможность. Присоединяемые БД должны иметь формат данных такой же, как и у основной БД, иначе — ошибка.

    Временная база данных

    Передайте пустую строку вместо имени файла в sqlite3_open() и будет создана временная БД в файле на диске. Причем, после закрытия соединения к БД, она будет удалена с диска.

    Тонкие настройки БД через команду PRAGMA

    SQL команда PRAGMA служит для задания всевозможных настроек у соединения или у самой БД:

      PRAGMA name; // запросить текущее значение параметра name
    
      PRAGMA name = value; // задать параметр name значением value
    


    Настройку соединения (очевидно) следует проводить сразу после открытия и до его использования.

    Полное описание всех параметров находится здесь.

    Остановлюсь на важнейших вещах.

    PRAGMA page_size = bytes; // размер страницы БД; страница БД - это единица обмена между диском и кэшом, разумно сделать равным размеру кластера диска (у меня 4096)
    
    PRAGMA cache_size = -kibibytes; // задать размер кэша соединения в килобайтах, по умолчанию он равен 2000 страниц БД
    
    PRAGMA encoding = "UTF-8";  // тип данных БД, всегда используйте UTF-8
    
    PRAGMA foreign_keys = 1; // включить поддержку foreign keys, по умолчанию - ОТКЛЮЧЕНА
    
    PRAGMA journal_mode = DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF;  // задать тип журнала, см. далее
    
    PRAGMA synchronous = 0 | OFF | 1 | NORMAL | 2 | FULL; // тип синхронизации транзакции, см. далее
    


    Журнал и фиксация транзакций

    Вот и подошли к теме, овладение которой сразу переводит вас на третий уровень магистра SQLite.

    SQLite тщательно блюдет целостность данных в БД (ACID), реализуя механизм изменения данных через транзакции.

    Кратко о транзакциях: транзакция либо полностью накатывается, либо полностью откатывается. Промежуточных состояний быть не может.

    Если вы не используете транзакции явно (BEGIN; ...; COMMIT;), то всегда создается неявная транзакция. Она стартует перед выполнением команды и коммитится сразу после.

    Отсюда, кстати, и жалобы на «медленность» SQLite. SQLite может вставлять и до 50 тыс записей в секунду, но фиксировать транзакций он не может больше, чем ~ 50 в секунду.

    Именно поэтому, не получается вставлять записи быстро, используя неявную транзакцию.

    При настройках по умолчанию SQLite гарантирует целостность БД даже при отключении питания в процессе работы.

    Достигается подобное изумительное поведение ведением журнала (специального файла) и хитроумным механизмом синхронизации изменений на диске.

    Кратенько обновление данных в БД работает так:

    — до любой модификации БД SQLite сохраняет изменяемые страницы из БД в отдельном файле (журнале), то есть просто копирует их туда;
    — убедившись, что копия страниц создана, SQLite начинает менять БД;
    — убедившись, что все изменения в БД «дошли до диска» и БД стала целостной, SQLite стирает журнал.

    Подробно атомарность механизма транзакций описана тут.

    Если SQLite открывает соединение к БД и видит, что журнал уже есть, он соображает, что БД находится в незавершенном состоянии и автоматически откатывает последнюю транзакцию.

    То есть механизм восстановления БД после сбоев, фактически, встроен в SQLite и работает незаметно для пользователя.

    По умолчанию журнал ведется в режиме DELETE .
    PRAGMA journal_mode = DELETE
    


    Это означает, что файл журнала удаляется после завершения транзакции. Сам факт наличия файла с журналом в этом режиме означает для SQLite, что транзакция не была завершена, база нуждается в восстановлении. Файл журнала имеет имя файла БД, к которому добавлено "-journal".

    В режиме TRUNCATE файл журнала обрезается до нуля (на некоторых системах это работает быстрее, чем удаление файла).

    В режиме PERSIST начало файла журнала забивается нулями (при этом его размер не меняется и он может занимать кучу места).

    В режиме MEMORY файл журнала ведется в памяти и это работает быстро, но не гарантирует восстановление базы при сбоях (копии данных-то нету на диске).

    А можно и совсем отключить журнал (PRAGMA journal_mode = OFF). В этой ситуации перестает работать откат транзакций (команда ROLLBACK) и база, скорее всего, испортится, если программа будет завершена аварийно.

    Для базы данных в памяти режим журнала может быть только либо MEMORY, либо OFF.

    Вернемся немного назад. Как же SQLite «убеждается», что база всегда будет целостной?

    Мы знаем, что современные системы используют хитроумное кэширование для повышения производительности и могут откладывать запись на диск.

    Допустим, SQLite завершил запись в БД и хочет стереть файл журнала, чтобы отметить факт фиксации транзакции.

    А вдруг файл сотрется раньше, чем обновится БД?

    Если в этот промежуток времени отключится питание, то журнала уже не будет, а БД еще не будет целостной — потеря данных!

    Короче говоря, хитроумный механизм фиксации изменений должен полагаться на некоторые гарантии со стороны дисковой системы и ОС.

    PRAGMA synchronous задает степень «паранойи» SQLite на это счет.

    Режим OFF (или 0) означает: SQLite считает, что данные фиксированы на диске сразу после того как он передал их ОС (то есть сразу после вызова соот-го API ОС).

    Это означает, что целостность гарантирована при аварии приложения (поскольку ОС продолжает работать), но не при аварии ОС или отключении питания.

    Режим синхронизации NORMAL (или 1) гарантирует целостность при авариях ОС и почти при всех отключениях питания. Существует ненулевой шанс, что при потере питания в самый неподходящий момент база испортится. Это некий средний, компромисный режим по производительности и надежности.

    Режим FULL гарантирует целостность всегда и везде и при любых авариях. Но работает, разумеется, медленнее, поскольку в определенных местах делаются паузы ожидания. И это режим по умолчанию.

    Итак, осталась неохваченной только тема журнала типа WAL.

    Режим журнала WAL

    По умолчанию, режим журнала БД всегда «возвращается» в DELETE. Допустим, мы открыли соединение к БД и установили режим PERSIST. Изменили данные, закрыли соединение.

    На диске остался файл журнала (начало которого забито нулями).

    Открываем соединение к БД снова. Если не задать режим журнала в этом соединении, он опять будет работать в DELETE. Как только мы обновим данные, механизм фиксации транзакций сотрет файл журнала.

    Режим журнала WAL работает иначе — он «постоянный». Как только мы перевели базу в режим WAL, она останется в этом режиме, пока ей явно не поменяют режим журнала на другой.

    Итак, зачем он нужен?

    Изначально SQLite проектировалась как встроенная БД. Архитектура разделения одновременного доступа к данным была устроена примитивно: одновременно несколько соединений могут читать БД, а вот записывать в данный момент времени может только одно соединение. Это, как минимум, означает, что пишущее соединение ждет «освобождения» БД от читающих. При попытке записать в «занятую» БД приложение получает ошибку SQLITE_BUSY (не путать с SQLITE_LOCKED!). Достигается этот механизм разделения доступа через API блокировки файлов (которые плохо работают на сетевых дисках, поэтому там не рекомендуется использовать SQLite; узнать больше )

    В режиме WAL (Write-Ahead Logging) «читатели» БД и «писатели» в БД уже не мешают друг другу, то есть допускается модификация данных при одновременном чтении. Короче говоря, это шаг в сторону больших и серьезных СУБД, в которых все так и есть. Утверждается также, что SQLite в WAL работает быстрее.

    Но есть и недостатки:
    — требуется некоторые дополнительные ништяки от ОС (unix и Windows имеют эти ништяки);
    — БД занимает несколько файлов (файлы «XXX-wal» и «XXX-shm»);
    — плохо работает на больших транзакциях (условно, если транзакция больше 50 Мбайт);
    — нельзя открыть такую БД в режиме «только чтение»;
    — возникает дополнительная операция checkpoint.

    Фактически, в режиме WAL данные БД разделяются между БД и файлом журнала. Операция checkpoint переносит данные в БД. По умолчанию, это делается автоматически, если журнал занял 1000 страниц БД.
    То есть, идут быстрые COMMIT-ы и вдруг какой-то COMMIT задумался и начал делать checkpoint. Если такое поведение нежелательно, можно делать checkpoint вручную (когда все спокойно), можно это делать и в отдельном процессе.

    Пределы

    Несмотря на миниатюрность, SQLite в реальности не накладывает серьезных ограничений на размеры полей, таблиц или БД.

    По умолчанию, BLOB или строкое значение могут занимать 1 Гбайт и это же ограничение размера одной записи (можно поднять до 2^31 — 1, параметр SQLITE_MAX_LENGTH).

    Количество столбцов: 2000 (можно поднять до 32767, SQLITE_MAX_COLUMN).

    Размер SQL оператора: 1 МБайт (1073741824 байт, SQLITE_MAX_SQL_LENGTH).

    Одновременный join: 64 таблицы.

    Присоединить баз к соединению: 10 (до 62, SQLITE_MAX_ATTACHED)

    Максимальное количество страниц в БД: 1073741823 (до 2147483646, SQLITE_MAX_PAGE_COUNT).

    Если задать размер страницы 65636 байт, то максимальный размер БД будет примерно 14 Терабайт.

    Максимальное число записей в таблице: 2^64 — 1, но на практике, конечно, ограничение размера вступит раньше.

    Узнать больше sqlite.org/limits.html

    UDP: Ссылки по оптимизации SQLite: 1 2 android-1 android-2

    Продолжение следует
    Поделиться публикацией

    Похожие публикации

    Комментарии 32

      +1
      благодарю за 3-ую часть.
        0
        Отдельное спасибо за присоединение базы данных в памяти, этого-то мне и не хватало. :)
          –1
          Спасибо за всю статью! :)
            +1
            Пожалуйста, не кидайте в меня гнилыми помидорами, а, напишите, что же все-таки, конкретно нужно сделать, чтобы БЫСТРО вставить 100500 записей в таблицу (желательно с примером кода)?
              +3
              Если база пустая (не жалко потерять):

              PRAGMA synchronous = 0;
              PRAGMA journal_mode = OFF;
              BEGIN;
              INSERT INTO XXX ...;
              INSERT INTO XXX ...;
              … 10000 вставок…
              COMMIT;

              BEGIN;
              INSERT INTO XXX ...;
              INSERT INTO XXX ...;
              … 10000 вставок…
              COMMIT;

              … пока все не вставится…

              PRAGMA synchronous = FULL;
              PRAGMA journal_mode = DELETE;
                +1
                Добавил в конец статьи ссылки про оптимизацию вставок в SQLite
                  +1
                  0. Прибить имеющиеся индексы
                  1. Заворачиваешь порции Insert-ов по несколько тысяч в транзакции
                  2. Разделяешь создание запросов и их исполнение по разным thread, ибо первое — CPU, а второе IO bound.
                  3. Ждешь окончания транзакций и перестраиваешь индексы.

                  Пример кода ( под iPhone + немного страшен из-за некоторых попыток оптимизации )
                  github.com/dodikk/CsvToSqlite

                  Дерзай.
                  +1
                  >>Увы, не существует возможности открыть два соединения к одной и той же БД в памяти.

                  А по ссылке www.sqlite.org/inmemorydb.html в разделе «In-memory Databases And Shared Cache» написанно, что можно.
                    0
                    Верно! Спасибо за информацию)

                    Не могу исправить в статье — получаю ошибку AJAX.

                    Увы, не существует возможности открыть два соединения к одной и той же БД в памяти.

                    UPD: Уже, оказывается, можно открыть два соединения к одной БД в памяти.

                    rc = sqlite3_open("file:memdb1?mode=memory&cache=shared", &db);
                    


                    ATTACH DATABASE 'file:memdb1?mode=memory&cache=shared' AS aux1;
                    

                      0
                      Исправил в статье.

                      Оказывается, нельзя опубликовать изменения без предпросмотра.
                    +1
                    Три раза в статье повторено, что нужно использовать только UTF-8. А можно пояснить — почему так?
                      0
                      a) на текущий момент парсер только UTF-8 (не исключает факта, что в будущем появится парсер UTF-16);
                      b) UTF-8, как правило, быстрее (меньше данных пишется на диск);
                      c) база в памяти занимает меньше места.
                        0
                        Спасибо. Только не совсем понятен первый пункт — какой именно парсер? В статьях упоминания про него не нашел.
                          0
                          Встроенный в SQLite парсер операторов SQL.
                          0
                          Много лет использую в SQLite кодировку windows-1251 вместо UTF-8. Никаких проблем с парсером это не создаёт. Но вот операции LIKE работают неправильно (не понимают регистр русских символов, конечно) и FTS-раширения тоже работают неправильно. Не стал переделывать collations, а вместо этого в FTS-индексах привожу всё к lowercase, и это снимает проблему.
                            0
                            Можно и так, SQLite пишет текст «как есть».
                        +1
                        Это означает, что целостность гарантирована при аварии приложения (поскольку ОС продолжает работать), но не при аварии ОС или отключении питания.

                        Режим синхронизации NORMAL (или 1) гарантирует целостность при авариях ОС и почти при всех отключениях питания. Существует ненулевой шанс, что при потере питания в самый неподходящий момент база испортится. Это некий средний, компромисный режим по производительности и надежности.

                        Режим FULL гарантирует целостность всегда и везде и при любых авариях. Но работает, разумеется, медленнее, поскольку в определенных местах делаются паузы ожидания. И это режим по умолчанию.


                        Блаженны верующие…
                        Режим FULL отличается от NORMAL только на макоси, где для FULL вызывается fnctl(fd, F_FULLFSYNC, ...) вместо fsync(). Для линукса всегда будет fdatasync, для винды — FlushFileBuffers. И ничего они не могут гарантировать, если диск или дисковый контроллер обманет. Хорошо (а, главное, честно) написано тут (ага, постгрес).

                        Учитывая вышесказанное, мне не очень понятно, что за смешные 50 транзакций в секунду. Постгрес выдает ~1000 (OpenVZ, Core2Duo E8400, HDD 7200RPM)
                          0
                          Цифра взята отсюда: www.sqlite.org/faq.html#q19

                          Также сами разработчики утверждают, что в режиме FULL делаются дополнительные lock. Я не проверял этот факт. Разумеется, фраза про целостность всегда и везде содержит некоторую иронию. Мне казалось, что это заметно.
                            +1
                            Опять же сошлюсь на свой (и тысяч клиентов нашего софта!) многолетний опыт использования SQLite на серверах: SQLite все-таки изредка не справляется с сохранением целостности БД при сбоях питания или зависаниях железа (перегрев). Независимо от настроек синхронизации и режима журнала. Притом, что у нас он используется на Windows, где от NTFS ожидается дополнительная помощь в этом деле… Но увы. Не зря в SQLite есть backup API :)
                              0
                              Вам и карты в руки с такой статистикой использования) Расскажите про свои настройки: UTF-8? WAL? Синхронизация — FULL?
                                0
                                Я тут в других сообщениях этой темы уже говорил об этом.
                            0
                            Все-таки, мне кажется, вы ошибаетесь насчет отличия FULL от NORMAL.

                            Комментарий из кода sqlite3.c:

                            /*
                            ** CAPI3REF: Synchronization Type Flags
                            **
                            ** When SQLite invokes the xSync() method of an
                            ** [sqlite3_io_methods] object it uses a combination of
                            ** these integer values as the second argument.
                            **
                            ** When the SQLITE_SYNC_DATAONLY flag is used, it means that the
                            ** sync operation only needs to flush data to mass storage.  Inode
                            ** information need not be flushed. If the lower four bits of the flag
                            ** equal SQLITE_SYNC_NORMAL, that means to use normal fsync() semantics.
                            ** If the lower four bits equal SQLITE_SYNC_FULL, that means
                            ** to use Mac OS X style fullsync instead of fsync().
                            **
                            ** Do not confuse the SQLITE_SYNC_NORMAL and SQLITE_SYNC_FULL flags
                            ** with the [PRAGMA synchronous]=NORMAL and [PRAGMA synchronous]=FULL
                            ** settings.  The [synchronous pragma] determines when calls to the
                            ** xSync VFS method occur and applies uniformly across all platforms.
                            ** The SQLITE_SYNC_NORMAL and SQLITE_SYNC_FULL flags determine how
                            ** energetic or rigorous or forceful the sync operations are and
                            ** only make a difference on Mac OSX for the default SQLite code.
                            ** (Third-party VFS implementations might also make the distinction
                            ** between SQLITE_SYNC_NORMAL and SQLITE_SYNC_FULL, but among the
                            ** operating systems natively supported by SQLite, only Mac OSX
                            ** cares about the difference.)
                            */
                            #define SQLITE_SYNC_NORMAL        0x00002
                            #define SQLITE_SYNC_FULL          0x00003
                            #define SQLITE_SYNC_DATAONLY      0x00010
                            
                            


                            Если поискать в коде места с «pPager->fullSync», то можно увидеть, что есть дополнительные операции в режиме FULL.
                              0
                              Нда, пожалуй, ошибаюсь (назовут все одинаково, сиди потом, грепай...), хотя это и не отменяет соображений насчет надежности fsync.
                              FULL от NORMAL отличается одной дополнительной синхронизацией журнала и выравниванием на следующий сектор при записи имени журнала. Т.е. оверхед относительно небольшой, и теоретически NORMAL и FULL тормозят примерно одинаково. Тогда, имхо, основная задержка возникает при записи самих данных (в постгресе синхронизируется только WAL, данные пишутся отдельным процессом в бекграунде).
                              В таком случае, в WAL режиме работать должно на порядок быстрее.
                                0
                                Ну, насчет «на порядок» не уверен) Но быстрее, да.
                                  0
                                  Да, WAL — оптимальный вариант, когда нужна приемлемая производительность без особого дрожания за целостность. Хотя в реализации WAL-режима в SQLite вплоть до текущих версий есть какая-то ошибка, из-за которой журнал иногда растёт больше заданного лимита (в многопоточном режиме использования). При этом всё продолжает работать без ошибок, но постепенно всё медленнее и медленнее, пока не переоткроешь БД в эксклюзивном режиме, тогда WAL записывается в основной файл, и всё нормализуется.
                                    0
                                    Может есть какое-то сильно затянутое по времени чтение?
                                      0
                                      Нет, там чтений больше чем на секунду у нас не бывает.
                            0
                            В столбец NONE значения заносятся «как есть» (этот тип используется по умолчанию, если не задан другой)
                            Поверил, но оказалось не совсем так, если 64-битное число хранить в таком поле, то оно портится (INTEGER тоже не подходит), приходится использовать поле TEXT.
                            Вероятно тип NONE преобразуется автоматически к TEXT или INTEGER, в зависимости от содержимого.
                              0
                              Не может быть такого. Проблема, скорее всего, в обвязывающем Sqlite. Какая платформа используется?
                                0
                                WinXP SP3, SQLite последний, все настройки по умолчанию.
                                  0
                                  Как встроена Sqlite? DLL, вкомпилена?
                                    0
                                    вкомпилена, компилятор VS2008, консольный проект 32-битный.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое