Использование SQLite

Джей А. Крейбич
2010

перевод В.Айсин

Моему двоюродному дедушке Альберту «Ункен Аль» Крейбичу.

1918-1994

Он взял мальчика, чей любимый вопрос был «почему?» и научил его задавать вопрос "как?" (Который также - к большому разочарованию его родителей и кухонного телефона - научил его радости отвечать на этот вопрос, особенно если это касалось плоскогубцев или отверток.)

--Jk

Предисловие

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

Использование SQLite в первую очередь написано для опытных разработчиков программного обеспечения, у которых никогда не было особой потребности в изучении реляционных баз данных. По той или иной причине вы теперь столкнулись с большой задачей по управлению данными и надеетесь, что такой продукт, как SQLite, сможет дать ответ. Чтобы помочь вам, различные главы охватывают язык SQL, API программирования SQLite C и основы проектирования реляционных баз данных, предоставляя вам все необходимое для успешной интеграции SQLite в ваши приложения и разработки.

Книга разделена на два основных раздела. Первая часть представляет собой традиционный набор глав, которые в первую очередь предназначены для чтения по порядку. В первых двух главах подробно рассматривается, что именно предоставляет SQLite и как его можно использовать. В третьей главе рассказывается о загрузке и сборке библиотеки. В четвертой и пятой главах дается введение в язык SQL, а в шестой главе рассматриваются концепции проектирования баз данных. Глава седьмая посвящена основам C API. Глава восьмая основывается на том, чтобы охватить более сложные темы, такие как хранение времени и даты, использование SQLite из языков сценариев и использование некоторых из более продвинутых расширений. В девятой и десятой главах рассказывается о написании собственных пользовательских функций, расширений и модулей SQL.

Для полноты картины за десятью главами следуют несколько справочных приложений. Эти ссылки охватывают все команды, выражения и встроенные функции SQL, поддерживаемые SQLite, а также документацию для полного API SQLite.

Версии SQLite

Первое издание этой книги относится к SQLite версии 3.6.23.1. Когда это будет опубликовано, работа над SQLite версии 3.7 завершится. SQLite 3.7 представляет новый режим журнала транзакций, известный как Write Ahead Logging или WAL (запись с опережающей записью). В некоторых средах WAL может обеспечить лучшую производительность одновременных транзакций, чем текущий журнал отката. Однако за такую производительность приходится платить. WAL имеет более строгие операционные требования и требует более продвинутой поддержки со стороны операционной системы.

После того, как WAL будет полностью протестирован и выпущен, поищите на сайте O'Reilly статью, в которой рассказывается об этой новой функции и о том, как получить от нее максимальную отдачу.

Списки электронной почты

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

sqlite-announce@sqlite.org
Этот список ограничен объявлениями о новых выпусках, предупреждениями о критических ошибках и другими важными событиями в сообществе SQLite. Трафик чрезвычайно низкий, и большинство сообщений отправляются командой разработчиков SQLite.
sqlite-users@sqlite.org
Это основной список поддержки SQLite. Он охватывает широкий круг тем, включая вопросы по SQL, вопросы по программированию и вопросы о том, как работает библиотека. Этот список умеренно загружен.
sqlite-dev@sqlite.org
Этот список предназначен для людей, работающих над внутренним кодом самой библиотеки SQLite. Если у вас есть вопросы о том, как использовать опубликованный API SQLite, эти вопросы должны быть включены в список пользователей sqlite. Трафик в этом списке довольно низкий.

Вы можете найти инструкции о том, как присоединиться к этим спискам рассылки на веб-сайте SQLite. Посетите http://www.sqlite.org/support.html для получения более подробной информации.

Список рассылки sqlite-users@sqlite.org может быть весьма полезным, но это умеренно загруженный список. Если вы всего лишь случайный пользователь и не хотите получать столько писем, вы также можете получить доступ к сообщениям в списке и выполнить поиск через веб-архив. Ссылки на несколько различных архивов доступны на странице поддержки SQLite.

Скачать примеры кода

Примеры кода, приведенные в этой книге, доступны для загрузки с веб-сайта O'Reilly. Вы можете найти ссылку на примеры на странице каталога книги по адресу http://oreilly.com/catalog/9780596521196/ (локальная ссылка: UsingSQLiteCode.tar.gz). Файлы включают в себя как примеры SQL, так и примеры на языке C, приведенные в следующих главах.

Как мы сюда попали

Чтобы превратить книгу из идеи в готовый продукт, нужно очень много людей. Хотя мое имя указано на обложке, это было бы невозможно без их помощи.

Во-первых, я хотел бы поблагодарить моего главного редактора Майка Лукидеса за дружбу и поддержку. Благодаря некоторым общим друзьям я впервые начал делать технические обзоры для Майка более восьми лет назад. На протяжении многих лет Майк мягко поощрял меня заняться моим собственным проектом.

Первый шаг на этом пути был сделан почти три года назад. Я загрузил набор экспорта базы данных из проекта Wikipedia и пытался разработать минимальную конфигурацию базы данных, которая (надеюсь) втиснула бы почти все текущие данные на небольшую карту флэш-памяти. Конечная цель состояла в том, чтобы предоставить локальную копию статей Википедии на имеющемся у меня читателе электронных книг. SQLite был естественным выбором. В какой-то момент, разочарованный попытками понять правильную последовательность вызовов, я вскинул руки и воскликнул: «Кто-то должен написать об этом книгу!» - Тынц! - Пресловутая лампочка погасла, и много, много (много...) поздних ночей спустя мы здесь.

За Майком стоит весь коллектив O'Reilly Media. Все, с кем я общался, делали все возможное, чтобы помочь мне, успокоить и решить мои проблемы - иногда все сразу. Производственный персонал понимает, как облегчить жизнь автору, чтобы мы могли сосредоточиться на написании и оставить детали кому-то другому.

Я хотел бы поблагодарить Д. Ричарда Хиппа, создателя и ведущего разработчика SQLite. Помимо координации непрерывной разработки SQLite и предоставления всем нам высококачественного программного продукта, он также был достаточно любезен, чтобы ответить на многочисленные вопросы, а также просмотреть окончательный вариант рукописи. Некоторые хитрые места подверглись нескольким исправлениям, и он всегда быстро пересматривал их и возвращал мне с дополнительными комментариями.

Технический обзор был также выполнен Джоном В. Марксом. Джон - давний личный и профессиональный друг с опытом работы с базами данных корпоративного класса. У него была возможность наставлять нескольких опытных разработчиков, когда они совершили свое первое путешествие в мир реляционных баз данных. Джон предоставил очень содержательную обратную связь и смог определить области, которые часто трудно понять новичкам.

Двумя последними моими техническими обозревателями были Джордан Хоукер и Эрин Мой. Хотя они являются знающими разработчиками, они относительно плохо знакомы с реляционными базами данных. Проходя через процесс обучения, они заставляли меня быть честным, когда я начинал делать слишком много предположений, и держали меня в курсе, когда я начинал слишком быстро забегать вперед.

Я также должен поблагодарить Майка Куласа и всех моих коллег в Volition, Inc. Помимо того, что Майк помог мне найти правильный баланс между моей профессиональной работой и книжной работой, Майк помог мне сориентироваться в политике нашей компании в области интеллектуальной собственности, убедившись, что все идет правильно. Многие коллеги также заслуживают благодарности за то, что просматривали небольшие разделы, рассматривали код, задавали много хороших вопросов и в остальном терпели мои высказывания о том, что у них не хватает времени в течение дня.

Отдаем дань уважения команде Aroma Café в центре города Шампейн, штат Иллинойс. Они всего в нескольких кварталах от моего рабочего места, и значительная часть этой книги была написана в их кафе. Большое спасибо Майклу и его сотрудникам, включая Ким, Сару, Николь и Джерри, за то, что всегда готовили горячий и сливочный мокко.

Наконец, я в огромном долгу перед своей женой Дебби Флигор и двумя нашими сыновьями. Они всегда были готовы уделить мне время для написания и проявляли огромное терпение и понимание. Все они дали больше, чем я имел право попросить, и это достижение принадлежит им в той же степени, что и мне.

Условные обозначения, используемые в этой книге

В этой книге используются следующие типографские условные обозначения:

Курсив
Обозначает новые термины, URL-адреса, адреса электронной почты, имена файлов и расширения файлов.
Фиксированный шрифт
Используется для списков программ, а также внутри абзацев для ссылки на элементы программы, такие как имена переменных или функций, базы данных, типы данных, переменные среды, операторы и ключевые слова.
Фиксированный жирный шрифт
Показывает команды или другой текст, который пользователь должен набирать буквально.
Фиксированный курсив
Показывает текст, который следует заменить значениями, заданными пользователем, или значениями, определяемыми контекстом.
Этот значок обозначает совет, предложение или общее примечание.
Этот значок указывает на предупреждение или предостережение.

Глава 1
Что такое SQLite?

Попросту говоря, SQLite - это общедоступный программный пакет, который предоставляет систему управления реляционными базами данных или СУБД. Системы реляционных баз данных используются для хранения пользовательских записей в больших таблицах. Помимо хранения данных и управления ими, ядро базы данных может обрабатывать сложные команды запросов, которые объединяют данные из нескольких таблиц для создания отчетов и сводок данных. Другие популярные продукты СУБД: Oracle Database, IBM DB2 и Microsoft SQL Server на коммерческой стороне, при этом MySQL и PostgreSQL являются популярными продуктами с открытым исходным кодом.

«Lite» в SQLite не относится к его возможностям. Скорее, SQLite является легковесным, когда речь идет о сложности настройки, административных накладных расходах и использовании ресурсов. SQLite определяется следующими особенностями:

Бессерверный
SQLite не требует для работы отдельного серверного процесса или системы. Библиотека SQLite напрямую обращается к файлам своего хранилища.
Нулевая конфигурация
Нет сервера - нет настройки. Создать экземпляр базы данных SQLite так же просто, как открыть файл.
Кроссплатформенность
Весь экземпляр базы данных находится в одном кроссплатформенном файле и не требует администрирования.
Автономный
Единая библиотека содержит всю систему базы данных, которая интегрируется непосредственно в хост-приложение.
Использует мало ресурсов
Сборка по умолчанию составляет менее мегабайта кода и требует всего несколько мегабайт памяти. С некоторыми изменениями можно значительно уменьшить как размер библиотеки, так и использование памяти.
Транзакционный
Транзакции SQLite полностью совместимы с ACID, что обеспечивает безопасный доступ из нескольких процессов или потоков.
Полнофункциональный
SQLite поддерживает большинство функций языка запросов, имеющихся в стандарте SQL92 (SQL2).
Очень надежный
Команда разработчиков SQLite очень серьезно относится к тестированию и проверке кода.

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

Автономный, сервер не требуется

В отличие от большинства продуктов СУБД, SQLite не имеет архитектуры клиент/сервер. Большинство крупномасштабных систем баз данных имеют большой серверный пакет, который составляет ядро базы данных. Сервер базы данных часто состоит из нескольких процессов, которые работают совместно для управления клиентскими соединениями, файловым вводом-выводом, кешами, оптимизацией запросов и обработкой запросов. Экземпляр базы данных обычно состоит из большого количества файлов, организованных в одно или несколько деревьев каталогов в файловой системе сервера. Чтобы получить доступ к базе данных, все файлы должны присутствовать и быть правильными. Это может затруднить перемещение или надежное резервное копирование экземпляра базы данных.

Все эти компоненты требуют ресурсов и поддержки со стороны главного компьютера. Передовой опыт также требует, чтобы хост-система была настроена с выделенными учетными записями пользователей служб, сценариями запуска и выделенным хранилищем, что делает сервер базы данных очень навязчивым программным обеспечением. По этой причине и из соображений производительности обычно выделяют систему главного компьютера исключительно для программного обеспечения сервера базы данных.

Для доступа к базе данных поставщик базы данных обычно предоставляет библиотеки клиентского программного обеспечения. Эти библиотеки должны быть интегрированы в любое клиентское приложение, которое хочет получить доступ к серверу базы данных. Эти клиентские библиотеки предоставляют API для поиска и подключения к серверу базы данных, а также для настройки и выполнения запросов и команд базы данных. На рис. 1-1 показано, как все сочетается в типичной клиент-серверной РСУБД.

Напротив, SQLite не имеет отдельного сервера. Весь механизм базы данных интегрирован в любое приложение, необходимое для доступа к базе данных. Единственный общий ресурс между приложениями - это единственный файл базы данных, который находится на диске. Если вам нужно переместить или создать резервную копию базы данных, вы можете просто скопировать файл. На рис. 1-2 показана инфраструктура SQLite.

Устранение сервера значительно снижает сложность. Это упрощает программные компоненты и почти исключает необходимость в расширенной поддержке операционной системы. В отличие от традиционного сервера РСУБД, который требует расширенной многозадачности и высокопроизводительного межпроцессного взаимодействия, SQLite требует немногим большего, чем возможность чтения и записи в какой-либо тип хранилища.

Рисунок 1-1. Традиционная клиент-серверная архитектура РСУБД, использующая клиентскую библиотеку.

Рисунок 1-2. Бессерверная архитектура SQLite.

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

warning Хотя SQLite не использует традиционную архитектуру клиент/сервер, часто говорят о приложениях как о «клиентах SQLite». Эта терминология часто используется для описания независимых приложений, которые одновременно обращаются к общему файлу базы данных SQLite, и не означает, что существует отдельный сервер.

SQLite предназначен для непосредственной интеграции в исполняемый файл. Это устраняет необходимость во внешней библиотеке и упрощает распространение и установку. Удаление внешних зависимостей также устраняет большинство проблем с управлением версиями. Если код SQLite встроен прямо в ваше приложение, вам никогда не придется беспокоиться о привязке к правильной версии клиентской библиотеки или о том, что клиентская библиотека совместима по версиям с сервером базы данных.

Однако удаление сервера накладывает некоторые ограничения. SQLite предназначен для удовлетворения потребностей локализованного хранилища, например доступа веб-сервера к локальной базе данных. Это означает, что он не очень подходит для ситуаций, когда нескольким клиентским машинам требуется доступ к централизованной базе данных. Эта ситуация более характерна для архитектуры клиент/сервер и лучше обслуживается системой баз данных, использующей ту же архитектуру.

Однофайловая база данных

SQLite упаковывает всю базу данных в один файл. Этот единственный файл содержит структуру базы данных, а также фактические данные, хранящиеся во всех различных таблицах и индексах. Формат файла является кроссплатформенным и доступен на любом компьютере, независимо от собственного порядка байтов или размера слова.

Наличие всей базы данных в одном файле упрощает создание, копирование или резервное копирование образа базы данных на диске. Целые базы данных можно отправить коллегам по электронной почте, опубликовать на веб-форуме или зарегистрировать в системе контроля версий. Целые базы данных можно перемещать, изменять и совместно использовать с той же легкостью, что и текстовый документ или файл электронной таблицы. Нет никаких шансов, что база данных станет поврежденной или недоступной из-за того, что один из дюжины файлов был случайно перемещен или переименован.

Возможно, самое главное, пользователи компьютеров привыкли ожидать, что документ, проект или другая «единица данных приложения» хранится в виде единого файла. Наличие всей базы данных в одном файле позволяет приложениям использовать экземпляры базы данных в качестве документов, хранилищ данных или данных о предпочтениях, не противореча ожиданиям клиентов.

Нулевая конфигурация

С точки зрения конечного пользователя, SQLite не требует ничего ни для установки, ни для настройки, не о чем беспокоиться. Хотя разработчикам доступно изрядное количество параметров настройки, они обычно скрыты от конечного пользователя. Устранение сервера и включение ядра базы данных непосредственно в ваше приложение приводит к тому, что вашим клиентам никогда не нужно знать, что они используют базу данных. Довольно практично разработать приложение так, чтобы выбор файла был единственным взаимодействием с клиентом - действием, которое им уже удобно.

Поддержка встроенных устройств

Небольшой размер кода SQLite и консервативное использование ресурсов делают его хорошо подходящим для встраиваемых систем с ограниченными операционными системами. Исходный код ANSI C склоняется к более старому, более консервативному стилю, который должен быть принят даже самым эксцентричным компилятором встроенного процессора. При использовании конфигурации по умолчанию размер скомпилированной библиотеки SQLite на большинстве платформ составляет менее 700 КБ, а для работы требуется менее 4 МБ памяти. За счет исключения более сложных функций библиотека часто может быть уменьшена до 300 КБ или меньше. С небольшими изменениями конфигурации библиотеку можно заставить работать с объемом памяти менее 256 КБ, в результате чего ее общий объем занимаемой памяти составляет не более половины мегабайта, плюс хранилище данных.

SQLite ожидает лишь минимальной поддержки со стороны своей среды хоста и написан по очень модульной системе. Распределитель внутренней памяти можно легко изменить или заменить, в то время как весь доступ к файлам и хранилищам осуществляется через интерфейс Virtual File System (VFS), который можно изменять в соответствии с потребностями и требованиями различных платформ. В общем, SQLite можно заставить работать практически на чем угодно с 32-битным процессором.

Уникальные черты

SQLite предлагает несколько функций, которых нет во многих других системах баз данных. Наиболее заметным отличием является то, что SQLite использует систему динамических типов данных для таблиц. Механизм SQLite позволит вам поместить любое значение практически в любой столбец, независимо от типа. Это серьезный отход от традиционных систем баз данных, которые, как правило, имеют статическую типизацию. Во многих отношениях система динамического типа в SQLite похожа на системы популярных языков сценариев, которые часто имеют один скалярный тип, который может принимать все, от целых чисел до строк. По моему собственному опыту, система динамической типизации решает гораздо больше проблем, чем вызывает.

Еще одна полезная функция - это возможность одновременно управлять более чем одной базой данных. SQLite позволяет одному соединению с базой данных одновременно связывать себя с несколькими файлами базы данных. Это позволяет SQLite обрабатывать операторы SQL, соединяющие несколько баз данных. Это упрощает объединение таблиц из разных баз данных с помощью одного запроса или массовое копирование данных с помощью одной команды.

SQLite также может создавать базы данных полностью в памяти. По сути, это «файлы» базы данных, которые не имеют резервного хранилища и проводят все свое время в файловом кэше. Несмотря на то, что базам данных в памяти не хватает долговечности и не обеспечивается полная поддержка транзакций, они работают очень быстро (при условии, что у вас достаточно оперативной памяти) и являются отличным местом для хранения временных таблиц и других временных данных.

Есть ряд других функций, которые делают SQLite чрезвычайно гибким. Многие из них, как и виртуальные таблицы, основаны на аналогичных функциях других продуктов, но со своими уникальными особенностями. Эти функции и расширения предоставляют ряд мощных инструментов для адаптации SQLite к вашей конкретной проблеме или ситуации.

Совместимая лицензия

SQLite и код SQLite не имеют пользовательской лицензии. На него не распространяется GNU General Public License (GPL) или какие-либо аналогичные лицензии на открытый/свободный исходный код. Команда разработчиков SQLite предпочла разместить исходный код SQLite в открытом доступе. Это означает, что они явно и сознательно отказались от любых притязаний на авторские права или права собственности на код или производные продукты.

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

Команда SQLite очень серьезно относится к этому решению. Особое внимание уделяется тому, чтобы избежать любых потенциальных патентов на программное обеспечение или запатентованных алгоритмов. Для всех вкладов в исходный код SQLite требуется официальное уведомление об авторских правах. Коммерческие взносы также требуют подписанных аффидевитов, в которых говорится, что авторы (и, если применимо, их работодатели) публикуют свои работы.

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

Очень надежный

Цель базы данных - хранить ваши данные в безопасности и организовывать их. Команда разработчиков SQLite знает, что никто не будет использовать продукт базы данных, имеющий репутацию ошибочного или ненадежного. Для поддержания высокого уровня надежности основная библиотека SQLite тщательно тестируется перед каждым релизом.

В целом стандартные наборы тестов SQLite состоят из более чем 10 миллионов модульных тестов и тестов запросов. «Тест на выдержку», проводимый перед каждым релизом, состоит из более 2,5 миллиардов тестов. Пакет обеспечивает 100% покрытие операторов и 100% покрытие ветвей, включая пограничные ошибки, такие как нехватка памяти и условия хранения. Набор тестов разработан, чтобы довести систему до установленных пределов и превзойти их, обеспечивая обширный охват как кода, так и рабочих параметров.

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

Жесткое тестирование также обеспечивает чрезвычайно надежную обратную совместимость. Команда SQLite очень серьезно относится к обратной совместимости. Форматы файлов, синтаксис SQL, программные API-интерфейсы и поведения имеют чрезвычайно убедительную историю обратной совместимости. Обновление до новой версии SQLite редко вызывает проблемы совместимости.

Помимо обеспечения надежности основной библиотеки, обширное тестирование также позволяет разработчикам SQLite быть более экспериментальными. Целые подсистемы кода SQLite могут быть (и были) извлечены и обновлены или заменены без особого беспокойства о совместимости или функциональных различиях - при условии, что все тесты пройдут успешно. Это позволяет команде вносить значительные изменения с относительно небольшим риском, постоянно продвигая продукт и улучшая его производительность.

Как и многие другие аспекты дизайна SQLite, меньшее количество ошибок означает меньше проблем и меньше поводов для беспокойства. Как и любая сложная программа, она просто работает.

Глава 2
Использование SQLite

SQLite необычайно гибок как в том, где его можно запускать, так и в том, как его можно использовать. В этой главе мы кратко рассмотрим некоторые роли, которые SQLite должен выполнять. Некоторые из этих ролей аналогичны тем, которые используются в традиционных продуктах СУБД клиент/сервер. Другие роли используют преимущества размера и простоты использования SQLite, предлагая решения, которые вы, возможно, не рассматриваете с полной базой данных клиент/сервер.

Младшая база данных

Многолетний опыт научил разработчиков тому, что большие клиент-серверные платформы РСУБД являются мощными инструментами для безопасного хранения, организации и управления данными. К сожалению, большинство крупных СУБД требуют значительных ресурсов и ухода. Это повышает их производительность и емкость, но также ограничивает то, как и где они могут быть практически развернуты.

SQLite предназначен для заполнения этих пробелов, предоставляя те же мощные и знакомые инструменты для безопасного хранения, организации и управления данными в небольших средах с более ограниченными ресурсами. SQLite предназначен для дополнения, а не замены более крупных платформ РСУБД в ситуациях, когда простота и удобство использования более важны, чем емкость и параллелизм.

Эта дополнительная роль позволяет приложениям и инструментам использовать управление реляционными данными (и многолетний опыт, связанный с этим), даже если они работают на небольших платформах без административного надзора. Разработчики могут посмеяться над идеей установки MySQL на настольную систему (или мобильный телефон!) Просто для поддержки приложения адресной книги, но с SQLite это не только становится возможным, но и становится полностью практичным.

Файлы приложений

Современные настольные приложения обычно работают со значительным количеством файлов. Большинство приложений и утилит имеют один или несколько файлов настроек. Также могут существовать общесистемные и индивидуальные файлы конфигурации, кеши и другие данные, которые необходимо отслеживать и хранить. Приложениям на основе документов также необходимо хранить фактические файлы документов и получать к ним доступ.

Использование библиотеки SQLite в качестве абстрактного уровня хранения имеет много преимуществ. Достаточное количество метаданных приложения, таких как кеши, данные о состоянии и данные конфигурации, хорошо сочетаются с реляционной моделью данных. Это позволяет относительно легко создать соответствующий проект базы данных, который четко и легко отображается во внутренние структуры данных приложения.

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

Даже если к приложению не предъявляются особо строгие реляционные требования, использование библиотеки SQLite в качестве контейнера для хранения по-прежнему дает значительные преимущества. Библиотека SQLite предоставляет дополнительные обновления, которые позволяют быстро и легко сохранять небольшие изменения. Система транзакций защищает все операции ввода-вывода файлов от завершения процесса и отключения питания, почти исключающую возможность повреждения файла. SQLite даже предоставляет свой собственный уровень кэширования файлов, так что очень большие файлы можно открывать и обрабатывать с ограниченным объемом памяти без какой-либо дополнительной работы со стороны приложения.

Файлы базы данных SQLite кроссплатформенны, что упрощает миграцию. Содержимым файла можно легко и безопасно поделиться с другими приложениями, не беспокоясь о подробных спецификациях формата файла. Общий формат файлов также упрощает доступ к файлам для автоматизированных сценариев или утилит для устранения неполадок. Несколько приложений могут даже получить доступ к одному и тому же файлу одновременно, а библиотека прозрачно позаботится обо всех необходимых блокировках файлов и синхронизации кеша.

Использование SQLite также может значительно упростить отладку и устранение неполадок, поскольку файлы можно проверять и обрабатывать с помощью стандартных инструментов базы данных. Вы даже можете использовать стандартные инструменты для проверки и изменения файла базы данных по мере его использования вашим приложением. Точно так же тестовые файлы могут быть сгенерированы программно вне приложения, что полезно для автоматических наборов тестирования.

Использование всего экземпляра базы данных в качестве контейнера для документов может показаться немного необычным, но это стоит рассмотреть. Преимущества значительны и должны помочь разработчику сосредоточиться на ядре своего приложения, а не беспокоиться о форматах файлов, кэшировании или синхронизации данных.

Кэш приложения

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

Базы данных в памяти часто используются для кэширования результатов, полученных с более традиционного сервера СУБД. Приложение может извлечь подмножество данных из удаленной базы данных, поместить его во временную базу данных, а затем обработать несколько подробных поисков и уточнений по локальной копии. Это особенно полезно при обработке предложений напечатанного текста или любого другого интерактивного элемента, для которого требуется очень быстрое время отклика.

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

Хотя выполнение операторов SQL для извлечения данных из структуры данных в памяти может показаться странным, это удивительно эффективно и может сократить время и сложность разработки. База данных также обеспечивает путь обновления, что делает тривиальным увеличение объема данных за пределами доступной памяти или сохранение данных при выполнении приложений, просто путем миграции в базу данных на диске.

Архивы и хранилища данных

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

В отличие от многих продуктов СУБД, библиотека SQLite имеет доступ к файлам базы данных, доступным только для чтения. Это позволяет читать хранилища данных непосредственно с оптического диска или другой файловой системы, доступной только для чтения. Это особенно полезно для систем с ограниченным пространством на жестком диске, таких как игровые приставки.

Резервный клиент/сервер

SQLite хорошо работает в качестве «резервной» базы данных в тех ситуациях, когда более надежная СУБД обычно была бы правильным выбором, если бы она была доступна. SQLite может быть особенно полезна для демонстрации и оценки приложений и инструментов, которые обычно зависят от базы данных.

Рассмотрим продукт для анализа данных, который предназначен для извлечения данных из реляционной базы данных для создания отчетов и графиков. Предлагать загрузки и ознакомительные копии такого программного обеспечения может быть затруднительно. Даже если загрузка доступна, программное обеспечение должно быть настроено и авторизовано для подключения к базе данных, содержащей соответствующие данные. Это представляет собой серьезный барьер для потенциального покупателя.

Теперь рассмотрим загрузку ознакомительной версии, которая включает поддержку объединенной демонстрационной базы данных SQLite. Просто загрузив и запустив программное обеспечение, клиенты могут взаимодействовать и экспериментировать с образцом базы данных. Это значительно снижает входной барьер, позволяя клиенту перейти от загрузки к запуску данных всего за несколько секунд.

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

Использование SQLite устраняет эти проблемы. База данных становится фоном, позволяя сосредоточить демонстрацию на продукте. Нет никаких проблем с администрированием базы данных. Простой формат файла также упрощает подготовку наборов данных для конкретных клиентов или демонстрацию функций продукта, которые существенно изменяют базу данных. Все это можно сделать, просто сделав копию файла базы данных перед продолжением.

Помимо оценок и демонстраций, поддержку SQLite можно использовать для продвижения «облегченной» или «персональной версии» более крупного продукта. Добавление продукта начального уровня, более подходящего и рентабельного для небольших установок, может открыть значительное количество новых клиентов, обеспечивая недорогое и легкое введение в линейку продуктов.

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

Инструмент обучения

Для студентов, желающих изучить SQL и основы реляционной модели, SQLite предоставляет чрезвычайно доступную среду, которую легко настроить, легко использовать и легко делиться. SQLite предлагает полноценную реляционную систему, которая поддерживает почти весь основной язык SQL, но не требует настройки сервера, администрирования и накладных расходов. Это позволяет студентам сосредоточиться на изучении SQL и манипулировании данными, не увязая в конфигурировании сервера и обслуживании базы данных.

Учитывая его компактный размер, просто разместить версию инструментов командной строки для Windows, Mac OS X и Linux вместе с несколькими базами данных на небольшом флэш-накопителе. Благодаря отсутствию процесса установки и полностью кроссплатформенным файлам базы данных это обеспечивает обучающую среду «с ходу», которая будет работать практически с любым компьютером.

Архитектура «база данных в файле» позволяет студентам легко делиться своей работой. Целые экземпляры базы данных можно прикрепить к электронному письму или опубликовать в дискуссионном форуме. Однофайловый формат также упрощает резервное копирование незавершенной работы, позволяя студентам экспериментировать и изучать различные решения, не беспокоясь о потере данных.

Общий механизм SQL

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

Виртуальные таблицы - чрезвычайно полезный способ создания отчетов или разрешения специальных запросов к журналам или любому набору табличных данных. Вместо того, чтобы писать набор настраиваемых инструментов поиска или отчетов, данные можно просто предоставить механизму SQLite. Это позволяет выражать отчеты и запросы на языке SQL, с которым уже знакомы многие разработчики. Он также позволяет использовать общие инструменты визуализации базы данных и генераторы отчетов.

В главе 10 показано, как создать модуль виртуальной таблицы, обеспечивающий прямой доступ к живым логам веб-сервера.

Не лучший выбор

Хотя SQLite зарекомендовал себя чрезвычайно гибким, есть некоторые роли, которые выходят за рамки его проектных целей. Хотя SQLite может работать в этих областях, он может быть не лучшим вариантом. Если вы столкнетесь с каким-либо из этих требований, возможно, будет более практичным рассмотреть более традиционный продукт РСУБД клиент/сервер.

Высокая скорость транзакций

SQLite может поддерживать умеренную скорость транзакций, но он не предназначен для поддержки уровня одновременного доступа, обеспечиваемого многими продуктами РСУБД клиент/сервер. Многие серверные системы могут обеспечивать блокировку на уровне таблиц или строк, что позволяет обрабатывать несколько транзакций параллельно без риска потери данных.

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

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

Чрезвычайно большие наборы данных

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

Если вам нужно хранить и обрабатывать несколько гигабайт и более данных, было бы разумно рассмотреть продукт, более ориентированный на производительность.

Контроль доступа

База данных SQLite не имеет данных аутентификации или авторизации. Вместо этого SQLite зависит от разрешений файловой системы для управления доступом к необработанному файлу базы данных. Это существенно ограничивает доступ одним из трех состояний: полный доступ для чтения/записи, доступ только для чтения или отсутствие доступа вообще. Доступ для записи является абсолютным и позволяет изменять как данные, так и структуру самой базы данных.

Хотя API SQLite предоставляет базовый механизм авторизации на уровне приложения, его легко обойти, если у пользователя есть прямой доступ к файлу базы данных. В целом это делает SQLite непригодным для хранилищ конфиденциальных данных, требующих индивидуального контроля доступа.

Клиент/сервер

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

Наличие доступа нескольких компьютеров к файлу SQLite через общий каталог также проблематично. Большинство сетевых файловых систем имеют плохие средства блокировки файлов. Без возможности правильно заблокировать файл и синхронизировать обновления файл базы данных может легко стать поврежденным.

Это не означает, что системы клиент/сервер не могут использовать SQLite. Например, многие веб-серверы используют SQLite. Это работает, потому что все процессы веб-сервера выполняются на одном компьютере и все обращаются к файлу базы данных из локального хранилища.

Репликация

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

Системы репликации могут быть построены на основе базового API базы данных, но такие системы имеют тенденцию быть несколько хрупкими. В целом, если вы ищете репликацию в реальном времени, особенно на уровне безопасности транзакций, вам потребуется более сложная платформа РСУБД.

Большинство этих требований относятся к сфере, где сложность и административные издержки приносятся в жертву емкости и производительности. Это имеет смысл для крупной клиент-серверной платформы РСУБД, но это несколько противоречит целям разработки SQLite - оставаться простым и не требующим обслуживания. Чтобы свести разочарование к минимуму, используйте подходящий для работы инструмент.

Пользователи с большими именами

На веб-сайте SQLite говорится, что «SQLite - это наиболее широко применяемый механизм баз данных SQL в мире». Это довольно смелое заявление, особенно если учесть, что, когда большинство людей думают о платформах реляционных баз данных, они обычно думают о таких именах, как Oracle, SQL Server и MySQL.

Это также утверждение, которое трудно подтвердить точными цифрами. Поскольку нет лицензионных соглашений или требований к раскрытию информации, трудно предположить, сколько существует баз данных SQLite. Никто, включая команду разработчиков SQLite, полностью не знает, кто и для каких целей использует SQLite.

Тем не менее, список известных пользователей SQLite составляет внушительный список. Веб-браузер Firefox и почтовый клиент Thunderbird используют несколько баз данных SQLite для хранения файлов cookie, истории, настроек и других данных учетной записи. Многие продукты Skype, Adobe и McAfee также используют механизм SQLite. Библиотека SQLite также интегрирована в ряд популярных языков сценариев, включая PHP и Python.

Apple, Inc., активно внедрила SQLite, а это означает, что каждый iPhone, iPod touch и iPad, а также каждая копия iTunes и многие другие приложения Macintosh поставляются с несколькими базами данных SQLite. Среды Symbian, Android, BlackBerry и Palm webOS обеспечивают встроенную поддержку SQLite, в то время как WinCE имеет стороннюю поддержку. Скорее всего, если у вас есть смартфон, на нем хранится ряд баз данных SQLite.

Все это составляет миллионы, если не миллиарды, баз данных SQLite в дикой природе. Нет сомнений в том, что большинство этих баз данных содержат всего несколько сотен килобайт данных, но именно в этих низкопрофильных средах SQLite создан для процветания.

Крупные клиент-серверные платформы РСУБД продемонстрировали тысячам разработчиков возможности систем управления реляционными данными. SQLite перенес эту мощь из серверной на настольные компьютеры и мобильные устройства по всему миру.

Глава 3
Сборка и установка SQLite

Эта глава посвящена созданию SQLite. Мы расскажем, как собрать и установить дистрибутив SQLite в Linux, Mac OS X и Windows. База кода SQLite изначально поддерживает все эти операционные системы, а предварительно скомпилированные библиотеки и исполняемые файлы для всех трех сред доступны на веб-сайте SQLite. Все загрузки, включая исходные и предварительно скомпилированные двоичные файлы, можно найти на веб-странице загрузки SQLite (http://www.sqlite.org/download.html).

Продукты SQLite

Проект SQLite состоит из четырех основных продуктов:

Ядро SQLite
Ядро SQLite содержит собственно ядро базы данных и общедоступный API. Ядро может быть встроено в статическую или динамическую библиотеку или может быть встроено непосредственно в приложение.
Инструмент командной строки sqlite3
Приложение sqlite3 - это инструмент командной строки, созданный на основе ядра SQLite. Это позволяет разработчику отправлять интерактивные команды SQL ядру SQLite. Это чрезвычайно полезно для разработки и отладки запросов.
Расширение tcl
SQLite имеет сильную историю с языком Tcl. Эта библиотека, по сути, является копией ядра SQLite с прикрепленными привязками Tcl. При компиляции в библиотеку этот код предоставляет интерфейсы SQLite языку Tcl через Tcl Extension Architecture (TEA). Помимо собственного C API, эти привязки Tcl являются единственным официальным программным интерфейсом, поддерживаемым непосредственно командой SQLite.
Инструмент анализатора SQLite
Анализатор SQLite используется для анализа файлов базы данных. Он отображает статистику о размере файла базы данных, фрагментации, доступном свободном пространстве и других точках данных. Это наиболее полезно для отладки проблем производительности, связанных с физическим расположением файла базы данных. Его также можно использовать, чтобы определить, подходит ли он для VACUUM (переупаковка и дефрагментация) базы данных. Веб-сайт SQLite предоставляет предварительно скомпилированные исполняемые файлы sqlite3_analyzer для большинства настольных платформ. Исходный код анализатора доступен только через дистрибутив исходных кодов разработки.

Большинство разработчиков в первую очередь будут интересоваться первыми двумя продуктами: ядром SQLite и инструментом командной строки sqlite3. Остальная часть главы будет посвящена этим двум продуктам. Процесс сборки расширения Tcl идентичен созданию ядра SQLite как динамической библиотеки. Инструмент анализатора обычно не создается, а просто загружается. Если вы хотите создать свою собственную копию с нуля, то для этого вам понадобится полное дерево разработки.

Бинарные дистрибутивы

Страница загрузки SQLite включает предварительно скомпилированные автономные версии инструмента командной строки sqlite3 для Linux, Mac OS X и Windows. Если вы хотите начать экспериментировать с SQLite, вы можете просто загрузить инструмент командной строки, распаковать его, запустить и начать вводить команды SQL. Возможно, вам даже не придется сначала загружать его - Mac OS X и большинство дистрибутивов Linux включают копию утилиты sqlite3 как часть операционной системы. Страница загрузки SQLite также включает предварительно скомпилированные автономные версии sqlite3_analyzer для всех трех операционных систем.

Бинарные динамические библиотеки ядра SQLite и расширения Tcl также доступны для Linux и Windows. Файлы Linux распространяются как общие объекты (файлы .so), а загружаемые файлы Windows содержат файлы DLL. Для Mac OS X недоступны предварительно скомпилированные библиотеки. Библиотеки требуются только в том случае, если вы пишете собственное приложение, но не хотите компилировать ядро SQLite непосредственно в свое приложение.

Распространение документации

Страница загрузки SQLite включает в себя распространение документации. Файл sqlite_docs_3 _x_x.zip содержит большую часть статического содержимого с веб-сайта SQLite. Документация на веб-сайте SQLite не версируется и всегда отражает синтаксис API и SQL для самой последней версии SQLite. Если вы не планируете постоянно обновлять свой дистрибутив SQLite, полезно получить копию документации, которая идет с версией SQLite, которую вы используете.

Исходные дистрибутивы

Большинство проектов с открытым исходным кодом предоставляют одну загрузку, которая позволяет настраивать, создавать и устанавливать программное обеспечение с помощью всего лишь нескольких команд. SQLite работает немного иначе. Поскольку наиболее распространенным способом использования SQLite является интеграция основного исходного кода непосредственно в хост-приложение, исходные дистрибутивы спроектированы таким образом, чтобы сделать интеграцию максимально простой. Большинство исходных дистрибутивов содержат только исходный код и предоставляют минимальную (если есть) конфигурацию или файлы поддержки сборки. Это упрощает интеграцию SQLite в хост-приложение, но если вы хотите создать библиотеку или приложение sqlite3, вам часто придется делать это вручную. Как мы увидим, это довольно просто.

Объединение

Официальное распространение кода известно как объединение (amalgamation). Объединение представляет собой единый исходный файл на C, который содержит все ядро SQLite. Он создается путем сборки отдельных файлов разработки в один исходный файл на C размером почти 4 мегабайта и длиной более 100 000 строк. Объединение вместе с соответствующим файлом заголовка - это все, что нужно для интеграции SQLite в ваше приложение.

У объединения есть два основных преимущества. Во-первых, все в одном файле чрезвычайно легко интегрировать SQLite в хост-приложение. Многие проекты просто копируют файлы объединения в свои собственные исходные каталоги. Также можно скомпилировать ядро SQLite в библиотеку и просто связать библиотеку с вашим приложением.

Во-вторых, слияние также помогает улучшить производительность. Многие оптимизации компилятора ограничиваются одной единицей трансляции. В C это единственный исходный файл. Поместив всю библиотеку в один файл, хороший оптимизатор может обработать весь пакет сразу. По сравнению с компиляцией отдельных исходных файлов, на некоторых платформах производительность увеличивается на 5% и более только за счет объединения.

Единственный недостаток использования объединения - размер. Некоторые отладчики имеют проблемы с файлами, длина которых превышает 65 535 строк. Обычно все работает правильно, но может быть сложно установить точки останова или просмотреть трассировку стека. Компиляция исходного файла длиной более 100 000 строк также требует значительных ресурсов. Хотя это не проблема для большинства настольных систем, это может выйти за рамки любых компиляторов, работающих на ограниченных платформах.

Исходные файлы

При работе с объединением есть четыре важных исходных файла:

sqlite3.c
Исходный файл объединения, который включает все ядро SQLite, а также общие расширения.
sqlite3.h
Заголовочный файл объединения, который предоставляет основной API.
sqlite3ext.h
Файл заголовка расширения, который используется для создания расширений SQLite.
shell.c
Источник приложения sqlite3, который предоставляет интерактивную оболочку командной строки.

Первые два, sqlite3.c и sqlite3.h, - это все, что нужно для интеграции SQLite в большинство приложений. Файл sqlite3ext.h используется для создания расширений и модулей. Создание расширений рассматривается в разделе Расширения SQLite. Файл shell.c содержит исходный код оболочки командной строки sqlite3. Все эти файлы могут быть созданы в Linux, Mac OS X или Windows без каких-либо дополнительных файлов конфигурации.

Загрузка исходников

Веб-сайт SQLite предлагает пять пакетов распространения исходного кода. Большинству людей будет интересен один из первых двух файлов.

sqlite-amalgamation-3_x_x.zip
Распространение объединения Windows.
sqlite-amalgamation-3.x.x.tar.gz
Распространение объединения Unix.
sqlite-3_x_x-tea.tar.gz
Распространение расширения Tcl.
sqlite-3.x.x.tar.gz
Распространение дерева исходных текстов Unix. Это не поддерживается, и файлы сборки не обслуживаются.
sqlite-source-3_x_x.zip
Исходный код Windows. Это не поддерживается.

Объединенный файл Windows состоит из четырех основных файлов и файла .def для создания библиотеки DLL. Никакие файлы makefile, проекта или решения не включены.

Файл объединения Unix, который работает в Linux, Mac OS X и многих других разновидностях Unix, содержит четыре основных файла плюс страницу руководства sqlite3. Дистрибутив Unix также содержит базовый сценарий конфигурации, а также другие файлы autoconf, сценарии и make-файлы. Файлы autoconf также должны работать в среде Minimalist GNU для Windows (MinGW) (http://www.mingw.org/).

Дистрибутив расширения Tcl - это специализированная версия объединения. Это интересно только тем, кто работает с языком Tcl. См. поставляемую документацию для получения более подробной информации.

Дерево исходных кодов Unix является неподдерживаемым устаревшим дистрибутивом. Так выглядел стандартный дистрибутив до того, как объединение стало официально поддерживаемым дистрибутивом. Он доступен для тех, у кого есть более старые среды сборки или ветки разработки, которые используют старый формат распространения. Этот дистрибутив также включает ряд файлов README, которые больше нигде не доступны.

Хотя исходные файлы обновляются, сценарии конфигурации и make-файлы, включенные в дистрибутив дерева исходных текстов Unix, больше не поддерживаются и не работают должным образом на большинстве платформ. Если у вас нет значительной необходимости использовать дистрибутив дерева исходных текстов, вам следует использовать вместо него один из дистрибутивов объединения.

Дистрибутив исходного кода Windows представляет собой, по сути, файл .zip каталога исходных текстов из дистрибутива дерева исходных текстов, за вычетом некоторых тестовых файлов. Это строго исходные файлы и файлы заголовков и не содержат скриптов сборки, make-файлов или файлов проекта.

Сборка

Существует несколько различных способов создания SQLite, в зависимости от того, что вы пытаетесь создать и где вы хотите его установить. Если вы пытаетесь интегрировать ядро SQLite в хост-приложение, самый простой способ сделать это - просто скопировать sqlite3.c и sqlite3.h в исходный каталог вашего приложения. Если вы используете IDE, файл sqlite3.c можно просто добавить в файл проекта вашего приложения и настроить с использованием правильных путей поиска и директив сборки. Если вы хотите создать собственную версию библиотеки SQLite или утилиты sqlite3, это также легко сделать вручную.

Весь исходный код SQLite написан на C. Он не может быть скомпилирован компилятором C++. Если вы получаете ошибки, связанные с определениями структур, скорее всего, вы используете компилятор C++. Убедитесь, что вы используете ванильный компилятор C.

Конфигурация

Если вы используете объединенный дистрибутив Unix, вы можете собрать и установить SQLite с помощью стандартного скрипта конфигурации. После загрузки дистрибутива его довольно легко распаковать, настроить и собрать исходный код:

$ tar xzf sqlite-amalgamation-3.x.x.tar.gz
$ cd sqlite-3.x.x
$ ./configure
  [...]
$ make

По умолчанию это объединит ядро SQLite как в статические, так и в динамические библиотеки. Он также создаст утилиту sqlite3. Они будут построены с включенными многими дополнительными функциями (такими как полнотекстовый поиск и поддержка R*Tree). После этого команда make install установит эти файлы вместе с файлами заголовков и страницей руководства sqlite3. По умолчанию все устанавливается в /usr/local, хотя это можно изменить, задав параметр --prefix=/path/to/install для настройки. Для получения информации о других параметрах сборки введите команду configure --help.

Вручную

Поскольку основное объединение SQLite состоит только из двух исходных файлов и двух файлов заголовков, его очень просто собрать вручную. Например, чтобы собрать оболочку sqlite3 в Linux или большинстве других систем Unix:

$ cc -o sqlite3 shell.c sqlite3.c -ldl -lpthread

Дополнительные библиотеки необходимы для поддержки динамического связывания и потоков. Mac OS X включает эти библиотеки в стандартную системную группу, поэтому при сборке для Mac OS X дополнительных библиотек не требуется:

$ cc -o sqlite3 shell.c sqlite3.c

Очень похожие команды и на Windows, с использованием компилятора Visual Studio C из командной строки:

> cl /Fesqlite3 shell.c sqlite3.c

Это объединит ядро SQLite и оболочку в одно приложение. Это означает, что для работы полученному исполняемому файлу sqlite3 не потребуется установленная библиотека.

Если вы хотите создать что-то с одним из установленных дополнительных модулей, вам необходимо определить соответствующие директивы компилятора. Это показывает, как создавать вещи в Unix с включенным расширением FTS3 (полнотекстовый поиск):

$ cc -DSQLITE_ENABLE_FTS3 -o sqlite3 shell.c sqlite3.c -ldl -lpthread

Или в Windows:

> cl /Fesqlite3 /DSQLITE_ENABLE_FTS3 shell.c sqlite3.c

Встраивание ядра SQLite в динамическую библиотеку немного сложнее. Нам нужно создать объектный файл, а затем построить библиотеку, используя этот объектный файл. Если вы уже создали утилиту sqlite3 и у вас есть файл sqlite3.o (или .obj), вы можете пропустить первый шаг. Во-первых, в Linux и большинстве систем Unix:

$ cc -c sqlite3.c
$ ld -shared -o libsqlite3.so sqlite3.o

Некоторым версиям Linux может также потребоваться опция -fPIC при компиляции.

Mac OS X использует немного другой формат динамической библиотеки, поэтому команда для его создания немного отличается. Также требуется явная ссылка на стандартную библиотеку C:

$ cc -c sqlite3.c
$ ld -dylib -o libsqlite3.dylib sqlite3.o -lc

И, наконец, создание Windows DLL (для которой требуется файл sqlite3.def):

> cl /c sqlite3.c
> link /dll /out:sqlite3.dll /def:sqlite3.def sqlite3.obj

Вам может потребоваться отредактировать файл sqlite3.def, чтобы добавить или удалить функции, в зависимости от того, какие директивы компилятора используются.

Настройка сборки

Ядро SQLite знает о большом количестве директив компилятора. В Приложении А все это подробно рассматривается. Многие из них используются для изменения стандартных значений по умолчанию или настройки некоторых максимальных размеров и ограничений. Директивы компилятора также используются для включения или отключения определенных функций и расширений. Всего существует несколько десятков директив.

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

Варианты сборки и установки

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

Создание общей библиотеки таким способом - один из наиболее простых способов интеграции и установки SQLite, но зачастую это не лучший подход. Проект SQLite довольно часто выпускает новые версии. Хотя они серьезно относятся к обратной совместимости, иногда в конфигурацию по умолчанию вносятся изменения. Также есть случаи, когда приложения становятся зависимыми от ошибок, связанных с конкретной версией, или неопределенного (или недокументированного) поведения. Существует также большое количество настраиваемых параметров сборки, которые поддерживает SQLite. Все эти проблемы могут быть трудными для создания общесистемной сборки, подходящей для каждого приложения, использующего SQLite.

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

Чтобы избежать этих проблем, рекомендуемым способом использования SQLite является интеграция всего механизма базы данных непосредственно в ваше приложение. Это можно сделать, создав статическую библиотеку и затем связав ее, или просто встроив исходный код объединения непосредственно в код вашего приложения. Этот метод обеспечивает действительно настраиваемую сборку, которая жестко привязана к приложению, которое ее использует, что исключает любую возможность несовместимости версий или сборок.

Пожалуй единственный раз, когда может быть целесообразно использовать динамическую библиотеку, - это когда вы строите на основе существующей установленной (и поддерживаемой системой) библиотеки. Это включает Mac OS X, многие дистрибутивы Linux, а также большинство телефонных сред. В этом случае вы зависите от операционной системы, чтобы поддерживать согласованную сборку. Обычно это работает для достаточно простых задач, но ваше приложение должно быть в некоторой степени гибким. Системные библиотеки часто замораживаются с каждым основным выпуском, но есть вероятность, что рано или поздно системное программное обеспечение (включая системные библиотеки SQLite) будет обновлено. Возможно, вашему приложению придется работать с разными версиями системной библиотеки, если вам необходимо поддерживать разные версии операционной системы. По всем этим же причинам не рекомендуется вручную заменять или обновлять системную копию SQLite.

Если вы все же решите использовать свою собственную частную библиотеку, будьте очень осторожны при компоновке. Слишком легко случайно создать ссылку на системную библиотеку, а не на вашу частную копию, если доступны обе.

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

Введение в sqlite3

После того, как вы установили некоторую форму SQLite, первым делом обычно нужно запустить sqlite3 и поиграть. Инструмент sqlite3 принимает команды SQL из интерактивной подсказки и передает эти команды ядру SQLite для обработки.

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

Для начала просто запустите команду SQLite. Если вы укажете имя файла (например, test.db), sqlite3 откроет (или создаст) этот файл. Если имя файла не указано, sqlite3 автоматически откроет безымянную временную базу данных:

$ sqlite3 test.db
SQLite version 3.6.23.1
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

Приглашение sqlite> означает, что sqlite3 готов принимать команды. Мы можем начать с некоторых основных выражений:

sqlite> SELECT 3 * 5, 10;
15|10
sqlite>

Команды SQL также можно вводить в несколько строк. Если конечная точка с запятой не найдена, предполагается, что оператор будет продолжен. В этом случае приглашение изменится на ...>, чтобы указать, что sqlite3 ожидает ввода дополнительных данных:

sqlite> SELECT 1 + 2,
    ...> 6 + 3;
    3|9
    sqlite>

Если вы когда-нибудь неожиданно окажетесь на приглашении ...>, убедитесь, что вы закончили предыдущую строку точкой с запятой.

Помимо обработки операторов SQL, существует ряд команд, специфичных для оболочки. Их иногда называют «точечными командами», потому что они начинаются с точки. Точечные команды управляют форматированием вывода оболочки, а также предоставляют ряд служебных функций. Например, команду .read можно использовать для выполнения файла, содержащего команды SQL.

Точечные команды должны вводиться в приглашении sqlite>. Они должны быть указаны в одной строке и не должны заканчиваться точкой с запятой. Вы не можете смешивать операторы SQL и точечные команды.

Две из наиболее полезных точечных команд (помимо .help) - это .headers и .mode. Обе они контролируют некоторые аспекты вывода базы данных. Включение заголовков и установка режима вывода на столбец приведет к созданию таблицы, которую большинству людей будет легче читать:

sqlite> SELECT 'abc' AS start, 'xyz' AS end;
abc|xyz
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT 'abc' AS start, 'xyz' AS end;
start      end
---------- ----------
abc        xyz
sqlite>

Также полезна команда .schema. Здесь будут перечислены все команды DDL (CREATE TABLE, CREATE INDEX и т. д.), используемые для определения базы данных. Более полный список всех параметров командной строки sqlite3 и команд с точкой см. в Приложении A.

Резюме

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

Если вы только начинаете и все, что вам нужно, это копия оболочки sqlite3, не слишком увлекайтесь всеми передовыми методами сборки. Вы можете загрузить один из предварительно скомпилированных исполняемых файлов или создать свой собственный с помощью однострочных команд, представленных в этой главе. Это поможет вам начать.

По мере развития ваших потребностей вам может потребоваться более конкретная сборка sqlite3, или вы можете начать рассматривать интеграцию библиотеки SQLite в свое собственное приложение. На этом этапе вы можете опробовать различные методы сборки и посмотреть, что лучше всего соответствует вашим потребностям и среде сборки.

Хотя объединение является несколько необычной формой для распространения исходного кода, оно оказалось весьма полезным и хорошо подходит для интеграции SQLite в более крупные проекты с минимальными затратами времени. Это также единственный официально поддерживаемый формат распространения исходного кода. Это хорошо работает для большинства проектов.

Глава 4
Язык SQL

В этой главе представлен обзор языка структурированных запросов (Structured Query Language) или SQL. Хотя иногда оно произносится как «продолжение», официальное произношение каждой буквы - «эс-кью-эл». Язык SQL является основным средством взаимодействия практически со всеми современными системами реляционных баз данных. SQL предоставляет команды для настройки таблиц, индексов и других структур данных в базе данных. Команды SQL также используются для вставки, обновления и удаления записей данных, а также для запроса этих записей для поиска определенных значений данных.

Все взаимодействие с реляционной базой данных осуществляется через язык SQL. Это верно при интерактивном вводе команд или при использовании API программирования. Во всех случаях данные хранятся, изменяются и извлекаются с помощью команд SQL. Часто люди просматривают список вызовов API в поисках функций, обеспечивающих прямой доступ программы к таблице или структурам данных индекса. Таких функций не существует. API структурирован вокруг подготовки и выдачи команд SQL ядру базы данных. Если вы хотите запросить таблицу или вставить значение с помощью API, вы должны создать и выполнить соответствующую команду SQL. Если вы хотите программировать реляционные базы данных, вы должны знать SQL.

Изучение SQL

Цель этой главы - познакомить вас со всеми основными командами SQL и показать некоторые из основных шаблонов использования. Когда вы впервые читаете эту главу, не думайте, что вам нужно усваивать все сразу. Получите представление о том, какие структуры поддерживает база данных и как их можно использовать, но не думайте, что вам нужно запоминать детали каждой последней команды.

Для тех, кто только начинает работать, наиболее важными командами являются CREATE TABLE, INSERT и SELECT. Это позволит вам создать таблицу, вставить некоторые данные в таблицу, а затем запросить данные и отобразить их. Как только вы освоитесь с этими командами, вы сможете более подробно изучить остальные. Не стесняйтесь вернуться к этой главе или к справочнику по командам в Приложении C. Справочник по командам содержит подробное описание каждой команды, включая некоторые из более сложных синтаксисов, которые не рассматриваются в этой главе.

Всегда помните, что SQL - это командный язык. Предполагается, что вы знаете, что делаете. Если вы вводите команды SQL напрямую через приложение sqlite3, программа не будет останавливаться и запрашивать подтверждение перед обработкой опасных или деструктивных команд. При вводе команд вручную всегда стоит делать паузу и оглядываться на то, что вы набрали, прежде чем нажимать «Return».

Если вы уже достаточно знакомы с языком SQL, можно смело пропустить эту главу. Большая часть информации здесь относится к языку SQL в целом, но есть некоторая информация о конкретном диалекте SQL, который распознает SQLite. Опять же, Приложение C предоставляет ссылку на конкретный синтаксис SQL, используемый SQLite.

Краткая предыстория

Хотя первая официальная спецификация SQL была опубликована в 1986 году Американским национальным институтом стандартов (ANSI), язык восходит к началу 1970-х годов и новаторской работе над реляционными базами данных, которая велась в IBM. Текущие стандарты SQL ратифицированы и опубликованы Международной организацией по стандартизации (ISO). Хотя новый стандарт публикуется каждые несколько лет, последний значительный набор изменений в базовом языке можно проследить до стандарта SQL: 1999 (также известного как «SQL3»). Последующие стандарты в основном касались хранения и обработки данных на основе XML. В целом, эволюция SQL прочно укоренилась в практических аспектах разработки баз данных, и во многих случаях новые стандарты служат только для ратификации и стандартизации синтаксиса или функций, которые уже некоторое время присутствуют в коммерческих продуктах баз данных.

Декларативность

Ядро SQL - декларативный язык. На декларативном языке вы заявляете, какими должны быть результаты, и позволяете языковому процессору выяснить, как обеспечить желаемые результаты. Сравните это с императивными языками, такими как C, Java, Perl или Python, где каждый шаг вычисления или операции должен быть явно записан, предоставляя программисту возможность шаг за шагом вести программу к правильному выводу.

Первые стандарты SQL были специально разработаны, чтобы сделать язык доступным и пригодным для использования «людьми, не имеющими отношения к компьютерам» - по крайней мере, в соответствии с определением этого термина в 1980-х годах. Это одна из причин, по которой команды SQL имеют несколько английский синтаксис. Большинство команд SQL имеют форму глагол-субъект. Например, CREATE (глагол) TABLE (тема), DROP INDEX, UPDATE table_name.

Почти англоязычная декларативная природа SQL имеет как преимущества, так и недостатки. Декларативные языки обычно более просты (особенно для непрограммистов) для понимания. Как только вы привыкнете к общей структуре команд, использование английских ключевых слов может облегчить запоминание синтаксиса. Фиксированная структура команд также упрощает для движка базы данных оптимизацию запросов и более эффективный возврат данных.

Однако предопределенная природа декларативных операторов иногда может казаться немного ограниченной, особенно в языке команд, таком как SQL, где отдельные изолированные команды создаются и выдаются по одной за раз. Если вам нужен запрос, который не вписывается в порядок обработки, определенный командой SELECT, вы можете столкнуться с необходимостью исправлять вместе вложенные запросы или временные таблицы. Это особенно верно, когда проблема, которую вы пытаетесь решить, по своей сути не связана с отношениями, и вам приходится преодолевать дополнительные препятствия, чтобы учесть это.

Несмотря на свои странности и в некоторой степени естественную эволюцию, SQL - мощный язык с удивительно широкой способностью выражать сложные операции. После того, как вы поймете, что заставляет SQL работать, вы часто можете найти умеренно простые решения даже для самых нестандартных проблем. Однако это может потребовать некоторой корректировки, особенно если ваш основной опыт работы связан с императивными языками.

Переносимость

Самый большой недостаток SQL состоит в том, что формальная стандартизация почти всегда следовала за общими реализациями. Почти каждый продукт базы данных (включая SQLite) имеет специальные расширения и улучшения основного языка, которые помогают отличать его от других продуктов или предоставляют функции или системы управления, которые не охвачены основным стандартом SQL. Часто эти улучшения связаны с повышением производительности, и их трудно игнорировать.

Хотя этот менее строгий подход к чистоте языка позволил SQL расти и развиваться в очень практических направлениях, это означает, что переносимость SQL в «реальном мире» не так уж и практична. Если вы строго ограничиваете себя стандартизованным синтаксисом SQL, вы можете достичь умеренной степени переносимости, но обычно это происходит за счет более низкой производительности и меньшей целостности данных. Как правило, приложения будут писать на конкретном диалекте SQL, который они используют, и не будут беспокоиться о совместимости между базами данных. Если совместимость между базами данных важна для конкретного приложения, нормальным подходом является разработка основного списка команд SQL, необходимых для приложения, с небольшими изменениями для каждого конкретного продукта базы данных.

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

У SQL есть и другие проблемы, но, учитывая его происхождение, он на удивление хорошо подходит для поставленной задачи. Любите его или ненавидите, это предпочтительный язык для реляционных баз данных, и он, вероятно, будет с нами в течение долгого времени.

Общий синтаксис

Прежде чем переходить к конкретным командам в SQL, стоит взглянуть на общую структуру языка. Как и большинство языков, SQL имеет довольно полный синтаксис выражения, который можно использовать для определения параметров команд. Более подробное описание поддержки выражений можно найти в приложении D.

Базовый синтаксис

SQL состоит из ряда различных команд, таких как CREATE TABLE или INSERT. Эти команды выдаются и обрабатываются по очереди. Каждая команда реализует отдельное действие или функцию системы баз данных.

Хотя в командах и ключевых словах SQL обычно используются все заглавные буквы, язык SQL не чувствителен к регистру*. Все команды и ключевые слова нечувствительны к регистру, как и идентификаторы (например, имена таблиц и имена столбцов).

Идентификаторы должны быть даны как литералы. При необходимости идентификаторы могут быть заключены в двойные кавычки (" "), соответствующие стандартам, чтобы разрешить включение в идентификатор пробелов или других нестандартных символов. SQLite также позволяет заключать идентификаторы в квадратные скобки ([ ]) или обратные кавычки (` `) для совместимости с другими популярными продуктами баз данных. SQLite оставляет за собой право использовать любой идентификатор, использующий в качестве префикса sqlite_.

SQL не чувствителен к пробелам, включая разрывы строк. Отдельные утверждения разделяются точкой с запятой. Если вы используете интерактивное приложение, такое как инструмент командной строки sqlite3, вам нужно будет использовать точку с запятой для обозначения конца оператора. Однако точка с запятой не является строго обязательной для отдельных операторов, так как это собственно разделитель операторов, а не их терминатор. При передаче команд SQL в программный API точка с запятой не требуется, если вы не передаете более одного оператора команды в одной строке.

Однострочные комментарии начинаются с двойного тире (--) и доходят до конца строки. SQL также поддерживает многострочные комментарии с использованием синтаксиса комментариев C (/* */).

Как и в большинстве языков, числовые литералы представлены голыми. Распознаются как целые (453), так и действительные (рациональные) числа (43.23), а также экспоненциальная научная запись (9.745e-6). Чтобы избежать неоднозначности в парсере, SQLite требует, чтобы десятичная точка всегда была представлена точкой (.), независимо от текущего параметра интернационализации.

Текстовые литералы заключаются в одинарные кавычки (' '). Чтобы представить строковый литерал, содержащий символ одинарной кавычки, используйте две одинарные кавычки в строке (publisher = 'O''Reilly'). Экранирование обратной косой чертой в стиле C (\') не является частью стандарта SQL и не поддерживаются SQLite. Литералы BLOB (двоичные данные) могут быть представлены как x (или X), за которым следует строковый литерал из шестнадцатеричных символов (x'A554E59C').

Текстовые литералы используют одинарные кавычки. Двойные кавычки зарезервированы для идентификаторов (имен таблиц, столбцов и т. д.). Экранирование обратной косой чертой в стиле C не является частью стандарта SQL.

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

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

Если выражение оценивается в контексте строки (например, выражение фильтрации), значение элемента строки может быть извлечено путем присвоения имени столбцу. Возможно, вам придется дополнить имя столбца именем таблицы или псевдонимом. Если вы используете запросы между базами данных, вам также может потребоваться указать, на какую базу данных вы ссылаетесь. Синтаксис:

[[database_name.]table_name.]column_name

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

* Если не указано иное, нечувствительность к регистру применяется только к символам ASCII. То есть к символам, представленным значениями меньше 128.

Трехзначная логика

SQL позволяет присвоить любому значению NULL. NULL сам по себе не является значением (SQLite фактически реализует его как уникальный тип без значения), но используется как маркер или флаг для представления неизвестных или отсутствующих данных. Идея состоит в том, что бывают случаи, когда значения для определенного элемента строки могут быть недоступны или неприменимы.

NULL может не быть значением, но его можно присвоить элементам данных, которые обычно имеют значения и поэтому могут отображаться в выражениях. Проблема в том, что NULL плохо взаимодействуют с другими значениями. Если NULL представляет неизвестное, которое может быть любым возможным значением, как мы можем узнать, истинно или ложно выражение NULL > 3?

Чтобы справиться с этой проблемой, SQL должен использовать концепцию, называемую трехзначной логикой (three-valued logic). Трехзначная логика часто обозначается аббревиатурой TVL или 3VL и более формально известна как троичная логика (ternary logic). 3VL по существу добавляет состояние «unknown» к знакомой системе логической логики «true/false».

Вот таблицы истинности для операторов 3VL NOT, AND и OR:

Value NOT Value
True False
False True
NULL NULL
3VL AND TRUE FALSE NULL
TRUE TRUE FALSE NULL
FALSE FALSE FALSE FALSE
NULL NULL FALSE NULL
3VL OR TRUE FALSE NULL
TRUE TRUE TRUE TRUE
FALSE TRUE FALSE NULL
NULL TRUE NULL NULL

3VL также определяет, как работают сравнения. Например, любая проверка равенства (=), включающая NULL, будет оцениваться как NULL, включая NULL = NULL. Помните, что NULL не является значением, это флаг неизвестного, поэтому выражение NULL = NULL спрашивает: «Это неизвестное равно этому неизвестному?» Единственный практический ответ: «Это неизвестно». Может равно, а может и нет. Подобные правила применяются к сравнениям больше, меньше и другим параметрам.

Вы не можете использовать оператор равенства (=) для проверки значений NULL. Вы должны использовать оператор IS NULL.

Если у вас возникли проблемы с разрешением выражения, просто помните, что NULL отмечает неизвестное или неразрешенное значение. Вот почему выражение False AND NULL ложно, а True AND NULL равно NULL. В случае первого выражения NULL можно заменить на true или false без изменения результата выражения. Это неверно для второго выражения, где результат неизвестен (другими словами, NULL), потому что вывод зависит от неизвестного ввода.

Простые операторы

SQLite поддерживает следующие унарные префиксные операторы:

- +
Они регулируют знак значения. Оператор «-» меняет знак значения, эффективно умножая его на -1,0. Оператор «+», по сути, не работает, оставляя значение с тем же знаком, что и раньше. Он не делает отрицательные значения положительными.
~
Как и в языке C, оператор «~» выполняет побитовую инверсию. Этот оператор не является частью стандарта SQL.
NOT
Оператор NOT меняет логическое выражение на противоположное, используя 3VL.

Также существует ряд бинарных операторов. Они перечислены здесь в порядке убывания приоритета.

||
Конкатенация строк. Это единственный оператор конкатенации строк, признанный стандартом SQL. Многие другие продукты для баз данных позволяют использовать «+» для конкатенации, но SQLite - нет.
+ - * / %
Стандартные арифметические операторы для сложения, вычитания, умножения, деления и модуля (остатка).
| & << >>
Поразрядные операторы or, and и shift-high/shift-low, как в языке C. Эти операторы не являются частью стандарта SQL.
< <= => >
Операторы сравнительного теста. Опять же, как и в языке C, у нас есть меньше, меньше или равно, больше или равно и больше. Эти операторы подчиняются 3VL SQL в отношении NULL.
= == != <>
Операторы проверки равенства. И «=», и «==» проверяют на равенство, а «!=» и «<>» проверяют на неравенство. Будучи логическими операторами, эти тесты подчиняются 3VL SQL в отношении NULL. В частности, value = NULL всегда будет возвращать NULL.
IN LIKE GLOB MATCH REGEXP
Эти пять ключевых слов представляют собой логические операторы, возвращающие, истинное, ложное или нулевое состояние. См. Приложение D для более подробной информации об этих операторах.
AND OR
Логические операторы. Опять же, они подчиняются 3VL SQL.

В дополнение к этим основам SQL поддерживает ряд специальных операций с выражениями. Дополнительные сведения об этих и любых других выражениях, относящихся к SQL, см. в приложении D.

Языки данных SQL

Команды SQL делятся на четыре основные категории или языка. Каждый язык определяет подмножество команд, которые служат связанной цели. Первый язык - это язык определения данных (Data Definition Language) или DDL, который относится к командам, которые определяют структуру таблиц, представлений, индексов и других контейнеров данных и объектов в базе данных. CREATE TABLE (используется для определения новой таблицы) и DROP VIEW (используется для удаления представления) являются примерами команд DDL.

Вторая категория команд известна как язык обработки данных (Data Manipulation Language) или DML. Это все команды, которые вставляют, обновляют, удаляют и запрашивают фактические значения данных из структур данных, определенных DDL. INSERT (используется для вставки новых значений в таблицу) и SELECT (используется для запроса или поиска данных из таблиц) являются примерами команд DML.

С DML и DDL связан язык управления транзакциями (Transaction Control Language) или TCL. Команды TCL могут использоваться для управления транзакциями команд DML и DDL. BEGIN (используется для начала транзакции с несколькими операциями) и COMMIT (используется для завершения и принятия транзакции) являются примерами команд TCL.</p>

Последняя категория - это язык управления данными (Data Control Language) или DCL. Основная цель DCL - предоставить или отменить управление доступом. Подобно разрешениям файлов, команды DCL используются, чтобы разрешить (или запретить) конкретным пользователям базы данных (или группам пользователей) разрешение на использование или доступ к определенным ресурсам в базе данных. Эти разрешения могут применяться как к DDL, так и к DML. Разрешения DDL могут включать возможность создания реальной или временной таблицы, в то время как разрешения DML могут включать возможность чтения, обновления или удаления записей определенной таблицы. GRANT (используется для назначения разрешения) и REVOKE (используется для удаления существующего разрешения) являются основными командами DCL.

SQLite поддерживает большинство стандартизированных команд DDL, DML и TCL, но в нем отсутствуют какие-либо команды DCL. Поскольку SQLite не имеет имен пользователей или логинов, он не имеет никакой концепции назначенных разрешений. Скорее, SQLite зависит от разрешений типа данных, чтобы определить, кто может открывать и получать доступ к базе данных.

Язык определения данных

DDL используется для определения структуры контейнеров данных и объектов в базе данных. Наиболее распространенными из этих контейнеров и объектов являются таблицы, индексы и представления. Как вы увидите, большинство объектов определяется с помощью варианта команды CREATE, например CREATE TABLE или CREATE VIEW. Команда DROP используется для удаления существующего объекта (и всех данных, которые он может содержать). Примеры включают DROP TABLE или DROP INDEX. Поскольку синтаксис команд сильно отличается, такие операторы, как CREATE TABLE или CREATE INDEX, обычно считаются отдельными командами, а не вариантами одной команды CREATE.

В некотором смысле команды DDL похожи на файлы заголовков C/C++. Команды DDL используются для определения структуры, имен и типов контейнеров данных в базе данных, точно так же, как файл заголовка обычно определяет определения типов, структуры, классы и другие структуры данных. Команды DDL обычно используются для установки и настройки новой базы данных перед вводом данных.

Команды DDL определяют базовую структуру базы данных и обычно запускаются при создании новой базы данных.

Команды DDL часто хранятся в файле сценария, поэтому структуру базы данных можно легко воссоздать. Иногда, особенно во время разработки, вам может потребоваться воссоздать только часть базы данных. Чтобы помочь в этом, большинство команд CREATE в SQLite имеют необязательное предложение IF NOT EXISTS.

Обычно оператор CREATE возвращает ошибку, если объект с запрошенным именем уже существует. Если присутствует необязательное предложение IF NOT EXISTS, эта ошибка подавляется и ничего не делается, даже если структура существующего объекта и нового объекта несовместимы. Точно так же большинство операторов DROP допускают необязательное предложение IF EXISTS, которое молча игнорирует любой запрос на удаление объекта, которого нет.

В следующих примерах варианты команд IF EXISTS и IF NOT EXISTS явно не вызываются. Подробную информацию о синтаксисе, поддерживаемом SQLite, см. в справочнике по командам SQLite в приложении C.

Таблицы

Самая распространенная команда DDL - CREATE TABLE. Никакие значения данных не могут храниться в базе данных до тех пор, пока не будет определена таблица для хранения этих данных. Как минимум, команда CREATE TABLE определяет имя таблицы плюс имя каждого столбца. В большинстве случаев вы захотите также определить тип для каждого столбца, хотя типы не являются обязательными при использовании SQLite. Необязательные ограничения, условия и значения по умолчанию также могут быть назначены каждому столбцу. Также могут быть назначены ограничения на уровне таблицы, если они включают несколько столбцов.

В некоторых больших системах СУБД команда CREATE TABLE может быть довольно сложной, определяя все виды параметров и конфигураций хранилища. Версия CREATE TABLE для SQLite несколько проще, но по-прежнему доступно множество опций. Полное объяснение команды см. в CREATE TABLE в приложении C.

Основы

Самый простой синтаксис CREATE TABLE выглядит примерно так:

CREATE TABLE table_name
(
    column_name column_type,
    [...]
);

Для идентификации новой таблицы необходимо указать имя таблицы. После этого остается простой список имен столбцов и их типов. Имена таблиц берутся из глобального пространства имен всех идентификаторов, включая имена таблиц, представлений и индексов.

Важны четкие и краткие имена идентификаторов. Как и в случае с самой базой данных, следует тщательно продумать имя таблицы, пытаясь определить точное значение содержащихся в ней данных. То же самое можно сказать и об именах столбцов. Имена таблиц и имена столбцов, как правило, часто упоминаются в запросах, поэтому есть желание сделать их как можно более краткими, но при этом четко обозначить их цель.

Типы столбцов

В большинстве баз данных используется строгая статическая типизация столбцов. Это означает, что элементы столбца могут содержать только значения, совместимые с определенным типом столбца. SQLite использует метод динамической типизации, известный как манифестная типизация (manifest typing). Для каждого значения строки манифестная типизация записывает тип значения вместе с данными значения. Это позволяет почти любому элементу любой строки содержать почти любой тип значения.

В самом строгом смысле SQLite поддерживает только пять конкретных типов данных. Они известны как классы хранения (storage classes) и представляют различные способы хранения данных на диске в SQLite. Каждое значение имеет один из этих пяти собственных классов хранения:

NULL
NULL считается отдельным типом. Тип NULL не содержит значения. Литеральные значения NULL представлены ключевым словом NULL.
Integer
Целое число со знаком. Целочисленные значения имеют переменную длину, составляющую 1, 2, 3, 4, 6 или 8 байтов, в зависимости от минимального размера, необходимого для хранения определенного значения. Целые числа имеют диапазон от -9 223 372 036 854 775 808 до +9 223 372 036 854 775 807 или примерно 19 цифр. Литеральные целые числа представлены любой простой последовательностью десятичныхё цифр (без запятых), которая не включает десятичную точку или показатель степени.
Float
Число с плавающей запятой, хранящееся как 8-байтовое значение в собственном формате процессора. Почти для всех современных процессоров это число двойной точности IEEE 754. Литеральные числа с плавающей запятой представлены любой пустой серией десятичных цифр, которые включают десятичную точку или показатель степени.
Text
Строка переменной длины, хранящаяся в кодировке базы данных (UTF-8, UTF-16BE или UTF-16LE). Буквальные текстовые значения представлены с помощью символьных строк в одинарных кавычках.
BLOB
Длина необработанных байтов, скопированных точно так, как указано. Литеральные BLOB-объекты представлены в виде шестнадцатеричных текстовых строк, которым предшествует x. Например, запись x'1234ABCD' представляет 4-байтовый BLOB. BLOB означает большой двоичный объект.

Текст SQLite и значения BLOB всегда имеют переменную длину. Максимальный размер текста или значения BLOB ограничен директивой времени компиляции. Предел по умолчанию составляет ровно один миллиард байт или чуть меньше полного гигабайта. Максимальное значение этой директивы - два гигабайта.

Поскольку элементы большинства столбцов могут содержать значения любого типа, понятие «тип» столбца может вводить в заблуждение. Вместо того, чтобы быть абсолютным типом, как в большинстве баз данных, тип столбца SQLite (как определено в CREATE TABLE) становится скорее предложением, чем жестким и быстрым правилом. Это называется сродством типа (type affinity) и по сути представляет собой желаемую категорию типа. Cродство каждого типа имеет определенные правила о том, какие типы значений он может хранить, и как различные значения будут преобразованы при сохранении в этом столбце. Как правило, сродство типов вызывает преобразование или миграцию типов только в том случае, если это может быть выполнено без потери данных или точности.

Каждый столбец таблицы должен иметь один из пяти типов соответствия:

Text
Столбец с привязкой к тексту будет хранить только значения типа NULL, текст или BLOB. Если вы попытаетесь сохранить значение с числовым типом (с плавающей точкой или целым числом), оно будет преобразовано в текстовое представление перед сохранением как тип текстового значения.
Numeric
Столбец с числовым соответствием будет хранить любой из пяти типов. Значения с целочисленными типами и типами с плавающей запятой, а также с типами NULL и BLOB сохраняются без преобразования. Каждый раз при сохранении значения текстового типа предпринимается попытка преобразовать значение в числовой тип (целое число или число с плавающей запятой). Если преобразование работает, значение сохраняется в соответствующем числовом типе. Если преобразование не удается, текстовое значение сохраняется без преобразования любого типа.
Integer
Столбец с целочисленным сродством работает практически так же, как числовое сродство. Единственное отличие состоит в том, что любое значение с типом float, в котором отсутствует дробная составляющая, будет преобразовано в целочисленный тип.
Float
Столбец с привязкой к числам с плавающей запятой также работает практически так же, как и числовое соответствие. Единственное отличие состоит в том, что большинство значений с целочисленными типами преобразуются в значения с плавающей запятой и сохраняются как тип с плавающей запятой.
None
Столбец без привязки не имеет предпочтения по сравнению с классом хранения. Каждое значение сохраняется в указанном типе без попытки что-либо преобразовать.

Поскольку сродство типов не является частью стандарта SQL, в SQLite есть ряд правил, которые пытаются сопоставить традиционные типы столбцов с наиболее логическим сродством типов. Сродство типа столбца определяется объявленным типом столбца в соответствии со следующими правилами (совпадения подстрок не зависят от регистра):

  1. Если тип столбца не задан, то ему присваивается сродство none.
  2. Если тип столбца содержит подстроку «INT», то столбцу присваивается сродство integer.
  3. Если тип столбца содержит любую из подстрок «CHAR», «CLOB» или «TEXT», тогда столбцу присваивается сродство text.
  4. Если тип столбца содержит подстроку «BLOB», то столбцу присваивается сродство none.
  5. Если тип столбца содержит любую из подстрок «REAL», «FLOA» или «DOUB», то ему присваивается сродство float.
  6. Если совпадений не найдено, столбцу присваивается сродство numeric.

Как следует из первого правила, тип столбца не является обязательным. SQLite позволит вам создать таблицу, просто назвав столбцы, например CREATE TABLE t (i, j, k);. Вы также заметите, что нет какого-либо конкретного списка распознаваемых типов столбцов. Вы можете использовать любой тип столбца, какой захотите, даже придумывая свои собственные имена.

Это может показаться немного быстрым и небрежным для системы набора текста, но работает довольно хорошо. Выделяя определенные подстроки, а не пытаясь определить конкретные типы, SQLite может обрабатывать операторы SQL (и их типы, специфичные для базы данных) практически из любой базы данных, при этом выполняя довольно хорошую работу по сопоставлению типов с соответствующим сродством. Единственный тип названия столбца, с которым вам нужно быть осторожным, - это, например, «floating point». Подстрока «int» в слове «point» вызовет правило 2 до того, как подстрока «floa» в слове «floating» перейдет к правилу 5, и сродство столбца в конечном итоге станет integer.

Ограничения столбца

Помимо имен и типов столбцов, определение таблицы может также накладывать ограничения на определенные столбцы или наборы столбцов. Более полное представление команды CREATE TABLE выглядит примерно так:

CREATE TABLE table_name
(
    column_name  column_type   column_constraints...,
    [... ,]

    table_constraints,
    [...]
);

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

Ограничения столбца можно использовать для определения настраиваемого порядка сортировки (COLLATE collation_name). Параметры сортировки определяют способ сортировки текстовых значений. В дополнение к определяемым пользователем параметрам сопоставления SQLite включает сопоставление NOCASE, которое игнорирует регистр при сортировке.

Также может быть присвоено значение по умолчанию (DEFAULT value). Почти все столбцы имеют значение по умолчанию NULL. Вы можете использовать ограничение столбца DEFAULT, чтобы установить другое значение по умолчанию. По умолчанию это может быть литерал или выражение. Выражения необходимо заключать в круглые скобки.

Чтобы помочь с настройками даты и времени по умолчанию, SQLite также включает три специальных ключевых слова, которые могут использоваться в качестве значения по умолчанию: CURRENT_TIME, CURRENT_DATE и CURRENT_TIMESTAMP. Они будут записывать время, дату или дату и время в формате UTC, соответственно, при вставке новой строки. См. «Функции даты и времени» для получения дополнительной информации о функциях даты и времени.

Ограничения столбца также могут накладывать ограничения на столбец, например, запрещать NULL (NOT NULL) или требовать уникального значения для каждой строки (UNIQUE). Помните, что NULL не считается значением, поэтому UNIQUE не подразумевает NOT NULL, и UNIQUE не подразумевает, что разрешен только один NULL. Если вы хотите, чтобы значения NULL не попадали в столбец UNIQUE, вам необходимо явно пометить столбец как NOT NULL.

Значения столбцов также могут подвергаться произвольным ограничениям, заданным пользователем, перед их назначением (CHECK ( expression )). Некоторые типы ограничений также позволяют вам указать действие, которое будет предпринято в ситуациях, когда ограничение будет нарушено. См. CREATE TABLE и UPDATE в приложении C для более подробной информации.

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

CREATE TABLE parts
(
    part_id   INTEGER   PRIMARY KEY,
    stock     INTEGER   DEFAULT 0   NOT NULL,
    desc      TEXT      CHECK( desc != '' )   -- empty strings not allowed
);

Чтобы обеспечить соблюдение ограничения столбца UNIQUE, для этого столбца будет автоматически создан уникальный индекс. Для каждого столбца (или набора столбцов) с пометкой UNIQUE будет создан отдельный индекс. Поддержание индекса связано с некоторыми расходами, поэтому имейте в виду, что принудительное выполнение ограничения столбца UNIQUE может иметь последствия для производительности.

Первичные ключи

В дополнение к этим другим ограничениям один столбец (или набор столбцов) может быть обозначен как PRIMARY KEY. Каждая таблица может иметь только один первичный ключ. Первичные ключи должны быть уникальными, поэтому обозначение столбца как PRIMARY KEY также подразумевает ограничение UNIQUE и приведет к созданию автоматического уникального индекса. Если столбец отмечен как UNIQUE, так и PRIMARY KEY, будет создан только один индекс.

В SQLite PRIMARY KEY не означает NOT NULL. Это противоречит стандарту SQL и считается ошибкой, но такое поведение существует так давно, что есть опасения по поводу его исправления и выхода из строя существующих приложений. В результате всегда рекомендуется явно отмечать хотя бы один столбец из каждого PRIMARY KEY как NOT NULL.

Есть также несколько веских причин для определения первичного ключа, которые будут обсуждаться в разделе «Таблицы и ключи», но единственная важная, конкретная вещь, которая вытекает из определения PRIMARY KEY, - это автоматический уникальный индекс. Есть также несколько незначительных сокращений синтаксиса.

Если, однако, столбец первичного ключа имеет тип, обозначенный как INTEGER (и именно INTEGER), то этот столбец становится «корневым» столбцом таблицы.

В SQLite должен быть какой-то столбец, который можно использовать для индексации базового хранилища таблицы. В некотором смысле этот столбец действует как главный индекс, который используется для хранения самой таблицы. Как и многие другие системы баз данных, SQLite автоматически создает для этой цели скрытый столбец ROWID. В разных системах баз данных используются разные имена, поэтому в целях обеспечения совместимости SQLite распознает имена ROWID, OID или _ROWID_ для ссылки на корневой столбец. Обычно столбцы ROWID не возвращаются (даже для подстановочных знаков столбцов), а их значения не включаются в файлы дампа.

Если таблица включает столбец INTEGER PRIMARY KEY, то этот столбец становится псевдонимом для автоматического столбца ROWID. Вы по-прежнему можете ссылаться на столбец по любому из имен ROWID, но вы также можете ссылаться на столбец по его «реальному» пользовательскому имени. В отличие от самого PRIMARY KEY, столбцы INTEGER PRIMARY KEY имеют автоматическое ограничение NOT NULL, связанное с ними. Они также строго типизированы, чтобы принимать только целочисленные значения.

Столбцы INTEGER PRIMARY KEY обладают двумя значительными преимуществами. Во-первых, поскольку столбец является псевдонимом корневого столбца ROWID таблицы, нет необходимости во вторичном индексе. Сама таблица действует как индекс, обеспечивая эффективный поиск без затрат на обслуживание внешнего индекса.

Во-вторых, столбцы INTEGER PRIMARY KEY могут автоматически предоставлять уникальные значения по умолчанию. Когда вы вставляете строку без явного значения для столбца ROWID (или псевдонима ROWID), SQLite автоматически выбирает значение, которое на единицу больше самого большого существующего значения в столбце. Это обеспечивает простой способ автоматической генерации уникальных ключей. При достижении максимального значения база данных будет случайным образом пробовать другие значения в поисках неиспользуемого ключа.

Столбцы INTEGER PRIMARY KEY могут быть дополнительно помечены как AUTOINCREMENT. В этом случае автоматически сгенерированные значения идентификаторов будут постоянно увеличиваться, предотвращая повторное использование значения идентификатора из ранее удаленной строки. Если достигнуто максимальное значение, вставки с автоматическими значениями INTEGER PRIMARY KEY больше не возможны. Однако это маловероятно, поскольку область типа INTEGER PRIMARY KEY достаточно велика, чтобы допускать 1000 вставок в секунду в течение почти 300 миллионов лет.

При использовании автоматических значений или значений AUTOINCREMENT всегда можно вставить явное значение ROWID (или псевдоним ROWID). За исключением обозначения INTEGER PRIMARY KEY, SQLite не предлагает никаких других функций автоматической последовательности.

Помимо PRIMARY KEY, столбцы также могут быть помечены как FOREIGN KEY. Эти столбцы ссылаются на строки в другой (чужой) таблице. Внешние ключи можно использовать для создания ссылок между строками в разных таблицах. См. «Таблицы и ключи» для получения подробной информации.

Ограничения таблицы

Определения таблиц также могут включать ограничения на уровне таблицы. В общем, ограничения таблицы и ограничения столбцов работают одинаково. Ограничения на уровне таблицы по-прежнему действуют для отдельных строк. Основное отличие состоит в том, что с помощью синтаксиса ограничения таблицы вы можете применить ограничение к группе столбцов, а не только к одному столбцу. Совершенно законно определять ограничение таблицы только с одним столбцом, фактически определяя ограничение столбца. Ограничения по нескольким столбцам иногда называют составными ограничениями (compound constraints).

На уровне таблицы SQLite поддерживает ограничения UNIQUE, CHECK и PRIMARY KEY. Ограничение проверки очень похоже, требует только выражения (CHECK (expression)). Как ограничения UNIQUE, так и PRIMARY KEY, если они заданы как ограничение таблицы, требуют списка столбцов (например, UNIQUE (column_name, [...]), PRIMARY KEY (column_name, [...])). Как и в случае ограничений столбцов, любое ограничение UNIQUE или PRIMARY KEY на уровне таблицы (которое подразумевает UNIQUE) автоматически создает уникальный индекс для соответствующих столбцов.

Ограничения таблицы, которые применяются к нескольким столбцам, используют набор столбцов как группу. Например, когда UNIQUE или PRIMARY KEY применяются более чем к одному столбцу, каждый отдельный столбец может иметь повторяющиеся значения. Ограничение только предотвращает репликацию набора значений в назначенных столбцах. Если вы хотите, чтобы каждый отдельный столбец также был UNIQUE, вам нужно добавить соответствующие ограничения для отдельных столбцов.

Рассмотрим таблицу, содержащую записи обо всех комнатах в кампусе, состоящем из нескольких зданий:

CREATE TABLE rooms
(
    room_number      INTEGER  NOT NULL,
    building_number  INTEGER  NOT NULL,
    [...,]

    PRIMARY KEY( room_number, building_number )
);

Очевидно, нам нужно разрешить более одной комнаты с номером 101. Нам также необходимо разрешить более одной комнаты в здании 103. Но в здании 103 должна быть только одна комната 101, поэтому мы применяем ограничение к обоим столбцам. В этом примере мы решили превратить эти столбцы в составной первичный ключ, поскольку номер здания и номер комнаты объединяются для типичного определения конкретной комнаты. В зависимости от конструкции остальной части базы данных, было бы одинаково правильно определить простое ограничение UNIQUE для этих двух столбцов и назначить произвольный столбец room_id в качестве первичного ключа.

Таблицы из запросов

Вы также можете создать таблицу из результатов запроса. Это немного другой синтаксис CREATE TABLE, который создает новую таблицу и предварительно загружает ее данными одной командой:

CREATE [TEMP] TABLE table_name AS SELECT query_statement;

Используя эту форму, вы не указываете количество столбцов, их имена или типы. Вместо этого выполняется инструкция запроса, а выходные данные используются для определения имен столбцов и предварительной загрузки данных в новую таблицу. С помощью этого синтаксиса невозможно назначить ограничения или модификаторы столбцов. Любой столбец результата, который является прямой ссылкой на столбец, унаследует привязку столбца, но всем столбцам будет дана привязка NONE. Оператор запроса состоит из команды SELECT. Более подробную информацию о SELECT можно найти в главе 5.

Таблицы, созданные таким образом, не обновляются динамически - команда запроса запускается только один раз при создании таблицы. После ввода данных в новую таблицу они остаются неизменными до тех пор, пока вы их не измените. Если вам нужен объект в виде таблицы, который может динамически обновляться, используйте VIEW (см. «Представления»).

В этом примере показано необязательное ключевое слово TEMP (также можно использовать полное слово TEMPORARY) в CREATE TEMP TABLE. Этот модификатор может использоваться в любом варианте CREATE TABLE, но часто используется вместе с вариантом ...AS SELECT..., показанным здесь. Временные таблицы имеют две особенности. Во-первых, временные таблицы могут быть видны только соединению с базой данных, которое их создало. Это позволяет одновременно повторно использовать имена таблиц, не беспокоясь о конфликте между разными клиентами. Во-вторых, все связанные временные таблицы автоматически очищаются и удаляются при закрытии соединения с базой данных.

Вообще говоря, CREATE TABLE...AS SELECT - не лучший выбор для создания стандартных таблиц. Если вам нужно скопировать данные из старой таблицы в новую, лучше использовать CREATE TABLE для определения пустой таблицы со всеми соответствующими модификаторами столбцов и ограничениями таблицы. Затем вы можете массово скопировать все данные в новую таблицу, используя вариант команды INSERT, которая позволяет использовать операторы запроса. См. «INSERT» для получения подробной информации.

Изменение таблиц

SQLite поддерживает ограниченную версию команды ALTER TABLE. В настоящее время ALTER TABLE поддерживает только две операции: добавить столбец (add column) и переименовать (rename). Вариант добавления столбца позволяет добавлять новые столбцы в существующую таблицу. Он не может их удалить. Новые столбцы всегда добавляются в конец списка столбцов. Применяются и некоторые другие ограничения.

Если вам нужно внести более существенные изменения, сохранив как можно больше данных, вы можете использовать вариант переименования, т.е. переименовать существующую таблицу, создать новую таблицу под исходным именем, а затем скопировать данные из старой таблицы в новую. После этого старую тфблицу можно безопасно удалить.

Для получения полной информации см. ALTER TABLE в приложении C.

Удаление таблиц

Команда CREATE TABLE используется для создания таблиц, а DROP TABLE - для их удаления. Команда DROP TABLE удаляет таблицу и все содержащиеся в ней данные. Определение таблицы также удаляется из системных каталогов базы данных.

Команда DROP TABLE очень проста. Единственный аргумент - это имя таблицы, которую вы хотите удалить:

DROP TABLE table_name;

Помимо удаления таблицы, DROP TABLE также удалит все индексы, связанные с таблицей. Будут удалены как автоматически созданные индексы (например, те, которые используются для обеспечения ограничения UNIQUE), так и индексы, созданные вручную.

Виртуальные таблицы

Виртуальные таблицы можно использовать для подключения к SQLite любого источника данных, включая другие базы данных. Виртуальная таблица создается с помощью команды CREATE VIRTUAL TABLE. Хотя она очень похожа на CREATE TABLE, есть важные отличия. Например, виртуальные таблицы нельзя сделать временными, и они не позволяют использовать предложение IF NOT EXISTS. Чтобы удалить виртуальную таблицу, вы используете обычную команду DROP TABLE.

Дополнительные сведения о виртуальных таблицах, включая полный синтаксис CREATE VIRTUAL TABLE, см. в главе 10.

Представления

Представления предоставляют способ упаковки запросов в предопределенный объект. После создания представления действуют более или менее как таблицы, доступные только для чтения. Как и таблицы, новые представления могут быть помечены как TEMP с тем же результатом. Основной синтаксис команды CREATE VIEW:

CREATE [TEMP] VIEW view_name AS SELECT query_statement

Синтаксис CREATE VIEW практически идентичен команде CREATE TABLE...AS SELECT. Это связано с тем, что обе команды служат одной цели с одним важным отличием. Результатом команды CREATE TABLE является новая таблица, содержащая полную копию данных. Оператор SELECT запускается ровно один раз, а выходные данные запроса сохраняются во вновь определенной таблице. После создания таблица будет содержать свою собственную независимую копию данных.

С другой стороны, представление полностью динамично. Каждый раз, когда на представление ссылаются или запрашивают, выполняется базовая инструкция SELECT для повторного создания представления. Это означает, что данные, отображаемые в представлении, автоматически обновляются по мере изменения данных в базовых таблицах. В некотором смысле представления похожи на именованные запросы.

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

Представления также обычно используются для создания удобных версий стандартных таблиц. Типичный пример - таблицы с записями даты и времени. Обычно любое значение времени или даты записывается в формате всемирного координированного времени (Coordinated Universal Time) или UTC. UTC - более подходящий формат для даты и времени, поскольку он недвусмысленен и не зависит от часового пояса. К сожалению, это также может сбивать с толку, если вы находитесь в нескольких часовых поясах. Часто бывает полезно создать представление, которое имитирует базовую таблицу, но преобразует все время и даты из UTC в местный часовой пояс. Таким образом, данные в исходных таблицах остаются неизменными, но представление осуществляется в более удобных для пользователя единицах.

Представления удаляются с помощью команды DROP VIEW:

DROP VIEW view_name;

Удаление представления не повлияет на таблицы, на которые оно ссылается.

Индексы

Индексы (indexes (или indices)) - это средство оптимизации поиска в базе данных путем предварительной сортировки и индексации одного или нескольких столбцов таблицы. В идеале это позволяет находить определенные строки в таблице без необходимости сканировать каждую строку в таблице. Таким образом, индексы могут значительно повысить производительность некоторых типов запросов. Однако индексы не бесплатны и требуют обновлений при каждом изменении таблицы, а также дополнительного места для хранения. Бывают даже ситуации, когда индекс вызывает падение производительности. См. «Индексы» для получения дополнительной информации о том, когда имеет смысл использовать индекс.

Базовый синтаксис для создания индекса определяет имя нового индекса, а также имена индексируемых таблиц и столбцов. Индексы всегда связаны с одной (и только одной) таблицей, но они могут включать один или несколько столбцов из этой таблицы:

CREATE [UNIQUE] INDEX index_name ON table_name ( column_name [, ...] );

Обычно индексы допускают повторяющиеся значения. Необязательное ключевое слово UNIQUE указывает, что повторяющиеся записи недопустимы, и любая попытка вставить или обновить таблицу с неуникальным значением вызовет ошибку. Для уникальных индексов, которые ссылаются на более чем один столбец, все столбцы должны совпадать, чтобы запись считалась повторяющейся. Как обсуждалось с CREATE TABLE, NULL не считается значением, поэтому индекс UNIQUE не предотвратит одно или несколько значений NULL. Если вы хотите предотвратить значения NULL, вы должны указать NOT NULL в исходном определении таблицы.

Каждый индекс привязан к определенной таблице, но все они имеют общее пространство имен. Хотя вы можете назвать индекс как угодно, стандартной практикой является присвоение имени индекса стандартному префиксу (например, idx_), а затем включение имени таблицы и, возможно, имен столбцов, включенных в индекс. Например:

REATE INDEX idx_employees_name ON employees ( name );

Это позволяет использовать длинные имена индексов, но, в отличие от имен таблиц или представлений, вы обычно ссылаетесь на имя индекса только при его создании и удалении.

Как и все команды DROP, команда DROP INDEX очень проста и требует только имени удаляемого индекса:

DROP INDEX index_name;

Удаление индекса приведет к удалению индекса из базы данных, но оставит связанную таблицу нетронутой.

Как описано ранее, команда CREATE TABLE автоматически создаст уникальные индексы для обеспечения ограничения UNIQUE или PRIMARY KEY. Все автоматические индексы начинаются с префикса sqlite_. Поскольку эти индексы необходимы для принудительного определения таблицы, их нельзя удалить вручную с помощью команды DROP INDEX. Удаление автоматических индексов изменит поведение таблицы, как определено исходной командой CREATE TABLE.

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

Язык манипулирования данными

Язык манипулирования данными предназначен для получения пользовательских данных в базе данных и из нее. После того, как все структуры данных и другие объекты базы данных были созданы с помощью команд DDL, команды DML могут использоваться для загрузки этих структур данных, полных полезных данных.

DML, поддерживаемый SQLite, делится на две основные категории. Первая категория состоит из команд «обновления», которые включают в себя собственно команду UPDATE, а также команды INSERT и DELETE. Как вы могли догадаться, эти команды используются для обновления (или изменения), вставки и удаления строк таблицы. Все эти команды каким-то образом изменяют сохраненные данные. Команды обновления являются основным средством управления всеми данными в базе данных.

Вторая категория состоит из команд «запроса», которые используются для извлечения данных из базы данных. Фактически, есть только одна команда запроса: SELECT. Команда SELECT не только выводит на печать возвращаемые значения, но и предоставляет большое количество опций для объединения различных таблиц и строк и других операций с данными перед возвратом окончательного результата.

SELECT, несомненно, самая сложная команда SQL. Это также, пожалуй, самая важная команда SQL. В этой главе будут рассмотрены только самые основы SELECT, а затем мы посвятим ей следующую главу, пройдя по частям все ее части. Чтобы подробно рассмотреть полный синтаксис команды, SELECT получает целую главу (глава 5).

Команды модификации строки

Для добавления, изменения и удаления данных из базы данных используются три команды. INSERT добавляет новые строки, UPDATE изменяет существующие строки, а DELETE удаляет строки. Эти три команды используются для сохранения всех фактических значений данных в базе данных. Все три команды обновления работают на уровне строк, добавляя, изменяя или удаляя указанные строки. Хотя все три команды могут работать с несколькими строками, каждая команда может напрямую воздействовать только на строки, содержащиеся в одной таблице.

INSERT

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

INSERT INTO table_name (column_name [, ...]) VALUES (new_value [, ...]);

Предоставляется имя таблицы, а также список столбцов и список значений. В обоих списках должно быть одинаковое количество элементов. Создается одна новая строка, и каждое значение записывается в соответствующий столбец. Столбцы могут быть перечислены в любом порядке, только если список столбцов и список значений совпадают правильно. Любые столбцы, которые не указаны в списке, получат значения по умолчанию:

INSERT INTO parts ( name, stock, status ) VALUES ( 'Widget', 17, 'IN STOCK' );

В этом примере мы пытаемся вставить новую строку в таблицу «parts». Обратите внимание на использование одинарных кавычек для текстовых литералов.

Технически список имен столбцов необязателен. Если не указан явный список столбцов, команда INSERT попытается объединить значения в пары с полным списком столбцов таблицы:

INSERT INTO table_name VALUES (new_value [, ...]);

Уловка с этим форматом заключается в том, что количество и порядок значений должны точно соответствовать количеству и порядку столбцов в определении таблицы. Это означает, что невозможно использовать значения по умолчанию даже в столбцах INTEGER PRIMARY KEY. Чаще всего это нежелательно. Этот формат также сложнее поддерживать в исходном коде приложения, поскольку он должен тщательно обновляться при изменении формата таблицы. В общем, рекомендуется всегда явно перечислять столбцы в операторе INSERT.

При массовом импорте данных обычно выполняется цикл по наборам данных, вызывая INSERT снова и снова. Обработка этих операторов по очереди может быть довольно медленной, поскольку каждая команда обновит как таблицу, так и все соответствующие индексы, а затем убедится, что данные полностью записаны на физический диск перед (наконец-то!) запуском следующего INSERT. Это довольно длительный процесс, поскольку он требует физического ввода-вывода.

Чтобы ускорить массовую вставку, обычно объединяют группы от 1000 до 10000 операторов INSERT в одну транзакцию. Группирование операторов вместе существенно увеличит общую скорость вставок за счет задержки физического ввода-вывода до конца транзакции. См. «Язык управления транзакциями» для получения дополнительной информации о транзакциях.

Массовые вставки можно ускорить, заключив в транзакцию большие группы команд INSERT.

Вторая версия INSERT позволяет вам определять значения с помощью оператора запроса. Это очень похоже на команду CREATE TABLE...AS SELECT, хотя таблица уже должна существовать. Это единственная версия INSERT, которая может вставлять более одной строки с помощью одной команды:

INSERT INTO table_name (column_name, [...]) SELECT query_statement;

Этот тип INSERT чаще всего используется для массового копирования данных из одной таблицы в другую. Это обычная операция, когда вам нужно обновить определение таблицы, но вы не хотите потерять все данные, которые уже существуют в базе данных. Старая таблица переименовывается, новая таблица определяется, и данные копируются из старой таблицы в новую таблицу с помощью команды INSERT INTO...SELECT. Эту форму также можно использовать для заполнения временных таблиц или копирования данных из одной присоединенной базы данных в другую.

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

Все версии команды INSERT также поддерживают необязательное условие разрешения конфликтов. Это условие конфликта определяет, что следует делать, если результаты INSERT нарушат ограничение базы данных. Наиболее распространенным примером является INSERT OR REPLACE, который вступает в игру, когда INSERT при выполнении вызывает нарушение ограничения UNIQUE. Если присутствует разрешение конфликта REPLACE, любая существующая строка, которая может вызвать нарушение ограничения UNIQUE, сначала удаляется, а затем разрешается продолжить INSERT. Этот конкретный шаблон использования настолько распространен, что целую фразу INSERT OR REPLACE можно заменить просто REPLACE. Например, REPLACE INTO table_name....

См. INSERT и UPDATE в приложении C для получения дополнительной информации о деталях разрешения конфликтов.

UPDATE

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

UPDATE table_name SET column_name=new_value [, ...] WHERE expression

Для команды требуется имя таблицы, за которым следует список пар имя/значение столбца, которые должны быть назначены. Какие строки обновляются, определяется условным выражением, которое проверяется для каждой строки таблицы. Наиболее распространенный шаблон использования использует выражение для проверки равенства в некотором уникальном столбце, таком как столбец PRIMARY KEY.

Если условие WHERE не задано, команда UPDATE попытается обновить указанные столбцы в каждой строке таблицы.

Не считается ошибкой, если выражение WHERE оценивается как ложное для каждой строки в таблице, что не приводит к фактическим обновлениям.

Вот более конкретный пример:

-- Update the price and stock of part_id 454:
UPDATE parts SET price = 4.25, stock = 75 WHERE part_id = 454;

В этом примере предполагается, что части таблицы содержат как минимум три столбца: price, stock и part_id. База данных найдет каждую строку с part_id равным 454. В этом случае можно предположить, что part_id является столбцом PRIMARY KEY, поэтому будет обновлена только одна строка. Столбцам цены и запасов в этой строке будут присвоены новые значения.

Полный синтаксис UPDATE можно найти в приложении C.

DELETE

Как вы могли догадаться, команда DELETE используется для удаления одной или нескольких строк из одной таблицы. Строки полностью удаляются из таблицы:

DELETE FROM table_name WHERE expression;

Команде требуется только имя таблицы и условное выражение для выделения строк. Выражение WHERE используется для выбора конкретных строк для удаления, точно так же, как оно используется в команде UPDATE.

Если условие WHERE не задано, команда DELETE попытается удалить каждую строку таблицы.

Как и в случае с UPDATE, не считается ошибкой, если выражение WHERE принимает значение false для каждой строки в таблице, что не приводит к фактическому удалению.

Некоторые конкретные примеры:

-- Delete the row with rowid 385:
DELETE FROM parts WHERE part_id = 385;

-- Delete all rows with a rowid greater than or equal to 43
-- and less than or equal to 246:
DELETE FROM parts WHERE part_id >= 43 AND part_id <= 246;

В этих примерах предполагается, что у нас есть таблица с именем parts, которая содержит по крайней мере один уникальный столбец с именем part_id.

Как уже отмечалось, если предложение WHERE не задано, команда DELETE попытается удалить каждую строку в таблице. SQLite оптимизирует этот конкретный случай, усекая всю таблицу, а не обрабатывая каждую отдельную строку. Усечение таблицы происходит намного быстрее, чем удаление каждой отдельной строки, но усечение обходит обработку отдельной строки. Если вы хотите обрабатывать каждую строку по мере ее удаления, укажите предложение WHERE, которое всегда имеет значение true:

DELETE FROM parts WHERE 1; -- delete all rows, force per-row processing

Наличие предложения WHERE предотвратит усечение, позволяя обрабатывать каждую строку по очереди.

Команда запроса

Последняя команда DML, которую следует рассмотреть, - это команда SELECT. SELECT используется для извлечения или возврата значений из базы данных. Практически каждый раз, когда вы хотите извлечь или вернуть какое-либо значение, вам нужно будет использовать команду SELECT. Обычно возвращаемые значения являются производными от содержимого базы данных, но SELECT также может использоваться для возврата значения простых выражений. Это отличный способ проверить выражения, например:

sqlite> SELECT 1+1, 5*32, 'abc'||'def', 1>2;
1+1         5*32        'abc' || 'def'  1>2
----------  ----------  --------------  ----------
2           160         abcdef          0

SELECT - это команда только для чтения и не будет изменять базу данных (если SELECT не встроен в другую команду, например CREATE TABLE...AS SELECT или INSERT INTO...SELECT).

Без сомнения, SELECT - самая сложная команда SQL как с точки зрения синтаксиса, так и с точки зрения функций. Синтаксис SELECT пытается представить общую структуру, способную выражать множество различных типов запросов. Хотя это в некоторой степени успешно, в некоторых областях SELECT отказался от простоты в пользу большей гибкости. В результате SELECT имеет большое количество необязательных предложений, каждое со своим собственным набором параметров и форматов.

Понимание того, как смешивать и сопоставлять эти необязательные предложения для получения желаемого результата, может занять некоторое время. Хотя самый простой синтаксис может быть показан с хорошим набором примеров, чтобы действительно осмыслить SELECT, лучше всего понять, как он на самом деле работает и чего пытается достичь.

Поскольку SELECT может быть очень сложным и поскольку SELECT - чрезвычайно важная команда, мы потратим всю следующую главу, очень внимательно изучая SELECT и каждое из его предложений. Будет некоторое обсуждение того, что происходит за кулисами, чтобы лучше понять, как читать и писать сложные запросы.

А пока мы просто дадим вам попробовать. Это должно предоставить достаточно информации, чтобы поэкспериментировать с другими командами в этой главе. Самая простая форма SELECT:

SELECT output_list FROM input_table WHERE row_filter;

Список вывода - это список выражений, которые должны быть вычислены и возвращены для каждой результирующей строки. Чаще всего это просто список столбцов. Список вывода может также включать подстановочный знак (*), указывающий, что должны быть возвращены все известные столбцы.

Предложение FROM определяет источник данных таблицы. В следующей главе будет показано, как таблицы могут быть связаны и объединены, но пока мы будем выполнять запросы по одной таблице за раз.

Предложение WHERE - это условное выражение фильтрации, которое применяется к каждой строке. По сути, это то же самое, что предложение WHERE в командах UPDATE и DELETE. Те строки, которые оцениваются как истинные, будут частью результата, а другие строки будут отфильтрованы.

Рассмотрим эту таблицу:

sqlite> CREATE TABLE tbl ( a, b, c, id INTEGER PRIMARY KEY );
sqlite> INSERT INTO tbl ( a, b, c ) VALUES ( 10, 10, 10 );
sqlite> INSERT INTO tbl ( a, b, c ) VALUES ( 11, 15, 20 );
sqlite> INSERT INTO tbl ( a, b, c ) VALUES ( 12, 20, 30 );

Мы можем вернуть всю таблицу следующим образом:

sqlite> SELECT * FROM tbl;
a           b           c           id
----------  ----------  ----------  ----------
10          11          12          1
10          15          20          2
10          20          30          3

Мы также можем просто вернуть определенные столбцы:

sqlite> SELECT a, c FROM tbl;
a           c
----------  ----------
10          20
10          12
11          30

Или конкретные строки:

sqlite> SELECT * FROM tbl WHERE id = 2;
a           b           c           id
----------  ----------  ----------  ----------
11          15          20          2

Для получения более подробной информации см. главу 5 и приложение C.

Язык управления транзакциями

Язык управления транзакциями используется вместе с языком управления данными для управления обработкой и раскрытием изменений. Транзакции являются фундаментальной частью того, как реляционные базы данных защищают целостность и надежность данных, которые они хранят. Транзакции автоматически используются во всех командах DDL и DML.

ACID транзакции

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

Классический пример транзакции - это база данных, в которой хранятся номера счетов и остатки. Если вы хотите перенести баланс с одной учетной записи на другую, это простой двухэтапный процесс: вычтите сумму из баланса одной учетной записи, а затем добавьте ту же сумму к балансу другой учетной записи. Этот процесс должен выполняться как единая логическая единица изменения, и его нельзя разбивать на части. Оба шага должны быть либо полностью успешными, что приведет к правильному переводу баланса, либо оба шага должны полностью завершиться неудачно, в результате чего обе учетные записи останутся неизменными. Любой другой результат, при котором один шаг успешен, а другой - нет, неприемлем.

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

Стандарт надежных и устойчивых транзакций - это тест ACID. ACID означает Atomic, Consistent, Isolated и Durable. Любая система транзакций, которую стоит использовать, должна обладать этими качествами.

Atomic
Транзакция должна быть атомарной в том смысле, что изменение нельзя разбить на более мелкие части. Когда транзакция фиксируется в базе данных, должна быть применена вся транзакция или вся транзакция не должна применяться. Применение только части транзакции должно быть невозможным.
Consistent
Транзакция также должна поддерживать согласованность базы данных. Типичная база данных имеет ряд правил и ограничений, которые помогают гарантировать, что хранимые данные верны и соответствуют структуре базы данных. Предполагая, что база данных запускается в согласованном состоянии, применение транзакции должно поддерживать согласованность базы данных. Это важно, потому что база данных может (и часто это неизбежно) стать несогласованной, пока транзакция открыта. Например, при переводе средств между вычитанием из одной учетной записи и добавлением на другую учетную запись существует момент, когда общая сумма средств, представленная в базе данных, изменяется и может стать несовместимой с записанной суммой. Это приемлемо, если транзакция в целом согласована на момент ее фиксации.
Isolated
Открытая транзакция также должна быть изолирована от других клиентов. Когда клиент открывает транзакцию и начинает выдавать отдельные команды изменения, результаты этих команд видны клиенту. Однако эти изменения не должны быть видны какой-либо другой системе, имеющей доступ к базе данных, и не должны быть интегрированы в постоянную запись базы данных до тех пор, пока транзакция не будет зафиксирована полностью. И наоборот, изменения, совершенные другими клиентами после начала транзакции, не должны быть видны этой транзакции. Изоляция необходима для того, чтобы транзакции были атомарными и последовательными. Если бы другие клиенты могли видеть полупримененные транзакции, транзакции не могли бы претендовать на атомарность по своей природе и не сохраняли бы целостность базы данных, как это видят другие клиенты.
Durable
Наконец, транзакция должна быть надежной. Если транзакция успешно зафиксирована, она должна стать постоянной и необратимой частью записи базы данных. После того, как будет возвращен статус успеха транзакции, не должно иметь значения, будет ли процесс остановлен, система потеряет питание или файловая система базы данных исчезнет - после перезапуска зафиксированные изменения должны присутствовать в базе данных. И наоборот, если система отключается до фиксации транзакции, то после перезапуска изменений, внесенных в транзакцию, не должно быть.

Большинство людей думают, что атомарность транзакций является их наиболее важным качеством, но все четыре аспекта должны работать вместе, чтобы обеспечить общую целостность базы данных. Особенно часто упускается из виду долговечность. SQLite очень старается гарантировать, что, если транзакция будет успешно зафиксирована, эти изменения будут физически записаны в постоянное хранилище и останутся там. Сравните это с традиционными операциями файловой системы, когда записи могут попадать в файловый кеш операционной системы. Обновления могут находиться в кэше от нескольких секунд до нескольких минут, прежде чем, наконец, будут помещены в хранилище. Даже в этом случае данные могут ждать в буферах устройства, прежде чем, наконец, будут сохранены в физическом хранилище. Хотя этот тип буферизации может повысить эффективность, это означает, что обычное приложение действительно понятия не имеет, когда его данные надежно сохранятся в постоянном хранилище.

Сбои в подаче питания и исчезновение файловых систем могут показаться редкостью, но на самом деле не в этом суть. Базы данных разработаны с учетом абсолютных требований, особенно когда речь идет о надежности. Кроме того, исчезновение файловой системы - не такая уж радикальная идея, если учесть распространенность флэш- и USB-накопителей. Исчезновение носителей и сбои питания становятся еще более обычным явлением, если учесть количество баз данных SQLite, которые можно найти на портативных устройствах с батарейным питанием, таких как мобильные телефоны и медиаплееры. Использование транзакций еще более важно на таких устройствах, поскольку практически невозможно запустить инструменты восстановления данных в такой среде. Эти типы устройств должны быть чрезвычайно надежными и независимо от того, что делает пользователь (включая вытаскивание флеш-накопителей в неудобное время), система должна оставаться последовательной и надежной. Использование транзакционной системы может обеспечить такую надежность.

Транзакции предназначены не только для записи данных. Открытие транзакции для расширенной операции только для чтения иногда полезно, если вам нужно собрать данные с помощью нескольких запросов. Открытие транзакции обеспечивает согласованность вашего представления о базе данных, гарантируя, что данные не изменяются между запросами. Это полезно, если, например, вы используете один запрос для сбора группы идентификаторов записей, а затем выполняете серию запросов для каждого значения идентификатора. Заключение всех запросов в транзакцию гарантирует, что все запросы будут видеть один и тот же набор данных.

SQL-транзакции

Обычно SQLite находится в режиме автоматической фиксации (autocommit). Это означает, что SQLite автоматически запускает транзакцию для каждой команды, обрабатывает команду и (при условии, что ошибок не было) автоматически фиксирует транзакцию. Этот процесс прозрачен для пользователя, но важно понимать, что даже индивидуально введенные команды обрабатываются в рамках транзакции, даже если никакие команды TCL не используются.

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

BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] [TRANSACTION]

Необязательные ключевые слова DEFERRED, IMMEDIATE и EXCLUSIVE специфичны для SQLite и управляют способом получения требуемых блокировок чтения/записи. Если к базе данных одновременно обращается только один клиент, режим блокировки в значительной степени не имеет значения. Когда к базе данных может обращаться более одного клиента, режим блокировки определяет, как сбалансировать одноранговый доступ с гарантированным успехом транзакции.

По умолчанию все транзакции (включая транзакции с автоматической фиксацией) используют режим DEFERRED. В этом режиме блокировки базы данных не выполняются, пока они не потребуются. Это наиболее «добрососедский» режим, который позволяет другим клиентам продолжать доступ к базе данных и использовать ее до тех пор, пока у транзакции не останется другого выбора, кроме как заблокировать их. Это позволяет другим клиентам продолжать использовать базу данных, но если блокировки недоступны, когда они требуются транзакции, транзакция завершится ошибкой и, возможно, потребуется откат и перезапуск.

Режим BEGIN IMMEDIATE пытается немедленно получить зарезервированную блокировку. В случае успеха он гарантирует, что блокировки записи будут доступны для транзакции, когда они необходимы, но по-прежнему позволяет другим клиентам продолжать доступ к базе данных для операций только для чтения. Режим EXCLUSIVE пытается заблокировать всех других клиентов, включая клиентов только для чтения. Хотя режимы IMMEDIATE и EXCLUSIVE являются более ограничительными для других клиентов, их преимущество состоит в том, что они выйдут из строя немедленно, если требуемые блокировки недоступны, а не после того, как вы введете свои команды DDL или DML.

После открытия транзакции вы можете продолжать выполнять другие команды SQL, включая команды DML и DDL. Вы можете думать об изменениях, вызванных этими командами, как о «предлагаемых» изменениях. Изменения видны только локальному клиенту и не были полностью и постоянно применены к базе данных. Если клиентский процесс прерывается или сервер теряет питание в середине открытой транзакции, транзакция и любые предлагаемые изменения будут потеряны, но остальная часть базы данных останется нетронутой и согласованной. Только после закрытия транзакции предлагаемые изменения фиксируются в базе данных и становятся «реальными». Команда COMMIT используется для закрытия транзакции и фиксации изменений в базе данных. Вы также можете использовать псевдоним END. Как и в случае с BEGIN, ключевое слово TRANSACTION необязательно.

COMMIT [TRANSACTION]
END [TRANSACTION]

После успешного возврата COMMIT все предложенные изменения полностью фиксируются в базе данных и становятся видимыми для других клиентов. В этот момент, если система потеряет питание или клиентский процесс будет остановлен, изменения сохранятся в базе данных.

Однако не всегда все идет хорошо. Вместо того, чтобы фиксировать предлагаемые изменения, транзакцию можно вручную откатить, эффективно отменив транзакцию и все изменения, которые она содержит. Откат набора предложенных изменений полезен при обнаружении ошибки. Это может быть ошибка базы данных, например, нехватка дискового пространства на полпути при вставке ряда связанных записей, или логическая ошибка приложения, например попытка назначить счет для заказа, которого не существует. В таких случаях обычно не имеет смысла продолжать транзакцию или оставлять несогласованные данные в базе данных. Практически единственный выбор - отступить и попробовать еще раз.

Чтобы отменить транзакцию и откатить все предложенные изменения, вы можете использовать команду ROLLBACK. Опять же, ключевое слово TRANSACTION необязательно:

ROLLBACK [TRANSACTION]

ROLLBACK отменит и вернет назад все предложенные изменения, внесенные в текущую транзакцию, а затем закроет транзакцию. Это не обязательно возвращает базу данных в ее предыдущее состояние, поскольку другие клиенты могли вносить изменения параллельно. ROLLBACK отменяет только предложенные изменения, сделанные этим клиентом в рамках текущей транзакции.

И COMMIT, и ROLLBACK завершат текущую транзакцию, вернув SQLite в режим автоматической фиксации.

Точки сохранения

Помимо транзакций, совместимых с ACID, SQLite также поддерживает точки сохранения (save-points). Точки сохранения позволяют отмечать определенные точки в транзакции. Затем вы можете принять или откатиться к отдельным точкам сохранения без необходимости фиксировать или откатывать всю транзакцию. В отличие от транзакций, вы можете иметь более одной активной точки сохранения одновременно. Точки сохранения иногда называют вложенными транзакциями (nested transactions).

Точки сохранения обычно используются в сочетании с крупными многоэтапными транзакциями, где для некоторых шагов или подпроцедур требуется возможность отката. Точки сохранения позволяют транзакции продолжаться и (при необходимости) откатывать на один шаг за раз. Они также позволяют приложению исследовать разные возможности, пытаясь выполнить одну процедуру, а если это не сработает, попробовать другую, без необходимости откатывать всю транзакцию, чтобы начать заново. В некотором смысле точки сохранения можно рассматривать как маркеры «отмены» в потоке команд SQL.

Вы можете создать точку сохранения с помощью команды SAVEPOINT. Поскольку можно определить несколько точек сохранения, вы должны указать имя для идентификации точки сохранения:

SAVEPOINT savepoint_name

Точки сохранения действуют как стек. Каждый раз, когда вы создаете новый, он помещается в верхнюю часть стека. Идентификаторы точки сохранения не обязательно должны быть уникальными. Если один и тот же идентификатор точки сохранения используется более одного раза, используется ближайший к вершине стека.

Чтобы освободить точку сохранения и принять все предложенные изменения, сделанные с момента установки точки сохранения, используйте команду RELEASE:

RELEASE [SAVEPOINT] savepoint_name

Команда RELEASE не фиксирует никаких изменений на диске. Скорее, она сглаживает все изменения в стеке точек сохранения в слой ниже названной точки сохранения. Затем точка сохранения удаляется. Любые точки сохранения, содержащиеся в названной точке сохранения, автоматически освобождаются.

Чтобы отменить набор команд и отменить все обратно на место, где была установлена точка сохранения, используйте команду ROLLBACK TO:

ROLLBACK [TRANSACTION] TO [SAVEPOINT] savepoint_name

В отличие от транзакции ROLLBACK, ROLLBACK TO точки сохранения не закрывает и не удаляет точку сохранения. ROLLBACK TO выполняет откат и отменяет любые изменения, внесенные с момента создания точки сохранения, но оставляет состояние транзакции точно таким, каким оно было после выполнения команды SAVE POINT.

Рассмотрим следующую серию операторов SQL. Отступ используется для отображения стека точек сохранения:

CREATE TABLE t (i);
BEGIN;
  INSERT INTO t (i) VALUES 1;
  SAVEPOINT aaa;
    INSERT INTO t (i) VALUES 2;
    SAVEPOINT bbb;
      INSERT INTO t (i) VALUES 3;

На этом этапе, если выдается команда ROLLBACK TO bbb, состояние базы данных будет таким, как если бы были введены следующие команды:

CREATE TABLE t (i);
BEGIN;
  INSERT INTO t (i) VALUES 1;
  SAVEPOINT aaa;
    INSERT INTO t (i) VALUES 2;
    SAVEPOINT bbb;

Опять же, обратите внимание, что откат к точке сохранения bbb по-прежнему оставляет точку сохранения на месте. Любые новые команды будут связаны с SAVEPOINT bbb. Например:

CREATE TABLE t (i);
BEGIN;
  INSERT INTO t (i) VALUES 1;
  SAVEPOINT aaa;
    INSERT INTO t (i) VALUES 2;
    SAVEPOINT bbb;
      DELETE FROM t WHERE i=1;

Продолжая, если бы была введена команда RELEASE aaa, мы получили бы эквивалент:

CREATE TABLE t (i);
BEGIN;
  INSERT INTO t (i) VALUES 1;
  INSERT INTO t (i) VALUES 2;
  DELETE FROM t WHERE i=1;

В этом случае предложенные изменения как в точках сохранения aaa, так и в прилагаемых точках сохранения bbb были выпущены и объединены. Однако транзакция все еще открыта, и для того, чтобы сделать предлагаемые изменения постоянными, по-прежнему потребуется COMMIT.

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

Если команда SAVEPOINT выполняется, когда SQLite находится в режиме автоматической фиксации, то есть вне транзакции, то будет запущена стандартная автоматическая фиксация BEGIN DEFERRED TRANSACTION. Однако, в отличие от большинства команд, транзакция автоматической фиксации не будет автоматически фиксироваться после возврата команды SAVEPOINT, оставляя систему внутри открытой транзакции. Автоматическая транзакция будет оставаться активной до тех пор, пока не будет освобождена исходная точка сохранения или пока внешняя транзакция не будет явно зафиксирована или откатится. Это единственная ситуация, когда RELEASE точки сохранения будет иметь прямое влияние на включающую транзакцию. Как и в случае с другими точками сохранения, при откате точки сохранения с автоматической фиксацией транзакция останется открытой, а исходная точка сохранения будет открытой, но пустой.

Системные каталоги

Многие системы реляционных баз данных, включая SQLite, хранят данные о состоянии системы в серии структур данных, известных как системные каталоги (system catalogs). Все системные каталоги SQLite начинаются с префикса sqlite_. Хотя многие из этих каталогов содержат внутренние данные, их можно запросить с помощью SELECT, как если бы они были стандартными таблицами. Большинство системных каталогов доступны только для чтения. Если вы столкнулись с неизвестной базой данных и не знаете, что в ней находится, изучение системных каталогов - хорошее место для начала.

Все невременные базы данных SQLite имеют каталог sqlite_master. Это основная запись всех объектов базы данных. Если в какой-либо из таблиц есть заполненный столбец AUTOINCREMENT, в базе данных также будет каталог sqlite_sequence. Этот каталог используется для отслеживания следующего допустимого значения последовательности (дополнительную информацию об AUTOINCREMENT см. в разделе «Первичные ключи»). Если использовалась команда SQL ANALYZE, она также сгенерирует одну или несколько таблиц sqlite_stat#, например sqlite_stat1 и sqlite_stat2. Эти таблицы содержат различную статистику о значениях и распределениях в различных индексах и используются, чтобы помочь оптимизатору запросов выбрать более эффективное решение для запросов. Для получения дополнительной информации см. ANALYZE в приложении C.

Самым важным из этих системных каталогов является таблица sqlite_master. Этот каталог содержит информацию обо всех объектах в базе данных, включая SQL, используемый для их определения. Таблица sqlite_master состоит из пяти столбцов:

Имя столбца Тип столбца Значение
type Text Тип объекта базы данных
name Text Идентификационное имя объекта
tbl_name Text Имя связанной таблицы
rootpage Integer Только для внутреннего использования
sql Text SQL, используемый для определения объекта

Столбец type может быть table (включая виртуальные таблицы), index, view или trigger. Столбец name дает имя самого объекта, а столбец tbl_name дает имя таблицы или представления, с которым связан объект. Для таблиц и представлений tbl_name - это просто копия столбца name. Последний столбец sql содержит полную копию исходной команды SQL, использованной для определения объекта, такой как команда CREATE TABLE или CREATE TRIGGER.

Для временных баз данных нет системного каталога sqlite_master. Вместо этого у них есть таблица sqlite_temp_master.

Подведение итогов

Это длинная глава, содержащая огромное количество информации. Даже если вы знакомы с рядом традиционных языков программирования, декларативная природа SQL часто требует времени, чтобы осмыслить. Один из лучших способов изучить SQL - просто поэкспериментировать. SQLite позволяет легко открыть тестовую базу данных, создать несколько таблиц и попробовать что-то. Если у вас возникли проблемы с пониманием деталей команды, обязательно поищите ее в приложении C. В дополнение к более подробным описаниям, приложение C содержит подробные синтаксические диаграммы.

Если вы хотите глубже изучить SQL, есть буквально сотни книг на выбор. Один только О'Рейли издает около дюжины заголовков только на языке SQL. Хотя есть некоторые различия между SQL, поддерживаемым SQLite, и другими основными системами баз данных, SQLite довольно точно следует стандарту. В большинстве случаев SQLite отклоняется от стандарта, он делает это в попытке поддержать общие нотации или использование в других популярных продуктах баз данных. Если вы работаете над оберткой, вы обращаетесь к некоторым концепциям более высокого уровня или базовым структурам запросов, вероятно, вам поможет учебник или книга, написанные практически для любого продукта базы данных. Может потребоваться небольшая настройка, чтобы запросы выполнялись под SQLite, но изменения обычно минимальны.

Популярные книги О'Рейли, посвященные языку SQL, включают Learning SQL (Beaulieu), SQL in a Nutshell (Kline, Kline, Hunt) и SQL Cookbook (Molinaro). Более подробные обсуждения можно найти в The Art of SQL (Faroult, Robson). Популярные справочники также включают SQL For Smarties (Celko, Morgan Kaufmann) и Introduction to SQL (van der Lans, Addison Wesley). Эти две большие, но очень полные. Существует также The SQL Guide to SQLite (van der Lans, lulu.com), в котором гораздо глубже рассматривается диалект SQL, специально используемый SQLite.

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

Прежде чем пытаться слишком много, обязательно прочтите следующую главу. Следующая глава посвящена команде SELECT. Помимо синтаксиса команды, он немного глубже погружается в то, что происходит за кулисами. Эти базовые знания должны значительно упростить разбиение и понимание сложных запросов. Это также должно упростить их написание.

Глава 5
Команда SELECT

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

С точки зрения синтаксиса и функциональности SELECT - самая сложная команда SQL. В большинстве приложений это также одна из наиболее часто используемых команд. Если вы хотите получить максимальную отдачу от своей базы данных (и ее дизайна), вам потребуется твердое понимание того, как правильно использовать SELECT.

Обычно возвращаемые значения являются производными от содержимого базы данных, но SELECT также может использоваться для возврата значения простых выражений. SELECT - это команда только для чтения и не будет изменять базу данных (если только SELECT не встроен в другую команду, например INSERT INTO...SELECT).

Таблицы SQL

Основная структура данных SQL - это таблица. Таблицы используются как для хранения, так и для обработки данных. Мы видели, как определять таблицы с помощью команды CREATE TABLE, но давайте рассмотрим некоторые детали.

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

Тело таблицы содержит все строки. Каждая строка состоит из одного элемента данных для каждого столбца. Все строки в таблице должны иметь одинаковое количество элементов данных, по одному для каждого столбца. Каждый элемент может содержать ровно одно значение данных (или NULL).

Таблицы SQL могут содержать повторяющиеся строки. Таблицы могут содержать несколько строк, где каждый определяемый пользователем столбец имеет эквивалентное соответствующее значение. На практике повторяющиеся строки обычно нежелательны, но разрешены.

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

Строки таблицы SQL не имеют определенного порядка.

Очень распространенная ошибка - предполагать, что данный запрос всегда будет возвращать строки в одном и том же порядке. Если вы специально не попросили запрос отсортировать возвращенные строки в определенном порядке, нет гарантии, что строки будут продолжать возвращаться в том же порядке. Не позволяйте коду вашего приложения зависеть от естественного порядка неупорядоченного запроса. Другая версия SQLite может по-разному оптимизировать запрос, что приведет к другому порядку строк. Даже такая простая вещь, как добавление или удаление индекса, может изменить порядок строк в несортированном результате.

Чтобы убедиться, что ваш код не делает никаких предположений о порядке строк, вы можете включить PRAGMA reverse_unordered_selects. Это заставит SQLite изменить естественный порядок строк любого оператора SELECT, который не имеет явного порядка (предложение ORDER BY). См. reverse_unordered_selects в приложении F для более подробной информации.

Канал SELECT

Синтаксис SELECT пытается представить общую структуру, способную выражать множество различных типов запросов. Для этого в SELECT есть большое количество необязательных предложений, каждое со своим собственным набором параметров и форматов.

Самый общий формат автономного оператора SQLite SELECT выглядит следующим образом:

SELECT [DISTINCT] select_heading
    FROM source_tables
    WHERE filter_expression
    GROUP BY grouping_expressions
        HAVING filter_expression
    ORDER BY ordering_expressions
    LIMIT count
        OFFSET count

Каждая команда SELECT должна иметь заголовок выбора, который определяет возвращаемые значения. Каждая дополнительная строка (FROM, WHERE, GROUP BY и т. д.) Представляет собой необязательное предложение.

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

Фактически, ядро базы данных использует несколько сокращений и выполняет множество оптимизаций при обработке запроса, но конечный результат всегда должен соответствовать тому, что вы получили бы от независимого прохождения каждого шага, по одному.

Пункты в операторе SELECT не оцениваются в том же порядке, в котором они написаны. Скорее порядок их оценки выглядит примерно так:

  1. FROM source_tables
    Обозначает одну или несколько исходных таблиц и объединяет их в одну большую рабочую таблицу.

  2. WHERE filter_expression
    Отфильтровывает определенные строки из рабочей таблицы.

  3. GROUP BY grouping_expressions
    Группирует наборы строк в рабочей таблице на основе аналогичных значений.

  4. SELECT select_heading
    Определяет столбцы набора результатов и (если применимо) группирующие агрегаты.

  5. HAVING filter_expression
    Отфильтровывает определенные строки из сгруппированной таблицы. Требуется GROUP BY.

  6. DISTINCT
    Удаляет повторяющиеся строки.

  7. ORDER BY ordering_expressions
    Сортирует строки набора результатов.

  8. OFFSET count
    Пропускает строки в начале набора результатов. Требуется LIMIT.

  9. LIMIT count
    Ограничивает вывод набора результатов определенным количеством строк.

Независимо от того, насколько большим или сложным может быть оператор SELECT, он обязательно следует этому основному шаблону. Чтобы понять, как работает любой запрос, разбейте его и рассмотрите каждый отдельный шаг. Убедитесь, что вы понимаете, как выглядит рабочая таблица перед каждым шагом, как этот шаг манипулирует и изменяет таблицу и как выглядит рабочая таблица, когда она передается на следующий шаг.

Предложение FROM

Предложение FROM берет одну или несколько исходных таблиц из базы данных и объединяет их в одну большую таблицу. Исходные таблицы обычно называются таблицами из базы данных, но они также могут быть представлениями или подзапросами (см. «Подзапросы» для получения дополнительной информации о подзапросах).

Таблицы объединяются с помощью оператора JOIN. Каждый JOIN объединяет две таблицы в большую таблицу. Три или более таблиц можно объединить, связав вместе несколько операторов JOIN. Операторы JOIN оцениваются слева направо, но существует несколько различных типов объединений, и не все из них коммутативны или ассоциативны. Это делает упорядочение и группировку очень важными. При необходимости можно использовать круглые скобки для правильной группировки объединений.

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

SQL определяет три основных типа объединений: CROSS JOIN, INNER JOIN и OUTER JOIN.

CROSS JOIN

CROSS JOIN сопоставляет каждую строку первой таблицы с каждой строкой второй таблицы. Если во входных таблицах есть столбцы x и y, соответственно, итоговая таблица будет иметь столбцы x+y. Если входные таблицы содержат n и m строк, соответственно, итоговая таблица будет иметь n·m строк. В математике CROSS JOIN известен как декартово произведение.

Синтаксис CROSS JOIN довольно прост:

SELECT ... FROM t1 CROSS JOIN t2 ...

На рис. 5-1 показано, как рассчитывается CROSS JOIN.

Поскольку CROSS JOIN может создавать очень большие таблицы, следует проявлять осторожность и использовать их только тогда, когда это необходимо.

Рисунок 5-1. В CROSS JOIN каждая строка из первой таблицы сопоставляется с каждой строкой во второй таблице.

INNER JOIN

INNER JOIN очень похож на CROSS JOIN, но имеет встроенное условие, которое используется для ограничения количества строк в результирующей таблице. Условное выражение обычно используется для объединения или сопоставления строк из двух исходных таблиц. INNER JOIN без какого-либо типа условного выражения (или выражения, которое всегда принимает истинное значение) приведет к CROSS JOIN. Если во входных таблицах есть столбцы x и y, соответственно, в итоговой таблице будет столбцов не более, чем x+y (в некоторых случаях их может быть меньше). Если входные таблицы имеют n и m строк, соответственно, итоговая таблица может иметь от нуля до n·m строк, в зависимости от условия. INNER JOIN - это наиболее распространенный тип соединения и тип соединения по умолчанию. Это делает ключевое слово INNER необязательным.

Есть три основных способа указать условное выражение. Первый - с выражением ON. Это дает простое выражение, которое оценивается для каждой потенциальной строки. Фактически присоединяются только те строки, которые имеют значение true. A JOIN...ON выглядит так:

SELECT ... FROM t1 JOIN t2 ON conditional_expression ...

Пример этого показан на рис. 5-2.

Рисунок 5-2. В INNER JOIN строки сопоставляются на основе условия.

Если во входных таблицах есть столбцы C и D, соответственно, JOIN...ON всегда будет приводить к столбцам C+D.

Условное выражение можно использовать для проверки чего угодно, но наиболее распространенный тип выражений проверяет равенство одинаковых столбцов в обеих таблицах. Например, в базе данных бизнес-сотрудников, вероятно, будет таблица employee, которая содержит (среди прочего) столбец name и столбец eid (ID сотрудника). Любая другая таблица, которая должна связать строки с конкретным сотрудником, также будет иметь столбец eid, который действует как указатель или ссылка на правильного сотрудника. Эта связь делает очень распространенными запросы с выражениями ON, подобными:

SELECT ... FROM employee JOIN resource ON employee.eid = resource.eid ...

Этот запрос приведет к созданию выходной таблицы, в которой строки из таблицы resource правильно сопоставлены с соответствующими строками в таблице employee.

У этого JOIN есть две проблемы. Во-первых, условие ON - это слишком много для того, чтобы напечатать что-то настолько распространенное. Во-вторых, результирующая таблица будет иметь два столбца eid, но для любой данной строки значения этих двух столбцов всегда будут идентичны. Чтобы избежать избыточности и сделать формулировку короче, условия внутреннего соединения могут быть объявлены с помощью выражения USING. Это выражение определяет список из одного или нескольких столбцов:

SELECT ... FROM t1 JOIN t2 USING ( col1 ,... ) ...

Запросы из базы данных сотрудников теперь будут выглядеть примерно так:

SELECT ... FROM employee JOIN resource USING ( eid ) ...

Для появления в условии USING имя столбца должно существовать в обеих таблицах. Для каждого перечисленного имени столбца условие USING проверяет равенство между парами столбцов. В итоговой таблице будет только по одному экземпляру каждого столбца из списка.

Если этого недостаточно, SQL предоставляет дополнительное сокращение. NATURAL JOIN похож на JOIN...USING, только он автоматически проверяет равенство между значениями каждого столбца, который существует в обеих таблицах:

SELECT ... FROM t1 NATURAL JOIN t2 ...

Если во входных таблицах есть столбцы x и y, соответственно, JOIN...USING или NATURAL JOIN приведет к любому столбцу от max(x,y) до x+y.

Предполагая, что eid является единственным идентификатором столбца, который появляется как в таблице employee, так и в таблице resource, наш бизнес-запрос становится чрезвычайно простым:

SELECT ... FROM employee NATURAL JOIN resource ...

Выражения NATURAL JOIN удобны, поскольку они очень лаконичные и позволяют изменять ключевую структуру различных таблиц без необходимости обновлять все соответствующие запросы. Они также могут быть немного опасными, если вы не будете соблюдать дисциплину при именовании столбцов. Поскольку ни один из столбцов не назван явно, проверка корректности соединения не выполняется. Например, если совпадающие столбцы не найдены, JOIN автоматически (и без предупреждения) перейдет в CROSS JOIN, как и любой другой INNER JOIN. Точно так же, если два столбца случайно получат одно и то же имя, NATURAL JOIN автоматически включит их в условие соединения, хотите вы этого или нет.

OUTER JOIN

OUTER JOIN является расширением INNER JOIN. Стандарт SQL определяет три типа выражений OUTER JOIN: LEFT, RIGHT и FULL. В настоящее время SQLite поддерживает только LEFT OUTER JOIN.

OUTER JOIN имеют условное выражение, идентичное INNER JOIN, выраженное с помощью ключевого слова ON, USING или NATURAL. Таблица исходных результатов рассчитывается аналогично. После вычисления первичного JOIN соединение OUTER возьмет все несвязанные строки из одной или обеих таблиц, дополнит их значениями NULL и добавит их в результирующую таблицу. В случае LEFT OUTER JOIN это делается с любыми несоответствующими строками из первой таблицы (таблица, которая появляется слева от слова JOIN).

На рис. 5-3 показан пример LEFT OUTER JOIN.

Рисунок 5-3. OUTER JOIN похож на INNER JOIN, но в таблицу результатов включаются только несовпадающие строки. Это показывает LEFT OUTER JOIN, где несопоставленные строки из левой (t1) таблицы добавляются к результатам.

Результат LEFT OUTER JOIN будет содержать по крайней мере один экземпляр каждой строки из левой таблицы. Если во входных таблицах есть столбцы x и y, соответственно, в итоговой таблице будет не более, чем столбцы x+y (точное количество зависит от того, какое условие используется). Если входные таблицы содержат n и m строк, соответственно, итоговая таблица может иметь от n до n·m строк.

Поскольку результат включает несовпадающие строки, OUTER JOIN часто специально используются для поиска неразрешенных или «висящих» строк.

Псевдонимы таблиц

Поскольку оператор JOIN объединяет столбцы разных таблиц в одну большую таблицу, могут быть случаи, когда в результирующей рабочей таблице будет несколько столбцов с одинаковым именем. Чтобы избежать двусмысленности, любая часть оператора SELECT может квалифицировать любую ссылку на столбец с именем исходной таблицы. Однако бывают случаи, когда и этого недостаточно. Например, бывают ситуации, когда вам нужно присоединить таблицу к самой себе, в результате чего рабочая таблица имеет два экземпляра одной и той же исходной таблицы. Это не только делает имя каждого столбца неоднозначным, но и делает невозможным их различение по имени исходной таблицы. Другая проблема связана с подзапросами, поскольку у них нет конкретных имен исходных таблиц.

Чтобы избежать двусмысленности в операторе SELECT, любому экземпляру исходной таблицы, представления или подзапроса может быть назначен псевдоним. Это делается с помощью ключевого слова AS. Например, в случае самосоединения мы можем назначить уникальный псевдоним для каждого экземпляра одной и той же таблицы:

SELECT ... FROM x AS x1 JOIN x AS x2 ON x1.col1 = x2.col2 ...

Или, в случае подзапроса:

SELECT ... FROM ( SELECT ... ) AS sub ...

Технически ключевое слово AS является необязательным, и за каждым именем исходной таблицы может просто следовать псевдоним. Однако это может сбивать с толку, поэтому рекомендуется использовать ключевое слово AS.

Если какой-либо из столбцов подзапроса конфликтует со столбцом из стандартной исходной таблицы, то теперь вы можете использовать классификатор sub в качестве имени таблицы. Например, sub.col1.

После назначения псевдонима таблицы исходное имя исходной таблицы становится недействительным и не может использоваться в качестве квалификатора столбца. Вместо этого вы должны использовать псевдоним.

Предложение WHERE

Предложение WHERE используется для фильтрации строк из рабочей таблицы, созданной предложением FROM. Это очень похоже на предложение WHERE в командах UPDATE и DELETE. Предоставляется выражение, которое оценивается для каждой строки. Любая строка, которая заставляет выражение оценивать значение false или NULL, отбрасывается. В итоговой таблице будет такое же количество столбцов, что и в исходной таблице, но может быть меньше строк. Не считается ошибкой, если предложение WHERE удаляет все строки в рабочей таблице. На рис. 5-4 показано, как работает предложение WHERE.

Рисунок 5-4. Предложение WHERE фильтрует строки на основе выражения фильтра.

Некоторые предложения WHERE могут быть довольно сложными, что приводит к длинной серии операторов AND, используемых для объединения подвыражений. Однако большинство фильтрует определенную строку, ища определенное значение ключа.

Предложение GROUP BY

Предложение GROUP BY используется для свертывания или «сглаживания» групп строк. Группы можно подсчитывать, усреднять или иным образом агрегировать. Если вам нужно выполнить какие-либо межстрочные операции, требующие данных из более чем одной строки, скорее всего, вам понадобится GROUP BY.

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

GROUP BY grouping_expression [COLLATE collation_name] [,...]

Процесс группировки состоит из двух этапов. Во-первых, список выражений GROUP BY используется для организации строк таблицы в разные группы. После определения групп заголовок SELECT (обсуждается в следующем разделе) определяет, как эти группы объединяются в одну строку. В итоговой таблице будет по одной строке для каждой группы.

Чтобы разбить рабочую таблицу на группы, список выражений оценивается в каждой строке таблицы. Все строки, которые производят эквивалентные значения, сгруппированы вместе. С каждым выражением можно указать необязательное сопоставление. Если выражение группировки включает текстовые значения, сопоставление используется для определения эквивалентных значений. Дополнительные сведения о сопоставлениях см. в разделе «Предложение ORDER BY».

На рис. 5-5 показано, как строки группируются вместе с предложением GROUP BY.

Рисунок 5-5. Предложение GROUP BY группирует строки на основе списка выражений группировки.

После группировки каждая коллекция строк сворачивается в одну строку. Обычно это делается с помощью агрегатных функций, определенных в заголовке SELECT, который описан в следующем разделе, на стр. 70.

Поскольку для GROUP BY обычно используются выражения, определенные в заголовке SELECT, можно просто ссылаться на выражения заголовка SELECT в списке выражений GROUP BY. Если выражение GROUP BY задано как буквальное целое число, это число используется как индекс столбца в таблице результатов, определяемой заголовком SELECT. Индексы начинаются с самого левого столбца. Выражение GROUP BY также может ссылаться на псевдоним столбца результатов. Псевдонимы столбцов результатов объяснены в следующем разделе.

Заголовок SELECT

Заголовок SELECT используется для определения формата и содержимого итоговой таблицы результатов. Любой столбец, который вы хотите отобразить в итоговой таблице результатов, должен быть определен выражением в заголовке SELECT. Заголовок SELECT - единственный обязательный шаг в конвейере команд SELECT.

Формат заголовка довольно простой, он состоит из списка выражений. Каждое выражение оценивается в контексте каждой строки, создавая окончательную таблицу результатов. Очень часто выражения являются простыми ссылками на столбцы, но они могут быть любым произвольным выражением, включающим ссылки на столбцы, литеральные значения или функции SQL. Чтобы сгенерировать окончательный результат запроса, список выражений оценивается один раз для каждой строки в рабочей таблице.

Кроме того, вы можете указать имя столбца с помощью ключевого слова AS:

SELECT expression [AS column_name] [,...]

Не путайте ключевое слово AS, используемое в заголовке SELECT, с ключевым словом, используемым в предложении FROM. Заголовок SELECT использует ключевое слово AS для присвоения имени столбца одному из выходных столбцов, в то время как предложения FROM используют ключевое слово AS для назначения псевдонима исходной таблицы.

Указывать имя выходного столбца необязательно, но рекомендуется. Имя столбца, присвоенное таблице результатов, не определяется строго, если пользователь не предоставляет псевдоним столбца AS. Если ваше приложение ищет конкретное имя столбца в результатах запроса, обязательно назначьте известное имя с помощью AS. Присвоение имени столбцу также позволит другим частям оператора SELECT ссылаться на выходной столбец по имени. Шаги в конвейере SELECT, которые обрабатываются перед заголовком SELECT, такие как предложения WHERE и GROUP BY, также могут ссылаться на выходные столбцы по имени, если выражение столбца не содержит агрегатной функции.

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

Хотя заголовок SELECT, похоже, фильтрует столбцы из рабочей таблицы, так же как предложение WHERE фильтрует строки, это не совсем правильно. Все столбцы исходной рабочей таблицы по-прежнему доступны для предложений, которые обрабатываются после заголовка SELECT. Например, можно отсортировать результаты (с помощью ORDER BY, который обрабатывается после заголовка SELECT), используя столбец, который не отображается в выходных данных запроса.

Было бы точнее сказать, что заголовок SELECT помечает определенные столбцы для вывода. Неиспользуемые столбцы удаляются только после обработки всего конвейера SELECT и готовности результатов к возврату. Рис. 5-6 иллюстрирует этот момент.

Рисунок 5-6. Заголовок SELECT помечает определенные столбцы для вывода. Неиспользуемые столбцы не удаляются до фактического возврата результата запроса. Более поздние предложения SELECT (например, ORDER BY) по-прежнему имеют доступ к столбцам, которые не являются частью результата запроса.

В дополнение к стандартным выражениям SELECT поддерживает два подстановочных знака. Простая звездочка (*) приведет к выводу каждого определяемого пользователем столбца из каждой исходной таблицы в предложении FROM. Вы также можете настроить таргетинг на определенную таблицу (или псевдоним таблицы), используя формат table_name.*. Хотя оба этих подстановочных знака могут возвращать более одного столбца, их можно смешивать с другими выражениями в списке выражений. Подстановочные знаки не могут использовать псевдоним столбца, поскольку они часто возвращают более одного столбца.

Имейте в виду, что символы подстановки SELECT не возвращают автоматически сгенерированные столбцы ROWID. Чтобы вернуть как ROWID, так и определяемые пользователем столбцы, просто запросите их оба:

SELECT ROWID, * FROM table;

Подстановочные знаки действительно включают любой определяемый пользователем столбец INTEGER PRIMARY KEY, который заменил стандартный столбец ROWID. См. «Первичные ключи» для получения дополнительной информации о взаимодействии столбцов ROWID и INTEGER PRIMARY KEY.

Помимо определения столбцов результата запроса, заголовок SELECT определяет, как группы строк (созданные предложением GROUP BY) объединяются в одну строку. Это делается с помощью агрегатных функций. Агрегатная функция принимает выражение столбца в качестве входных данных и агрегирует или объединяет все значения столбцов из строк группы и создает одно выходное значение. Общие агрегатные функции включают count(), min(), max() и avg(). Приложение E содержит полный список всех встроенных агрегатных функций.

Любой столбец или выражение, которые не передаются через агрегатную функцию, будет предполагать, какое значение содержится в последней строке группы. Однако, поскольку таблицы SQL неупорядочены и заголовок SELECT обрабатывается до предложения ORDER BY, мы действительно не знаем, какая строка является «последней». Это означает, что значения для любого неагрегированного вывода будут взяты из некоторой по существу случайной строки в группе. На рис. 5-7 показано, как это работает.

Рисунок 5-7. Заголовок SELECT сгладит любые группы строк, созданные GROUP BY. На этом рисунке показано, как разные столбцы из одной группы строк объединяются в выходную строку. Любое значение, не вычисленное агрегатной функцией, берется из последней строки. Поскольку столбец A использовался как выражение GROUP BY, известно, что все строки имеют одно и то же значение, и его можно безопасно вернуть. Столбец B обрабатывается агрегатной функцией, и его также можно безопасно вернуть. Столбец C возвращать небезопасно, поскольку порядок строк в группе не определен.

В некоторых случаях выбор значения из случайной строки - неплохая вещь. Например, если выражение заголовка SELECT также используется как выражение GROUP BY, то мы знаем, что этот столбец имеет эквивалентное значение в каждой строке группы. Независимо от того, какую строку вы выберете, вы всегда получите одно и то же значение.

Проблемы могут возникнуть, когда заголовок SELECT использует ссылки на столбцы, которые не были частью предложения GROUP BY и не были переданы через агрегатные функции. В таких случаях нет детерминированного способа выяснить, каким будет выходное значение. Чтобы избежать этого, при использовании предложения GROUP BY выражения заголовка SELECT должны использовать только ссылки на столбцы в качестве входных данных агрегатной функции, или выражения заголовка должны соответствовать тем, которые используются в предложении GROUP BY.

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

SELECT col1, sum( col2 ) FROM tbl GROUP BY col1; -- well formed

Это хорошо сформулированное заявление. Предложение GROUP BY показывает, что строки группируются на основе значений в col1. Это делает безопасным появление col1 в заголовке SELECT, поскольку каждая строка в определенной группе будет иметь эквивалентное значение в col1. Заголовок SELECT также ссылается на col2, но он передается в агрегатную функцию. Агрегатная функция возьмет все значения col2 из разных строк в группе и выдаст логический ответ - в данном случае численное суммирование.

Результатом этого оператора будут две колонки. В первом столбце будет одна строка для каждого уникального значения из col1. Каждая строка второго столбца будет иметь сумму всех значений в столбце col2, которые связаны со значением col1, указанным в первом столбце результатов. Более подробные примеры можно найти в конце главы.

Следующее утверждение сформировано неправильно:

SELECT col1, col2 FROM tbl GROUP BY col1; -- NOT well formed

Как и раньше, строки группируются на основе значения в col1, что делает безопасным появление col1 в заголовке SELECT. Однако столбец col2 выглядит пустым и не является агрегатным параметром. Когда этот оператор запускается, второй столбец вернет результат, который будет содержать случайные значения из исходного столбца col2.

Хотя каждая строка в группе должна иметь эквивалентное значение в столбце или выражении, которое использовалось в качестве ключа группировки, это не всегда означает, что значения точно такие же. Если использовалось сопоставление, такое как NOCASE, разные значения (например, 'ABC' и 'abc') считаются эквивалентными. В этих случаях невозможно узнать конкретное значение, которое будет возвращено из заголовка SELECT. Например:

CREATE TABLE tbl ( t );
INSERT INTO tbl VALUES ( 'ABC' );
INSERT INTO tbl VALUES ( 'abc' );
SELECT t FROM tbl GROUP BY t COLLATE NOCASE;

Этот запрос вернет только одну строку, но невозможно узнать, какое конкретное значение будет возвращено.

Наконец, если заголовок SELECT содержит агрегатную функцию, но оператор SELECT не имеет предложения GROUP BY, вся рабочая таблица рассматривается как одна группа. Поскольку сглаженные группы всегда возвращают одну строку, это приведет к тому, что запрос вернет только одну строку, даже если рабочая таблица не содержит строк.

Предложение HAVING

Функционально предложение HAVING идентично предложению WHERE. Предложение HAVING состоит из выражения фильтра, которое оценивается для каждой строки рабочей таблицы. Любая строка, имеющая значение false или NULL, отфильтровывается и удаляется. В итоговой таблице будет такое же количество столбцов, но может быть меньше строк.

Основное различие между предложением WHERE и предложением HAVING заключается в том, где они появляются в конвейере SELECT. Предложение HAVING обрабатывается после предложений GROUP BY и SELECT, что позволяет HAVING фильтровать строки на основе результатов любого агрегата GROUP BY. Предложения HAVING могут даже иметь свои собственные агрегаты, что позволяет им фильтровать агрегированные результаты, не являющиеся частью заголовка SELECT.

Предложения HAVING должны содержать только выражения фильтра, зависящие от вывода GROUP BY. Вся остальная фильтрация должна выполняться в предложении WHERE.

Оба предложения HAVING и WHERE могут ссылаться на имена столбцов результатов, определенные в заголовке SELECT, с помощью ключевого слова AS. Основное отличие состоит в том, что предложение WHERE может ссылаться только на выражения, не содержащие агрегатных функций, а предложение HAVING может ссылаться на любой столбец результатов.

Ключевое слово DISTINCT

Ключевое слово DISTINCT просканирует набор результатов и удалит все повторяющиеся строки. Это гарантирует, что возвращенные строки составляют правильный набор. При определении того, является ли строка дубликатом, учитываются только столбцы и значения, указанные в заголовке SELECT. Это один из немногих случаев, когда NULL можно сравнивать и такие строки тоже будут исключены.

Поскольку SELECT DISTINCT должен сравнивать каждую строку с каждой другой строкой, это дорогостоящая операция. В хорошо спроектированной базе данных это также требуется редко. Поэтому его использование несколько необычно.

Предложение ORDER BY

Предложение ORDER BY используется для сортировки или упорядочивания строк таблицы результатов. Предоставляется список из одного или нескольких выражений сортировки. Первое выражение используется для сортировки таблицы. Второе выражение используется для сортировки любых эквивалентных строк из первой сортировки и так далее. Каждое выражение можно отсортировать по возрастанию или убыванию.

Базовый формат предложения ORDER BY выглядит так:

ORDER BY expression [COLLATE collation_name] [ASC|DESC] [,...]

Выражение оценивается для каждой строки. Очень часто выражение представляет собой простую ссылку на столбец, но может быть любым выражением. Полученное значение затем сравнивается со значениями, созданными другими строками. Если указано, именованное сопоставление используется для сортировки значений. Параметры сортировки определяют конкретный порядок сортировки текстовых значений. Ключевые слова ASC или DESC могут использоваться для принудительной сортировки в возрастающем или убывающем порядке. По умолчанию значения сортируются в порядке возрастания с использованием параметров сортировки по умолчанию.

Выражение ORDER BY может использовать любой исходный столбец, включая те, которые не отображаются в результате запроса. Как и GROUP BY, если выражение ORDER BY состоит из буквального целого числа, предполагается, что это индекс столбца. Индексы столбцов начинаются слева с 1, поэтому фраза ORDER BY 2 отсортирует таблицу результатов по второму столбцу.

Поскольку SQLite позволяет хранить разные типы данных в одном столбце, сортировка может стать немного интереснее. При сортировке столбца смешанного типа значения NULL будут отсортированы вверх. Затем целочисленные и действительные значения будут смешаны вместе в правильном числовом порядке. За числами будут следовать текстовые значения со значениями BLOB в конце. Попытки преобразовать типы не будут. Например, текстовое значение, содержащее строковое представление числа, будет отсортировано с другими текстовыми значениями, а не с числовыми значениями.

В случае числовых значений естественный порядок сортировки четко определен. Текстовые значения сортируются с помощью активного сопоставления, тогда как значения BLOB всегда сортируются с использованием сопоставления BINARY. SQLite поставляется с тремя встроенными функциями сопоставления. Вы также можете использовать API для определения ваших собственных функций сопоставления. Три встроенных сопоставления:

BINARY
Текстовые значения сортируются в соответствии с семантикой вызова memcmp() POSIX. Кодировка текстового значения не принимается во внимание, по сути, рассматривается как большая двоичная строка. Значения BLOB всегда сортируются с помощью этого сопоставления. Это сопоставление по умолчанию.
NOCASE
Так же, как BINARY, только символы верхнего регистра ASCII преобразуются в нижний регистр перед выполнением сравнения. Преобразование регистра строго выполняется для 7-битных значений ASCII. Обычный дистрибутив SQLite не поддерживает сопоставления с поддержкой UTF.
RTRIM
То же, что и BINARY, игнорируются только завершающие (правые) пробелы.

Хотя ORDER BY чрезвычайно полезен, его следует использовать только тогда, когда это действительно необходимо, особенно с очень большими таблицами результатов. Хотя SQLite иногда может использовать индекс для вычисления результатов запроса по порядку, во многих случаях SQLite должен сначала вычислить весь набор результатов, а затем отсортировать его, прежде чем будут возвращены строки. В этом случае таблица промежуточных результатов должна храниться в памяти или на диске до тех пор, пока она не будет полностью вычислена, а затем ее можно будет отсортировать.

В целом, существует множество ситуаций, когда ORDER BY оправдан, или даже требуется. Просто имейте в виду, что его использование может повлечь за собой значительные затраты, и вы не должны иметь привычки привязывать его к каждому запросу «просто потому».

Предложения LIMIT и OFFSET

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

Есть три способа определить LIMIT и OFFSET:

LIMIT limit_count
LIMIT limit_count OFFSET offset_count
LIMIT offset_count, limit_count
Обратите внимание, что если и limit, и offset задаются в третьем формате, порядок чисел меняется на обратный.

Вот несколько примеров. Обратите внимание, что значение OFFSET определяет количество пропущенных строк, а не позицию первой строки:

LIMIT 10           -- returns the first 10 rows (rows 1 - 10)
LIMIT 10 OFFSET 3  -- returns rows 4  - 13
LIMIT 3 OFFSET 20  -- returns rows 21 - 23
LIMIT 3, 20        -- returns rows 4  - 23 (different from above!)

Хотя это не является строго обязательным, вы обычно хотите определить ORDER BY, если вы используете LIMIT. Без ORDER BY нет четко определенного порядка результата, что делает ограничение и смещение несколько бессмысленными.

Продвинутые методы

Помимо базового синтаксиса SELECT, существует несколько продвинутых методов для выражения более сложных запросов.

Подзапросы

Команда SELECT обеспечивает большую гибкость, но бывают случаи, когда одна команда SELECT не может полностью выразить запрос. Чтобы помочь в таких ситуациях, SQL поддерживает подзапросы (subqueries). Подзапрос - это не что иное, как оператор SELECT, встроенный в другой оператор SELECT. Подзапросы также известны как подвыборки.

Подзапросы чаще всего встречаются в предложении FROM, где они действуют как вычисляемая исходная таблица. Этот тип подзапроса может возвращать любое количество строк или столбцов и аналогичен созданию представления или выполнению запроса, записи результатов во временную таблицу и последующему обращению к этой таблице в основном запросе. Основное преимущество использования встроенного подзапроса состоит в том, что оптимизатор запросов может объединить подзапрос с основным оператором SELECT и рассмотреть всю проблему в целом, что часто приводит к более эффективному плану запроса.

Чтобы использовать подзапрос в предложении FROM, просто заключите его в круглые скобки. Следующие два оператора приведут к одинаковому результату:

SELECT * FROM TblA AS a JOIN TblB AS b;
SELECT * FROM TblA AS a JOIN (SELECT * FROM TblB) AS b;

Подзапросы могут отображаться в других местах, включая общие выражения, используемые в любой команде SQL. Операторы EXISTS и IN используют подзапросы. Фактически, вы можете использовать подзапрос в любом месте, где выражение ожидает список литеральных значений (однако подзапрос нельзя использовать для создания списка идентификаторов). См. приложение D для получения более подробной информации о выражениях SQL.

Составные операторы SELECT

Помимо подзапросов, несколько операторов SELECT могут быть объединены вместе, чтобы сформировать составной (compound) SELECT. Составные операторы SELECT используют операторы набора для строк, генерируемых серией операторов SELECT.

Для правильного объединения каждый оператор SELECT должен генерировать одинаковое количество столбцов. Имена столбцов из первого оператора SELECT будут использоваться для общего результата. Только последний оператор SELECT может иметь предложение ORDER BY, LIMIT или OFFSET, которое применяется к полной таблице составных результатов. Синтаксис составного SELECT выглядит так:

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ...

compound_operator

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ...

[...]

ORDER BY ... LIMIT ... OFFSET ...

Для включения дополнительных операторов SELECT можно использовать несколько составных операторов.

UNION ALL
Оператор UNION ALL объединяет все строки, возвращаемые каждым оператором SELECT, в одну большую таблицу. Если два блока SELECT генерируют N и M строк соответственно, итоговая таблица будет иметь N+M строк.
UNION
Оператор UNION очень похож на оператор UNION ALL, но он удаляет любые повторяющиеся строки, включая дубликаты, которые поступили из одного и того же блока SELECT. Если два блока SELECT генерируют N и M строк соответственно, результирующая таблица может иметь от 1 до N+M строк.
INTERSECT
Оператор INTERSECT вернет один экземпляр любой строки, обнаруженной (один или несколько раз) в обоих блоках SELECT. Если два блока SELECT генерируют N и M строк соответственно, результирующая таблица может иметь от 0 до MIN(N,M) строк.
EXCEPT
Оператор EXCEPT вернет все строки в первом блоке SELECT, которых нет во втором блоке SELECT. По сути, это оператор вычитания. Если в первом блоке есть повторяющиеся строки, все они будут удалены одной совпадающей строкой во втором блоке. Если два блока SELECT генерируют N и M строк соответственно, результирующая таблица может иметь от 0 до N строк.

SQLite поддерживает составные операторы UNION, UNION ALL, INTERSECT и EXCEPT. На рис. 5-8 показан результат каждой операции.

Рисунок 5-8. Составные операторы UNION ALL, UNION, INTERSECT и EXCEPT.

После объединения всех составных операторов любые завершающие ORDER BY, LIMIT и OFFSET применяются к конечной таблице результатов. В случае составных операторов SELECT выражения, присутствующие в любом предложении ORDER BY, должны точно соответствовать одному из столбцов результатов или использовать индекс столбца.

Альтернативная нотация JOIN

Есть два стиля записи соединений. Стиль, показанный ранее в этой главе, известен как нотация явного соединения (explicit join notation). Она названа так потому, что использует ключевое слово JOIN для явного описания того, как каждая таблица присоединяется к следующей. Нотация явного соединения также известна как нотация соединения ANSI (ANSI join notation), поскольку она была введена, когда SQL прошел процесс стандартизации.

Старая исходная нотация соединения известна как нотация неявного соединения (implicit join notation). Используя эту нотацию, предложение FROM представляет собой просто список таблиц, разделенных запятыми. Таблицы в списке объединяются с использованием декартова произведения, а соответствующие строки извлекаются с дополнительными условиями WHERE. Фактически, он переводит каждое соединение в состояние CROSS JOIN, а затем перемещает условия соединения из предложения FROM в предложение WHERE.

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

SELECT ...
    FROM employee JOIN resource ON ( employee.eid = resource.eid )
    WHERE ...

Это тот же самый оператор, написанный с использованием нотации неявного соединения:

SELECT ...
    FROM employee, resource
    WHERE employee.eid = resource.eid AND ...

Между двумя обозначениями нет разницы в производительности: это чисто синтаксис.

В общем, явное представление (первое) стало стандартным способом работы. Большинство людей находят явную нотацию более простой для чтения, что делает цель запроса более прозрачной и легкой для понимания. Я всегда чувствовал, что явная нотация немного чище, поскольку она помещает полную спецификацию соединения в предложение FROM, оставляя предложение WHERE свободным для фильтров, специфичных для запроса. Используя явную нотацию, предложение FROM (и только предложение FROM) полностью и независимо указывает, что вы выбираете «from».

Явная нотация также позволяет вам более точно определять тип и порядок каждого JOIN. В SQLite вы должны использовать явную нотацию, если хотите OUTER JOIN - неявная нотация может использоваться только для указания CROSS JOIN или INNER JOIN.

Если вы изучаете SQL впервые, я настоятельно рекомендую вам освоиться с явной нотацией. Просто имейте в виду, что существует много кода SQL (включая старые книги и учебные пособия), использующего старую неявную нотацию.

Примеры SELECT

Команда SELECT очень сложна, и может быть трудно понять, как эти разные предложения могут быть объединены во что-то полезное. Некоторые из них станут более очевидными в следующей главе, когда мы рассмотрим стандартные практики проектирования баз данных, но для начала мы рассмотрим несколько примеров.

Все эти примеры будут использовать эти данные:

CREATE TABLE x ( a, b );
INSERT INTO x VALUES ( 1, 'Alice' );
INSERT INTO x VALUES ( 2, 'Bob' );
INSERT INTO x VALUES ( 3, 'Charlie' );

CREATE TABLE y ( c, d );
INSERT INTO y VALUES ( 1, 3.14159 );
INSERT INTO y VALUES ( 1, 2.71828 );
INSERT INTO y VALUES ( 2, 1.61803 );

CREATE TABLE z ( a, e );
INSERT INTO z VALUES ( 1, 100 );
INSERT INTO z VALUES ( 1, 150 );
INSERT INTO z VALUES ( 3, 300 );
INSERT INTO z VALUES ( 9, 900 );

В этих примерах показан инструмент командной строки sqlite3. Следующие точечные команды были введены, чтобы облегчить понимание вывода. Последняя команда заставит sqlite3 печатать строку [NULL] всякий раз, когда встречается NULL. Обычно NULL дает пустой вывод, неотличимый от пустой строки:

.headers on
.mode column
.nullvalue [NULL]

Этот набор данных доступен на странице загрузки книги на веб-сайте O'Reilly как файл SQL и база данных SQLite. (UsingSQLiteCode.tar.gz.) Я предлагаю вам сесть с копией sqlite3 и попробовать эти команды. Попробуйте поэкспериментировать с разными вариантами.

Если какой-то из примеров не совсем понятен, просто разбейте инструкцию SELECT на отдельные части и шаг за шагом выполняйте их.

Простые SELECTы

Начнем с простого выбора, который возвращает все столбцы и строки в таблице x. Синтаксис SELECT * по умолчанию возвращает все столбцы:

sqlite> SELECT * FROM x;

a           b
----------  ----------
1           Alice
2           Bob
3           Charlie

Мы также можем возвращать выражения, а не только столбцы:

sqlite> SELECT d, d*d AS dSquared FROM y;
d           dSquared
----------  ------------
3.14159     9.8695877281
2.71828     7.3890461584
1.61803     2.6180210809

Простые JOINы

Теперь некоторые джойны. По умолчанию простое ключевое слово JOIN указывает на INNER JOIN. Однако, если на JOIN не ставится никаких дополнительных условий, оно возвращается к CROSS JOIN. В результате все три запроса дают одинаковые результаты. В последней строке используется нотация неявного соединения.

sqlite> SELECT * FROM x JOIN y;
sqlite> SELECT * FROM x CROSS JOIN y;
sqlite> SELECT * FROM x, y;

a           b           c           d
----------  ----------  ----------  ----------
1           Alice       1           3.14159
1           Alice       1           2.71828
1           Alice       2           1.61803
2           Bob         1           3.14159
2           Bob         1           2.71828
2           Bob         2           1.61803
3           Charlie     1           3.14159
3           Charlie     1           2.71828
3           Charlie     2           1.61803

В случае перекрестного соединения каждая строка в таблице a сопоставляется с каждой строкой в таблице y. Поскольку в обеих таблицах было три строки и два столбца, набор результатов состоит из девяти строк (3·3) и четырех столбцов (2+2).

JOIN...ON

Затем довольно простое внутреннее соединение с использованием базового условия объединения ON:

sqlite> SELECT * FROM x JOIN y ON a = c;

a           b           c           d
----------  ----------  ----------  ----------
1           Alice       1           3.14159
1           Alice       1           2.71828
2           Bob         2           1.61803

Этот запрос по-прежнему генерирует четыре столбца, но в набор результатов включаются только те строки, которые удовлетворяют условию соединения.

Следующий оператор требует, чтобы столбцы были уточнены, поскольку и таблица x, и таблица z имеют столбец. Обратите внимание, что возвращаются два разных столбца, по одному из каждой исходной таблицы:

sqlite> SELECT * FROM x JOIN z ON x.a = z.a;

a           b           a           e
----------  ----------  ----------  ----------
1           Alice       1           100
1           Alice       1           150
3           Charlie     3           300

JOIN...USING, NATURAL JOIN

Если мы используем синтаксис NATURAL JOIN или USING, повторяющийся столбец будет удален. Поскольку и таблица x, и таблица z имеют только общий столбец a, оба этих оператора дают одинаковый результат:

sqlite> SELECT * FROM x JOIN z USING ( a );
sqlite> SELECT * FROM x NATURAL JOIN z;

a           b           e
----------  ----------  ----------
1           Alice       100
1           Alice       150
3           Charlie     300

OUTER JOIN

LEFT OUTER JOIN вернет те же результаты, что и INNER JOIN, но также будет включать строки из таблицы x (левая/первая таблица), которые не были сопоставлены:

sqlite> SELECT * FROM x LEFT OUTER JOIN z USING ( a );

a           b           e
----------  ----------  ----------
1           Alice       100
1           Alice       150
2           Bob         [NULL]
3           Charlie     300

В этом случае строка Bob из таблицы x не имеет соответствующей строки в таблице z. Эти значения столбцов, обычно предоставляемые таблицей z, дополняются NULL, а затем строка включается в набор результатов.

JOIN-соединения

Также возможен JOIN нескольких таблиц вместе. В этом случае мы соединяем таблицу x с таблицей y, а затем присоединяем результат к таблице z:

sqlite> SELECT * FROM x JOIN y ON x.a = y.c LEFT OUTER JOIN z ON y.c = z.a;

a           b           c           d           a           e
----------  ----------  ----------  ----------  ----------  ----------
1           Alice       1           3.14159     1           100
1           Alice       1           3.14159     1           150
1           Alice       1           2.71828     1           100
1           Alice       1           2.71828     1           150
2           Bob         2           1.61803     [NULL]      [NULL]

Если вы не видите, что здесь происходит, работайте с объединениями по одному. Сначала посмотрите, что даст FROM x JOIN y ON x.a = y.c (показано в одном из предыдущих примеров). Затем посмотрите, как этот набор результатов будет сочетаться с таблицей z с помощью LEFT OUTER JOIN.

Самосоединение JOIN

В нашем последнем примере соединения показано самосоединение, при котором таблица соединяется сама с собой. Это создает два уникальных экземпляра одной и той же таблицы и требует использования псевдонимов таблиц:

sqlite> SELECT * FROM x AS x1 JOIN x AS x2 ON x1.a + 1 = x2.a;

a           b           a           b
----------  ----------  ----------  ----------
1           Alice       2           Bob
2           Bob         3           Charlie

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

Примеры WHERE

Далее предложение WHERE используется для фильтрации строк. Мы можем выделить конкретную строку:

sqlite> SELECT * FROM x WHERE b = 'Alice';

a           b
----------  ----------
1           Alice

Или диапазон значений:

sqlite> SELECT * FROM y WHERE d BETWEEN 1.0 AND 3.0;

c           d
----------  ----------
1           2.71828
2           1.61803

В этом случае выражение WHERE ссылается на выходной столбец по присвоенному имени:

sqlite> SELECT c, d, c+d AS sum FROM y WHERE sum < 4.0;

c           d           sum
----------  ----------  ----------
1           2.71828     3.71828
2           1.61803     3.61803

Примеры GROUP BY

Теперь давайте посмотрим на несколько операторов GROUP BY. Здесь мы группируем таблицу z по столбцу a. Поскольку в z.a есть три уникальных значения, на выходе будет три строки. Однако только группа a=1 имеет более одной строки. Мы можем видеть это в значениях count(), возвращаемых вторым столбцом:

sqlite> SELECT a, count(a) AS count FROM z GROUP BY a;

a           count
----------  ----------
1           2
3           1
9           1

Это похожий запрос, только теперь второй выходной столбец представляет собой сумму всех значений z.e в каждой группе:

sqlite> SELECT a, sum(e) AS total FROM z GROUP BY a;

a           total
----------  ----------
1           250
3           300
9           900

Мы даже можем вычислить собственное среднее значение и сравнить его с агрегатом avg():

sqlite> SELECT a, sum(e), count(e),
   ...>    sum(e)/count(e) AS expr, avg(e) AS agg
   ...>    FROM z GROUP BY a;

a           sum(e)      count(e)    expr        agg
----------  ----------  ----------  ----------  ----------
1           250         2           125         125.0
3           300         1           300         300.0
9           900         1           900         900.0

Предложение HAVING можно использовать для фильтрации строк на основе результатов агрегирования sum():

sqlite> SELECT a, sum(e) AS total FROM z GROUP BY a HAVING total > 500;

a           total
----------  ----------
9           900

Примеры ORDER BY

Вывод также можно отсортировать. Большинство этих примеров уже выглядят отсортированными, но в основном это случайность. Предложение ORDER BY применяет определенный порядок:

sqlite> SELECT * FROM y ORDER BY d;

c           d
----------  ----------
2           1.61803
1           2.71828
1           3.14159

Пределы и смещения также могут применяться для выбора определенных строк из упорядоченного результата. Однако концептуально это довольно просто.

Эти таблицы и запросы доступны как часть книги для скачивания. Не стесняйтесь загружать данные в sqlite3 и пробовать разные запросы. Не беспокойтесь о создании операторов SELECT, использующих все доступные предложения. Начните с простых запросов, где вы сможете понять все шаги, а затем начните комбинировать предложения для построения более крупных и сложных запросов.

Что дальше

Теперь, когда мы более внимательно рассмотрели команду SELECT, вы познакомились с большей частью основного языка SQL. В следующей главе мы более подробно рассмотрим структуру базы данных. Это поможет вам расположить таблицы и данные таким образом, чтобы усилить сильные стороны системы баз данных. Понимание некоторых основных идей дизайна также должно прояснить, почему SELECT работает именно так.

Глава 6
Дизайн базы данных

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

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

Таблицы и ключи

Таблицы могут выглядеть как простые двухмерные сетки простых значений, но четко определенная таблица имеет достаточно большую структуру. Разные столбцы могут играть разные роли. Некоторые столбцы действуют как уникальные идентификаторы, определяющие назначение и цель каждой строки. Другие столбцы содержат вспомогательные данные. третьи действуют как внешние ссылки, связывающие строки в одной таблице со строками в другой таблице. При разработке таблицы важно понимать, зачем нужен каждый столбец и какую роль играет каждый столбец.

Ключи определяют таблицу

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

Иногда первичный ключ представляет собой фактическое уникальное значение данных, такое как номер комнаты или имя хоста. Очень часто первичный ключ - это просто произвольный идентификационный номер, такой как идентификационный номер сотрудника или студента. Важным моментом является то, что первичные ключи должны быть уникальными для каждой возможной записи в таблице, а не только для строк, которые находятся там прямо сейчас. Вот почему имена обычно не используются в качестве первичных ключей - в большой группе людей слишком легко получить повторяющиеся (или очень похожие) имена.

Хотя в столбцах, составляющих первичный ключ, нет ничего особенного, сами ключи играют очень важную роль в проектировании и использовании базы данных. Их роль в качестве уникального идентификатора для каждой строки делает их аналогичными ключу хеш-таблицы или ключу словарного класса структуры данных. По сути, они представляют собой «поисковые» значения для строк таблицы.

Первичный ключ можно определить с помощью команды CREATE TABLE. Явная идентификация первичного ключа приведет к тому, что система базы данных автоматически создаст UNIQUE индекс для всех столбцов первичного ключа. Объявление первичного ключа также позволяет использовать некоторые сокращения синтаксиса при установлении отношений между таблицами.

Например, это определение таблицы определяет поле employee_id как первичный ключ:

CREATE TABLE employee (
    employee_id   INTEGER   PRIMARY KEY   NOT NULL,
    name          TEXT   NOT NULL
    /* ...etc... */
);

Дополнительную информацию о синтаксисе, используемом для определения первичного ключа, см. в разделе «Первичные ключи».

В документации схемы первичные ключи часто обозначаются аббревиатурой «PK». Первичные ключи также часто подчеркиваются двойным подчеркиванием при составлении таблиц, как показано на рис. 6-1.

Рисунок 6-1. Первичные ключи иногда обозначаются аббревиатурой PK. При построении схемы таблицы также часто используется двойное подчеркивание.

Многие запросы к базе данных используют первичный ключ таблицы в качестве ввода или вывода. У базы данных может быть запрос на возврат строки с заданным ключом, например, «вернуть запись для сотрудника № 953». Также часто запрашивают набор ключей, например, «собрать значения идентификаторов для всех сотрудников, нанятых более двух лет назад». Затем этот набор ключей может быть присоединен к другой таблице как часть отчета.

Внешние ключи

Помимо идентификации каждой строки в таблице, первичные ключи также играют центральную роль в объединении таблиц. Поскольку первичный ключ действует как уникальный идентификатор строки, вы можете создать ссылку на конкретную строку, записав первичный ключ строки. Это называется внешним ключом. Внешний ключ - это копия или запись первичного ключа, который используется как ссылка или указатель на другую (или «внешнюю») строку, чаще всего в другой таблице.

Как и первичный ключ, столбцы, составляющие внешний ключ, можно определить с помощью команды CREATE TABLE. В этом примере мы определяем формат назначения задачи. Каждая задача назначается конкретному сотруднику путем ссылки на поле employee_id из таблицы сотрудников:

CREATE TABLE task_assignment (
    task_assign_id   INTEGER   PRIMARY KEY,
    task_name        TEXT      NOT NULL,
    employee_id      INTEGER   NOT NULL   REFERENCES employee( employee_id )
    /* ...etc... */
);

Ограничение REFERENCES указывает, что этот столбец является внешним ключом. Ограничение указывает, на какую таблицу ссылаются и, необязательно, на какой столбец. Если столбец не указан, внешний ключ будет ссылаться на первичный ключ (это означает, что ссылка на столбец, использованная в предыдущем примере, не требуется, поскольку employee_id является первичным ключом таблицы employee). Подавляющее большинство внешних ключей будут ссылаться на первичный ключ, но если используется столбец, отличный от первичного ключа, этот столбец должен иметь ограничение UNIQUE или он должен иметь индекс UNIQUE с одним столбцом.

Внешний ключ также можно определить как ограничение таблицы. В этом случае может быть несколько локальных столбцов, которые ссылаются на несколько столбцов в указанной таблице. Столбцы, на которые указывает ссылка, должны быть многоколоночным первичным ключом, или в противном случае они должны иметь многоколоночный индекс UNIQUE. Определение внешнего ключа может включать несколько других необязательных частей. Полный синтаксис см. в приложении C.

В отличие от собственного первичного ключа таблицы, внешние ключи не обязательно должны быть уникальными. Это связано с тем, что несколько значений внешнего ключа (несколько строк) в одной таблице могут ссылаться на одну и ту же строку в другой таблице. Это называется отношением "один ко многим". См. раздел «Отношения один ко многим». Внешние ключи часто помечаются аббревиатурой «FK», как показано на рис. 6-2.

Рисунок 6-2. Внешние ключи - это копии первичного ключа из другой строки. Внешние ключи действуют как ссылки или указатели на другие строки. Их часто обозначают аббревиатурой FK.

Ограничения внешнего ключа

Объявление внешних ключей в определении таблицы позволяет базе данных применять ограничения внешнего ключа. Ограничения внешнего ключа используются для синхронизации ссылок на внешний ключ. Среди прочего, ограничения внешнего ключа могут предотвратить «висячие ссылки», требуя, чтобы все значения внешнего ключа правильно соответствовали значению строки из столбцов таблицы, на которую указывает ссылка. Внешние ключи также могут иметь значение NULL. NULL четко отмечает внешний ключ как неназначенный, что немного отличается от недопустимого значения. Во многих случаях неназначенные внешние ключи не соответствуют структуре базы данных. В этом случае столбцы внешнего ключа должны быть объявлены с ограничением NOT NULL.

Используя наш предыдущий пример, ограничения внешнего ключа потребуют, чтобы каждый элемент task_assignment.employee_id содержал значение действительного employee.employee_id. По умолчанию внешний ключ NULL также разрешен, но мы определили столбец task_assignment.employee_id с ограничением NOT NULL. Это требует, чтобы каждая задача ссылалась на действительного сотрудника.

Встроенная поддержка внешнего ключа была добавлена в SQLite 3.6.19, но по умолчанию отключена. Вы должны использовать команду PRAGMA foreign_keys, чтобы включить ограничения внешнего ключа. Это было сделано, чтобы избежать проблем с существующими приложениями и файлами баз данных. В будущей версии SQLite по умолчанию могут быть включены ограничения внешнего ключа. Если ваше приложение зависит от этого параметра, оно должно явно включать или выключать его.

Изменения таблицы внешнего ключа или таблицы, на которую имеется ссылка, потенциально могут вызвать нарушение ограничения внешнего ключа. Например, если оператор попытается обновить значение task_assignment.employee_id до недопустимого employee_id, ограничение внешнего ключа будет нарушено. Точно так же, если строке employee было присвоено новое значение employee_id, любые существующие ссылки task_assignment, указывающие на старое значение, станут недействительными. Это также нарушит ограничение внешнего ключа.

Если база данных обнаруживает изменение, которое может вызвать нарушение внешнего ключа, можно предпринять несколько действий. Действие по умолчанию - запретить изменение. Например, если вы попытались удалить сотрудника, которому еще были назначены задачи, удаление не удастся. Вам нужно будет удалить задачи или передать их другому сотруднику, прежде чем вы сможете удалить исходного сотрудника.

Также доступны другие разрешения конфликтов. Например, при использовании определения внешнего ключа ON DELETE CASCADE удаление сотрудника приведет к тому, что база данных автоматически удалит все задачи, назначенные этому сотруднику. Дополнительные сведения о разрешении конфликтов и других дополнительных параметрах внешнего ключа см. на веб-сайте SQLite. Актуальную документацию по поддержке внешних ключей SQLite можно найти на http://www.sqlite.org/foreignkeys.html.

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

Общие идентификационные ключи

Если вы посмотрите на большинство проектов баз данных, вы заметите, что в большом количестве таблиц есть столбец с общим идентификатором, который используется в качестве первичного ключа. Идентификатор обычно представляет собой произвольное целочисленное значение, которое автоматически присваивается при вставке новых строк. Номер может быть назначен постепенно, или он может быть назначен с помощью механизма последовательности, который гарантированно никогда не назначит один и тот же номер идентификатора дважды.

Когда столбец SQLite определен как INTEGER PRIMARY KEY, этот столбец заменит скрытый столбец ROWID, который действует как корневой столбец каждой таблицы. Использование INTEGER PRIMARY KEY позволяет значительно повысить производительность. Это также позволяет SQLite автоматически назначать упорядоченные значения идентификаторов. Для получения дополнительной информации см. «Первичные ключи».

Поначалу общее поле идентификатора может показаться дизайнерской уловкой. Если каждая таблица должна иметь конкретную и четко определенную роль, то первичный ключ должен отражать то, что делает любую данную строку уникальной, частично отражая основное определение элементов в таблице. Использование универсального и произвольного идентификатора для определения этой уникальности, похоже, упускает суть.

С теоретической точки зрения это может быть правильно, но это одна из тех областей, где теория сталкивается с реальностью, и реальность обычно побеждает.

Фактически, многие наборы данных не имеют действительно уникального представления. Например, имена людей недостаточно уникальны, чтобы их можно было использовать в качестве ключей базы данных. Имена достаточно уникальны, и они хорошо справляются с идентификацией людей лично, но им не хватает неотъемлемой и полной уникальности, которой требует хороший дизайн базы данных. Имена также меняются время от времени, например, когда люди женятся.

Чем больше вы копаетесь, тем больше обнаруживаете, что мир полон таких данных. Данные, которые достаточно уникальны и стабильны для случайного использования, но не являются действительно, абсолютно уникальными или достаточно фиксированными, чтобы использовать их в качестве интеллектуального ключа базы данных. В таких ситуациях лучшим решением будет просто использовать произвольный идентификатор, который полностью контролируется разработчиком базы данных, даже если он не имеет смысла вне базы данных. Этот тип ключа известен как суррогатный ключ.

Также бывают ситуации, когда первичный ключ состоит из трех или четырех (или более!) столбцов. Это довольно редко, но бывают ситуации, когда это случается. Если у вас есть большой многоколоночный первичный ключ в середине сложного набора отношений, создание и сопоставление всех этих многоколоночных внешних ключей может оказаться большой проблемой. Чтобы упростить такие ситуации, часто проще просто создать произвольный идентификатор и использовать его в качестве первичного ключа.

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

Последний комментарий по именам ключей. Часто возникает соблазн назвать общее поле идентификатора чем-нибудь простым, например id. В конце концов, если у вас есть таблица employee, может показаться несколько излишним называть первичный ключ employee_id; вы получаете множество ссылок на столбцы, которые читают employee.employee_id, когда кажется, что employee.id достаточно ясен.

Что ж, само по себе это достаточно ясно, но первичные ключи, как правило, отображаются в других таблицах как внешние ключи. В то время как employee.employee_id может быть немного избыточным, имя task_assignment.employee_id - нет. Это имя также дает вам важные подсказки о функции столбца (внешний ключ) и о том, на какую таблицу и столбец он ссылается (столбец employee_id, который является столбцом PK таблицы employee). Использование одного и того же имени для первичных и внешних ключей делает внутреннее значение и связь намного более очевидными. Он также позволяет использовать ярлыки, такие как синтаксис NATURAL JOIN или JOIN...USING( ). Обе эти формы требуют, чтобы совпадающие столбцы имели одно и то же имя.

Использование более явного имени также позволяет избежать проблемы наличия нескольких таблиц, каждая из которых имеет столбец с именем id. Такое общее название может несколько запутать, если вы объедините три или четыре таблицы. Хотя это и не обязательно, префикс каждого имени столбца, сохранение уникальных имен первичных ключей в базе данных (и использование одного и того же имени для внешних ключей) может сделать замысел дизайна базы данных намного более ясным.

Будьте конкретны

Самым большим камнем преткновения для начинающих разработчиков баз данных является то, что они создают недостаточно таблиц. Менее опытные разработчики склонны рассматривать таблицы как большие и монументальные конструкции. Обычно возникает ощущение, что таблицы важны, и что каждая таблица нуждается в достаточном количестве столбцов, содержащих значительные объемы данных, чтобы оправдать свое существование. Если для изменения дизайна требуется новый столбец или набор точек данных, возникает сильное желание объединить все вместе в большие централизованные таблицы, а не разбивать все на логические группы.

Менталитет «таблицы должны быть большими» быстро приведет к плохому дизайну. Хотя в основе большинства проектов будет несколько больших значимых таблиц, типичный дизайн также будет иметь изрядное количество меньших вспомогательных таблиц, которые могут содержать только два или три столбца данных. Фактически, в некоторых случаях вы можете создать таблицы, которые состоят только из внешних ссылок на другие таблицы. Создание новой таблицы - единственное, что вам нужно для определения новой структуры данных или контейнера данных, поэтому каждый раз, когда вам понадобится новый контейнер, он будет указывать на новую таблицу.

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

Общие структуры и отношения

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

Отношения один-к-одному

Самым основным видом межтабличных отношений является взаимно однозначное отношение (one-to-one relationship). Как вы можете догадаться, этот тип отношений устанавливает ссылку из одной строки в одной таблице на одну строку в другой таблице. Чаще всего взаимно-однозначные отношения представлены тем, что внешний ключ в одной таблице ссылается на первичный ключ в другой таблице. Если столбец внешнего ключа сделан уникальным, будет разрешена только одна ссылка. Как показано на рис. 6-3, уникальный внешний ключ создает взаимно однозначную связь между строками двух таблиц.

Рисунок 6-3. Во взаимно-однозначном отношении таблица B имеет внешний ключ, который ссылается на первичный ключ таблицы A. Это связывает каждый внешний ключ, отличный от NULL в таблице B, с некоторой строкой в таблице A.

В самом строгом смысле отношения внешнего ключа - это отношения один к нулю или один к одному. Когда две таблицы вовлечены во взаимно-однозначные отношения, нет ничего, что могло бы заставить каждый первичный ключ иметь входящую ссылку из внешнего ключа. В этом отношении, если внешний ключ допускает NULL, также могут быть неназначенные внешние ключи.

Отношения «один-к-одному» обычно используются для создания подробных таблиц (detail tables). Как следует из названия, подробная таблица обычно содержит детали, которые связаны с записями в более заметной таблице. Таблицы подробностей могут использоваться для хранения данных, относящихся только к небольшому подразделу приложения базы данных. Отделение подробных данных от основных таблиц позволяет различным разделам структуры базы данных развиваться и расти гораздо более независимо.

Подробные таблицы также могут быть полезны, когда расширенные данные применяются только к ограниченному количеству записей. Например, на веб-сайте может быть таблица sales_items, в которой перечислена общая информация (цена, инвентарь, вес и т. д.) для всех доступных товаров. Затем данные, относящиеся к конкретному типу, могут храниться в подробных таблицах, например cd_info для компакт-дисков (имя исполнителя, название альбома и т. д.) или dvd_info (режисеры, студия и т. д.) для DVD. Хотя таблица sales_items будет иметь уникальное взаимно однозначное отношение с каждой таблицей информации, зависящей от типа, на каждую отдельную строку в таблице sales_item будет ссылаться только одна подробная таблица.

Отношения «один к одному» также можно использовать для изоляции очень больших элементов данных, например BLOBов. Рассмотрим базу данных сотрудников, содержащую изображения каждого сотрудника. Из-за накладных расходов на хранение данных и ввод-вывод может быть неразумно включать столбец photo непосредственно в таблицу employee, но легко создать таблицу фотографий, которая ссылается на таблицу employee. Рассмотрим эти две таблицы:

CREATE TABLE employee (
    employee_id   INTEGER   NOT NULL   PRIMARY KEY,
    name          TEXT      NOT NULL
    /* ...etc... */
);

CREATE TABLE employee_photo (
    employee_id   INTEGER   NOT NULL   PRIMARY KEY
                  REFERENCES employee,
    photo_data    BLOB
    /* ...etc... */
);

Этот пример немного уникален, потому что столбец employee_photo.employee_id является как первичным ключом для таблицы employee_photo, так и внешним ключом для таблицы employee. Поскольку нам нужны отношения «один к одному», имеет смысл просто объединить первичные ключи в пары. Поскольку этот внешний ключ не допускает использование ключей NULL, каждая строка employee_photo должна соответствовать определенной строке employee. Однако база данных не гарантирует, что у каждого employee будет соответствующая фотография employee_photo.

Отношения один-ко-многим

Отношения «один ко многим» устанавливают связь между одной строкой в одной таблице и несколькими строками в другой таблице. Это достигается путем расширения отношения «один-к-одному», что позволяет одной стороне содержать повторяющиеся ключи. Отношения «один ко многим» часто используются для связывания списков или массивов элементов с одной строкой. Например, несколько адресов доставки могут быть связаны с одной учетной записью. Если не указано иное, «многие» обычно означает «ноль или более».

Единственное различие между отношением «один к одному» и отношением «один ко многим» состоит в том, что отношение «один ко многим» допускает дублирование значений внешнего ключа. Это позволяет нескольким строкам в одной таблице (множественная таблица) ссылаться на одну строку в другой таблице (одна таблица). При построении отношений «один ко многим» внешний ключ всегда должен быть на стороне многих.

Рис 6-4 иллюстрирует эту взаимосвязь более подробно.

Рисунок 6-4. При построении отношения «один ко многим» первичные ключи должны быть уникальными, но столбцы внешнего ключа могут содержать повторяющиеся значения. Это означает, что отношение «один ко многим» должно иметь внешний ключ на стороне «многие». Здесь мы видим, что значение внешнего ключа в таблице B относится к первичному ключу в таблице A.

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

Если вам нужно представить список или массив, попробуйте использовать отношение «один ко многим».

То же самое верно всякий раз, когда вы начинаете рассматривать наборы столбцов, такие как item0, item1, item2 и т. д., которые все предназначены для хранения экземпляров одного и того же типа значения. Таким конструкциям присущи ограничения, и процесс вставки, обновления и удаления становится довольно сложным. Гораздо проще просто разбить данные в отдельную таблицу и установить правильную взаимосвязь.

Одним из примеров отношений «один ко многим» являются музыкальные альбомы и песни, которые они содержат. В каждом альбоме есть список песен, связанных с этим альбомом. Например:

CREATE TABLE albums (
        album_id INTEGER   NOT NULL   PRIMARY KEY,
        album_name TEXT );

CREATE TABLE tracks (
        track_id INTEGER   NOT NULL   PRIMARY KEY,
        track_name TEXT,
        track_number INTEGER,
        track_length INTEGER, -- in seconds
        album_id INTEGER   NOT NULL   REFERENCES albums );

У каждого альбома и трека есть уникальный идентификатор. У каждого трека также есть ссылка на свой альбом по внешнему ключу. Рассмотреть возможность:

INSERT INTO albums VALUES ( 1, "The Indigo Album" );
INSERT INTO tracks VALUES ( 1, "Metal Onion", 1, 137, 1 );
INSERT INTO tracks VALUES ( 2, "Smooth Snake", 2, 212, 1 );
INSERT INTO tracks VALUES ( 3, "Turn A", 3, 255, 1 );

INSERT INTO albums VALUES ( 2, "Morning Jazz" );
INSERT INTO tracks VALUES ( 4, "In the Bed", 1, 214, 2 );
INSERT INTO tracks VALUES ( 5, "Water All Around", 2, 194, 2 );
INSERT INTO tracks VALUES ( 6, "Time Soars", 3, 265, 2 );
INSERT INTO tracks VALUES ( 7, "Liquid Awareness", 4, 175, 2 );

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

sqlite> SELECT album_name, track_name, track_number
   ...>   FROM albums JOIN tracks USING ( album_id )
   ...>   ORDER BY album_name, track_number;

album_name    track_name  track_number
------------  ----------  ------------
Morning Jazz  In the Bed  1
Morning Jazz  Water All   2
Morning Jazz  Time Soars  3
Morning Jazz  Liquid Awa  4
The Indigo A  Metal Onio  1
The Indigo A  Smooth Sna  2
The Indigo A  Turn A      3

Мы также можем управлять группировкой треков:

sqlite> SELECT album_name, sum( track_length ) AS runtime, count(*) AS tracks
   ...>   FROM albums JOIN tracks USING ( album_id )
   ...>   GROUP BY album_id;

album_name        runtime     tracks
----------------  ----------  ----------
The Indigo Album  604         3
Morning Jazz      848         4

Этот запрос группирует дорожки на основе их альбома, а затем объединяет данные дорожек вместе.

Отношения многие-ко-многим

Следующий шаг - отношения «многие ко многим». Отношение многие ко многим (many-to-many relationship) связывает одну строку в первой таблице со многими строками во второй таблице, одновременно позволяя связывать отдельные строки во второй таблице с несколькими строками в первой таблице. В некотором смысле отношения «многие ко многим» - это на самом деле два отношения «один ко многим», построенные друг на друге.

На рис. 6-5 показан классический пример людей и групп «многие ко многим». Один человек может принадлежать к разным группам, в то время как каждая группа состоит из множества разных людей. Общие операции - найти все группы, к которым принадлежит человек, или найти всех людей в группе.

Рисунок 6-5. Отношения «многие ко многим» подобны двум отношениям «один ко многим», построенным друг над другом. В этом примере каждый отдельный человек может быть членом одной или нескольких групп, а каждая группа может состоять из одного или нескольких человек.

Отношения «многие ко многим» немного сложнее, чем другие отношения. Хотя таблицы имеют отношение «многие ко многим» друг с другом, записи в обеих таблицах должны оставаться уникальными. Мы не можем дублировать строки с персоналом или сгруппированные строки с целью сопоставления ключей. Это проблема, поскольку каждый внешний ключ может ссылаться только на одну строку. Это делает невозможным для внешнего ключа одной таблицы (например, группы) ссылаться на несколько строк другой таблицы (например, людей).

Чтобы решить эту проблему, мы вернемся к предыдущему совету: если вам нужно добавить список в строку, разбейте этот список на отдельную таблицу и установите связь «один ко многим» с новой таблицей. Вы не можете напрямую представить отношение «многие ко многим» только с двумя таблицами, но вы можете взять пару отношений «один ко многим» и связать их вместе. Для связи требуется небольшая таблица, известная как таблица связи (link table) или таблица мостов (bridge table), которая находится между двумя многочисленными таблицами. Для каждого отношения «многие ко многим» требуется уникальная таблица мостов.

В своей основной форме таблица моста состоит только из двух внешних ключей - по одному для каждой из таблиц, которые она связывает вместе. Каждая строка таблицы моста связывает одну строку в первой таблице с одной строкой во второй таблице. В нашем примере от людей к группам таблица ссылок определяет членство одного человека в одной группе. Это показано на рис. 6-6.

Рисунок 6-6. Для реализации отношения «многие ко многим» требуется таблица моста. В этом примере каждая строка таблицы моста представляет членство одного человека в одной группе. Обратите внимание, что первичный ключ таблицы моста - это многоколоночный ключ (p_id, g_id). Это сохраняет уникальность членства.

Логическое представление отношения «многие ко многим» на самом деле является отношением «один ко многим к одному» с таблицей моста (действующей как запись членства) посередине. Каждый человек имеет отношение «один ко многим» с членством, так же как группы имеют отношение «один ко многим» с членством. В дополнение к связыванию двух таблиц вместе, таблица моста может также содержать дополнительную информацию о самом членстве, например дату первого соединения или дату истечения срока действия.

Вот три таблицы. Таблицы people и groups достаточно очевидны. Таблица p_g_bridge действует как таблица моста между таблицей people и таблицей groups. Оба столбца являются ссылками на внешние ключи: один для people, а другой - для groups. Установление первичного ключа для обоих столбцов внешнего ключа гарантирует, что членство останется уникальным:

CREATE TABLE people ( pid INTEGER   PRIMARY KEY, name TEXT, ... );
CREATE TABLE groups ( gid INTEGER   PRIMARY KEY, name TEXT, ... );
CREATE TABLE p_g_bridge(
        pid INTEGER   NOT NULL   REFERENCES people,
        gid INTEGER   NOT NULL   REFERENCES groups,
        PRIMARY KEY ( pid, gid )
);

В этом запросе будут перечислены все группы, к которым принадлежит человек:

SELECT groups.name AS group_name
  FROM people JOIN p_g_bridge USING ( pid ) JOIN groups USING ( gid )
  WHERE people.name = search_person_name;

Запрос просто связывает people с groups с помощью таблицы мостов, а затем отфильтровывает соответствующие строки.

Нам не всегда нужны все три таблицы. Этот запрос подсчитывает всех членов группы без использования таблицы people:

SELECT name AS group_name, count(*) AS members
  FROM groups JOIN p_g_bridge USING ( gid )
  GROUP BY gid;

Мы можем выполнить множество других запросов, например найти все группы, в которых нет участников:

SELECT name AS group_name
  FROM groups LEFT OUTER JOIN p_g_bridge USING ( gid )
  WHERE pid IS NULL;

Этот запрос выполняет внешнее соединение таблицы groups с таблицей p_g_bridge. Любые несовпадающие групповые строки будут дополнены NULL в столбце p_g_bridge.pid. Поскольку этот столбец помечен как NOT NULL, мы знаем, что единственный возможный способ сделать строку в этом столбце NULL - это внешнее соединение, то есть строка не соответствует ни одному членству. Очень похожий запрос можно использовать для поиска людей, у которых нет членства.

Иерархии и деревья

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

Общие операции с деревом включают поиск всех подузлов в данном узле или запрос глубины данного узла. Этим операциям присуща рекурсия - концепция, которую SQL не поддерживает. Это может привести к неуклюжим запросам или сложным представлениям.

Существует два распространенных метода представления древовидной связи с использованием таблиц базы данных. Первая - это модель смежности (adjacency model), которая использует простое представление, которое легко изменить, но сложно запросить. Другое распространенное представление - это вложенный набор (nested set), который позволяет выполнять относительно простые запросы, но за счет более сложного представления, изменение которого может быть дорогостоящим.

Модель смежности

Дерево - это в основном набор узлов, которые имеют отношение «один ко многим» между родителями и потомками. Мы уже знаем, как определить отношение «один ко многим»: дать каждому дочернему элементу внешний ключ, указывающий на первичный ключ родителя. Единственная хитрость в том, что по обе стороны отношений сидит одна и та же таблица.

Например, вот базовая таблица модели смежности:

CREATE TABLE tree (
    node   INTEGER   NOT NULL   PRIMARY KEY,
    name   TEXT,
    parent INTEGER   REFERENCES tree );

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

Если мы хотим представить это дерево:

A
  A.1
    A.1.a
  A.2
    A.2.a
    A.2.b
  A.3

Мы бы использовали следующие данные:

INSERT INTO tree VALUES ( 1, 'A',     NULL );
INSERT INTO tree VALUES ( 2, 'A.1',   1 );
INSERT INTO tree VALUES ( 3, 'A.1.a', 2 );
INSERT INTO tree VALUES ( 4, 'A.2',   1 );
INSERT INTO tree VALUES ( 5, 'A.2.a', 4 );
INSERT INTO tree VALUES ( 6, 'A.2.b', 4 );
INSERT INTO tree VALUES ( 7, 'A.3',   1 );

Следующий запрос даст список узлов и родителей, присоединив к себе древовидную таблицу:

sqlite> SELECT n.name AS node, p.name AS parent
   ...>   FROM tree AS n JOIN tree AS p ON n.parent = p.node;

node        parent
----------  ----------
A.1         A
A.1.a       A.1
A.2         A
A.2.a       A.2
A.2.b       A.2
A.3         A

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

Что непросто, так это древовидные операции, такие как подсчет всех узлов в поддереве или вычисление глубины конкретного узла (часто используется для форматирования вывода). Вы можете выполнять ограниченный обход (например, поиск прародителя), присоединяя древовидную таблицу к самой себе несколько раз, но вы не можете написать один запрос, который может вычислить ответ на эти типы вопросов на дереве произвольного размера. Единственный выбор - написать подпрограммы приложения, которые проходят по разным уровням дерева, вычисляя ответы, которые вы ищете.

В целом модель смежности легко понять, а деревья легко изменить. Модель основана на внешних ключах и может в полной мере использовать встроенную ссылочную целостность базы данных. Основным недостатком является то, что многие типы запросов к общим данным требуют, чтобы код приложения перебирал несколько отдельных запросов к базе данных и помогал в вычислении ответов.

Вложенный набор

Как следует из названия, представление вложенного набора зависит от вложенных групп узлов внутри других групп. Вместо того, чтобы представлять какой-либо тип отношений родитель-потомок, каждый узел содержит ограничивающие данные о полном поддереве под ним. С помощью некоторой умной математики это позволяет нам запрашивать все виды информации об узле.

Вложенная таблица множеств может выглядеть так:

CREATE TABLE nest (
    name TEXT,
    lower INTEGER   NOT NULL   UNIQUE,
    upper INTEGER   NOT NULL   UNIQUE,
    CHECK ( lower < upper ) );

Вложенный набор можно визуализировать, преобразовав древовидную структуру в представление в скобках. Затем мы подсчитываем круглые скобки и записываем верхнюю и нижнюю границы каждого вложенного набора. Номера индексов также могут быть вычислены с обходом дерева в глубину, где нижняя граница - это количество до посещения, а верхняя граница - это количество после посещения.

A( A.1( A.1.a( ) ), A.2( A.2.a( ), A.2.b( )  ), A.3(  )  )
 1    2      3 4 5     6      7 8       9 10 11    12 13 14

Или в SQL:

INSERT INTO nest VALUES ( 'A',      1, 14 );
INSERT INTO nest VALUES ( 'A.1',    2,  5 );
INSERT INTO nest VALUES ( 'A.1.a',  3,  4 );
INSERT INTO nest VALUES ( 'A.2',    6, 11 );
INSERT INTO nest VALUES ( 'A.2.a',  7,  8 );
INSERT INTO nest VALUES ( 'A.2.b',  9, 10 );
INSERT INTO nest VALUES ( 'A.3',   12, 13 );

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

SELECT name FROM nest WHERE lower + 1 = upper;

Вы можете определить глубину узла, посчитав всех его предков:

sqlite> SELECT n.name AS name, count(*) AS depth
   ...>   FROM nest AS n JOIN nest AS p
   ...>   ON p.lower <= n.lower AND p.upper >= n.upper
   ...>   GROUP BY n.name;

name        depth
----------  ----------
A           1
A.1         2
A.1.a       3
A.2         2
A.2.a       3
A.2.b       3
A.3         2

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

Вложенные наборы очень эффективны при вычислении многих типов запросов, но их изменение стоит дорого. Чтобы математика работала правильно, в нумерации не должно быть пробелов. Это означает, что любая вставка или удаление требует перенумерации значительного числа записей в таблице. Это неплохо для чего-то с несколькими десятками узлов, но может быстро оказаться непрактичным для дерева с сотнями узлов.

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

Больше информации

Это всего лишь краткий обзор того, как представлять древовидные отношения. Если вам нужно реализовать дерево, я предлагаю вам выполнить несколько поисков в Интернете по модели смежности (adjacency model) или вложенному набору (nested set). Многие из более крупных книг по SQL, упомянутых в разделе «Заключение», также содержат разделы, посвященные дереву и иерархиям.

Нормальная форма

Вы не углубитесь в большинство книг по дизайну баз данных, не прочитав обсуждение нормализации и нормальных форм. Нормальные формы (Normal Forms) - это серия форм или спецификаций дизайна таблиц, которые описывают лучший способ размещения данных в базе данных. Чем выше нормальная форма, тем больше нормализована база данных. Каждая форма основывается на предыдущей, добавляя дополнительные правила или условия, которые необходимо соблюдать. Нормализация (Normalization) - это процесс устранения дублирования данных, более четкого определения ключевых отношений и в целом перехода к более идеализированной форме базы данных. Различные таблицы в одной базе данных могут находиться на разных уровнях.

Большинство людей распознают пять нормальных форм, которые просто называются от первой нормальной формы до пятой нормальной формы. Часто они обозначаются сокращенно от 1NF до 5NF. Есть также несколько именованных форм, например, Нормальная форма Бойса-Кодда (BCNF). Большинство этих других форм примерно эквивалентны одной из пронумерованных форм. Например, BCNF - это небольшое расширение Третьей нормальной формы. Некоторые люди также признают более высокие уровни нормализации, такие как Шестая нормальная форма и выше, но эти крайние уровни нормализации выходят далеко за рамки практических задач большинства разработчиков баз данных.

Нормализация

Процесс нормализации полезен по двум причинам. Во-первых, нормализация определяет критерии проектирования, которые могут служить руководством в процессе проектирования. Если у вас есть набор таблиц, с которыми оказывается сложно работать, это часто указывает на более глубокую проблему дизайна или исходных положений. Процесс нормализации предоставляет набор правил и условий, которые могут помочь выявить проблемные места, а также предоставить возможные решения для более согласованной и чистой реорганизации данных.

Другое преимущество, которое проявляется в большей степени во время выполнения, состоит в том, что целостность данных намного проще обеспечить и поддерживать в нормализованной базе данных. Хотя общий дизайн базы данных часто более сложен (т. е. чем таблиц), отдельные части обычно намного проще и выполняют более четко определенные роли. Это часто приводит к повышению производительности INSERT, UPDATE и DELETE, поскольку изменения часто меньше и более локализованы.

Локализация данных - основная цель нормализации данных. Большинство обычных форм имеют дело с устранением избыточных или реплицированных данных, так что каждый уникальный токен данных сохраняется один - и только один раз - в базе данных. Все остальное просто ссылается на эту окончательную копию. Это упрощает обновление, поскольку есть только одно место, где нужно применить обновление, но также делает данные более согласованными, поскольку невозможно, чтобы несколько копий данных не были синхронизированы друг с другом. При работе над дизайном схемы вы должны постоянно задавать себе вопрос: «Если этот фрагмент данных изменится, в скольких разных местах мне нужно будет внести это изменение?» Если ответ отличный от единицы, скорее всего, вы не в нормальной форме.

Денормализация

Нормализация базы данных и распределение данных по разным таблицам означает, что запросы обычно включают объединение нескольких таблиц вместе. Иногда это может приводить к проблемам с производительностью, особенно для сложных отчетов, требующих данных из большого количества таблиц. Эти проблемы могут иногда приводить к процессу денормализации, когда дублирующиеся копии одних и тех же данных намеренно вводятся для уменьшения количества объединений, необходимых для общих запросов. Обычно это делается в системах, которые в основном предназначены только для чтения, таких как базы данных хранилищ данных, и часто выполняется путем вычисления временных таблиц из правильно нормализованных исходных данных.

Хотя большое количество объединений может привести к проблемам с производительностью, оптимизация базы данных похожа на оптимизацию кода - не начинайте слишком рано и не делайте предположений. В целом преимущества нормализации намного перевешивают затраты. Правильная база данных, которая работает немного медленнее, бесконечно более полезна, чем очень быстрая база данных, которая возвращает неправильные или непоследовательные ответы.

Первая нормальная форма

Первая нормальная форма (First Normal Form) или 1NF - это самый низкий уровень нормализации. В первую очередь это связано с тем, чтобы таблица была в правильном формате. Есть три условия, которые должны быть выполнены, чтобы таблица находилась в 1NF.

Первое условие касается оформления порядка. Чтобы быть в 1NF, отдельные строки таблицы не могут иметь какой-либо значимый или неотъемлемый порядок. Каждая строка должна быть изолированной отдельной записью. Значение данных в одной строке не может зависеть ни от одного из значений данных из соседних строк, ни от порядка вставки, ни от некоторого порядка сортировки. Это условие обычно легко выполнить, поскольку SQL не гарантирует какой-либо порядок строк.

Второе условие - уникальность. Каждая строка в таблице 1NF должна быть уникальной и должна быть уникальной для тех столбцов, которые содержат значимые данные для приложения. Например, если единственной разницей между двумя строками является столбец ROWID, поддерживаемый базой данных, то эти строки на самом деле не уникальны. Однако вполне нормально рассматривать произвольный идентификатор последовательности (такой как INTEGER PRIMARY KEY) как часть данных приложения. Это условие устанавливает, что таблица должна иметь PRIMARY KEY определенного типа, состоящий из одного или нескольких столбцов, которые создают уникальное определение того, что представляет собой таблица.

Третье и последнее условие для 1NF требует, чтобы каждый столбец каждой строки содержал одно (и только одно) логическое значение, которое не может быть разбито дальше. Проблема не в составных типах, таких как даты (которые могут быть разбиты на целые числа дня, месяца и года), а в массивах или списках логических значений. Например, вы не должны записывать текстовое значение, которое содержит список логических независимых значений, разделенных запятыми. Массивы или списки следует разбивать на собственные отношения «один ко многим».

Вторая нормальная форма

Вторая нормальная форма (Second Normal Form) или 2NF, касается составных ключей (многоколоночных ключей) и того, как другие столбцы связаны с такими ключами. 2NF имеет только одно условие: каждый столбец, не являющийся частью первичного ключа, должен относиться к первичному ключу в целом, а не только к его частям.

Рассмотрим таблицу, в которой перечислены все конференц-залы в большом корпоративном кампусе. Как минимум, в таблице conf_room есть столбцы для building_numb и room_numb. Взятые вместе, эти две колонки однозначно идентифицируют любой конференц-зал на территории всего кампуса, так что это будет наш составной первичный ключ.

Затем рассмотрим столбец, например seating_capacity. Значения в этом столбце напрямую зависят от каждого конкретного конференц-зала. Это, по определению, делает столбец зависимым как от номера здания, так и от номера комнаты. Включение столбца seating_capacity не нарушит 2NF.

Теперь рассмотрим столбец типа building_address. Этот столбец зависит от столбца building_numb, но не зависит от столбца room_numb. Поскольку building_address зависит только от части первичного ключа, включение этого столбца в таблицу conf_room нарушит 2NF.

Поскольку 2NF специально предназначена для ключей с несколькими столбцами, любая таблица с первичным ключом с одним столбцом, которая находится в 1NF, автоматически попадает в 2NF.

Чтобы распознать столбец, который может нарушать 2NF, поищите столбцы с повторяющимися значениями. Если повторяющиеся значения имеют тенденцию совпадать с повторяющимися значениями в одном из столбцов первичного ключа, это явный признак проблемы. Например, столбец building_address будет иметь несколько повторяющихся значений (при условии, что в большинстве зданий есть более одного конференц-зала). Повторяющиеся значения адреса могут быть сопоставлены с повторяющимися значениями в столбце building_numb. Это выравнивание показывает, как столбец адреса привязан только к столбцу building_numb, а не ко всему первичному ключу.

Третья нормальная форма

Третья нормальная форма (Third Normal Form) или 3NF, расширяет 2NF, устраняя транзитивные ключевые зависимости. Транзитивная зависимость - это когда A зависит от B, а B зависит от C, и поэтому A зависит от C. 3NF требует, чтобы каждый столбец непервичного ключа имел прямую (нетранзитивную) зависимость от первичного ключа.

Например, рассмотрим базу данных инвентаризации, которая используется для отслеживания ноутбуков в малом бизнесе. Таблица laptop будет иметь первичный ключ, который однозначно идентифицирует каждый ноутбук, например, контрольный номер инвентаря. Скорее всего, в таблице будут другие столбцы, в которых будут указаны марка и модель машины, серийный номер и, возможно, дата покупки. В нашем примере таблица laptop также будет включать столбец responsible_person_id. Когда сотруднику назначается ноутбук, в этот столбец помещается его идентификационный номер сотрудника.

Внутри строки значение столбца responsible_person_id напрямую зависит от первичного ключа. Другими словами, каждому ноутбуку назначается конкретное ответственное лицо, что делает значения в столбце responsible_person_id напрямую зависимыми от первичного ключа таблицы laptop.

Теперь посмотрим, что происходит, когда мы добавляем столбец, например, responsible_person_email. Это столбец, содержащий адрес электронной почты ответственного лица. Значение этого столбца по-прежнему зависит от первичного ключа таблицы laptop. У каждого отдельного ноутбука есть определенное поле responsible_person_email, которое так же уникально, как и поле responsible_person_id.

Проблема в том, что значения в столбце responsible_person_email не зависят напрямую от конкретного ноутбука. Скорее, столбец электронной почты привязан к responsible_person_id, а responsible_person_id, в свою очередь, зависит от конкретного ноутбука. Эта транзитивная зависимость нарушает 3NF, указывая на то, что столбец responsesible_person_email не принадлежит ей.

В таблице employee мы также найдем столбец person_id и столбец email. Это вполне приемлемо, если person_id является первичным ключом (вероятно). Это сделало бы столбец email напрямую зависимым от первичного ключа, сохраняя таблицу в 3NF.

Хороший способ распознать столбцы, которые могут нарушить 3NF, - это поиск пар или наборов несвязанных столбцов, которые необходимо синхронизировать друг с другом. Рассмотрим таблицу laptop. Если система была переназначена новому человеку, вы всегда должны обновлять как столбец responsible_person_id, так и столбец responsible_person_email. Необходимость синхронизировать столбцы друг с другом - явный признак зависимости друг от друга, а не от первичного ключа.

Высшие нормальные формы

Мы не собираемся вдаваться в подробности BCNF, четвертой или пятой (или последующих) нормальных форм, кроме как упомянуть, что четвертая и пятая нормальные формы начинают иметь дело с межтабличными отношениями и тем, как различные таблицы взаимодействуют друг с другом. Большинство разработчиков баз данных прилагают серьезные усилия, чтобы все было реализовано в 3NF, а затем перестают об этом беспокоиться. Оказывается, что если вы разбираетесь в вещах и создаете дизайн таблиц, которые относятся к 3NF, весьма высоки шансы, что ваши таблицы также будут соответствовать условиям для 4NF и 5NF, если не выше. В значительной степени высшие нормальные формы представляют собой формальные способы решения некоторых крайних случаев, которые несколько необычны, особенно в более простых проектах.

Хотя условия нормальных форм основываются друг на друге, типичный процесс проектирования на самом деле не повторяется по отдельным формам. Вы не сядете с новым дизайном и не измените его, пока все не станет 1NF, просто чтобы развернуться и погадать с дизайном, пока все не станет 2NF, и так далее, изолированно, шаг за шагом. Как только вы поймете идеи и концепции, лежащие в основе Первой, Второй и Третьей нормальных форм, проектирование непосредственно в 3NF станет вашей второй натурой. Преодоление условий по одному может помочь вам отсеять особенно сложные проблемные места, но вам не понадобится много времени, чтобы понять, когда дизайн выглядит чистым, а когда что-то «просто не так».

Основная концепция, которую следует помнить, заключается в том, что каждая таблица должна пытаться представить одну и только одну вещь. Первичный ключ (ключи) для этой таблицы должен однозначно и по сути определять концепцию, лежащую в основе таблицы. Все остальные столбцы должны содержать вспомогательные данные, относящиеся к этой единственной концепции. Говоря о первых трех нормальных формах в статье CACM 1982 года, Уильям Кент написал, что каждый неключевой столбец «...должен предоставить факты о ключе, только о ключе и ни о чем, кроме ключа». Если вы включите в свои проекты только один формальный аспект теории баз данных, это будет отличным началом.

Индексы

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

Понимание того, где и когда создавать индексы, может быть важным фактором в достижении хорошей производительности, особенно по мере роста наборов данных. Без надлежащих индексов система базы данных не имеет другого выбора, кроме как выполнять полное сканирование таблицы при каждом поиске. Сканирование таблиц может быть особенно дорогим при объединении таблиц.

Однако индексы не бесплатны. Каждый индекс должен поддерживать взаимно однозначное соответствие между записями индекса и строками таблицы. Если новая строка вставляется, обновляется или удаляется из таблицы, то же изменение должно быть внесено во все связанные индексы. Каждый новый индекс будет добавлять дополнительные служебные данные к командам INSERT, UPDATE и DELETE. Индексы также занимают место как на диске, так и в кэше страниц SQLite. Правильный, удачно размещенный индекс стоит своих затрат, но неразумно просто создавать случайные индексы в надежде, что один из них окажется полезным. Плохо размещенный индекс по-прежнему влечет за собой все издержки и может фактически замедлить выполнение запросов. Так что нужно постараться не сделать хуже, чем было.

Как они работают

Каждый индекс связан с определенной таблицей. Индексы могут быть одиночными или многоколоночными, но все столбцы данного индекса должны принадлежать одной и той же таблице. Нет ограничений на количество индексов, которые может иметь таблица, и нет ограничений на количество индексов, которым может принадлежать столбец. Вы не можете создать индекс для представления или виртуальной таблицы.

Внутри нормальной таблицы строки хранятся в индексированной структуре. SQLite использует для этой цели B-Tree, которое представляет собой особый тип сбалансированного дерева с несколькими дочерними элементами. Детали не важны, кроме понимания того, что по мере того, как строки вставляются в дерево, строки сортируются, организуются и оптимизируются, так что строку с конкретным известным ROWID можно получить относительно напрямую и быстро.

Когда вы создаете индекс, система базы данных создает другую древовидную структуру для хранения данных индекса. Вместо того, чтобы использовать столбец ROWID в качестве ключа сортировки, дерево сортируется и организуется с использованием столбца или столбцов, указанных в определении индекса. Запись индекса состоит из копии значений из каждого из проиндексированных столбцов, а также копии соответствующего значения ROWID. Это позволяет очень быстро найти проиндексированную запись, используя значения из проиндексированных столбцов.

Если взять такую таблицу:

CREATE TABLE tbl ( a, b, c, d );

А затем создать в ней индекс, который выглядит так:

CREATE INDEX idx_tbl_a_b ON tbl ( a, b );

База данных создает внутреннюю структуру данных, концептуально аналогичную:

SELECT a, b, ROWID FROM tbl ORDER BY a, b;

Если SQLite нужно быстро найти все строки, где, например, a = 45, он может использовать отсортированный индекс, чтобы быстро перейти к этому диапазону значений и извлечь соответствующие записи индекса. Если он ищет значение b, он может просто извлечь его из индекса и сделать это. Если нам нужно какое-либо другое значение в строке, оно должно получить всю строку. Это делается путем поиска ROWID. Последнее значение любой записи индекса - это ROWID соответствующей строки таблицы. Как только SQLite имеет список значений ROWID для всех строк, где a = 45, он может эффективно искать эти строки в исходной таблице и извлекать всю строку. Если все работает правильно, процесс поиска небольшого набора записей индекса, а затем их использование для поиска небольшого набора строк таблицы будет намного быстрее и эффективнее, чем выполнение полного сканирования таблицы.

Должен быть разнообразным

Чтобы иметь положительное влияние на производительность запросов, индексы должны быть разнообразными. Стоимость выборки одной строки с помощью индекса значительно выше, чем выборки одной строки с помощью сканирования таблицы. Чтобы снизить общую стоимость, индекс должен преодолеть эти накладные расходы, исключив подавляющее большинство выборок строк. За счет значительного уменьшения количества поисков строк можно снизить общую стоимость запроса, даже если поиск отдельных строк обходится дороже.

Точка безубыточности для показателей индекса находится где-то в диапазоне от 10% до 20%. Любой запрос, который извлекает больше строк из таблицы, лучше справится со сканированием таблицы, в то время как любой запрос, который извлекает меньше строк, получит улучшение за счет использования индекса. Если индекс не может изолировать строки до этого уровня, нет причин для его присутствия. Фактически, если оптимизатор запросов по ошибке использует индекс, который возвращает большой процент строк, индекс может снизить производительность и замедлить работу.

Это вызывает две проблемы. Во-первых, если вы пытаетесь повысить производительность запроса, который извлекает умеренный процент строк, добавление индекса вряд ли поможет. Индекс может повысить производительность только в том случае, если он используется для целевого количества строк.

Во-вторых, даже если запрос запрашивает конкретное значение, это все равно может привести к более высокому проценту извлеченных строк, если индексированные столбцы не являются достаточно уникальными. Например, если столбец имеет только четыре уникальных значения, успешный запрос никогда не получит менее примерно 25% строк (при условии разумного распределения значений или запросов). Добавление индекса к этому типу столбца не улучшит производительность. Создание индекса для столбца true/false было бы еще хуже.

Когда оптимизатор запросов пытается выяснить, следует ли использовать индекс или нет, у него обычно очень мало информации. По большей части предполагается, что если индекс существует, это должен быть хороший индекс, и он будет стремиться его использовать. Команда ANALYZE может помочь смягчить это, предоставляя статистическую информацию оптимизатору запросов (подробнее см. ANALYZE), но поддерживать ее в актуальном состоянии и поддерживать в хорошем состоянии во многих средах может быть сложно.

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

Ключи INTEGER PRIMARY KEY

Когда вы объявляете один или несколько столбцов PRIMARY KEY, система базы данных автоматически создает уникальный индекс по этим столбцам. Основная цель этого индекса - обеспечить выполнение ограничения UNIQUE, которое подразумевается для каждого PRIMARY KEY. Также бывает, что во многих операциях с базой данных обычно используется первичный ключ, например, естественные объединения или условный поиск в командах UPDATE или DELETE. Даже если индекс не требовался для принудительного применения ограничения UNIQUE, велики шансы, что вы все равно захотите индексировать эти столбцы.

Есть и другие преимущества использования INTEGER PRIMARY KEY. Как мы уже обсуждали, когда объявляется INTEGER PRIMARY KEY, этот столбец заменяет автоматический столбец ROWID и становится корневым столбцом для дерева. По сути, столбец становится индексом, используемым для хранения самой таблицы, устраняя необходимость во втором внешнем индексе.

Столбцы INTEGER PRIMARY KEY также обеспечивают лучшую производительность, чем стандартные индексы. Ключи INTEGER PRIMARY KEY имеют прямой доступ к полному набору данных строки. Обычный индекс просто ссылается на значение ROWID, которое затем ищется в корне таблицы. КлючиINTEGER PRIMARY KEY могут обеспечивать индексированный поиск, но пропустить этот второй шаг поиска и получить прямой доступ к данным таблицы.

Если вы используете общие значения идентификаторов строк, стоит попытаться определить их как столбцы INTEGER PRIMARY KEY. Это не только уменьшит размер базы данных, но и сделает ваши запросы более быстрыми и эффективными.

Значение порядка

При создании многоколоночного индекса для повышения производительности очень важен порядок столбцов. Если индекс имеет только один столбец, этот столбец используется как ключ сортировки. Если в определении индекса указаны дополнительные столбцы, дополнительные столбцы будут использоваться как вторичные, третичные и т. д. ключи сортировки. Все данные сортируются по первому столбцу, затем любые группы повторяющихся значений сортируются по второму столбцу и так далее. Это очень похоже на обычную телефонную книгу. Большинство телефонных справочников сортируются по фамилии. Любая группа общих фамилий затем сортируется по имени, а затем по отчеству или инициалам.

Чтобы использовать многоколоночный индекс, запрос должен содержать условия, которые могут использовать ключи сортировки в том же порядке, в котором они появляются в определении индекса. Если запрос не содержит условия, которое исключает первый столбец, индекс использовать нельзя.

Попробуйте поискать имя в телефонной книге. Вы можете использовать телефонную книгу, чтобы быстро найти номер телефона «Jennifer T. Smith». Сначала вы ищите фамилию «Smith». Затем вы уточняете поиск, ищите имя «Jennifer» и, наконец, букву «T» в середине (если требуется). Эта последовательность должна позволить вам очень быстро сосредоточиться на конкретной записи.

Телефонная книга не очень полезна, если позволяет быстро найти лишь всех людей с именем «Jennifer». Несмотря на то, что первое имя является частью индекса телефонной книги, это не первый столбец индекса. Если наш запрос не может предоставить конкретное условие для первого столбца многоколоночного индекса, индекс не может быть использован, и наш единственный вариант - выполнить полное сканирование. В этот момент обычно более эффективно выполнить полное сканирование самой таблицы, а не индекса.

Нет необходимости использовать каждый столбец, достаточно использовать столбцы по порядку. Если вы ищете в телефонной книге очень уникальное имя, возможно, вам не придется искать по имени или отчеству. Или, возможно, вы хотите найти каждого «Smith». В этом случае условиям запроса удовлетворяет только первый столбец.

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

Один за раз

Многоколоночные индексы очень полезны в некоторых ситуациях, но существуют ограничения на то, когда их можно эффективно использовать. В некоторых случаях более целесообразно создать несколько индексов с одним столбцом в одной таблице.

Это тоже имеет ограничения. Вы не можете создать одностолбцовый индекс для каждого столбца таблицы и ожидать, что система запросов будет смешивать и сопоставлять любые индексы, которые ей нужны. Как только подмножество строк извлекается из таблицы (посредством индекса или полного сканирования таблицы), этот набор строк концептуально становится независимой временной таблицей, которая больше не является частью исходной таблицы. В этот момент любой индекс, связанный с исходной таблицей, становится недоступным.

Как правило, оптимизатор запросов может использовать только один индекс для каждого экземпляра таблицы для каждого запроса. Несколько индексов в одной таблице имеют смысл только в том случае, если у вас есть разные запросы, которые извлекают строки с использованием разных столбцов (или имеют несколько ограничений UNIQUE). Различные запросы, использующие разные столбцы, могут выбрать наиболее подходящий индекс, но каждый отдельный запрос будет использовать только один индекс для каждого экземпляра таблицы. Если вы хотите, чтобы в одном запросе использовалось несколько индексированных столбцов, вам понадобится многоколоночный индекс.

Основное исключение - серия условий OR. Если предложение WHERE имеет цепочку условий OR для одного или нескольких столбцов одной и той же таблицы, тогда бывают случаи, когда SQLite может продолжить и использовать несколько индексов для одной и той же таблицы в одном запросе.

Сводка индекса

Несмотря на всю свою мощь, индексы часто вызывают большое разочарование. Правильный индекс приведет к огромному увеличению производительности, а неправильный индекс может значительно замедлить работу. Все индексы увеличивают стоимость модификации таблиц и увеличивают размер базы данных. Правильно размещенный индекс часто стоит накладных расходов, но может быть трудно понять, что делает индекс удачным.

Еще больше усложняет ситуацию то, что вы не можете указывать оптимизатору запросов, когда и как использовать ваши индексы - планировщик запросов принимает свои собственные решения. Автоматизация означает, что запросы будут работать несмотря ни на что, но это также может затруднить определение того, используется ли индекс. Еще сложнее понять, почему игнорируется индекс.

По сути, это оставляет разработчику базы данных возможность заново догадываться об оптимизаторе запросов. Поиск изменений и оптимизаций следует проводить методом проб и ошибок. Что еще хуже, по мере того, как ваше приложение изменяется и развивается, изменения в структуре запросов или шаблонах могут вызвать изменение в использовании индекса. В общем, это может быть трудная ситуация.

Традиционно здесь появляется роль DBA или администратора базы данных (Database Administrator). Этот человек отвечает за обслуживание и поддержку большого сервера базы данных. Они занимаются такими вещами, как мониторинг эффективности запросов, настройка параметров настройки и индексов, а также обеспечение актуальности всех статистических данных. К сожалению, вся эта идея несколько противоречит сути SQLite.

Так как же поддерживать производительность? Для начала - солидный дизайн. Хорошо созданная и нормализованная база данных будет иметь большое значение для определения ваших шаблонов доступа. Если вы потратите время на правильную идентификацию и определение ваших первичных ключей, это позволит системе баз данных создать соответствующие индексы и даст оптимизатору запросов (и будущим разработчикам) некоторое представление о вашем дизайне и намерениях.

Также помните, что (за исключением ограничений UNIQUE) база данных будет работать правильно без каких-либо индексов. Это может быть медленным, но все ответы будут правильными. Это позволяет вам беспокоиться в первую очередь о дизайне, а потом о производительности. Если у вас есть рабочий дизайн, всегда можно добавлять индексы постфактум, без необходимости изменять структуру таблицы или команды запроса.

Запросы к полностью нормализованной базе данных обычно содержат довольно много операций JOIN. Почти все эти соединения выполняются по первичным и внешним ключам. Первичные ключи индексируются автоматически, но в большинстве случаев вам нужно вручную создавать индексы для ваших внешних ключей. С их помощью в вашей базе данных уже должны быть определены наиболее важные индексы.

Начните отсюда. Измерьте производительность различных типов запросов, которые использует ваше приложение, и найдите проблемные области. Глядя на эти запросы, попытайтесь найти столбцы, в которых индекс может повысить производительность. Ищите любые ограничения и условия, которым может быть полезен индекс, особенно в условиях соединения, которые ссылаются на неключевые столбцы. Используйте EXPLAIN и EXPLAIN QUERY PLAN, чтобы понять, как SQLite обращается к данным. Эти команды также можно использовать для проверки того, использует ли запрос индекс или нет. Для получения дополнительной информации см. EXPLAIN в приложении C. Вы также можете использовать функцию sqlite3_stmt_status(), чтобы получить более взвешенное представление об эффективности оператора. См. sqlite3_stmt_status() в приложении G для получения более подробной информации.

В общем, не стоит слишком зацикливаться на точном размещении индекса. Для работы с таблицами большего размера вам может потребоваться несколько хорошо размещенных индексов, но стандартные индексы PRIMARY KEY в большинстве случаев работают на удивление хорошо. Дальнейшее размещение индекса следует рассматривать как настройку производительности, и о нем не следует беспокоиться, пока не будет выявлена реальная необходимость. Никогда не забывайте, что у индексов есть стоимость, поэтому вам не следует создавать их, если вы не можете определить необходимость.

Передача опыта проектирования

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

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

Таблицы - это типы

Наиболее распространенное заблуждение - думать о таблицах как об экземплярах составной структуры данных. Таблица очень похожа на массив или динамический список, поэтому ошибиться легко.

Таблицы следует рассматривать как определения типов. Никогда не следует использовать именованную таблицу в качестве организатора данных или группировки записей. Скорее, каждое определение таблицы следует рассматривать как определение структуры данных или определение класса. Команды SQL DDL, такие как CREATE TABLE, концептуально похожи на те файлы заголовков C/C++, которые определяют структуры данных и классы приложения. Сама таблица должна рассматриваться как глобальный пул управления для всех экземпляров этого типа. Если вам нужен новый экземпляр этого типа, вы просто вставляете новую строку. Если вам нужно сгруппировать или каталогизировать наборы экземпляров, делайте это с помощью ассоциаций ключей, а не путем создания новых таблиц.

Если вы когда-нибудь обнаружите, что создаете серию таблиц с одинаковыми определениями, это обычно серьезное предупреждение. Каждый раз, когда ваше приложение использует манипуляции со строками для программного построения имен таблиц, это тоже серьезное предупреждение, особенно если имена таблиц являются производными от значений, хранящихся в другом месте в базе данных.

Ключи - это обратные указатели

Еще один камень преткновения - правильное использование ключей. Ключи очень похожи на указатели. Первичный ключ используется для идентификации уникального экземпляра структуры данных. Это похоже на адрес записи. Все, что хочет ссылаться на эту запись, должно записывать свой адрес как указатель или, в случае баз данных, как внешний ключ. Внешние ключи по сути являются указателями базы данных.

Хитрость в том, что ссылки на базу данных идут в обратном порядке. Вместо указателей, указывающих на владение («Я управляю этим»), внешние ключи указывают тип владения («Мной управляет это»).

В C или C++, если основная запись данных управляет списком подзаписей, основная запись данных будет иметь своего рода список указателей. Каждый указатель будет ссылаться на конкретную подзапись, связанную с этой основной записью. Если вы имеете дело с основной записью данных и вам нужен список подзаписей, вы просто посмотрите на список указателей.

Базы данных делают наоборот. В отношении «один ко многим» основная запись (строка «один») будет просто иметь первичный ключ. Все подзаписи (сторона строк «многие») будут иметь внешние ключи, указывающие на основную запись. Если вы имеете дело с основной записью и вам нужен список подзаписей, вы просите систему базы данных посмотреть глобальный пул всех экземпляров подзаписи и вернуть только те подзаписи, которыми управляет эта основная запись.

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

Сделай одно дело

Мой последний совет по дизайну более общий. Как и в случае со структурами данных или классами, фундаментальная идея таблицы заключается в том, что она должна представлять экземпляры одной единственной идеи или «вещи». Она может представлять собой набор существительных или вещей, таких как люди, или она может представлять глаголы или действия, такие как журнал транзакций. Таблицы могут даже представлять менее конкретные вещи, такие как таблица моста «многие ко многим», в которой записывается членство. Но независимо от того, что это такое, каждая таблица должна иметь одну и только одну четко определенную роль в базе данных. Обычно проблема не в большом количестве таблиц, а в их слишком малом количестве.

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

Итоги

Как и в случае с разработкой приложений, проектирование баз данных - это отчасти наука, а отчасти искусство. Это может показаться довольно сложным, с ключами, которые нужно настроить, различными отношениями, которые нужно определить, нормальными формами, которым нужно следовать, и индексами, которые нужно создать.

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

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

Глава 7.
Интерфейс программирования C

Утилита командной строки sqlite3 предназначена для предоставления конечным пользователям интерактивного интерфейса. Это чрезвычайно полезно для разработки и тестирования SQL-запросов, отладки файлов базы данных и экспериментов с новыми функциями SQLite, но она никогда не предназначалась для взаимодействия с другими приложениями. Хотя утилиту командной строки можно использовать для очень простых сценариев и автоматизированных задач, если вы хотите написать приложение, которое серьезно использует библиотеку SQLite, ожидается, что вы будете использовать программный интерфейс.

Собственный интерфейс программирования SQLite основан на языке C, и это интерфейс, который будет рассмотрен в этой главе. Если вы работаете над чем-то другим, существуют оболочки и расширения для многих других языков, включая наиболее популярные языки сценариев. За исключением интерфейса Tcl, все эти оболочки предоставляются третьими сторонами и не являются частью продукта SQLite. Дополнительную информацию о языковых оболочках см. в разделе «Языки сценариев и другие интерфейсы».

Использование C API позволяет вашему приложению напрямую взаимодействовать с библиотекой SQLite и ядром базы данных. Вы можете связать статическую или динамическую сборку библиотеки SQLite с вашим приложением или просто включить исходный файл объединения в процесс сборки вашего приложения. Лучший выбор зависит от вашей конкретной ситуации. См. «Параметры сборки и установки» для получения дополнительных сведений.

C API довольно обширен и обеспечивает полный доступ ко всем функциям SQLite. Фактически, утилита командной строки sqlite3 написана с использованием общедоступного C API. В этой главе рассматриваются основные функции API, а в следующих главах рассматриваются более сложные функции.

Обзор API

Даже при использовании программного интерфейса основным способом взаимодействия с вашими данными является отправка команд SQL ядру базы данных. В этой главе основное внимание уделяется ядру API, который используется для передачи командных строк SQL механизму базы данных. Важно понимать, что не существует общедоступных функций для просмотра внутренней структуры таблицы или, например, доступа к древовидной структуре индекса. Вы должны использовать SQL для запроса данных из базы данных. Чтобы добиться успеха с SQLite API, вам нужно не только понимать C API, но также необходимо знать SQL, чтобы формировать осмысленные и эффективные запросы.

Структура

C API для SQLite 3 включает более дюжины структур данных, изрядное количество констант и более сотни различных вызовов функций. Хотя API несколько велик, его использование не должно быть сложным. Значительное количество функций являются узкоспециализированными и нечасто используются большинством разработчиков. Многие из оставшихся функций являются простыми вариациями одной и той же базовой операции. Например, существует дюжина вариантов функции sqlite3_value_xxx(), таких как sqlite3_value_int(), sqlite3_value_double() и sqlite3_value_text(). Все эти функции выполняют одну и ту же базовую операцию и могут считаться простыми разновидностями одного и того же базового интерфейса.

Когда я говорю о целой категории функций в тексте или псевдокоде, я буду просто называть их функциями sqlite3_value_xxx(). Большая часть документации SQLite ссылается на них как на sqlite3_value_*(), но я предпочитаю использовать нотацию xxx, чтобы избежать путаницы с указателями. Фактических функций SQLite3 с буквенной последовательностью xxx в имени не существует.

Все вызовы функций и типы данных общедоступного API имеют префикс sqlite3_, указывающий, что они являются частью версии 3.x продукта SQLite. Большинство констант, таких как коды ошибок, используют префикс SQLITE_. Различия в дизайне и API между SQLite 2.x и 3.x были достаточно значительными, чтобы гарантировать полное изменение всех имен и структур API. Глубина этих изменений потребовала, чтобы любой, кто обновился с SQLite 2 до SQLite 3, изменил свое приложение, поэтому изменение имен функций API только помогло сохранить различия между именами и решить любые вопросы о версии. Разные имена также позволяли приложениям, находившимся в процессе перехода, связываться с обеими библиотеками одновременно.

Помимо префикса sqlite3_, вызовы общедоступных функций можно идентифицировать по строчным буквам и знакам подчеркивания в их именах. В частных функциях используются слова с заглавной буквы (также известные как CamelCase). Например, sqlite3_create_function() - это общедоступная функция API (используется для регистрации определяемой пользователем функции SQL), а sqlite3CreateFunc() - это внутренняя функция, которую нельзя вызывать напрямую. Внутренние функции отсутствуют в общедоступном файле заголовка, не документированы и могут быть изменены в любое время.

Стабильность публичного интерфейса чрезвычайно важна для команды разработчиков SQLite. Существующий вызов функции API не будет изменен после того, как он станет общедоступным. Единственное возможное исключение - это совершенно новые интерфейсы, отмеченные как экспериментальные, и даже экспериментальные интерфейсы имеют тенденцию становиться довольно надежными после нескольких выпусков.

Если требуется исправленная версия вызова функции, новая функция обычно будет представлена с суффиксом _v2. Например, когда была представлена более гибкая версия существующей функции sqlite3_open(), старая версия функции была сохранена как есть, и была представлена новая, улучшенная sqlite3_open_v2(). Хотя в настоящее время не существует функций _v3 (или выше), возможно, они будут введены в будущем.

Добавляя новую функцию, а не изменяя параметры существующей функции, новый код может использовать преимущества новых функций, в то время как существующий немодифицированный код может продолжать связываться с обновленными версиями библиотеки SQLite. Использование другого имени функции также означает, что если новое приложение когда-либо случайно связывается со старой версией библиотеки, результатом будет ошибка ссылки, а не сбой программы, что значительно упрощает отслеживание и решение проблемы. .

Строки и Юникод

Существует ряд функций API, которые имеют вариант с числом 16 в конце имени. Например, доступны как функция sqlite3_column_text(), так и функция sqlite3_column_text16(). Первый запрашивает текстовое значение в формате UTF-8, а второй - текстовое значение в UTF-16.

Все строки в файле базы данных SQLite хранятся с использованием одной и той же кодировки. Файлы базы данных SQLite поддерживают кодировки UTF-8, UTF-16LE и UTF-16BE. Кодировка базы данных определяется при создании базы данных.

Независимо от базы данных вы можете вставлять или запрашивать текстовые значения в UTF-8 или UTF-16. SQLite автоматически преобразует текстовые значения между кодировкой базы данных и кодировкой API. Кодировка UTF-16, передаваемая 16 API, всегда будет в собственном порядке байтов машины. Буферы UTF-16 используют тип данных void* C. Тип данных wchar_t не используется, поскольку его размер не фиксирован, и не все платформы определяют 16-битный тип.

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

Все длины строк указываются в байтах, а не в символах, даже если в строке используется многобайтовая кодировка, например UTF-16.

Это различие важно учитывать при использовании международных строк.

Коды ошибок

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

Во всех случаях, если функция завершается успешно, она возвращает константу SQLITE_OK, которая имеет нулевое значение. Если что-то пошло не так, функции API вернут один из стандартных кодов ошибки, чтобы указать характер ошибки.

Совсем недавно был представлен набор расширенных кодов ошибок. Это дает более конкретное указание на то, что пошло не так. Однако для обеспечения обратной совместимости эти расширенные коды доступны только тогда, когда вы их активируете.

Ситуация достаточно сложна, чтобы ее обсудить позже в этой главе. Будет намного проще объяснить различные коды ошибок, когда вы увидите, как работает API. См. «Коды результатов и коды ошибок» для получения более подробной информации.

Я также должен сделать стандартную оговорку «делай, как я говорю, а не как я делаю» о правильной проверке кодов ошибок и возвращении результатов. Примеры кода в этой главе и в других местах этой книги имеют тенденцию к чрезвычайно сжатой (как и почти никакой) проверке ошибок. Это сделано для того, чтобы примеры были краткими и понятными. Излишне говорить, что это не лучший подход для производственного кода. При работе с собственным кодом поступайте правильно и проверяйте коды ошибок.

Структуры и распределения

Хотя собственный SQLite API часто называют C/C++ API, технически интерфейс доступен только на C. Как упоминалось в разделе «Сборка», исходный код SQLite строго основан на C, и поэтому может быть скомпилирован компилятором C. После компиляции библиотеку можно легко связать и вызвать из кода C и C++, а также из любого другого языка, который следует соглашениям о связывании C для вашей платформы.

Хотя API написан на C, он имеет отчетливый объектный привкус. Большая часть состояния программы содержится в серии непрозрачных структур данных, которые действуют как объекты. Наиболее распространенные структуры данных - это соединения с базой данных и подготовленные операторы. Вы никогда не должны напрямую обращаться к полям этих структур данных. Вместо этого предоставляются функции для создания, уничтожения и управления этими структурами во многом так же, как методы объектов используются для управления экземплярами объектов. В результате получается дизайн API, похожий на объектно-ориентированный дизайн. Фактически, если вы загрузите одну из сторонних оболочек C++, вы заметите, что оболочки имеют тенденцию быть довольно тонкими из-за большей части своей структуры базовых функций C и структур данных.

Важно, чтобы вы позволяли SQLite выделять собственные структуры данных и управлять ими. Дизайн API означает, что вы никогда не должны вручную выделять одну из этих структур или помещать эти структуры в стек. API предоставляет вызовы для внутреннего выделения необходимых структур данных, их инициализации и возврата. Точно так же для каждой функции, которая выделяет структуру данных, есть функция, которая используется для ее очистки и освобождения. Как и в случае с управлением памятью, вы должны быть уверены, что эти вызовы сбалансированы и что каждая созданная структура данных в конечном итоге будет освобождена.

Больше информации

Ядро API SQLite фокусируется на открытии соединений с базой данных, подготовке операторов SQL, связывании значений параметров, выполнении операторов и, наконец, пошаговом просмотре результатов. Этим процедурам и посвящена данная глава.

Существуют также интерфейсы для создания ваших собственных функций SQL, загрузки динамических модулей и создания виртуальных таблиц, управляемых кодом. Мы рассмотрим некоторые из этих более сложных интерфейсов в других главах.

Помимо этого, существует множество функций управления и настройки. Не все из них рассматриваются в основной части книги, но ссылку на полный API можно найти в приложении G. Если описание функции оставляет у вас дополнительные вопросы, не забудьте проверить это приложение для получения более подробной информации.

Инициализация библиотеки

Прежде чем приложение сможет использовать библиотеку SQLite для каких-либо действий, необходимо сначала инициализировать библиотеку. Этот процесс выделяет некоторые стандартные ресурсы и устанавливает любые специфичные для ОС структуры данных. По умолчанию большинство основных вызовов функций API автоматически инициализируют библиотеку, если она еще не была инициализирована. Однако рекомендуется инициализировать библиотеку вручную.

int sqlite3_initialize( )
Инициализирует библиотеку SQLite. Эта функция должна вызываться перед любой другой функцией в SQLite API. Вызов этой функции после того, как библиотека уже инициализирована, безвреден. Эту функцию можно вызвать после завершения работы для повторной инициализации библиотеки. Возвращаемое значение SQLITE_OK указывает на успех.

Когда приложение завершит работу с библиотекой SQLite, библиотеку следует закрыть.

int sqlite3_shutdown( )
Освобождает все ресурсы, выделенные sqlite3_initialize(). Вызов этой функции до инициализации библиотеки или после того, как библиотека уже выключена, безвреден. Возвращаемое значение SQLITE_OK указывает на успех.

Из-за функций автоматической инициализации многие приложения никогда не вызывают ни одну из этих функций. Скорее они вызывают sqlite3_open() или одну из других основных функций и зависят от автоматической инициализации библиотеки. В большинстве случаев это достаточно безопасно, но для максимальной совместимости лучше всего вызывать эти функции явно.

Подключения к базе данных

Прежде чем мы сможем подготовить или выполнить операторы SQL, мы должны установить соединение с базой данных. Чаще всего это делается путем открытия или создания файла базы данных SQLite3. Когда вы закончите соединение с базой данных, его необходимо закрыть. Это проверяет отсутствие невыполненных операторов или выделенных ресурсов перед закрытием файла базы данных.

Открытие

Подключения к базе данных выделяются и устанавливаются с помощью одной из команд sqlite3_open_xxx(). Они передают соединение с базой данных в виде структуры данных sqlite3. Есть три варианта:

int sqlite3_open( const char *filename, sqlite3 **db_ptr ) int sqlite3_open16( const void *filename, sqlite3 **db_ptr )

Открывает файл базы данных и выделяет структуру данных sqlite3. Первый параметр - это имя файла базы данных, который вы хотите открыть, в виде строки с завершающим нулем. Второй параметр - это ссылка на указатель sqlite3, который используется для обратной передачи нового соединения. Если возможно, база данных будет открыта для чтения/записи. В противном случае она будет открыт только для чтения. Если данный файл базы данных не существует, он будет создан.

Первый вариант предполагает, что имя файла базы данных закодировано в UTF-8, а второй предполагает, что имя файла базы данных закодировано в UTF-16.

int sqlite3_open_v2( const char *filename, sqlite3 **db_ptr, int flags, const char *vfs_name )

Вариант _v2 предлагает больший контроль над созданием и открытием файла базы данных. Первые два параметра совпадают. Предполагается, что имя файла находится в кодировке UTF-8. Варианта UTF-16 этой функции не существует.

Третий параметр - это набор флагов битового поля. Эти флаги позволяют указать, должен ли SQLite пытаться открыть базу данных для чтения/записи (SQLITE_OPEN_READWRITE) или только для чтения (SQLITE_OPEN_READONLY). Если вы запрашиваете доступ для чтения/записи, но доступен только доступ только для чтения, база данных будет открыта в режиме только для чтения.

Этот вариант открытия не создаст новый файл с неизвестным именем файла, если вы явно не разрешите это с помощью флага SQLITE_OPEN_CREATE. Это работает, только если база данных открывается в режиме чтения/записи.

Есть также ряд других флагов, касающихся управления потоками и кешем. См. Sqlite3_open() в приложении G для получения более подробной информации. Стандартная версия open эквивалентна значениям флагов (SQLITE_READWRITE | SQLITE_CREATE).

Последний параметр позволяет вам назвать модуль VFS (виртуальная файловая система (Virtual File System)), который будет использоваться с этим подключением к базе данных. Система VFS действует как уровень абстракции между библиотекой SQLite и любой базовой системой хранения (например, файловой системой). Почти во всех случаях вы захотите использовать модуль VFS по умолчанию и можете просто передать указатель NULL.

Для нового кода рекомендуется использовать вызов sqlite3_open_v2(). Новый вызов позволяет лучше контролировать то, как база данных открывается и обрабатывается.

Использование двойного указателя поначалу может немного сбивать с толку, но идея, лежащая в его основе, достаточно проста. Указатель на указатель на самом деле не более чем указатель, который передается по ссылке. Это позволяет вызову функции изменять переданный указатель. Например:

sqlite3   *db = NULL;
rc = sqlite3_open_v2( "database.sqlite3", &db, SQLITE_OPEN_READWRITE, NULL );
/* hopefully, db now points to a valid sqlite3 structure */

Обратите внимание, что db - это указатель sqlite3 (sqlite3*), а не фактическая структура sqlite3. Когда мы вызываем sqlite3_open_xxx() и передаем ссылку на указатель, функция open выделяет новую структуру данных sqlite3, инициализирует ее и устанавливает наш указатель, указывающий на нее.

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

Стандартного расширения файла для файла базы данных SQLite3 не существует, хотя .sqlite3, .db и .db3 являются популярными вариантами. Следует избегать расширения .sdb, поскольку это расширение имеет особое значение на некоторых платформах Microsoft Windows и может значительно снизить производительность ввода-вывода.

Кодировка строки, используемая файлом базы данных, определяется функцией, которая используется для создания файла. Использование sqlite3_open() или sqlite3_open_v2() приведет к созданию базы данных с кодировкой UTF-8 по умолчанию. Если sqlite3_open16() используется для создания базы данных, кодировка строки по умолчанию будет UTF-16 в собственном порядке байтов машины. Вы можете изменить кодировку строк по умолчанию с помощью команды SQL PRAGMA encoding. См. encoding в приложении F для более подробной информации.

Особые случаи

Помимо распознавания стандартных имен файлов, SQLite распознает несколько специализированных строк имен файлов. Если данное имя файла является указателем NULL или пустой строкой (""), то создается анонимная временная база данных на диске. Доступ к анонимной базе данных возможен только через соединение с базой данных, которое ее создало. Каждый вызов создает новый уникальный экземпляр базы данных. Как и все временные элементы, эта база данных будет уничтожена при закрытии соединения.

Если используется имя файла :memory:, то создается временная база данных в оперативной памяти. Базы данных в памяти находятся в кэше базы данных и не имеют резервного хранилища. Этот стиль базы данных чрезвычайно быстр, но требует достаточного объема оперативной памяти для хранения всего образа базы данных. Как и в случае с анонимными базами данных, каждый открытый вызов создает новую уникальную базу данных в памяти, что делает невозможным доступ к данной базе данных более чем одному соединению.

Базы данных в памяти являются отличными структурированными кешами. Невозможно напрямую создать образ базы данных в памяти на диск, но вы можете скопировать содержимое базы данных в памяти на диск (или с диска в память) с помощью API резервного копирования базы данных. См. раздел sqlite3_backup_init() в приложении G для получения более подробной информации.

Закрытие

Чтобы закрыть и разорвать соединение с базой данных, вызовите sqlite3_close().

int sqlite3_close( sqlite3 *db )
Закрывает соединение с базой данных и освобождает все связанные структуры данных. Все временные элементы, связанные с этим подключением, будут удалены. Для успешного выполнения все подготовленные операторы, связанные с этим подключением к базе данных, должны быть завершены. См. «Сброс и финализация» для получения более подробной информации.

Любой указатель, возвращаемый вызовом sqlite3_open_xxx(), включая указатель NULL, можно передать sqlite3_close(). Эта функция проверяет, нет ли незавершенных изменений в базе данных, затем закрывает файл и освобождает структуру данных sqlite3. Если в базе данных все еще есть нефинализированные операторы, будет возвращена ошибка SQLITE_BUSY. В этом случае вам нужно исправить проблему и снова вызвать sqlite3_close().

В большинстве случаев sqlite3_open_xxx() возвращает указатель на структуру sqlite3, даже если код возврата указывает на проблему. Это позволяет вызывающему абоненту получить сообщение об ошибке с помощью sqlite3_errmsg(). (См. «Коды результатов и коды ошибок».) В этих ситуациях вы все равно должны вызвать sqlite3_close(), чтобы освободить структуру sqlite3.

Пример

Вот схема программы, которая открывает базу данных, выполняет некоторые операции, а затем закрывает ее. Большинство других примеров в этой главе построены на этом примере путем вставки кода в середину:

#include "sqlite3.h"
#include <stdlib.h>

int main( int argc, char **argv )
{
    char            *file = ""; /* default to temp db */
    sqlite3         *db = NULL;
    int             rc = 0;

    if ( argc > 1 )
        file = argv[1];

    sqlite3_initialize( );
    rc = sqlite3_open_v2( file, &db, SQLITE_OPEN_READWRITE |
                                     SQLITE_OPEN_CREATE, NULL );
    if ( rc != SQLITE_OK) {
        sqlite3_close( db );
        exit( -1 );
    }

    /* perform database operations */

    sqlite3_close( db );
    sqlite3_shutdown( );
}

Имя файла по умолчанию - пустая строка. Если передать в sqlite3_open_xxx(), это приведет к созданию временной базы данных на диске, которая будет удалена, как только соединение с базой данных будет закрыто. Если указан хотя бы один аргумент, первый аргумент будет использоваться как имя файла. Если база данных не существует, она будет создана, а затем открыта для чтения/записи. Затем она немедленно закрывается.

Если этот пример запускается с новым именем файла, он не создаст допустимый файл базы данных. Библиотека SQLite откладывает запись заголовка базы данных до тех пор, пока не будет выполнена какая-либо фактическая операция с данными. Эта «ленивая» инициализация дает приложению возможность настроить любые соответствующие прагмы, такие как кодировка текста, размер страницы и формат файла базы данных, прежде чем файл базы данных будет полностью создан.

Подготовленный оператор

Как только соединение с базой данных установлено, мы можем приступить к выполнению команд SQL. Обычно это делается путем подготовки и пошагового выполнения операторов. Операторы хранятся в структурах данных sqlite3_stmt.

Жизненный цикл оператора

Жизненный цикл подготовленного оператора немного сложен. В отличие от соединений с базой данных, которые обычно открываются, используются в течение некоторого периода времени, а затем закрываются, оператор может находиться в нескольких различных состояниях. Оператор может быть подготовлен, но не запущен, или он может находиться в процессе обработки. После того, как оператор завершится, его можно сбросить и повторно выполнить несколько раз, прежде чем в конечном итоге будет завершен и выпущен.

Жизненный цикл типичного sqlite3_stmt выглядит примерно так (в псевдокоде):

/* create a statement from an SQL string */
sqlite3_stmt *stmt = NULL;
sqlite3_prepare_v2( db, sql_str, sql_str_len, &stmt, NULL );

/* use the statement as many times as required */
while( ... )
{
    /* bind any parameter values */
    sqlite3_bind_xxx( stmt, param_idx, param_value... );
    ...

    /* execute statement and step over each row of the result set */
    while ( sqlite3_step( stmt ) == SQLITE_ROW )
    {
        /* extract column values from the current result row */
        col_val = sqlite3_column_xxx( stmt, col_index );
        ...
    }

    /* reset the statement so it may be used again */
    sqlite3_reset( stmt );
    sqlite3_clear_bindings( stmt ); /* optional */
}

/* destroy and release the statement */
sqlite3_finalize( stmt );
stmt = NULL;

Процесс подготовки преобразует командную строку SQL в подготовленный оператор. Затем этот оператор может иметь значения, связанные с любыми параметрами оператора. Затем оператор выполняется или «перешагивает». В случае запроса каждый шаг будет делать новую строку результатов доступной для обработки. Затем значения столбцов текущей строки могут быть извлечены и обработаны. Оператор выполняется поэтапно, строка за строкой, пока не закончатся доступные строки.

Затем оператор может быть сброшен, что позволит повторно выполнить его с новым набором привязок. Подготовка оператора может быть довольно дорогостоящей, поэтому распространенной практикой является максимально возможное повторное использование операторов. Наконец, когда оператор больше не используется, можно завершить структуру данных sqlite3_stmt. Это освобождает все внутренние ресурсы и sqlite3_stmt, эффективно удаляя оператор.

Подготовка

Чтобы преобразовать командную строку SQL в подготовленный оператор, используйте одну из функций sqlite3_prepare_xxx():

int sqlite3_prepare( sqlite3 *db, const char *sql_str, int sql_str_len, sqlite3_stmt **stmt, const char **tail ) int sqlite3_prepare16( sqlite3 *db, const void *sql_str, int sql_str_len, sqlite3_stmt **stmt, const void **tail )

Настоятельно рекомендуется, чтобы все новые разработки использовали версию этих функций _v2.

int sqlite3_prepare_v2( sqlite3 *db, const char *sql_str, int sql_str_len, sqlite3_stmt **stmt, const char **tail ) int sqlite3_prepare16_v2( sqlite3 *db, const void *sql_str, int sql_str_len, sqlite3_stmt **stmt, const void **tail )

Преобразует командную строку SQL в подготовленный оператор. Первый параметр - это соединение с базой данных. Второй параметр - это команда SQL, закодированная в строке UTF-8 или UTF-16. Третий параметр указывает длину командной строки в байтах. Четвертый параметр - это ссылка на указатель оператора. Это используется для передачи указателя на новую структуру sqlite3_stmt.

Пятый параметр - это ссылка на строку (указатель на символ). Если командная строка содержит несколько операторов SQL и этот параметр не равен NULL, указатель будет установлен на начало следующего оператора в командной строке.

Эти вызовы _v2 принимают те же параметры, что и исходные версии, но внутреннее представление создаваемой структуры sqlite3_stmt несколько отличается. Это дает возможность расширенной и автоматической обработки ошибок. Эти различия обсуждаются позже в разделе «Коды результатов и коды ошибок».

Если параметр длины отрицательный, длина будет автоматически вычислена вызовом подготовки. Для этого требуется, чтобы командная строка была правильно завершена нулем. Если длина положительна, она представляет максимальное количество байтов, которые будут проанализированы. Для оптимальной производительности предоставьте строку с завершающим нулем и передайте допустимое значение длины, которое включает символ завершения нуля. Если командная строка SQL, переданная в sqlite3_prepare_xxx(), состоит только из одного оператора SQL, нет необходимости завершать ее точкой с запятой.

После подготовки оператора, но до его выполнения, вы можете привязать значения параметров к оператору. Параметры инструкции позволяют вставлять в командную строку SQL специальный токен, который представляет неопределенное буквальное значение. Затем вы можете привязать определенные значения к токенам параметров перед выполнением инструкции. После выполнения оператор может быть сброшен и могут быть присвоены новые значения параметров. Это позволяет вам подготовить оператор один раз, а затем повторно выполнить его несколько раз с разными значениями параметров. Это обычно используется с такими командами, как INSERT, которые имеют общую структуру, но многократно выполняются с разными значениями.

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

Шаг

Подготовка оператора SQL приводит к синтаксическому анализу командной строки и преобразованию в набор команд с байтовым кодом. Этот байт-код загружается в Virtual Database Engine (VDBE) SQLite для выполнения. Перевод не является последовательным индивидуальным делом. В зависимости от структуры базы данных (например, индексов) оптимизатор запросов может генерировать очень разные последовательности команд VDBE для аналогичных команд SQL. Размер и гибкость библиотеки SQLite во многом можно отнести к архитектуре VDBE.

Для выполнения кода VDBE вызывается функция sqlite3_step(). Эта функция проходит через текущую последовательность команд VDBE до тех пор, пока не встретится какой-либо тип прерывания программы. Это может произойти, когда становится доступной новая строка или когда программа VDBE достигает своего конца, указывая, что больше нет доступных данных.

В случае запроса SELECT sqlite3_step() вернет один раз для каждой строки в наборе результатов. Каждый последующий вызов sqlite3_step() будет продолжать выполнение оператора до тех пор, пока не станет доступна следующая строка или пока оператор не достигнет своего конца.

Определение функции довольно простое:

int sqlite3_step( sqlite3_stmt *stmt )
Попытки выполнить предоставленный подготовленный оператор. Если строка набора результатов становится доступной, функция вернет значение SQLITE_ROW. В этом случае отдельные значения столбцов можно извлечь с помощью функций sqlite3_column_xxx(). Дополнительные строки можно вернуть, выполнив дальнейшие вызовы sqlite3_step(). Если выполнение оператора подходит к концу, будет возвращен код SQLITE_DONE. Как только это произойдет, sqlite3_step() нельзя будет снова вызвать с этим подготовленным оператором, пока оператор не будет сначала сброшен с помощью sqlite3_reset().

Если первый вызов sqlite3_step() возвращает SQLITE_DONE, это означает, что оператор был успешно выполнен, но не было данных о результатах, которые можно было бы сделать доступными. Это типичный случай для большинства команд, кроме SELECT. Если sqlite3_step() вызывается повторно, команда SELECT вернет SQLITE_ROW для каждой строки набора результатов, прежде чем окончательно вернуть SQLITE_DONE. Если команда SELECT не возвращает строк, она вернет SQLITE_DONE при первом вызове sqlite3_step().

Есть также несколько команд PRAGMA, которые возвращают значение. Даже если возвращаемое значение является простым скалярным, это значение будет возвращено как набор результатов из одной строки и одного столбца. Это означает, что первый вызов sqlite3_step() вернет SQLITE_ROW, указывая, что данные результата доступны. Кроме того, если PRAGMA count_changes имеет значение true, команды INSERT, UPDATE и DELETE будут возвращать количество строк, которые они изменили, в виде однострочного целочисленного значения с одним столбцом.

Каждый раз, когда sqlite3_step() возвращает SQLITE_ROW, данные новой строки доступны для обработки. Значения строк можно проверить и извлечь из оператора с помощью функций sqlite3_column_xxx(), которые мы рассмотрим далее. Чтобы возобновить выполнение инструкции, просто вызовите sqlite3_step() еще раз. Обычно sqlite3_step() вызывается в цикле, обрабатывая каждую строку до тех пор, пока не будет возвращен SQLITE_DONE.

Строки возвращаются, как только они вычисляются. Во многих случаях это распределяет затраты на обработку между всеми вызовами sqlite3_step() и позволяет достаточно быстро вернуть первую строку. Однако, если в запросе есть предложение GROUP BY или ORDER BY, оператор может быть вынужден сначала собрать все строки в наборе результатов, прежде чем он сможет завершить окончательную обработку. В этих случаях для того, чтобы первая строка стала доступной, может потребоваться значительное время, но последующие строки должны быть возвращены очень быстро.

Столбцы результатов

Каждый раз, когда sqlite3_step() возвращает код SQLITE_ROW, в операторе становится доступна новая строка набора результатов. Вы можете использовать функции sqlite3_column_xxx() для проверки и извлечения значений столбцов из этой строки. Многие из этих функций требуют параметра индекса столбца (cidx). Как и массивы C, первый столбец в наборе результатов всегда имеет нулевой индекс, начиная слева.

int sqlite3_column_count( sqlite3_stmt *stmt )
Возвращает количество столбцов в результате запроса. Если инструкция не возвращает значения, будет возвращено нулевое значение счетчика. Допустимые индексы столбцов - от нуля до счетчика минус один. (N столбцов имеют индексы от 0 до N-1).
const char* sqlite3_column_name( sqlite3_stmt *stmt, int cidx ) const void* sqlite3_column_name16( sqlite3_stmt *stmt, int cidx )

Возвращает имя указанного столбца в виде строки в кодировке UTF-8 или UTF-16. Возвращенная строка - это имя, указанное в предложении AS в заголовке SELECT. Например, эта функция вернет person_id для нулевого столбца оператора SQL SELECT pid AS person_id,.... Если выражение AS не было задано, имя технически не определено и может изменяться от одной версии SQLite к другой. Это особенно верно для столбцов, которые состоят из выражения.

Возвращенные указатели будут оставаться действительными до тех пор, пока одна из этих функций не будет снова вызвана для того же индекса столбца или пока оператор не будет уничтожен с помощью sqlite3_finalize(). Указатели будут оставаться действительными (и неизмененными) при вызовах sqlite3_step() и sqlite3_reset(), поскольку имена столбцов не меняются от одного выполнения к другому. Эти указатели не следует передавать в sqlite3_free().

int sqlite3_column_type( sqlite3_stmt *stmt, int cidx )

Возвращает собственный тип (класс хранения) значения, найденного в указанном столбце. Допустимые коды возврата могут быть SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB или SQLITE_NULL. Чтобы получить правильный собственный тип данных, эту функцию следует вызывать перед любой попыткой извлечения данных.

Эта функция возвращает тип фактического значения, найденного в текущей строке. Поскольку SQLite позволяет хранить разные типы в одном столбце, тип, возвращаемый для определенного индекса столбца, может варьироваться от строки к строке. Таким же образом вы обнаруживаете присутствие NULL.

Эти функции sqlite3_column_xxx() позволяют вашему коду получить представление о том, как выглядит доступная строка. Определив правильный тип значения, вы можете извлечь значение с помощью одной из этих типизированных функций sqlite3_column_xxx(). Все эти функции принимают одни и те же параметры: указатель оператора и индекс столбца.

const void* sqlite3_column_blob( sqlite_stmt *stmt, int cidx )
Возвращает указатель на значение BLOB из заданного столбца. Указатель может быть недействительным, если длина BLOB равна нулю. Указатель также может иметь значение NULL, если требовалось преобразование типа.
double sqlite3_column_double( sqlite_stmt *stmt, int cidx )
Возвращает 64-битное значение с плавающей запятой из заданного столбца.
int sqlite3_column_int( sqlite_stmt *stmt, int cidx )
Возвращает 32-разрядное целое число со знаком из заданного столбца. Значение будет усечено (без предупреждения), если столбец содержит целочисленное значение, которое невозможно представить в 32-битном формате.
sqlite3_int64 sqlite3_column_int64( sqlite_stmt *stmt, int cidx )
Возвращает 64-разрядное целое число со знаком из заданного столбца.
const unsigned char* sqlite3_column_text( sqlite_stmt *stmt, int cidx ) const void* sqlite3_column_text16( sqlite_stmt *stmt, int cidx )
Возвращает указатель на строку в кодировке UTF-8 или UTF-16 из заданного столбца. Строка всегда будет завершаться нулевым символом в конце, даже если это пустая строка. Обратите внимание, что возвращаемый указатель char не имеет знака и, вероятно, потребует приведения. Указатель также может иметь значение NULL, если требовалось преобразование типа.
sqlite3_value* sqlite3_column_value( sqlite_stmt *stmt, int cidx )
Возвращает указатель на незащищенную структуру sqlite3_value. Незащищенные структуры sqlite3_value не могут безопасно подвергаться преобразованию типов, поэтому не следует пытаться извлечь примитивное значение из этой структуры с помощью функций sqlite3_value_xxx(). Если вам нужно примитивное значение, вы должны использовать одну из других функций sqlite3_column_xxx(). Единственное безопасное использование возвращенного указателя - это вызвать sqlite3_bind_value() или sqlite3_result_value(). Первый используется для привязки значения к другому подготовленному оператору, а второй используется для возврата значения в пользовательской функции SQL (см. «Привязка значений» или «Возврат результатов и ошибок»).

Нет функции sqlite3_column_null(). В ней нет необходимости. Если собственный тип данных - NULL, дополнительных значений или сведений о состоянии для извлечения не требуется.

Любые указатели, возвращаемые этими функциями, становятся недействительными, если другой вызов любой функции sqlite3_column_xxx() выполняется с использованием того же индекса столбца или при следующем вызове sqlite3_step(). Указатели также станут недействительными, если оператор будет сброшен или завершен. SQLite позаботится обо всем управлении памятью, связанной с этими указателями.

Если вы запрашиваете тип данных, который отличается от собственного значения, SQLite попытается преобразовать это значение. Таблица 7-1 описывает правила преобразования, используемые SQLite.

Таблица 7-1. Правила преобразования типов SQLite.

Исходный тип Запрошенный тип Преобразованное значение
NULL Integer 0
NULL Float 0.0
NULL Text NULL указатель
NULL BLOB NULL указатель
Integer Float Преобразовано в float
Integer Text ASCII число
Integer BLOB То же, что и текст
Float Integer Округлено до нуля
Float Text ASCII число
Float BLOB То же, что и текст
Text Integer Внутренний atoi()
Text Float Внутренний atof()
Text BLOB Без изменений
BLOB Integer Преобразовано в текст, atoi()
BLOB Float Преобразовано в текст, atof()
BLOB Text Добавлен терминатор

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

Хотя числовые значения возвращаются напрямую, текст и значения BLOB возвращаются в буфере. Чтобы определить размер этого буфера, вам нужно запросить количество байтов. Это можно сделать с помощью одной из этих двух функций.

int sqlite3_column_bytes( sqlite3_stmt *stmt, int cidx )
Возвращает количество байтов в BLOB или текстовом значении в кодировке UTF-8. При возврате размера текстового значения размер будет включать терминатор.
int sqlite3_column_bytes16( sqlite3_stmt *stmt, int cidx )
Возвращает количество байтов в текстовом значении в кодировке UTF-16, включая терминатор.

Имейте в виду, что эти функции могут вызывать преобразование данных в текстовые значения. Это преобразование может сделать недействительным любой ранее возвращенный указатель. Например, если вы вызываете sqlite3_column_text() для получения указателя на строку в кодировке UTF-8, а затем вызываете sqlite3_column_bytes16() для того же столбца, внутреннее значение столбца будет преобразовано из строки в кодировке UTF-8 в строку в кодировке UTF-16. Это сделает недействительным указатель на символ, который изначально был возвращен sqlite3_column_text().

Точно так же, если вы сначала вызываете sqlite3_column_bytes16(), чтобы получить размер строки в кодировке UTF-16, а затем вызываете sqlite3_column_text(), внутреннее значение будет преобразовано в строку UTF-8 до того, как будет возвращен указатель на строку. Это сделает недействительным значение длины, которое было изначально возвращено.

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

/* correctly extract a blob */
buf_ptr = sqlite3_column_blob( stmt, n );
buf_len = sqlite3_column_bytes( stmt, n );

/* correctly extract a UTF-8 encode string */
buf_ptr = sqlite3_column_text( stmt, n );
buf_len = sqlite3_column_bytes( stmt, n );

/* correctly extract a UTF-16 encode string */
buf_ptr = sqlite3_column_text16( stmt, n );
buf_len = sqlite3_column_bytes16( stmt, n );

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

Вы всегда должны использовать sqlite3_column_bytes() для определения размера BLOB.

Сброс и завершение

Когда вызов sqlite3_step() возвращает SQLITE_DONE, оператор успешно завершил выполнение. На этом этапе вы больше ничего не можете сделать с оператором. Если вы хотите снова использовать оператор, его необходимо сначала сбросить.

int sqlite3_reset( sqlite3_stmt *stmt )
Сбрасывает подготовленный оператор, чтобы он был готов к следующему выполнению. Оператор должен быть сброшен, как только вы закончите его использовать. Это обеспечит снятие всех блокировок.

Функцию sqlite3_reset() можно вызвать в любое время после вызова sqlite3_step(). Допустимо вызывать sqlite3_reset() до завершения выполнения оператора (то есть до того, как sqlite3_step() вернет SQLITE_DONE или индикатор ошибки). Вы не можете отменить запущенный вызов sqlite3_step() таким образом, но можете сократить возврат дополнительных значений SQLITE_ROW.

Например, если вам нужны только первые шесть строк набора результатов, вполне допустимо вызвать sqlite3_step() только шесть раз, а затем сбросить инструкцию, даже если sqlite3_step() продолжит возвращать SQLITE_ROW.

Функция sqlite3_reset() просто сбрасывает оператор, но не освобождает его. Чтобы уничтожить подготовленный оператор и освободить его память, оператор должен быть завершен.

int sqlite3_finalize( sqlite3_stmt *stmt )
Уничтожает подготовленный оператор и освобождает все связанные ресурсы.

Функцию sqlite3_finalize() можно вызвать в любое время для любого успешно подготовленного оператора. Все подготовленные операторы, связанные с подключением к базе данных, должны быть завершены до закрытия подключения к базе данных.

Хотя обе эти функции могут возвращать ошибки, они всегда выполняют свою функцию. Любая возвращенная ошибка была вызвана последним вызовом sqlite3_step(). См. «Коды результатов и коды ошибок» для получения более подробной информации.

Рекомендуется сбросить или завершить оператор, как только вы закончите его использовать. Вызов sqlite3_reset() или sqlite3_finalize() гарантирует, что оператор снимет любые блокировки, которые он может удерживать, и освободит все ресурсы, связанные с предыдущим выполнением оператора. Если приложение хранит операторы в течение длительного периода времени, они должны оставаться в состоянии сброса, готовые к привязке и выполнению.

Переходы операторов

Подготовленные операторы имеют значительный объем состояния. Помимо привязанных в данный момент значений параметров и других деталей, каждый подготовленный оператор всегда находится в одном из трех основных состояний. Первое - это состояние «ready». Любая свежеприготовленная инструкция или инструкция сброса будут «ready». Это указывает на то, что оператор готов к выполнению, но еще не был запущен. Второе состояние - «running», что указывает на то, что оператор начал выполняться, но еще не завершился. Конечное состояние - «done», что указывает на завершение выполнения инструкции.

Важно знать текущее состояние оператора. Хотя некоторые функции API можно вызывать в любое время (например, sqlite3_reset()), другие функции API можно вызывать только тогда, когда оператор находится в определенном состоянии. Например, функции sqlite3_bind_xxx() можно вызывать только тогда, когда оператор находится в состоянии «ready». На рис. 7-1 показаны различные состояния и то, как оператор переходит из одного состояния в другое.

Рисунок 7-1. Ппереходы подготовленных операторов. Оператор может находиться в одном из трех состояний. В зависимости от текущего состояния действительны только некоторые функции API. Вызов функции в неподходящем состоянии приведет к ошибке SQLITE_MISUSE.

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

Примеры

Вот два примера использования подготовленных операторов. В первом примере выполняется оператор CREATE TABLE, сначала подготавливая строку SQL, а затем вызывая функцию sqlite3_step() для выполнения оператора:

sqlite3_stmt  *stmt = NULL;

/* ... open database ... */

rc = sqlite3_prepare_v2( db, "CREATE TABLE tbl ( str TEXT )", -1, &stmt, NULL );
if ( rc != SQLITE_OK) exit( -1 );

rc = sqlite3_step( stmt );
if ( rc != SQLITE_DONE ) exit ( -1 );

sqlite3_finalize( stmt );

/* ... close database ... */

Оператор CREATE TABLE - это DDL-команда, которая не возвращает значения какого-либо типа, и ее необходимо выполнить только один раз, чтобы полностью выполнить команду. Не забудьте сбросить или завершить операторы, как только они закончат выполнение. Также помните, что все операторы, связанные с подключением к базе данных, должны быть полностью завершены, прежде чем соединение можно будет закрыть.

Второй пример немного сложнее. Этот код выполняет SELECT и перебирает sqlite3_step(), извлекая все строки в таблице. Каждое значение отображается по мере извлечения:

const char     *data = NULL;
sqlite3_stmt   *stmt = NULL;

/* ... open database ... */

rc = sqlite3_prepare_v2( db, "SELECT str FROM tbl ORDER BY 1", -1, &stmt, NULL );
if ( rc != SQLITE_OK) exit( -1 );

while( sqlite3_step( stmt ) == SQLITE_ROW ) {
    data = (const char*)sqlite3_column_text( stmt, 0 );
    printf( "%s\n", data ? data : "[NULL]" );
}

sqlite3_finalize( stmt );

/* ... close database ... */

В этом примере не проверяется тип значения столбца. Поскольку значение будет отображаться в виде строки, код зависит от внутреннего процесса преобразования SQLite и всегда запрашивает текстовое значение. Единственная сложность заключается в том, что указатель на строку может иметь значение NULL, поэтому нам нужно быть готовыми справиться с этим в операторе printf().

Связанные параметры

Параметры оператора - это специальные токены, которые вставляются в командную строку SQL перед передачей одной из функций sqlite3_prepare_xxx(). Они действуют как заполнитель для любого буквального значения, такого как просто число или строка с одинарными кавычками. После подготовки оператора, но до его выполнения, вы можете привязать определенные значения к каждому параметру оператора. Когда вы закончите выполнение оператора, вы можете сбросить его, привязать новые значения к параметрам и снова выполнить оператор - только на этот раз с новыми значениями.

Токены параметров

SQLite поддерживает пять различных стилей параметров оператора. Эти короткие строковые токены помещаются непосредственно в командную строку SQL, которую затем можно передать одной из функций sqlite3_prepare_xxx(). После подготовки оператора ссылки на отдельные параметры указываются по индексу.

?
Анонимный параметр с автоматическим индексом. По мере обработки оператора каждому анонимному параметру присваивается уникальное последовательное значение индекса, начиная с единицы.
?<index>
Параметр с явным числовым индексом. Повторяющиеся индексы позволяют связывать одно и то же значение в нескольких местах в одном операторе.
:<name>
Именованный параметр с автоматическим индексом. Повторяющиеся имена позволяют связывать одно и то же значение в нескольких местах в одном операторе.
@<name>
Именованный параметр с автоматическим индексом. Повторяющиеся имена позволяют связывать одно и то же значение в нескольких местах в одном операторе. Работает точно так же, как параметр двоеточия.
$<name>
Именованный параметр с автоматическим индексом. Повторяющиеся имена позволяют связывать одно и то же значение в нескольких местах в одном операторе. Это расширенный синтаксис для поддержки переменных Tcl. Если вы не занимаетесь программированием на Tcl, я предлагаю вам использовать формат двоеточия.

Чтобы понять, как они работают, рассмотрим этот оператор INSERT:

INSERT INTO people (id, name) VALUES ( ?, ? );

Два параметра инструкции представляют вставляемые значения id и name. Индексы параметров начинаются с единицы, поэтому первый параметр, представляющий значение id, имеет индекс, равный единице, а параметр, используемый для ссылки на значение name, имеет индекс, равный двум.

Обратите внимание, что второй параметр, который, скорее всего, является текстовым значением, не заключен в одинарные кавычки. Одиночные кавычки являются частью строкового литерала и не требуются для значения параметра.

Параметры оператора не следует заключать в кавычки. Обозначение '?' обозначает односимвольное текстовое значение, а не параметр.

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

Вы также можете использовать явные значения индекса:

INSERT INTO people (id, name) VALUES ( ?1, ?2 );

Использование явных индексов параметров имеет два основных преимущества. Во-первых, у вас может быть несколько экземпляров одного и того же значения индекса, что позволяет привязать одно и то же значение к более чем одному месту в одном операторе.

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

Этот уровень абстракции можно продвинуть еще дальше, используя именованные параметры. В этом случае вы позволяете SQLite присваивать значения индексов параметров по своему усмотрению аналогично анонимным параметрам. Разница в том, что вы можете попросить SQLite сообщить вам значение индекса для определенного параметра на основе имени, которое вы ему дали. Рассмотрим это утверждение:

INSERT INTO people (id, name) VALUES ( :id, :name );

В этом случае значения параметров довольно явные. Как мы увидим в следующем разделе, код, который привязывает значения к этим параметрам, также довольно явный, он делает очень ясным то, что происходит. Лучше всего то, что не имеет значения, добавляются ли новые параметры. Пока существующие имена остаются неизменными, код будет правильно находить и связывать именованные параметры.

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

SELECT * FROM ?;   -- INCORRECT: Cannot use a parameter as an identifier

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

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

Лично я предпочитаю использовать параметры стиля двоеточие-имя. Использование именованных параметров устраняет необходимость знать какие-либо конкретные значения индекса, позволяя вам просто ссылаться на имя во время выполнения. Использование коротких значимых имен также может облегчить понимание как ваших операторов SQL, так и кода связывания.

Значения привязки

Когда вы впервые подготавливаете оператор с параметрами, все параметры начинаются с присвоенного им значения NULL. Перед выполнением оператора вы можете привязать определенные значения к каждому параметру, используя семейство функций sqlite3_bind_xxx().

Доступно девять функций sqlite3_bind_xxx(), а также ряд служебных функций. Эти функции можно вызывать в любое время после подготовки оператора, но до первого вызова sqlite3_step(). После вызова sqlite3_step() эти функции нельзя будет вызвать снова, пока оператор не будет сброшен.

Все функции sqlite3_bind_xxx() имеют одинаковые первый и второй параметры и возвращают одинаковый результат. Первый параметр всегда является указателем на sqlite3_stmt, а второй - индекс параметра для привязки. Помните, что для анонимных параметров первое значение индекса начинается с единицы. По большей части третий параметр - это значение для привязки. Четвертый параметр, если он присутствует, указывает длину значения данных в байтах. Пятый параметр, если он присутствует, является указателем функции на обратный вызов управления памятью.

Помните, что значения индекса привязки начинаются с единицы (1), в отличие от индексов столбца результатов, которые начинаются с нуля (0).

Все функции связывания возвращают целочисленный код ошибки, который в случае успеха равен SQLITE_OK.

Функции привязки:

int sqlite3_bind_blob( sqlite3_stmt *stmt, int pidx, const void *data, int data_len, mem_callback )
Связывает BLOB двоичных данных произвольной длины.
int sqlite3_bind_double( sqlite3_stmt *stmt, int pidx, double data )
Связывает 64-битное значение с плавающей запятой.
int sqlite3_bind_int( sqlite3_stmt *stmt, int pidx, int data )
Связывает 32-разрядное целое число со знаком.
int sqlite3_bind_int64( sqlite3_stmt *stmt, int pidx, sqlite3_int64 )
Связывает 64-битное целое число со знаком.
int sqlite3_bind_null( sqlite3_stmt *stmt, int pidx )
Связывает тип данных NULL.
int sqlite3_bind_text( sqlite3_stmt *stmt, int pidx, const char *data, int data_len, mem_callback )
Связывает текстовое значение в кодировке UTF-8 произвольной длины. Длина указывается в байтах, а не в символах. Если параметр длины отрицательный, SQLite вычислит длину строки до нулевого терминатора, но не включая его. Рекомендуется, чтобы длина, вычисляемая вручную, не включала терминатор (терминатор будет включен при возврате значения).
int sqlite3_bind_text16( sqlite3_stmt *stmt, int pidx, const void *data, int data_len, mem_callback )
Связывает текстовое значение в кодировке UTF-16 произвольной длины. Длина указывается в байтах, а не в символах. Если параметр длины отрицательный, SQLite вычислит длину строки до нулевого терминатора, но не включая его. Рекомендуется, чтобы длина, вычисляемая вручную, не включала терминатор (терминатор будет включен при возврате значения).
int sqlite3_bind_zeroblob( sqlite3_stmt *stmt, int pidx, int len )
Связывает BLOB двоичных данных произвольной длины, где каждый байт равен нулю (0x00). Единственный дополнительный параметр - это значение длины в байтах. Эта функция особенно полезна для создания больших BLOB-объектов, которые затем можно обновить с помощью интерфейса инкрементных BLOB-объектов. См. sqlite3_blob_open() в приложении G для получения более подробной информации.

В дополнение к этим специфичным для типа функциям связывания существует также специализированная функция:

int sqlite3_bind_value( sqlite3_stmt *stmt, int pidx, const sqlite3_value *data_value )
Связывает тип и значение структуры sqlite3_value. Структура sqlite3_value может содержать данные любого формата.

Текстовые и BLOB-варианты sqlite3_bind_xxx() требуют, чтобы вы передали указатель буфера для значения данных. Обычно этот буфер и его содержимое должны оставаться действительными до тех пор, пока новое значение не будет привязано к этому параметру или пока оператор не будет завершен. Поскольку это может произойти в коде позже, у этих функций связывания есть пятый параметр, который управляет обработкой и, возможно, освобождением буферной памяти.

Если пятым параметром является либо NULL, либо константа SQLITE_STATIC, SQLite возьмет на себя автоматизированный подход и предположит, что буферная память либо статическая, либо код вашего приложения заботится об обслуживании и освобождении любой памяти.

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

Последний вариант - передать действительный указатель на функцию void mem_callback(void * ptr). Этот обратный вызов будет вызываться, когда SQLite завершит работу с буфером и хочет его освободить. Если буфер был выделен с помощью sqlite3_malloc() или sqlite3_realloc(), вы можете напрямую передать ссылку на sqlite3_free(). Если вы выделили буфер с другим набором вызовов управления памятью, вам необходимо передать ссылку на функцию-оболочку, которая вызывает соответствующую функцию освобождения памяти.

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

Чтобы помочь вам определить, какой индекс параметра использовать, есть три служебные функции:

int sqlite3_bind_parameter_count( sqlite3_stmt *stmt )
Возвращает целое число, указывающее наибольший индекс параметра. Если не используются явные числовые индексы ( ?<number> ), это будет количество уникальных параметров, которые появляются в операторе. Если используются явные числовые индексы, в числовой последовательности могут быть пробелы.
int sqlite3_bind_parameter_index( sqlite3_stmt *stmt, const char *name )
Возвращает индекс именованного параметра. Имя должно включать любой начальный символ (например, «:») и должно быть указано в UTF-8, даже если оператор был подготовлен из UTF-16. Если не удается найти параметр с совпадающим именем, возвращается ноль.
const char* sqlite3_bind_parameter_name( sqlite3_stmt *stmt, int pidx )
Возвращает полное текстовое представление определенного параметра. Текст всегда имеет кодировку UTF-8 и включает ведущий символ.

Используя функцию sqlite3_bind_parameter_index(), вы можете легко найти и привязать именованные параметры. Функции sqlite3_bind_xxx() правильно обнаружат недопустимый диапазон индекса, что позволит вам найти индекс и связать значение в одной строке:

sqlite3_bind_int(stmt, sqlite3_bind_parameter_index(stmt, ":pid"), pid);

Если вы хотите очистить все привязки до их начальных значений по умолчанию NULL, вы можете использовать функцию sqlite3_clear_bindings():

int sqlite3_clear_bindings( sqlite3_stmt *stmt )
Удаляет все привязки параметров в инструкции. После вызова ко всем параметрам будет привязан NULL. Это приведет к тому, что обратный вызов управления памятью будет вызываться для любого текста или значений BLOB, которые были связаны с допустимым указателем функции. В настоящее время эта функция всегда возвращает SQLITE_OK.

Если вы хотите быть абсолютно уверены в том, что связанные значения не будут просачиваться от выполнения одного оператора к другому, лучше очищать привязки каждый раз, когда вы сбрасываете оператор. Если вы выполняете ручное управление памятью в буферах данных, вы можете освободить любую память, используемую связанными значениями после вызова этой функции.

Безопасность и производительность

Использование связанных параметров дает значительные преимущества с точки зрения безопасности. Часто люди будут манипулировать строками SQL, чтобы заменить значения, которые они хотят использовать. Например, рассмотрите возможность создания оператора SQL на C с помощью строковой функции snprintf():

snprintf(buf, buf_size,
         "INSERT INTO people( id, name ) VALUES ( %d, '%s' );",
         id_val, name_val);

В этом случае нам нужны одинарные кавычки вокруг строкового значения, поскольку мы пытаемся сформировать буквальное представление. Если мы передадим эти значения C:

id_val = 23;
name_val = "Fred";

То получим следующий оператор SQL в нашем буфере:

INSERT INTO people( id, name ) VALUES ( 23, 'Fred');

Это кажется достаточно простым, но опасность такого оператора состоит в том, что переменные необходимо дезинфицировать, прежде чем они будут переданы в оператор SQL. Например, рассмотрите эти значения:

id_val = 23;
name_val = "Fred' ); DROP TABLE people;";

Это приведет к тому, что наш snprintf() создаст следующую последовательность команд SQL, при этом отдельные команды будут разделены на отдельные строки для ясности:

INSERT INTO people( id, name ) VALUES ( 23, 'Fred' );
DROP TABLE people;
' );

Хотя это последнее утверждение не имеет смысла, второе утверждение вызывает беспокойство.

К счастью, все не так плохо, как кажется. Функции sqlite3_prepare_xxx() будут готовить только один оператор (до первой точки с запятой), если вы явно не передадите оставшуюся часть строки команды SQL другому вызову sqlite3_prepare_xxx(). Это ограничивает то, что можно сделать в таком случае, если только ваш код автоматически не подготавливает и не выполняет несколько операторов из одного буфера команд.

Однако имейте в виду, что интерфейсы, предоставляемые многими языками сценариев, будут делать именно это и автоматически обрабатывать несколько переданных операторов SQL за один вызов. Удобные функции SQLite, включая sqlite3_exec(), также будут автоматически обрабатывать несколько команд SQL, переданных через одну строку. Что делает sqlite3_exec() особенно опасным, так это то, что вспомогательные функции не позволяют использовать связанные значения, вынуждая вас программно создавать операторы команд SQL и открывая вам проблемы. Позже в этой главе мы более подробно рассмотрим sqlite3_exec() и то, почему обычно это не лучший выбор.

Даже если SQLite обработает только первую команду, ущерб все равно может быть нанесен с помощью подзапросов и других команд. Плохой ввод также может привести к сбою оператора. Рассмотрим результат, если значение имени будет:

Fred', 'extra junk

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

Этот тип атаки известен как атака с использованием SQL-инъекции. Атака с использованием SQL-инъекции вставляет фрагменты команд SQL в значения данных, заставляя базу данных выполнять произвольные команды SQL. К сожалению, очень часто веб-сайты подвержены такому виду атак. Это также граничит с непростительной беспечностью, потому что, как правило, этого очень легко избежать.

Одним из способов защиты от SQL-инъекций является попытка дезинфицировать любые строковые значения, полученные из ненадежного источника. Например, вы можете попытаться заменить все символы одинарных кавычек двумя символами одинарных кавычек (стандартный механизм выхода SQL). Однако это может быть довольно сложным, и вы полностью верите в способность кода правильно дезинфицировать ненадежные строки.

Гораздо более простой способ защититься от SQL-инъекций - использовать параметры оператора SQL. Инъекционные атаки зависят от значения данных, представленного как буквальное значение в операторе команды SQL. Атака работает только в том случае, если значение атаки передается через синтаксический анализатор SQL, где он изменяет значение окружающих команд SQL.

В случае параметров SQL связанные значения никогда не проходят через синтаксический анализатор SQL. Оператор SQL анализируется только тогда, когда команда подготовлена. Если вы используете параметры, механизм SQL анализирует только токены параметров. Позже, когда вы привязываете значение к определенному параметру, это значение привязывается непосредственно в его собственном формате (т.е. строка, целое число и т. д.) и не передается через синтаксический анализатор SQL. Пока вы внимательно относитесь к тому, как извлекать и отображать строку, совершенно безопасно напрямую привязать ненадежное строковое значение к значению параметра, не опасаясь SQL-инъекции.

Помимо предотвращения атак с использованием инъекций, параметры также могут быть быстрее и использовать меньше памяти, чем операции со строками. Для использования такой функции, как snprintf(), требуется шаблон команды SQL и достаточно большой выходной буфер. Функции обработки строк также нуждаются в рабочей памяти, плюс вам могут потребоваться дополнительные буферы для копирования и очистки значений. Кроме того, ряд типов данных, таких как целые числа и числа с плавающей запятой (и особенно BLOBы), часто занимают значительно больше места в строковом представлении. Это еще больше увеличивает использование памяти. Наконец, после создания последнего буфера команд все данные необходимо передать через синтаксический анализатор SQL, где значения буквальных данных преобразуются обратно в их собственный формат и сохраняются в дополнительных буферах.

Сравните это с использованием ресурсов для подготовки и привязки оператора. При использовании параметров оператор SQL-команды по существу статичен и может использоваться как есть, без модификации или дополнительных буферов. Парсеру не нужно заниматься преобразованием и хранением буквальных значений. Фактически, значения данных обычно никогда не покидают свой собственный формат, дополнительно экономя время и память, избегая преобразования в строковое представление и из него.

Использование параметров - это безопасный и разумный выбор, даже в ситуациях, когда оператор используется только один раз. Это может занять несколько дополнительных строк кода, но процесс будет более безопасным, быстрым и более эффективным с точки зрения памяти.

Пример

В этом примере выполняется инструкция INSERT. Хотя этот оператор выполняется только один раз, он по-прежнему использует параметры привязки для защиты от возможных атак с использованием инъекций. Это избавляет от необходимости дезинфицировать входное значение.

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

char            *data = ""; /* default to empty string */
sqlite3_stmt    *stmt = NULL;
int             idx = -1;

/* ... set "data" pointer ... */
/* ... open database ... */

rc = sqlite3_prepare_v2( db, "INSERT INTO tbl VALUES ( :str )", -1, &stmt, NULL );
if ( rc != SQLITE_OK) exit( -1 );

idx = sqlite3_bind_parameter_index( stmt, ":str" );
sqlite3_bind_text( stmt, idx, data, -1, SQLITE_STATIC );

rc = sqlite3_step( stmt );
if (( rc != SQLITE_DONE )&&( rc != SQLITE_ROW )) exit ( -1 );

sqlite3_finalize( stmt );

/* ... close database ... */

В этом случае мы ищем либо возвращаемое значение SQLITE_DONE, либо SQLITE_ROW. Возможны оба варианта. Хотя сам INSERT будет полностью выполнен при первом вызове sqlite3_step(), если PRAGMA count_changes включен, тогда оператор может вернуть значение. В этом случае мы хотим игнорировать любое возможное возвращаемое значение, не вызывая ошибки, поэтому мы должны проверить оба возможных кода возврата. Для получения дополнительной информации см. count_changes в приложении F.

Возможные ловушки

Важно понимать, что со всеми параметрами должен быть связан какой-то литерал. Как только вы подготовите оператор, все параметры будут установлены в NULL. Если вы не можете связать альтернативное значение, с параметром все еще будет ассоциироваться буквальный NULL. У этого есть несколько ответвлений, которые не всегда очевидны.

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

В случае оператора INSERT невозможно принудительно использовать значение по умолчанию. Например, если у вас есть следующее утверждение:

INSERT INTO membership ( pid, gid, type ) VALUES ( :pid, :gid, :type );

Даже если для столбца типа доступно значение по умолчанию, этот оператор не может его использовать. Если вы не можете привязать значение к параметру :type, будет вставлено NULL, а не значение по умолчанию. Единственный способ вставить значение по умолчанию в столбец типа - использовать оператор, который на него не ссылается, например:

INSERT INTO membership ( pid, gid ) VALUES ( :pid, :gid );

Это означает, что если вы сильно зависите от значений по умолчанию, определенных базой данных, вам может потребоваться подготовить несколько вариантов оператора INSERT, чтобы охватить различные случаи, когда доступны разные значения данных. Конечно, если код вашего приложения знает правильные значения по умолчанию, он может просто привязать это значение к нужному параметру.

Другая область, где параметры могут вызывать сюрпризы, - это сравнения NULL. Например, рассмотрим утверждение:

SELECT * FROM employee WHERE manager = :manager;

Это работает для обычных значений, но если NULL привязан к параметру :manager, никакие строки не будут возвращены. Если вам нужна возможность проверить наличие NULL в столбце диспетчера, убедитесь, что вы используете оператор IS:

SELECT * FROM employee WHERE manager IS :manager;

Для получения дополнительной информации см. IS в приложении D.

Такое поведение также усложняет «складывание» условных выражений. Например, если у вас есть инструкция:

SELECT * FROM employee WHERE manager = :manager AND project = :project;

вы должны предоставить значимое значение как для :manager, так и для :project. Если вам нужна возможность поиска по менеджеру, по проекту или по менеджеру и по проекту, вам нужно подготовить несколько операторов или добавить немного больше логики:

...WHERE ( manager = :manager OR :manager IS NULL )
     AND ( project = :project OR :project IS NULL );

Этот запрос будет игнорировать одно (или оба) условия значения, если вы присвоите NULL соответствующим параметрам. Это выражение не позволит вам явно искать NULL, но это можно сделать с помощью дополнительных параметров и логики. Подготовка более гибких операторов уменьшает количество уникальных операторов, которыми вам нужно управлять, но также делает их более сложными и может замедлить их выполнение. Если они становятся слишком сложными, возможно, имеет смысл просто определить новый набор операторов, а не добавлять все больше и больше логики параметров к одному и тому же оператору.

Функции удобства

SQLite включает ряд удобных функций, которые можно использовать для подготовки, выполнения и завершения оператора SQL за один вызов. Большинство этих функций существует по историческим причинам и, как следует из названия, для удобства.

Хотя они не полностью устарели, есть ряд причин, по которым их использование не совсем рекомендуется. Во-первых, поймите, что под капотом ничего особенного нет. Обе эти функции в конечном итоге вызывают одни и те же вызовы sqlite3_prepare_xxx(), sqlite3_step() и sqlite3_finalize(), которые и так доступны в API. Эти функции не работают ни быстрее, ни эффективнее.

Во-вторых, поскольку API не поддерживает использование связанных параметров, вы вынуждены использовать строковые манипуляции для создания команд SQL. Это означает, что эти функции обрабатываются медленнее и гораздо более уязвимы для атак с использованием SQL-инъекций. Это особенно опасно, потому что все вспомогательные функции предназначены для автоматической обработки нескольких операторов SQL из одной командной строки. Если входные строки не очищены должным образом, эта ситуация фактически дает любому, кто предоставляет входные данные, полный доступ к ядру базы данных, включая возможность удалять данные или отбрасывать целые таблицы.

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

Несмотря на все их недостатки, остается тот простой факт, что эти функции очень удобны. Если вы просто пытаетесь собрать быстрый и грязный фрагмент кода, эти функции предоставляют простые средства для этого. Они также вполне приемлемы для команд DDL, таких как CREATE TABLE. Для любого типа команды DML, особенно для тех, которые включают значения из неподтвержденных источников, я настоятельно рекомендую использовать обычные процедуры подготовки, шага и завершения. В итоге вы получите более безопасный код и лучшую производительность.

Первая функция допускает довольно типичное выполнение любой командной строки SQL.

int sqlite3_exec( sqlite3 *db, const char *sql, callback_ptr, void *userData, char **errMsg )

Подготавливает и выполняет один или несколько операторов SQL, вызывая дополнительный обратный вызов для каждой строки набора результатов для каждого оператора. Первый параметр - это действительное соединение с базой данных. Второй параметр - это строка в кодировке UTF-8, состоящая из одного или нескольких операторов SQL. Третий параметр - указатель на функцию обратного вызова. Ниже представлен прототип этой функции. Указатель на функцию может иметь значение NULL. Четвертый параметр - это указатель пользовательских данных, который будет передан функции обратного вызова. Значение может быть любым, включая NULL. Пятый параметр - это ссылка на указатель символа. Если сгенерирована ошибка и этот параметр не равен NULL, sqlite3_exec() выделит строковый буфер и вернет его. Если переданный указатель не равен NULL, вы несете ответственность за освобождение буфера с помощью sqlite3_free() после того, как закончите с ним.

Если строка SQL состоит из нескольких операторов SQL, разделенных точкой с запятой, каждый оператор будет выполняться по очереди.

Если вызов успешен и все операторы обрабатываются без ошибок, возвращается SQLITE_OK. В противном случае возможен практически любой из других кодов возврата, поскольку эта функция выполняет весь процесс подготовки и выполнения оператора.

Функция sqlite3_exec() достаточно всеобъемлющая и может использоваться для выполнения любого оператора SQL. Если вы выполняете табличный запрос и хотите получить доступ к набору результатов, вам потребуется обеспечить указатель на функцию, который ссылается на определяемый пользователем обратный вызов. Этот обратный вызов будет вызываться один раз для каждой возвращаемой строки. Если вы выполняете инструкцию SQL, которая обычно не возвращает никакого значения базы данных, нет необходимости предоставлять функцию обратного вызова. Успех или неудача команды SQL будет указан в возвращаемом значении.

Функция sqlite3_exec() делает любые результаты базы данных доступными через определяемую пользователем функцию обратного вызова. Когда вычисляется каждая строка результата, вызывается обратный вызов, чтобы сделать данные строки доступными для вашего кода. По сути, каждый внутренний вызов sqlite3_step(), который приводит к возвращаемому значению SQLITE_ROW, приводит к обратному вызову.

Формат обратного вызова выглядит так:

int user_defined_exec_callback( void *userData, int numCol, char **colData, char **colName )

Эта функция не является частью SQLite API. Скорее, это показывает требуемый формат для определяемого пользователем обратного вызова sqlite3_exec(). Первый параметр - это указатель пользовательских данных, переданный в качестве четвертого параметра sqlite3_exec(). Второй параметр указывает, сколько столбцов существует в этой строке. И третий, и четвертый параметры возвращают массив строк (указатели на символы). Третий параметр содержит значения данных для этой строки, а четвертый параметр содержит имена столбцов. Все значения возвращаются в виде строк. Нет информации о типе.

Обычно обратный вызов должен возвращать нулевое значение. Если возвращается ненулевое значение, выполнение останавливается, и sqlite3_exec() возвращает SQLITE_ABORT.

Второй, третий и четвертый параметры действуют очень похоже на традиционные переменные C argc и argv (и дополнительный argv) в main (int argc, char ** argv), традиционном запуске каждой программы на C. Массивы значений и имен столбцов всегда будут одного и того же размера для любого заданного обратного вызова, но конкретный размер массивов и имена столбцов могут измениться в процессе обработки строки SQL с несколькими операторами. Нет необходимости выпускать какие-либо из этих значений. Как только ваша функция обратного вызова вернется, sqlite3_exec() будет обрабатывать все управление памятью.

Если вы предпочитаете не связываться с обратным вызовом, вы можете использовать sqlite3_get_table() для одновременного извлечения всей таблицы. Однако имейте в виду, что это может потреблять большой объем памяти и должно использоваться осторожно.

Хотя технически sqlite3_get_table() можно вызвать с любой командной строкой SQL, она специально разработана для работы с операторами SELECT.

int sqlite3_get_table( sqlite3 *db, const char *sql, char ***result, int *numRow, int *numCol, char **errMsg );

Подготавливает и выполняет командную строку SQL, состоящую из одного или нескольких операторов SQL. Полное содержимое набора результатов возвращается в виде массива строк UTF-8.

Первый параметр - это соединение с базой данных. Второй параметр - это командная строка SQL в кодировке UTF-8, которая состоит из одного или нескольких операторов SQL. Третий параметр - это ссылка на одномерный массив строк (указатели на символы). Результаты запроса передаются по этой ссылке. Четвертый и пятый параметры - это целочисленные ссылки, которые передают количество строк и количество столбцов, соответственно, в массиве результатов. Шестой и последний параметр является ссылкой на символьную строку и используется для возврата любого сообщения об ошибке.

Массив результатов состоит из (numCol * (numRow + 1)) записей. Записи от нуля до numCol - 1 содержат имена столбцов. Каждый дополнительный набор записей numCol содержит одну строку данных.

Если вызов успешен и все операторы обрабатываются без ошибок, возвращается SQLITE_OK. В противном случае возможен практически любой из других кодов возврата, поскольку эта функция выполняет весь процесс подготовки и выполнения оператора.

void sqlite3_free_table( char **result )
Правильно освобождает память, выделенную успешным вызовом sqlite3_get_table(). Не пытайтесь освободить эту память самостоятельно.

Как указано, вы должны освободить результат вызова sqlite3_get_table() с вызовом sqlite3_free_table(). Это правильно высвободит отдельные распределения, использованные для построения значения результата. Как и в случае с sqlite3_exec(), вы должны вызывать sqlite3_free() для любого возвращаемого значения errMsg.

Результирующий массив представляет собой одномерный массив символьных указателей. Вы должны вычислить свои собственные смещения в массиве, используя формулу:

/* offset to access column C of row R of **result */
int offset = ((R + 1) * numCol) + C;
char *value = result[offset];

«+ 1», используемый для вычисления смещения строки, необходим для пропуска имен столбцов, которые хранятся в первой строке результата. Это предполагает, что первая строка и столбец будут доступны с нулевым индексом.

В качестве вспомогательной функции в sqlite3_get_table() нет ничего особенного. Фактически, это просто оболочка для sqlite3_exec(). Он не предлагает дополнительных преимуществ в производительности по сравнению с интерфейсами подготовки, шага и завершения. Фактически, между всеми преобразованиями типов, присущими sqlite3_exec(), и всем выделением памяти sqlite3_get_table() имеет значительные накладные расходы по сравнению с другими методами.

Поскольку sqlite3_get_table() является оболочкой для sqlite3_exec(), можно передать командную строку SQL, состоящую из нескольких операторов SQL. Однако в случае sqlite3_get_table() это нужно делать с осторожностью.

Если передано более одного оператора SELECT, невозможно определить, где заканчивается один набор результатов и начинается следующий. Все результирующие строки объединяются в один большой массив результатов. Все операторы должны возвращать одинаковое количество столбцов, иначе вся команда sqlite3_get_table() завершится ошибкой. Кроме того, только первый оператор вернет имена столбцов. Чтобы избежать этих проблем, лучше всего вызывать sqlite3_get_table() с отдельными командами SQL.

Есть ряд причин, по которым эти удобные функции могут быть не лучшим выбором. Их использование требует создания оператора SQL-команды с использованием функций обработки строк, и этот процесс имеет тенденцию к ошибкам. Однако, если вы настаиваете, лучше всего использовать одну из встроенных функций построения строк SQLite: sqlite3_mprintf(), sqlite3_vmprintf() или sqlite3_snprintf(). См. приложение G для более подробной информации.

Коды результатов и коды ошибок

Возможно, вы заметили, что я довольно долго умалчиваю о кодах результатов, которые можно ожидать от ряда этих вызовов API. К сожалению, обработка ошибок в SQLite немного сложна. В какой-то момент было признано, что исходный механизм сообщения об ошибках был слишком общим и несколько сложным в использовании. Для решения этих проблем был добавлен более новый «расширенный» набор кодов ошибок, но эту новую систему нужно было расположить поверх существующей системы без нарушения обратной совместимости. В результате у нас есть как старые, так и новые коды ошибок, а также конкретные вызовы API, которые изменяют значение некоторых кодов. Все это создает довольно сложную ситуацию.

Стандартные коды

Прежде чем мы перейдем к тому, когда что-то пойдет не так, давайте быстро посмотрим, когда все идет хорошо. Как правило, любой вызов API, который просто должен указать, «что сработало», вернет константу SQLITE_OK. Однако не все коды возврата, отличные от SQLITE_OK, являются ошибками. Напомним, что sqlite3_step() возвращает SQLITE_ROW или SQLITE_DONE, чтобы указать конкретное состояние возврата.

Таблица 7-2 содержит краткий обзор стандартных кодов ошибок. На этом этапе жизненного цикла разработки маловероятно, что будут добавлены дополнительные стандартные коды ошибок. Однако дополнительные расширенные коды ошибок могут быть добавлены в любое время.

Таблица 7-2. Стандартные коды возврата SQLite

Константа кода возврата Значение кода возврата
SQLITE_OK Операция прошла успешно
SQLITE_ERROR Общая ошибка
SQLITE_INTERNAL Внутренняя ошибка библиотеки SQLite
SQLITE_PERM В разрешении на доступ отказано
SQLITE_ABORT Пользовательский код или SQL запросили прерывание
SQLITE_BUSY Файл базы данных заблокирован (обычно его можно восстановить)
SQLITE_LOCKED Таблица заблокирована
SQLITE_NOMEM Не удалось выделить память
SQLITE_READONLY Попытка записи в базу данных, доступную только для чтения
SQLITE_INTERRUPT sqlite3_interrupt() была вызвана
SQLITE_IOERR Какой-то тип ошибки ввода-вывода
SQLITE_CORRUPT Файл базы данных неверно сформирован
SQLITE_FULL База данных заполнена
SQLITE_CANTOPEN Не удалось открыть запрошенный файл базы данных
SQLITE_EMPTY Файл базы данных пуст
SQLITE_SCHEMA Схема базы данных изменилась
SQLITE_TOOBIG ТЕКСТ или BLOB превышают лимит
SQLITE_CONSTRAINT Прерывание из-за нарушения ограничений
SQLITE_MISMATCH Несоответствие типов данных
SQLITE_MISUSE API используется неправильно
SQLITE_NOLFS Хостовая ОС не может обеспечить требуемую функциональность
SQLITE_AUTH В авторизации отказано
SQLITE_FORMAT Ошибка формата вспомогательной базы данных
SQLITE_RANGE Неверный параметр привязки index
SQLITE_NOTADB Файл не является базой данных

Многие из этих ошибок довольно специфичны. Например, SQLITE_RANGE будет возвращен только одной из функций sqlite3_bind_xxx(). Другие коды, такие как SQLITE_ERROR, почти не предоставляют информации о том, что пошло не так.

Особый интерес для разработчика представляет SQLITE_MISUSE. Это указывает на попытку использовать структуру данных или вызов API неверным или иным образом недопустимым способом. Например, попытка привязать новое значение к подготовленному оператору, который находится в середине последовательности sqlite3_step(), приведет к ошибке неправильного использования. Иногда вы получаете SQLITE_MISUSE, который возникает из-за того, что не удалось должным образом обработать предыдущую ошибку, но во многих случаях это хороший показатель того, что существует еще несколько базовых концептуальных недоразумений относительно того, как библиотека предназначена для работы.

Расширенные коды

Расширенные коды были добавлены позже в цикле разработки SQLite. Они предоставляют более подробную информацию о причине ошибки. Однако, поскольку они могут изменить значение, возвращаемое определенным условием ошибки, по умолчанию они отключены. Вам необходимо явно включить их для старых вызовов API, указав библиотеке SQLite, что вы знаете о расширенных кодах ошибок и готовы их принять.

Все стандартные коды ошибок помещаются в младший байт целочисленного значения, возвращаемого большинством вызовов API. Все расширенные коды основаны на одном из стандартных кодов ошибок, но предоставляют дополнительную информацию в байтах более высокого порядка. Таким образом, расширенные коды могут предоставить более конкретную информацию о причине ошибки. В настоящее время большинство расширенных кодов ошибок предоставляют конкретные сведения о результате SQLITE_IOERR. Вы можете найти полный список расширенных кодов ошибок на http://sqlite.org/c3ref/c_ioerr_access.html.

Функции обработки ошибок

Следующие API-интерфейсы используются для включения расширенных кодов ошибок и извлечения дополнительной информации о любых текущих состояниях ошибок.

int sqlite3_extended_result_codes( sqlite3 *db, int onoff )
Включает или отключает расширенные коды результатов и ошибок для этого соединения с базой данных. Для подключений к базе данных, возвращаемых любой версией sqlite3_open_xxx(), по умолчанию расширенные коды отключены. Вы можете включить их, передав ненулевое значение во втором параметре. Эта функция всегда возвращает SQLITE_OK - нет способа извлечь текущее состояние кода результата.
int sqlite3_errcode( sqlite3 *db )
Если операция с базой данных возвращает состояние, отличное от SQLITE_OK, последующий вызов этой функции вернет код ошибки. По умолчанию возвращается только стандартный код ошибки, но если включены расширенные коды результатов, он также может возвращать один из расширенных кодов.
int sqlite3_extended_errcode( sqlite3 *db )
По сути то же самое, что и sqlite3_errcode(), за исключением того, что всегда возвращаются расширенные результаты.
const char* sqlite3_errmsg( sqlite3 *db ) const void* sqlite3_errmsg16( sqlite3 *db )
Возвращает понятную человеку строку ошибки на английском языке с завершающим нулем, закодированную в UTF-8 или UTF-16. Любые дополнительные вызовы API SQLite, использующие это соединение с базой данных, могут привести к тому, что эти указатели станут недействительными, поэтому вы должны либо использовать строку перед попыткой любых других операций, либо вам следует сделать частную копию. Эти функции также могут возвращать нулевой указатель, поэтому проверьте значение результата перед его использованием. Расширенные коды ошибок не используются.

Допустимо не включать расширенные коды ошибок и смешивать вызовы sqlite3_errcode() и sqlite3_extended_errcode().

Поскольку состояние ошибки хранится в соединении с базой данных, в многопоточном приложении легко получить условные обозначения гонки. Если вы разделяете соединение с базой данных между потоками, лучше всего обернуть ваш основной вызов API и код проверки ошибок в критическом разделе. Вы можете получить блокировку мьютекса соединения с базой данных с помощью sqlite3_db_mutex(). См. sqlite3_db_mutex() в приложении G для получения более подробной информации.

Точно так же система обработки ошибок не может справиться с множественными ошибками. Если есть ошибка, которая не отмечена, следующий вызов основной функции API, скорее всего, вернет SQLITE_MISUSE, указывая на попытку использовать недопустимую структуру данных. В этой и подобных ситуациях, когда было обнаружено несколько ошибок, состояние сообщения об ошибке может стать несогласованным. Вам необходимо проверять и обрабатывать любые ошибки после каждого вызова API.

Подготовленные операторы v2

Помимо стандартного и расширенного кодов, новые версии _v2 sqlite3_prepare_xxx() изменяют способ обработки ошибок подготовленных операторов. Хотя более новая и исходная версии sqlite3_prepare_xxx() используют одни и те же параметры, sqlite3_stmt, возвращаемый версиями _v2, немного отличается.

Наиболее заметная разница в том, как обрабатываются ошибки sqlite3_step(). Для операторов, подготовленных с помощью исходной версии sqlite3_prepare_xxx(), большинство ошибок в sqlite3_step() вернет довольно общий SQLITE_ERROR. Чтобы выяснить специфику ситуации, вам нужно вызвать sqlite3_reset() или sqlite3_finalize() для извлечения более подробного кода ошибки. Это, конечно, приведет к сбросу или завершению инструкции, что ограничит ваши возможности восстановления.

Все работает немного иначе, если оператор был подготовлен с версией _v2. В этом случае sqlite3_step() вернет конкретную ошибку напрямую. Вызов sqlite3_step() может возвращать стандартный код или расширенный код, в зависимости от того, включены ли расширенные коды или нет. Это позволяет разработчику напрямую извлечь ошибку и предоставляет больше возможностей для восстановления.

Другое важное отличие заключается в том, как обрабатываются изменения схемы базы данных. Если выдается какая-либо команда языка определения данных (например, DROP TABLE), есть вероятность, что подготовленный оператор больше не действителен. Например, подготовленный оператор может ссылаться на таблицу или индекс, которых больше нет. Единственный способ решить любые возможные проблемы - это перестроить операцию.

Версии _v2 sqlite3_prepare_xxx() делают копию оператора SQL, используемого для подготовки оператора. (Этот SQL можно извлечь. Подробнее см. sqlite3_sql() в приложении G.) Сохраняя внутреннюю копию SQL, оператор может представлять себя при изменении схемы базы данных. Это делается автоматически каждый раз, когда SQLite обнаруживает необходимость перестроить оператор.

Операторы, созданные с помощью исходной версии prepare, не сохранили копию команды SQL, поэтому они не смогли восстановить себя. В результате при каждом изменении схемы вызов API, включающий любой ранее подготовленный оператор, будет возвращать SQLITE_SCHEMA. Затем программа должна будет воспроизвести оператор, используя исходный SQL, и повторить попытку. Если изменение схемы было достаточно значительным, чтобы SQL больше не действовал, sqlite3_prepare_xxx() вернет соответствующую ошибку, когда программа попытается повторно подготовить команду SQL.

Операторы, созданные с помощью prepare версии _v2, по-прежнему могут возвращать SQLITE_SCHEMA. Если обнаружено изменение схемы и оператор не может автоматически повторно подготовиться, он все равно вернет SQLITE_SCHEMA. Однако при подготовке _v2 это теперь считается фатальной ошибкой, так как нет возможности восстановить инструкцию.

Вот параллельное сравнение основных различий между исходной версией prepare и версией _v2:

Оператор, подготовленный с использованием исходной версии Оператор, подготовленный с помощью версии v2
Создано с помощью sqlite3_prepare() или sqlite3_prepare16(). Создано с помощью sqlite3_prepare_v2() или sqlite3_prepare16_v2().
Большинство ошибок в sqlite3_step() возвращают SQLITE_ERROR. sqlite3_step() напрямую возвращает конкретные ошибки.
Чтобы получить полную ошибку? необходимо вызвать sqlite3_reset() или sqlite3_finalize(). Могут быть возвращены стандартные или расширенные коды ошибок. Больше ничего называть не нужно. sqlite3_step() может возвращать стандартный или расширенный код ошибки.
Изменения схемы заставят любую операторную функцию вернуть SQLITE_SCHEMA. Приложение должно вручную завершить и заново составить отчет. Изменения схемы заставят оператор заново подготовиться.
Если предоставленный приложением SQL более недействителен, подготовка завершится ошибкой. Если внутренний SQL больше не действителен, любая операторная функция вернет SQLITE_SCHEMA. Это фатальная ошибка оператора, и единственный выход - завершить оператор.
Исходный SQL не связан с оператором. Оператор хранит копию SQL, используемого для подготовки. SQL можно восстановить с помощью sqlite3_sql().
Ограниченная отладка. Можно использовать sqlite3_trace().

Поскольку обработка ошибок _v2 намного проще и из-за возможности автоматического восстановления после изменений схемы, настоятельно рекомендуется, чтобы все новые разработки использовали sqlite3_prepare_xxx() версии _v2.

Транзакции и ошибки

Транзакции и контрольные точки добавляют уникальный поворот к процессу исправления ошибок. Обычно SQLite работает в режиме автоматической фиксации. В этом режиме SQLite автоматически помещает каждую команду SQL в отдельную транзакцию. С точки зрения API, это время с момента первого вызова sqlite3_step() до возврата SQLITE_DONE функцией sqlite3_step() (или когда вызывается sqlite3_reset() или sqlite3_finalize()).

Если каждый оператор заключен в отдельную транзакцию, восстановление после ошибки будет достаточно простым. Каждый раз, когда SQLite оказывается в состоянии ошибки, он может просто откатить текущую транзакцию, эффективно отменив текущую команду SQL и вернув базу данных в то состояние, в котором она была до запуска команды.

После выполнения команды BEGIN TRANSACTION SQLite больше не находится в режиме автоматической фиксации. Транзакция открывается и остается открытой до тех пор, пока не будет дана команда END TRANSACTION или COMMIT TRANSACTION. Это позволяет объединить несколько команд в одну транзакцию. Хотя это полезно для группировки серии дискретных команд в атомарное изменение, это также ограничивает возможности SQLite для восстановления после ошибок.

Когда во время явной транзакции обнаруживается ошибка, SQLite пытается сохранить работу и отменить только текущий оператор. К сожалению, это не всегда возможно. Если что-то пойдет не так, у SQLite иногда не останется иного выбора, кроме как откатить текущую транзакцию.

Ошибки, которые, скорее всего, приведут к откату: SQLITE_FULL (база данных или диск заполнена), SQLITE_IOERR (ошибка ввода-вывода на диске или заблокированный файл), SQLITE_BUSY (база данных заблокирована), SQLITE_NOMEM (нехватка памяти) и SQLITE_INTERRUPT (прерывание, запрошенное приложением). Если вы обрабатываете явную транзакцию и получаете одну из этих ошибок, вам необходимо иметь дело с возможностью отката транзакции.

Чтобы выяснить, какое действие было выполнено SQLite, вы можете использовать функцию sqlite3_get_autocom mit().

int sqlite3_get_autocommit( sqlite3 *db )
Возвращает текущее состояние фиксации. Ненулевое возвращаемое значение указывает, что база данных находится в режиме автоматической фиксации, а не в явной транзакции. Нулевое значение указывает, что база данных в настоящее время находится внутри явной транзакции.

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

Хотя бывают ситуации, когда можно восстановить и продолжить транзакцию, рекомендуется всегда выдавать ROLLBACK при обнаружении одной из этих ошибок. В ситуациях, когда SQLite уже был вынужден откатить транзакцию и вернулся в режим автоматической фиксации, ROLLBACK не сделает ничего, кроме возврата ошибки, которую можно безопасно проигнорировать.

Блокировка базы данных

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

Система блокировки включает несколько разных уровней блокировок, которые используются для уменьшения конфликтов и предотвращения взаимоблокировок. Детали несколько сложны, но система позволяет нескольким соединениям читать файл базы данных параллельно, но любая операция записи требует полного, монопольного доступа ко всему файлу базы данных. Если вам нужна полная информация, см. http://www.sqlite.org/lockingv3.html.

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

Однако, если более одного соединения пытаются получить доступ к одной и той же базе данных одновременно, рано или поздно они столкнутся друг с другом. Обычно, если операция требует блокировки, которую соединение с базой данных не может получить, SQLite возвращает ошибку SQLITE_BUSY или, в некоторых более крайних случаях, SQLITE_IOERR (расширенный код SQLITE_IOERR_BLOCKED). Все функции sqlite3_prepare_xxx(), sqlite3_step(), sqlite3_reset() и sqlite3_finalize() могут возвращать SQLITE_BUSY. Функции sqlite3_backup_step() и sqlite3_blob_open() также могут возвращать SQLITE_BUSY, поскольку эти функции внутренне используют sqlite3_prepare_xxx() и sqlite3_step(). Наконец, sqlite3_close() может возвращать SQLITE_BUSY, если есть незавершенные операторы, связанные с подключением к базе данных, но это не связано с блокировкой.

Чтобы получить доступ к необходимому замку, нужно просто дождаться, пока текущий держатель закончит работу и снимет блокировку. В большинстве случаев это не очень длительный период времени. Ожидание может выполняться либо приложением, которое может ответить на SQLITE_BUSY, просто пытаясь повторно обработать оператор и повторить попытку, либо это может быть выполнено с помощью обработчика занятости.

Обработчики занятости

Обработчик занятости - это функция обратного вызова, которая вызывается библиотекой SQLite каждый раз, когда она не может получить блокировку, но определила, что можно безопасно попробовать и дождаться ее. Обработчик занятости может дать команду SQLite продолжать попытки получить блокировку или отказаться и вернуть ошибку SQLITE_BUSY.

SQLite включает внутренний обработчик занятости, который использует таймер. Если вы установите период тайм-аута, SQLite будет продолжать попытки получить требуемые блокировки, пока не истечет время таймера.

int sqlite3_busy_timeout( sqlite3 *db, int millisec )
Устанавливает для данного соединения с базой данных использование внутреннего обработчика занятости на основе таймера. Если второй параметр больше нуля, обработчик настроен на использование значения тайм-аута в миллисекундах (тысячных долях секунды). Если второй параметр равен нулю или отрицателен, любой обработчик занятости будет очищен.

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

int sqlite3_busy_handler( sqlite3 *db, callback_func_ptr, void *udp )
Устанавливает обработчик занятости для данной базы данных. Второй параметр - это указатель функции на обработчик занятости, а третий параметр - указатель пользовательских данных, который передается в функцию обратного вызова. Установка указателя функции NULL удалит обработчик занятости.
int user_defined_busy_handler_callback( void *udp, int incr )

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

Возвращаемое значение нуля приведет к тому, что SQLite откажется и вернет ошибку SQLITE_BUSY, в то время как ненулевое возвращаемое значение заставит SQLite продолжать попытки получить блокировку. Если блокировка успешно установлена, обработка команды продолжится. Если блокировка не получена, обработчик занятости будет вызван снова.

Имейте в виду, что каждое соединение с базой данных имеет только один обработчик занятости. Вы не можете установить обработчик занятости приложения и настроить значение тайм-аута занятости одновременно. Любой вызов одной из этих функций аннулирует другую.

Тупиковые ситуации

Установка обработчика занятости не решит всех проблем. Бывают ситуации, когда ожидание блокировки приведет к тупиковой ситуации соединения с базой данных. Тупиковая ситуация возникает, когда каждая пара соединений с базой данных имеет некоторый набор блокировок, и обоим необходимо получить дополнительные блокировки для завершения своей задачи. Если каждое соединение пытается получить доступ к блокировке, удерживаемой в данный момент другим соединением, оба соединения останутся в тупике. Это может произойти, если два соединения с базой данных попытаются записать в базу данных одновременно. В этом случае нет смысла в том, чтобы оба соединения с базой данных ожидали снятия блокировок, поскольку единственный способ продолжить - это если одно из соединений разорвется и освободит все свои блокировки.

Если SQLite обнаруживает потенциальную ситуацию взаимоблокировки, он пропускает обработчик занятости и немедленно возвращает SQLITE_BUSY одним из подключений к базе данных. Это сделано, чтобы побудить приложения снять блокировки и выйти из тупика. Преодоление тупиковой ситуации является обязанностью соответствующего приложения (приложений) - SQLite не может справиться с этой ситуацией за вас.

Избегайте SQLITE_BUSY

При разработке кода для системы, требующей любой степени параллелизма с базами данных, самый простой подход - использовать sqlite3_busy_timeout() для установки значения тайм-аута, приемлемого для вашего приложения. Начните с примерно 250-2000 миллисекунд и оттуда отрегулируйте. Это поможет уменьшить количество кодов ответов SQLITE_BUSY, но не устранит их.

Единственный способ полностью избежать использования SQLITE_BUSY - гарантировать, что к базе данных никогда не будет подключаться более одного соединения с базой данных. Это можно сделать, установив для PRAGMA lock_mode значение EXCLUSIVE.

Если это неприемлемо, приложение может использовать транзакции, чтобы упростить обработку кода возврата SQLITE_BUSY. Если приложение может успешно начать транзакцию с BEGIN EXCLUSIVE TRANSACTION, это исключит возможность получения SQLITE_BUSY. Сам BEGIN может вернуть SQLITE_BUSY, но в этом случае приложение может просто сбросить оператор BEGIN с помощью sqlite3_reset() и повторить попытку. Недостатком BEGIN EXCLUSIVE является то, что он может быть запущен только тогда, когда никакое другое соединение не обращается к базе данных, включая любые транзакции только для чтения. После запуска эксклюзивной транзакции она также блокирует доступ к базе данных для всех других подключений, включая транзакции только для чтения.

Чтобы обеспечить больший параллелизм, приложение может использовать BEGIN IMMEDIATE TRANSACTION. Если транзакция IMMEDIATE запущена успешно, очень маловероятно, что приложение получит SQLITE_BUSY, пока не будет выполнен оператор COMMIT. Во всех случаях (включая COMMIT), если встречается SQLITE_BUSY, приложение может сбросить инструкцию, подождать и повторить попытку. Как и в случае с BEGIN EXCLUSIVE, инструкция BEGIN IMMEDIATE может возвращать SQLITE_BUSY, но приложение может просто сбросить инструкцию BEGIN и повторить попытку. Транзакция BEGIN IMMEDIATE может быть запущена, пока другие соединения считывают данные из базы данных. После запуска новые соединения на запись не будут разрешены, но соединения только для чтения могут продолжать обращаться к базе данных до тех пор, пока немедленная транзакция не будет вынуждена изменить файл базы данных. Обычно это происходит, когда транзакция фиксируется. Если все соединения с базой данных используют BEGIN IMMEDIATE для всех транзакций, которые изменяют базу данных, то взаимоблокировка невозможна, и все ошибки SQLITE_BUSY (как для модулей записи IMMEDIATE, так и для других считывателей) могут быть обработаны повторной попыткой.

Наконец, если приложение может успешно начать транзакцию любого типа (включая транзакцию по умолчанию, DEFERRED), оно никогда не должно получать SQLITE_BUSY (или рисковать тупиковой ситуацией), если оно не пытается изменить базу данных. Сам BEGIN может вернуть SQLITE_BUSY, но приложение может сбросить инструкцию BEGIN и повторить попытку.

Попытки изменить базу данных в рамках транзакции BEGIN DEFERRED (или в рамках автоматической фиксации) - единственные ситуации, когда база данных может заблокироваться, и единственные ситуации, когда ответ на SQLITE_BUSY должен выходить за рамки простого ожидания и повторной попытки (или разрешения занятый обработчик справится с этим). Если приложение выполняет изменения в отложенной транзакции, оно должно быть готово к возможной тупиковой ситуации.

Избежание тупиковых ситуаций

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

Во-первых, простые. Функции sqlite3_prepare_xxx(), sqlite3_backup_step() и sqlite3_blob_open() не могут вызвать тупик. Если код SQLITE_BUSY возвращается из одной из этих функций в любое время, просто подождите и вызовите функцию еще раз.

Если функция sqlite3_step(), sqlite3_reset() или sqlite3_finalize() возвращает SQLITE_BUSY из отложенной транзакции, приложение должно отступить и повторить попытку. Для операторов, которые не являются частью явной транзакции, подготовленный оператор можно просто сбросить и повторно выполнить. Для операторов, которые находятся внутри явно отложенной транзакции, вся транзакция должна быть откатана и запущена с самого начала. В большинстве случаев это произойдет при первой попытке изменить базу данных. Просто помните, что вся причина отката заключается в том, что какое-то другое соединение с базой данных должно изменить базу данных. Если приложение выполнило несколько операций чтения для подготовки операции записи, было бы лучше перечитать эту информацию в новой транзакции, чтобы убедиться, что данные все еще действительны.

Что бы вы ни делали, не игнорируйте ошибки SQLITE_BUSY. Они могут быть редкими, но они также могут стать источником большого разочарования при неправильном обращении.

Когда BUSY становится BLOCKED

Когда соединение с базой данных требует изменения базы данных, устанавливается блокировка, которая делает базу данных доступной только для чтения. Это позволяет другим соединениям продолжать читать базу данных, но не позволяет им вносить изменения. Фактические изменения хранятся в кэше страниц базы данных и еще не записываются в файл базы данных. Запись изменений сделает изменения видимыми для других подключений к базе данных, нарушив правило изоляции транзакций. Поскольку изменения еще не зафиксированы, их можно кэшировать в памяти.

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

Этот процесс позволяет транзакциям только для чтения продолжать выполняться, пока выполняется транзакция записи. Считыватели должны быть заблокированы только тогда, когда писатель действительно совершает свою транзакцию. Однако ключевым предположением в этом процессе является то, что изменения помещаются в кеш страницы базы данных и их не нужно записывать до тех пор, пока транзакция не будет зафиксирована. Если кеш заполняется страницами, содержащими ожидающие изменения, писатель не имеет другого выбора, кроме как установить монопольную блокировку базы данных и очистить кеш до этапа фиксации. Транзакцию можно откатить в любой момент, но автору записи должен быть предоставлен немедленный доступ к исключительной блокировке записи, чтобы выполнить очистку кеша.

Если эта блокировка недоступна немедленно, писатель вынужден прервать всю транзакцию. Транзакция записи будет отменена, и будет возвращен расширенный код результата SQLITE_IOERR_BLOCKED (стандартный код SQLITE_IOERR). Поскольку транзакция автоматически откатывается, у приложения не так много вариантов, кроме как начать транзакцию заново.

Чтобы избежать этой ситуации, лучше начинать большие транзакции, которые изменяют множество строк с явным BEGIN EXCLUSIVE. Этот вызов может завершиться ошибкой с SQLITE_BUSY, но приложение может просто повторять команду до тех пор, пока она не завершится успешно. После запуска монопольной транзакции транзакция записи будет иметь полный доступ к базе данных, что исключает возможность SQLITE_IOERR_BLOCKED, даже если транзакция выльется из кеша до фиксации. Также может помочь увеличение размера кеша базы данных.

Служебные функции

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

Управление версиями

Для запроса версии библиотеки SQLite доступно несколько функций. Каждому вызову API соответствует макрос #define, объявляющий одно и то же значение.

SQLITE_VERSION const char* sqlite3_libversion( )
Возвращает версию библиотеки SQLite в виде строки UTF-8.
SQLITE_VERSION_NUMBER int sqlite3_libversion_number( )
Возвращает версию библиотеки SQLite в виде целого числа. Формат - MNNNPPP, где M - мажорная версия (в данном случае 3), N - минорная версия, а P - патч. Этот формат допускает выпуски до 3.999.999. Если выпущен подпункт патча, он не будет указан в этом номере версии.
SQLITE_SOURCE_ID const char* sqlite3_sourceid( )
Возвращает отметку о регистрации кода, используемого в этом выпуске. Строка состоит из даты, отметки времени и хэша SHA1 источника из исходного репозитория.

Если вы создаете собственное приложение, вы можете использовать макросы #define и вызовы функций, чтобы убедиться, что вы используете правильный заголовок для доступной библиотеки. Значения #define берутся из файла заголовка и устанавливаются при компиляции вашего приложения. Вызов функций возвращает те же значения, которые были заложены в библиотеку при ее компиляции.

Если вы используете какую-либо динамическую библиотеку, вы можете использовать эти макросы и функции, чтобы предотвратить связывание вашего приложения с версией библиотеки, отличной от той, с которой оно было изначально скомпилировано. Однако это может быть не очень хорошо, так как это также предотвратит обновления. Если вам нужно заблокировать определенную версию, вам, вероятно, следует использовать статическую библиотеку.

Если вы хотите проверить достоверность динамической библиотеки, возможно, лучше сделать что-то вроде этого:

if ( SQLITE_VERSION_NUMBER > sqlite3_libversion_number( ) ) {
    /* library too old; report error and exit. */
}

Помните, что макрос будет содержать версию, использованную при компиляции вашего кода, в то время как вызов функции вернет версию библиотеки SQLite. В этом случае мы сообщаем об ошибке, если библиотека более старая (меньшая версия), чем та, которая использовалась для компиляции и сборки кода приложения.

Управление памятью

Когда SQLite необходимо динамически выделять память, он обычно вызывает обработчик памяти по умолчанию базовой операционной системы. Это заставляет SQLite выделять свою память из кучи приложения. Однако SQLite также можно настроить для управления собственной внутренней памятью (см. sqlite3_config() в приложении G). Это особенно важно для встроенных и портативных устройств, где память ограничена, а превышение доступности может привести к проблемам со стабильностью.

В любом случае, вы можете получить доступ к любому менеджеру памяти, который использует SQLite, с помощью этих функций управления памятью SQLite:

void* sqlite3_malloc( int numBytes )
Выделяет и возвращает буфер указанного размера. Если память не может быть выделена, возвращается NULL-указатель. Память всегда будет выровнена по 8 байт (64 бита). Это замена стандартной функции malloc() библиотеки C.
void* sqlite3_realloc( void *buffer, int numBytes )

Используется для изменения размера выделения памяти. Буферы могут быть больше или меньше. Учитывая буфер, ранее возвращенный sqlite3_malloc(), и количество байтов, *_realloc() выделит новый буфер указанного размера и скопирует столько старого буфера, сколько поместится в новый буфер. Затем он освободит старый буфер и вернет новый. Если новый буфер не может быть выделен, возвращается NULL, а исходный буфер не освобождается.

Если указатель буфера равен NULL, вызов эквивалентен вызову sqlite3_malloc(). Если параметр numBytes равен нулю или отрицателен, вызов эквивалентен вызову sqlite3_free().

Это замена стандартной функции realloc() библиотеки C.

void sqlite3_free( void *buffer )

Освобождает буфер памяти, ранее выделенный sqlite3_malloc() или sqlite3_realloc(). Также используется для освобождения результатов или буферов ряда функций API SQLite, которые вызывают внутри себя sqlite3_malloc().

Это замена стандартной функции free() библиотеки C.

Хотя для ряда вызовов SQLite требуется использование sqlite3_free(), код приложения может использовать любое наиболее подходящее управление памятью. Эти функции становятся чрезвычайно полезными при написании пользовательских функций, виртуальных таблиц или любого типа загружаемого модуля. Поскольку этот тип кода предназначен для работы в любой среде SQLite, вы, вероятно, захотите использовать эти функции управления памятью, чтобы обеспечить максимальную переносимость вашего кода.

Резюме

Обладая информацией из этой главы, вы сможете написать код, открывающий файл базы данных и выполняющий для него команды SQL. Благодаря этой способности и правильным командам SQL вы сможете создавать новые базы данных, указывать таблицы, вставлять данные и строить сложные запросы. Для большого количества приложений этого достаточно, чтобы удовлетворить большинство их потребностей в базе данных.

В следующих главах рассматриваются более сложные функции SQLite C API. Это включает в себя возможность определять свои собственные функции SQL. Это позволит вам расширить SQL, используемый SQLite, с помощью простых функций, а также агрегаторов (используемых с GROUP BY) и сортировки параметров сортировки. В дополнительных главах будет рассказано, как реализовать виртуальные таблицы и другие расширенные функции.

Глава 8.
Дополнительные функции и API

В этой главе затрагивается ряд различных областей, в основном связанных с функциями и интерфейсами, выходящими за рамки базового механизма базы данных. В первом разделе рассматриваются функции времени и даты SQLite, которые представлены в виде небольшого набора скалярных функций. Мы также кратко рассмотрим некоторые стандартные расширения, поставляемые с SQLite, такие как расширение интернационализации ICU, модуль текстового поиска FTS3 и модуль R*Tree. Мы также рассмотрим некоторые альтернативные интерфейсы, доступные для SQLite на разных языках сценариев и в других средах. В заключение мы кратко обсудим некоторые моменты, на которые следует обращать внимание при разработке на мобильных или встроенных системах.

Дата и время

Большинство продуктов для реляционных баз данных имеют несколько собственных типов данных, которые имеют дело с датами, временем, отметками времени и продолжительностью записи всех видов. SQLite этого не делает. Вместо того, чтобы иметь определенные типы данных, SQLite предоставляет небольшой набор функций преобразования времени и даты. Эти функции могут использоваться для преобразования информации о времени, дате или продолжительности в один из более общих типов данных, например число или текстовое значение, или наоборот.

Этот подход хорошо согласуется с простыми и гибкими целями проектирования SQLite. Даты и время могут быть очень сложными. Нечетные часовые пояса и изменение правил перехода на летнее время (DST) могут усложнить значения времени, в то время как даты за пределами последних нескольких сотен лет подлежат календарным системам, которые менялись и модифицировались на протяжении всей истории. Создание собственного типа потребует выбора определенной календарной системы и определенного набора правил преобразования, которые могут подходить или не подходить для поставленной задачи. Это одна из причин, по которой типичная база данных имеет так много разных типов данных времени и даты.

Использование внешних функций преобразования намного более гибкое. Разработчик может выбрать формат и тип данных, которые лучше всего соответствуют потребностям приложения. Использование более простых базовых типов данных также намного лучше подходит для системы динамической типизации SQLite. Более общий подход также позволяет упростить внутренний код и уменьшить его размер, что является плюсом для большинства сред SQLite.

Требования к приложению

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

Во-первых, вам нужно выяснить, что вы пытаетесь сохранить. Это может быть время дня, например, отдельное значение часа, минуты, секунды без какой-либо связанной даты. Вам может потребоваться запись даты, которая относится к определенному дню, месяцу, году, но не имеет связанного времени. Многим приложениям требуются метки времени, которые включают дату и время для обозначения определенного момента времени. Некоторым приложениям необходимо записывать определенный день года, но не конкретный год (например, праздник). Многие приложения также используют длительности (временные дельты). Даже если приложение не хранит длительности или смещения, они часто вычисляются для целей отображения, таких как промежуток времени между «сейчас» и определенным событием.

Также стоит учитывать диапазон и точность, необходимые для вашего приложения. Как уже говорилось, даты в далеком прошлом (или далеком будущем) плохо представлены некоторыми календарными системами. База данных, в которой хранятся бронирования конференц-зала, может требовать минутной точности, в то время как база данных, содержащая дампы сетевых пакетов, может требовать точности до микросекунды или выше.

Представления

Как и в случае со многими другими типами данных, класс данных, которые приложение должно хранить, наряду с требуемым диапазоном и точностью, часто определяет, какое представление использовать. Двумя наиболее распространенными представлениями, используемыми SQLite, являются форматированное текстовое значение или значение с плавающей запятой.

Юлианский день

Самое простое и компактное представление - это Юлианский день. Это единственное значение с плавающей запятой, используемое для подсчета количества дней, прошедших с полудня по гринвичскому времени 24 ноября 4714 г. до н.э. SQLite использует пролептический григорианский календарь для этого представления. Полночь 1 января 2010 года по юлианскому календарю составляет 2455197,5. При хранении в виде 64-битного значения с плавающей запятой современные даты имеют точность чуть лучше одной миллисекунды.

Многие разработчики никогда не сталкивались с календарем по юлианскому календарю, но концептуально он не сильно отличается от более знакомого значения POSIX time() - просто используется другое значение (дни, а не секунды) и другая отправная точка.

Значения по юлианскому дню имеют относительно компактный формат хранения и с ними довольно легко работать. Длительности и различия вычислить просто и эффективно, и они используют то же представление данных, что и моменты времени. Значения автоматически нормализуются, и их можно использовать с помощью простых математических операций. Юлианские значения также могут выражать очень широкий диапазон дат, что делает их полезными для исторических записей. Главный недостаток в том, что они требуют преобразования перед отображением.

Текстовые значения

Другое популярное представление - форматированное текстовое значение. Обычно они используются для хранения значения даты, значения времени или их комбинации. Хотя SQLite распознает несколько форматов, чаще всего даты указываются в формате YYYY-MM-DD, а время - в формате HH:MM:SS с использованием значения часа от 00 до 23. Если требуется полная временная метка, эти значения можно комбинировать. Например, YYYY-MM-DD HH:MM:SS. Хотя этот стиль даты может быть не самым естественным представлением, эти форматы основаны на международном стандарте ISO 8601 для представления даты и времени. У них также есть преимущество сортировки в хронологическом порядке с помощью простой строковой сортировки.

Основное преимущество использования текстовых представлений в том, что их очень легко читать. Сохраненные значения не требуют какого-либо перевода и могут быть легко просмотрены и поняты в их собственном формате. Вы также можете выбрать, какие части значения данных требуются, при сохранении только даты или только времени дня, что делает его немного более ясным в отношении цели значения. Или, по крайней мере, это было бы правдой, если бы не проблема часового пояса. Как мы увидим, время и дата редко хранятся относительно местного часового пояса, поэтому даже текстовые значения обычно требуют преобразования перед отображением.

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

Текстовые значения также требуют тщательной нормализации всех входных значений в стандартизованный формат. Многие операции, такие как сортировка и простые сравнения, требуют, чтобы значения использовали один и тот же формат. Альтернативные форматы могут привести к тому, что эквивалентные значения времени будут представлены неэквивалентными текстовыми значениями. Это может привести к противоречивым результатам любых процедур, которые работают непосредственно с текстовым представлением. Проблема не ограничивается отдельными столбцами. Если значение времени или даты используется в качестве ключевого столбца или столбца соединения, эти операции будут работать правильно только в том случае, если все значения времени и даты используют один и тот же формат.

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

Часовые пояса

Вы могли заметить, что ни один из этих форматов не поддерживает поле часового пояса. SQLite предполагает, что вся информация о времени и дате хранится в формате UTC или всемирном координированном времени. UTC - это, по сути, среднее время по Гринвичу, хотя есть некоторые незначительные технические различия.

Использование времени UTC дает ряд существенных преимуществ. В первую очередь, UTC не зависит от местоположения. Это может показаться мелочью, но если ваша база данных находится на мобильном устройстве, она перемещается. Иногда он будет перемещаться по часовым поясам. Любое отображаемое значение времени лучше сдвигать вместе с устройством.

Если ваша база данных доступна через Интернет, велика вероятность, что к ней будут обращаться из более чем одного часового пояса. Короче говоря, вы не можете игнорировать проблему часового пояса, и рано или поздно вам придется переводить между часовыми поясами. Наличие универсального базового формата значительно упрощает этот процесс.

Аналогичным образом, на UTC не влияет переход на летнее время (Daylight Saving Time (DST)). Нет никаких сдвигов, скачков или повторов значений UTC. Правила перехода на летнее время чрезвычайно сложны и могут легко отличаться в зависимости от местоположения, времени года или даже самого года, поскольку время перехода смещается и перемещается. DST по существу добавляет второй, зависящий от календаря часовой пояс для любого местоположения, усугубляя проблемы с преобразованием местоположения и местного времени. Все эти проблемы могут вызвать много головной боли.

В конце концов, есть очень мало веских причин использовать что-либо, кроме UTC. Как следует из названия, он обеспечивает универсальную систему времени, которая наилучшим образом представляет уникальные моменты времени без какого-либо контекста или перевода. Может показаться глупым преобразовывать значения в UTC по мере их ввода и преобразовывать их обратно в местное время для их отображения, но у этого есть преимущество в том, что он работает правильно, даже если изменяется местный часовой пояс или изменяется состояние DST. К счастью, SQLite упрощает все эти преобразования.

Функции времени и даты

Почти вся функциональность времени и даты в SQLite обеспечивается пятью функциями SQL. Одна из этих функций по сути является универсальным переводчиком, предназначенным для преобразования практически любого формата времени или даты в любой другой формат. Остальные четыре функции действуют как удобные оболочки, которые обеспечивают фиксированный, предопределенный формат вывода.

В дополнение к функциям преобразования SQLite также предоставляет три литеральных выражения. Когда выражение оценивается, эти литералы будут переведены в соответствующее значение времени, которое представляет «сейчас».

Функция преобразования

Основная утилита для управления значениями времени и даты - это SQL-функция strftime():

strftime( format, time, modifier, modifier... )

Функция SQL strftime() смоделирована после функции C POSIX strftime(). Он использует маркеры форматирования стиля printf() для указания выходной строки. Первый параметр - это строка формата, которая определяет формат возвращаемого текстового значения. Второй параметр - это значение времени источника, которое представляет базовое время ввода. За ним следует ноль или более модификаторов, которые можно использовать для сдвига или преобразования входного значения перед его форматированием. Обычно все эти параметры являются текстовыми выражениями или текстовыми литералами, хотя значение времени может быть числовым.

Помимо любых буквальных символов, строка формата может содержать любой из следующих маркеров:

Например, формат времени HH:MM:SS.sss (включая доли секунды) может быть представлен строкой формата '%H:%M:%f'.

SQLite понимает ряд входных значений. Если формат строки времени не распознается и не может быть декодирован, strftime() вернет NULL. Будут распознаваться все следующие форматы ввода:

В случае второго, третьего и четвертого форматов существует единственный буквальный символ пробела между частью даты и частью времени. Пятый, шестой и седьмой форматы содержат букву T между датой и временем. Этот формат определен рядом стандартов ISO, включая стандартный формат временных меток XML. Предполагается, что последние два формата являются юлианским днем или (с модификатором) значением времени POSIX. Эти последние два не требуют определенного количества цифр и могут быть переданы как числовые значения.

Внутри strftime() всегда будет вычислять полную временную метку, которая содержит как дату, так и время. Любые поля, которые не указаны во входной строке времени, будут принимать значения по умолчанию. Значения часа, минуты и секунды по умолчанию - ноль или полночь, а дата по умолчанию - 1 января 2000 года.

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

Первые семь модификаторов просто добавляют или вычитают указанное количество времени. Это делается путем перевода времени и даты в отдельное представление и последующего добавления или вычитания указанного значения. Однако это может привести к неверным датам. Например, применение модификатора '+1 month' к дате '2010-01-31' приведет к дате '2010-02-31', которой не существует. Чтобы избежать этой проблемы, после применения каждого модификатора значения даты и времени возвращаются к допустимым датам. Например, гипотетическая дата '2010-02-31' будет заменена на '2010-03-03', поскольку ненормализованной датой было три дня после конца февраля.

Тот факт, что нормализация выполняется после применения каждого модификатора, означает, что порядок модификаторов может быть очень важным. Следует тщательно продумать, как применяются модификаторы, иначе вы можете столкнуться с неожиданными результатами. Например, применение модификатора '+1 month' с последующим '-1 month' к дате '2010-01-31' приведет к дате '2010-02-03', что на три дня меньше исходного. значение. Это связано с тем, что первый модификатор нормализуется до '2010-03-03', который затем возвращается к '2010-02-03'. Если модификаторы применяются в обратном порядке, '-1 month' преобразует нашу дату сначала в '2009-12-31', а модификатор '+1 month' затем преобразует дату обратно в исходную дату начала в '2010-01-31'.

Три модификатора start of... сдвигают текущую дату назад во времени к указанной точке, а модификатор weekday сдвигает дату вперед с нуля на шесть дней, чтобы найти дату, которая приходится на указанный день недели. Допустимые значения weekday: 0-6, для воскресенья - 0.

Модификатор unixepoch может использоваться только как начальный модификатор числового значения времени. В этом случае предполагается, что значение представляет время POSIX, а не юлианский день, и переводится соответствующим образом. Хотя модификатор unixepoch должен отображаться как первый модификатор, дополнительные модификаторы все же могут применяться.

Последние два модификатора используются для перевода между представлением времени в формате UTC и местным временем. Имя модификатора описывает назначение перевода, поэтому localtime предполагает ввод в формате UTC и производит локальный вывод. И наоборот, модификатор utc предполагает ввод местного времени и производит вывод в формате UTC. SQLite зависит от локальной операционной системы (и ее часового пояса и конфигурации летнего времени) для этих переводов. В результате эти модификаторы подвержены любым ошибкам и ошибкам, которые могут присутствовать в библиотеках времени и даты операционной системы хоста.

Функции удобства

Чтобы помочь стандартизированным текстовым форматам избежать ошибок и обеспечить более удобный способ преобразования дат и времени в их наиболее распространенные представления, SQLite имеет ряд удобных функций. По сути, это функции-оболочки вокруг strftime(), которые выводят дату или время в фиксированном формате. Все четыре функции принимают один и тот же набор параметров, который по сути совпадает с параметрами, используемыми strftime(), за вычетом исходной строки формата.

date( timestring, modifier, modifier... )
Переводит строку времени, применяет любые модификаторы и выводит дату в формате YYYY-MM-DD. Эквивалентно строке формата '%Y-%m-%d'.
time( timestring, modifier, modifier... )
Преобразует строку времени, применяет любые модификаторы и выводит дату в формате HH:MM:SS. Эквивалентно строке формата '%H:%M:%S'.
datetime( timestring, modifier, modifier... )
Преобразует строку времени, применяет любые модификаторы и выводит дату в формате YYYY-MM-DD HH:MM:SS. Эквивалентно строке формата '%Y-%m-%d %H:%M:%S'.
julianday( timestring, modifier, modifier... )
Переводит строку времени, применяет любые модификаторы и выводит юлианский день. Эквивалентно строке формата '%J'. Эта функция немного отличается от функции strftime(), поскольку strftime() вернет юлианский день в виде текстового представления числа с плавающей запятой, а эта функция вернет фактическое число с плавающей запятой.

Все четыре функции распознают одну и ту же строку времени и значения модификатора, которые использует strftime().

Литералы времени

SQLite распознает три буквальных выражения. Когда вычисляется выражение, содержащее один из этих литералов, литерал принимает соответствующее текстовое представление текущей даты или времени в формате UTC.

CURRENT_TIME
Предоставляет текущее время в формате UTC. Формат будет HH:MM:SS со значением часа от 00 до 23 включительно. Это то же самое, что и выражение SQL time( 'now' ).
CURRENT_DATE
Предоставляет текущую дату в формате UTC. Формат будет YYYY-MM-DD. Это то же самое, что и выражение SQL date( 'now' ).
CURRENT_TIMESTAMP
Предоставляет текущую дату и время в формате UTC. Формат будет YYYY-MM-DD HH:MM:SS. Между сегментами даты и времени есть один пробел. Это то же самое, что и в выражении SQL datetime( 'now' ). Обратите внимание, что имя функции SQL - datetime(), а литерал - _TIMESTAMP.

Поскольку эти литералы возвращают соответствующее значение в формате UTC, такое выражение, как SELECT CURRENT_TIMESTAMP; может не вернуть ожидаемый результат. Чтобы получить дату и время в локальном представлении, вам нужно использовать такое выражение, как:

SELECT datetime( CURRENT_TIMESTAMP, 'localtime' );

В этом случае буквальный CURRENT_TIMESTAMP также может быть заменен на 'now'.

Примеры

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

Вот пример того, как получить локальную метку времени и сохранить ее как значение по юлианскому времени в формате UTC:

julianday( input_value, 'utc' )

Этот тип выражения может появиться в инструкции INSERT. Чтобы вставить текущее время, его можно упростить до значения 'now', которое всегда указывается в формате UTC:

julianday( 'now' )

Если вы хотите отобразить юлианское значение из базы данных, вам нужно преобразовать юлианское значение UTC в местный часовой пояс и отформатировать его. Сделать это можно так:

datetime( jul_date, 'localtime' )

Также может быть уместным поместить подобное выражение в представление.

Если вы хотите представить дату в формате, более удобном для читателей из США, вы можете сделать что-то вроде этого:

strftime( '%m/%d/%Y', '2010-01-31', 'localtime' );

Это отобразит дату как 01/31/2010. Второй параметр также может быть юлианским значением или любым другим распознанным форматом даты.

Чтобы получить текущее значение времени POSIX (всегда в формате UTC):

strftime( '%s', 'now' )

Или для отображения местной даты и времени с учетом значения времени POSIX:

datetime( time_value, 'unixepoch', 'localtime' )

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

julianday( :year || '-' || :month || '-' || :day )

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

Расширение интернационализации ICU

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

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

Это проблема сопоставления с образцом, сортировки или всего, что зависит от сравнения текстовых значений. Например, большинство систем сортировки текста игнорируют различия в регистре слов. Некоторые языки также игнорируют определенные знаки ударения, но часто эти правила зависят от конкретного знака ударения и символа. Иногда правила и соглашения, используемые в языке, меняются от места к месту. По умолчанию SQLite понимает только 7-битную систему символов ASCII. Любая кодировка символов 128 или выше будет рассматриваться как двоичное значение без учета соглашений об использовании заглавных букв или эквивалентности. Хотя для английского языка этого часто бывает достаточно, для других языков обычно недостаточно.

Для более полной поддержки интернационализации вам необходимо создать SQLite с включенным расширением ICU. Проект International Components for Unicode - это библиотека с открытым исходным кодом, которая реализует огромное количество языковых функций. Эти функции настроены для разных регионов. Расширение SQLite ICU позволяет SQLite использовать различные аспекты библиотеки ICU, обеспечивая сортировку и сравнение с учетом локали, а также версии функций upper() и lower() с учетом локали.

Чтобы использовать расширение ICU, вы должны сначала загрузить и собрать библиотеку ICU. Исходный код библиотеки вместе с инструкциями по сборке можно загрузить с веб-сайта проекта http://www.icu-project.org/. Затем вы должны создать SQLite с включенным расширением ICU и связать его с библиотекой ICU. Чтобы включить расширение ICU в сборке объединения, определите директиву компилятора SQLITE_ENABLE_ICU.

Вы захотите взглянуть на исходный документ README. В нем объясняется, как использовать расширение для создания сопоставлений и операторов, зависящих от локали. Вы можете найти копию файла README в дистрибутиве с полным исходным кодом (в каталоге ext/icu) или в Интернете по адресу http://www.sqlite.org/src/artifact?ci=trunk&filename=ext/icu/README.txt.

Главный недостаток библиотеки ICU - размер. В дополнение к самой библиотеке, информация о локали для всех языков и местоположений составляет значительную часть. Эти дополнительные данные могут не иметь значения для настольной системы, но могут оказаться непрактичными для портативного или встроенного устройства.

Хотя расширение ICU может предоставлять возможности сортировки и сравнения с учетом местоположения, вам все равно необходимо выбрать конкретный языковой стандарт для определения этих правил сортировки и сравнения. Это достаточно просто, если вы работаете только с одним языком в одном месте, но это может быть довольно сложно, когда языки смешаны. Если вам приходится иметь дело с кросс-локальными сортировками или другими сложными проблемами интернационализации, возможно, будет проще включить эту логику в код вашего приложения.

Модуль полнотекстового поиска

SQLite включает движок полнотекстового поиска (FTS). Текущая версия известна как FTS3. Механизм FTS3 предназначен для каталогизации и индексации больших объемов текста. После проиндексирования движок FTS3 может быстро искать документы на основе различных типов поиска по ключевым словам. Хотя исходный код FTS3 в настоящее время поддерживается командой SQLite, части движка изначально были предоставлены членами группы инженеров Google.

Движок FTS3 представляет собой модуль виртуальной таблицы. Виртуальные таблицы похожи на представления в том, что они обертывают источник данных, чтобы он выглядел и действовал как обычная таблица. Представления получают свои данные из оператора SELECT, в то время как виртуальные таблицы зависят от определяемых пользователем функций C. Все функции, необходимые для реализации виртуальной таблицы, заключены в расширение, известное как модуль. Дополнительные сведения о том, как работают модули SQLite и виртуальные таблицы, см. в главе 10.

Полнотекстовый поиск - это важная и развивающаяся технология, и это одна из областей, в которой планируется усовершенствовать и усовершенствовать эту книгу по мере ее выхода в печать. Хотя в этом разделе дается краткий обзор основных функций FTS3, если вы задумались о модуле FTS3, я бы посоветовал вам просмотреть полную документацию на веб-сайте SQLite.

Механизм FTS3 включен во все стандартные дистрибутивы исходного кода SQLite (включая объединение), но по умолчанию отключен. Чтобы включить базовую функциональность FTS, определите директиву компилятора SQLITE_ENABLE_FTS3 при построении библиотеки SQLite. Чтобы включить более продвинутый синтаксис сопоставления, также определите SQLITE_ENABLE_FTS3_PARENTHESIS.

Создание и заполнение таблиц FTS

После того, как SQLite был скомпилирован с включенным движком FTS3, вы можете создать таблицу документа с оператором SQL, подобным этому:

CREATE VIRTUAL TABLE table_name USING FTS3 ( col1,... );

Помимо указания имени таблицы, вы можете определить ноль или более имен столбцов. Имя столбца - единственная информация, которая будет фактически использоваться. Любая информация о типе или ограничения столбца будут проигнорированы. Если имена столбцов не указаны, FTS автоматически создаст один столбец с именем content.

Таблицы FTS часто используются для хранения целых документов, и в этом случае им нужен только один столбец. В других случаях они используются для хранения различных категорий связанной информации и требуют нескольких столбцов. Например, если вы хотите хранить сообщения электронной почты в таблице FTS, может иметь смысл создать отдельные столбцы для строк "SUBJECT:", "FROM:", "TO:" и тела сообщения. Это позволит вам ограничить поиск определенным столбцом (и данными, которые он содержит). Спецификация столбца для таблицы FTS во многом определяется тем, как вы хотите искать данные. FTS также предоставляет оптимизированный способ поиска поискового запроса по всем проиндексированным столбцам.

Вы можете использовать стандартные операторы INSERT, UPDATE и DELETE для управления данными в таблице FTS. Как и в традиционных таблицах, в таблицах FTS есть столбец ROWID, который содержит уникальное целое число для каждой записи в таблице. На этот столбец также можно ссылаться, используя псевдоним DOCID. В отличие от традиционных таблиц, ROWID таблицы FTS стабилен благодаря вакууму (VACUUM в приложении C), поэтому на него можно надежно ссылаться через внешний ключ. Кроме того, в таблицах FTS есть внутренний столбец с тем же именем, что и имя таблицы. Этот столбец используется для специальных операций. Вы не можете вставлять или обновлять данные в этом столбце.

Любую виртуальную таблицу, включая таблицы FTS, можно удалить с помощью стандартной команды DROP TABLE.

Поиск в таблицах FTS

Таблицы FTS разработаны таким образом, что любой оператор SELECT будет работать правильно. Вы даже можете искать определенные текстовые значения или шаблоны напрямую с помощью операторов == или LIKE. Они будут работать, хотя будут несколько медленными, поскольку стандартные операторы потребуют полного сканирования таблицы.

Настоящая мощь системы FTS исходит от настраиваемого оператора MATCH. Этот оператор может использовать индексы, построенные вокруг отдельных текстовых значений, что позволяет очень быстро выполнять поиск по большим объемам текста. Как правило, поиск выполняется с использованием запроса, подобного следующему:

SELECT * FROM fts_table WHERE fts_column MATCH search_term;

Термин поиска, используемый оператором MATCH, имеет семантику, очень похожую на те, которые используются в поисковой системе в Интернете. Термины поиска разбиваются и сопоставляются со словами и терминами, найденными в текстовых значениях таблицы FTS. Как правило, оператор FTS MATCH не чувствителен к регистру и сопоставляется только с целыми словами. Например, поисковый запрос 'data' будет соответствовать 'research data', но не 'database'. Порядок поисковых запросов не имеет значения. Термины 'cat dog' и 'dog cat' будут соответствовать одному и тому же набору строк.

По умолчанию MATCH будет сопоставлять только те записи, которые содержат каждое слово или термин, найденный в поисковом запросе. Если расширенный синтаксис включен, более сложные логические операторы также могут использоваться для определения более сложных шаблонов поиска.

Обычно условия сопоставляются только с указанным столбцом. Однако в каждой таблице FTS есть специальный скрытый столбец, имя которого совпадает с именем самой таблицы. Если этот столбец указан в выражении соответствия, поиск будет выполняться во всех столбцах. Это упрощает поиск по типу «найти все».

Подробнее

Модуль FTS является довольно продвинутым и предлагает большое количество опций поиска и оптимизаций индекса. Если вы планируете использовать движок FTS в своем приложении, я настоятельно рекомендую вам потратить некоторое время на чтение онлайн-документации (http://www.sqlite.org/ fts3.html). Официальная документация довольно обширна и подробно описывает более сложные функции поиска с примерами.

Помимо объяснения различных шаблонов поиска, онлайн-документация также охватывает оптимизацию и обслуживание индекса. Хотя такой уровень детализации не всегда требуется, он может быть полезен для приложений, которые сильно зависят от FTS. В документах также объясняется, как предоставить настраиваемый токенизатор для адаптации механизма FTS к конкретным приложениям.

Для тех, кто хочет копнуть еще глубже, следующие разделы документа объясняют некоторые внутренние механизмы индекса FTS. Предоставляется информация о теневых таблицах, используемых механизмом FTS, а также о процессе, используемом для создания индекса токена. Этот уровень знаний не требуется для использования системы FTS, но он чрезвычайно полезен, если вы хотите изменить код или если вам просто интересно, что происходит под капотом.

Модули R*Trees и Spatial Indexing

Модуль R*Tree - это стандартное расширение SQLite, которое предоставляет структуру индекса, оптимизированную для многомерных ранжированных данных. Имя R*Tree относится к внутреннему алгоритму, используемому для организации и запроса сохраненных данных. Например, в двумерном R*Tree строки могут содержать прямоугольники в форме минимального и максимального значения долготы, а также минимальной и максимальной широты. Можно выполнять запросы, чтобы быстро найти все строки, которые содержат или перекрывают определенное геологическое местоположение или область. Добавление дополнительных параметров, например высоты, позволяет выполнять более сложный и конкретный поиск.

R*Tree не ограничиваются только пространственной информацией, но могут использоваться с любым типом данных числового диапазона, который включает пары минимальных и максимальных значений. Например, таблица R*Tree может использоваться для индексации времени начала и окончания событий. Затем индекс может быстро вернуть все события, которые были активны в определенный момент или в определенный период времени.

Реализация R*Tree, включенная в SQLite, может индексировать до пяти измерений данных (пять наборов пар min/max). Таблицы состоят из целочисленного столбца первичного ключа, за которым следуют от одной до пяти пар столбцов с плавающей запятой. В результате получится таблица с нечетным числом от 3 до 11 столбцов. Значения данных всегда должны указываться парами. Если вы хотите сохранить точку, просто используйте одно и то же значение как для минимального, так и для максимального компонента.

Как правило, R*Tree действуют как подробные таблицы для более традиционных таблиц. В традиционной таблице могут храниться любые данные, необходимые для определения рассматриваемого объекта, включая ключевую ссылку на данные R*Tree. Таблица R*Tree используется только для хранения размерных данных.

Многомерные R*Tree, особенно те, которые используются для хранения ограничивающих прямоугольников или ограничивающих объемов, часто являются приближениями к индексируемым ими записям. В этих случаях R*Tree не всегда могут предоставить точный набор результатов, но используются для эффективного предоставления первого приближения. По сути, R*Tree используется в качестве начального фильтра для быстрого и эффективного отсеивания всех строк, кроме небольшого процента. Для получения окончательного набора результатов можно применить более конкретное (и часто более дорогое) выражение фильтра. В большинстве случаев оптимизатор запросов понимает, как лучше всего использовать R*Tree, поэтому оно применяется перед любыми другими условными выражениями.

R*Tree довольно мощные, но они служат очень специфическим потребностям. Из-за этого мы не будем тратить время на раскрытие всех деталей. Если индекс R*Tree похож на то, что ваше приложение может использовать в своих интересах, я рекомендую вам проверить онлайн-документацию (http://www.sqlite.org/rtree.html). Это предоставит полное описание того, как создавать, заполнять и использовать индекс R*Tree.

Языки сценариев и другие интерфейсы

Как и большинство продуктов баз данных, SQLite имеет привязки, которые позволяют получить доступ к функциям API C из ряда различных языков сценариев и других сред. Во многих случаях эти расширения пытаются следовать стандартизированному интерфейсу базы данных, разработанному языковым сообществом. В других случаях драйвер или расширение сценария пытается точно представить собственный API SQLite.

За исключением расширения Tcl, все эти пакеты были разработаны сообществом пользователей SQLite. Таким образом, они не являются частью основного проекта SQLite и не поддерживаются командой разработчиков SQLite. Если у вас возникли проблемы с установкой или настройкой одного из этих драйверов, обращение за поддержкой в основной список рассылки пользователя SQLite может дать ограниченные результаты. Возможно, вам повезет больше в списке рассылки проекта или, если это не удастся, на более общем форуме поддержки, ориентированном на конкретный язык.

Как это часто бывает с этим типом программного обеспечения, поддержка и долгосрочное обслуживание могут быть несколько случайными. На момент публикации большинство перечисленных здесь драйверов имеют хорошую историю поддержания актуальности и синхронизации с новыми выпусками библиотеки SQLite. Однако, прежде чем строить весь проект вокруг определенной оболочки или расширения, убедитесь, что проект все еще активен.

Perl

Предпочтительным модулем Perl является DBD::SQLite, он доступен на CPAN (http://www.cpan.org). Этот пакет предоставляет стандартизированный, совместимый с DBI интерфейс для SQLite, а также ряд настраиваемых функций, которые обеспечивают поддержку специфических функций SQLite.

DBI предоставляет стандартный интерфейс для обработки команд SQL. Пользовательские функции обеспечивают дополнительное покрытие API SQLite и дают возможность определять функции, агрегаты и сопоставления SQL с помощью Perl. Хотя пользовательские функции не обеспечивают полного охвата API SQLite, большинство наиболее распространенных операций включены.

PHP

По мере развития языка PHP меняются и методы доступа SQLite. PHP5 включает несколько различных расширений SQLite, которые предоставляют как интерфейсы, зависящие от производителя, так и драйверы для стандартизованного интерфейса PDO (объекты данных PHP).

Есть два расширения, зависящих от производителя. Расширение sqlite было включено и включено по умолчанию с PHP 5.0 и обеспечивает поддержку библиотеки SQLite v2. Расширение sqlite3 было включено и задействовано по умолчанию с PHP 5.3.0 и, как вы могли догадаться, предоставляет интерфейс для текущей библиотеки SQLite 3. Библиотека sqlite3 предоставляет довольно простой интерфейс классов для командных API SQL. Она также поддерживает создание функций и агрегатов SQL с помощью PHP.

PHP 5.1 представил интерфейсы PDO. Расширение PDO - это последнее решение проблемы предоставления унифицированных механизмов доступа к базе данных. PDO действует как замена интерфейсов PEAR-DB и MDB2, имеющихся в других версиях PHP. Расширение PDO_SQLITE предоставляет драйвер PDO для текущей библиотеки SQLite v3. Помимо поддержки стандартных методов доступа к PDO, этот драйвер также предоставляет пользовательские методы для создания функций и агрегатов SQL с использованием PHP.

Учитывая, что существует очень небольшая функциональная разница между библиотекой поставщика SQLite 3 и библиотекой PDO SQLite 3, я предлагаю в новой разработке использовать драйвер PDO.

Python

Доступны два популярных интерфейса Python. Каждая оболочка соответствует разному набору потребностей и требований. На момент написания оба модуля находились в активной разработке.

Модуль PySQLite (http://code.google.com/p/pysqlite/) предлагает стандартизированный интерфейс, совместимый с Python DB-API 2.0, для механизма SQLite. PySQLite позволяет приложениям разрабатывать относительно независимый от базы данных интерфейс. Это очень полезно для систем, которым необходимо поддерживать более одной базы данных. Использование стандартизованного интерфейса также позволяет быстро создавать прототипы с помощью SQLite, оставляя при этом возможность перехода к более крупным и сложным системам баз данных. Начиная с Python 2.5, PySQLite стал частью стандартной библиотеки Python.

Модуль APSW ((Another Python SQLite Wrapper; http://code.google.com/p/apsw/) преследует совсем другую цель разработки. APSW предоставляет очень минимальный уровень абстракции, который разработан, чтобы максимально имитировать собственный SQLite C API. APSW не пытается обеспечить совместимость с любым другим продуктом баз данных, но обеспечивает очень широкий охват библиотеки SQLite, включая многие низкоуровневые функции. Это обеспечивает очень точный контроль, включая возможность создавать определяемые пользователем функции SQL, агрегаты и сопоставления в Python. APSW можно даже использовать для написания реализации виртуальной таблицы на Python.

Оба модуля имеют свои сильные стороны. Какой модуль подходит для вашего приложения, зависит от ваших потребностей. Если ваша база данных достаточно проста и вам нужен стандартизованный интерфейс, который позволяет выполнять миграцию в будущем, то PySQLite лучше подходит. Если вас не интересуют другие движки баз данных, но вам нужен очень подробный контроль над SQLite, то стоит взглянуть на APSW.

Java

Для языка Java доступен ряд интерфейсов. Некоторые из них являются оболочками для собственного C API, а другие соответствуют стандартизированному API совместимости с базами данных Java (JDBC).

Одной из старых оболочек является Java SQLite (http://www.ch-werner.de/javasqlite/), которая обеспечивает поддержку как SQLite 2, так и SQLite 3. Ядро этой библиотеки использует собственный интерфейс Java (JNI) для создания интерфейс, основанный на собственном интерфейсе C. Библиотека также содержит интерфейс JDBC. Это хороший выбор, если вам нужен прямой доступ к SQLite API.

Более современным драйвером только для JDBC является пакет SQLiteJDBC (http://www.xerial.org/trac/Xerial/wiki/SQLiteJDBC). Это довольно хороший дистрибутив, поскольку файл JAR содержит как классы Java, так и собственные библиотеки SQLite для Windows, Mac OS X и Linux на базе Intel. Это упрощает кроссплатформенное распространение. Драйвер также активно используется Xerial, поэтому его обычно поддерживают в хорошем состоянии.

Tcl

SQLite имеет прочную историю с языком Tcl. Фактически, то, что мы теперь знаем как SQLite, начало свою жизнь как расширение Tcl. Многие инструменты тестирования и разработки для SQLite написаны на Tcl. Помимо собственного C API, расширение Tcl - единственный API, поддерживаемый основной командой SQLite.

Чтобы включить привязки Tcl, загрузите дистрибутив TEA (Tcl Extension Architecture) исходного кода SQLite с веб-сайта SQLite (http://www.sqlite.org/download.html). Эта версия кода, по сути, представляет собой объединенный дистрибутив с привязками Tcl, добавленными в конец. Это будет встроено в расширение Tcl, которое затем можно будет импортировать в любую среду Tcl. Документацию по интерфейсу Tcl можно найти по адресу http://www.sqlite.org/tclsqlite.html.

ODBC

Спецификация ODBC (Open Database Connectivity) предоставляет стандартизированный API базы данных для широкого спектра продуктов баз данных. Как и многие языковые расширения, драйверы ODBC действуют как мост между библиотекой ODBC и конкретным API базы данных. Использование ODBC позволяет разработчикам писать в один API, который затем может использовать любое количество коннекторов для взаимодействия с широким спектром продуктов баз данных. Многие универсальные инструменты баз данных используют ODBC для поддержки широкого спектра систем баз данных.

Самый известный коннектор для SQLite - это SQLiteODBC (http://www.ch-werner.de/sqliteodbc/). SQLiteODBC протестирован с рядом библиотек ODBC, что гарантирует его совместимость с большинством инструментов и приложений, использующих поддержку ODBC.

.NET

Существует ряд независимых проектов SQLite, использующих технологии .NET. Некоторые из них являются простыми оболочками C#, которые делают немного больше, чем предоставляют контекст объекта для API SQLite. Другие проекты пытаются интегрировать SQLite в более крупные структуры, такие как ADO (объекты данных ActiveX).

Одним из наиболее известных проектов с открытым исходным кодом является пакет System.Data.SQLite (http:// sqlite.phxsoftware.com/). Этот пакет обеспечивает широкую поддержку ADO, а также поддержку LINQ.

Также доступны коммерческие драйверы ADO и LINQ. Дополнительную информацию см. в wiki по SQLite.

C++

Хотя к API SQLite C можно напрямую обращаться из приложений C++, некоторые люди предпочитают более объектно-ориентированный интерфейс. Если вы предпочитаете использовать существующую библиотеку, существует несколько доступных оболочек. Вы можете проверить веб-сайт SQLite или выполнить поиск в Интернете, если вам интересно.

Имейте в виду, что некоторые из этих библиотек содержатся в хорошем состоянии. Возможно, вам будет лучше просто написать и поддерживать свои собственные классы-оболочки. SQLite API имеет довольно объектно-зависимый дизайн, при этом большинство функций выполняют какие-либо манипуляции или действия с определенной структурой данных SQLite. В результате большинство оболочек C++ несколько тонкие и обеспечивают не более чем синтаксический перевод. Поддержание частной оболочки обычно не является серьезным бременем.

Просто помните, что основная библиотека SQLite - это C, а не C++, и ее нельзя скомпилировать с большинством компиляторов C++. Даже если вы решите обернуть API SQLite в интерфейс на основе классов C++, вам все равно потребуется скомпилировать базовую библиотеку SQLite с помощью компилятора C.

Другие языки

В дополнение к уже перечисленным языкам существуют оболочки, библиотеки и расширения для множества других языков и сред. В вики-разделе веб-сайта SQLite есть обширный список сторонних драйверов по адресу http://www.sqlite.org/cvstrac/wiki?p=SqliteWrappers. Многие из перечисленных драйверов больше не поддерживаются активно, поэтому обязательно изучите веб-сайты проектов, прежде чем вкладывать средства в конкретный драйвер. Те, которые заведомо заброшены, помечаются как таковые, но поддерживать такую информацию в актуальном состоянии сложно.

Мобильная и встраиваемая разработка

Поскольку мощность и емкость смартфонов, мобильных устройств и других встраиваемых систем продолжают расти, эти устройства могут обрабатывать большие и более сложные данные. Многие мобильные устройства ориентированы на организацию, поиск и отображение больших объемов структурированных данных. Это может быть что-то столь же простое, как адресная книга, или что-то гораздо более сложное, например приложения для отображения и маршрутизации.

Во многих случаях требования приложений к хранению данных и управлению ими очень хорошо соответствуют реляционной модели, предоставляемой SQLite. SQLite - это довольно небольшой и очень ресурсоемкий продукт, поэтому он хорошо работает в ограниченных средах. Модель «база данных в файле» также упрощает и ускоряет копирование или резервное копирование хранилищ данных. Учитывая все эти факторы, неудивительно, что почти все основные SDK для смартфонов поддерживают SQLite из коробки или позволяют легко скомпилировать его для своей платформы.

Память

Большинство мобильных устройств имеют ограниченные ресурсы памяти. Приложения должны учитывать использование своей памяти и часто должны ограничивать ресурсы, которые могут быть потреблены. В большинстве случаев большая часть использования памяти SQLite происходит из кеша страниц. Выбирая разумный размер страницы и размер кеша, можно контролировать большую часть использования памяти. Помните, что каждая открытая или присоединенная база данных обычно имеет свой собственный независимый кеш. Размер страницы можно настроить при создании базы данных с помощью команды PRAGMA page_size, а размер кеша можно изменить в любое время с помощью PRAGMA cache_size. См. page_size и cache_size в приложении F для более подробной информации.

Имейте в виду, что большие размеры кеша не всегда значительно лучше. Поскольку некоторые типы флеш-систем хранения не имеют эффективного времени поиска, иногда можно использовать относительно небольшой кэш страниц, сохраняя при этом приемлемую производительность. Более быстрое время отклика системы хранения снижает стоимость извлечения страниц из памяти, уменьшая влияние кеша. То, насколько быстро система хранения может реагировать, во многом зависит от типов флеш-чипов и от того, как они настроены в устройстве, но в зависимости от вашей системы вы можете счесть приемлемым использование относительно небольшого кэша. Это должно помочь держать ваш объем памяти под контролем.

Если вы работаете в чрезвычайно ограниченной среде, вы можете предварительно выделить буферы и сделать их доступными для SQLite через интерфейс sqlite3_config(). Для кучи SQLite, временных буферов и кеша страниц могут быть назначены разные буферы. Если буферы настроены до инициализации библиотеки SQLite, вся память будет выделяться из этих буферов. Это позволяет хост-приложению точно контролировать ресурсы памяти, которые может использовать SQLite.

Большинство других проблем довольно очевидны. Например, использование баз данных в памяти обычно не рекомендуется на устройствах с привязкой к памяти, если только база данных не очень мала, а прирост производительности является значительным. Точно так же следует помнить о запросах, которые могут генерировать большие промежуточные наборы результатов, например о предложениях ORDER BY. В некоторых случаях может иметь смысл вытащить часть обработки или бизнес-логики из базы данных в ваше приложение, где у вас будет лучший контроль над использованием ресурсов.

Устройство хранения

Почти все мобильные устройства используют те или иные твердотельные носители. Хранилище может быть встроенным, или это может быть карта расширения, такая как карта SD (Secure Digital), или даже внешний флэш-накопитель. Хотя эти системы хранения обеспечивают те же базовые функции, что и их «реальные компьютерные» аналоги, эти устройства хранения часто имеют заметно отличающиеся рабочие характеристики от традиционных устройств массового хранения.

Если возможно, попробуйте сопоставить размер страницы SQLite с размером собственного блока системы хранения. Соответствие размеров блоков позволит системе быстрее и эффективнее записывать страницы базы данных. Вы хотите, чтобы размер страницы базы данных был таким же, как у одного или нескольких целых блоков файловой системы. Страницы, использующие частичные блоки, будут работать намного медленнее. Однако вы не хотите делать страницу слишком большой, иначе вы ограничите производительность своего кеша. Чтобы найти правильный баланс, нужно немного поэкспериментировать.

Обычно SQLite сильно зависит от блокировки файловой системы для обеспечения надлежащей поддержки параллелизма. К сожалению, эта функциональность может быть ограничена на мобильных и встроенных платформах. Во избежание проблем лучше отказаться от нескольких подключений к одному и тому же файлу базы данных даже из одного приложения. Если требуется несколько подключений, убедитесь, что операционная система обеспечивает правильную блокировку, или используйте альтернативную систему блокировки. Также рассмотрите возможность настройки соединений с базой данных для получения и удержания любых блокировок (используйте команду PRAGMA locking_mode; см. locking_mode в приложении F). Хотя это делает доступ эксклюзивным для одного соединения, это увеличивает производительность, но при этом обеспечивает защиту.

Может возникнуть соблазн отключить механизм синхронизации и ведения журнала SQLite, но вы должны учитывать любые возможные последствия отключения этих процедур. Хотя при отключении синхронизаций и файлов журнала часто наблюдается значительный прирост производительности, существует также значительная опасность безвозвратного повреждения данных.

Во-первых, мобильные устройства разряжаются. Как мы все знаем, батареи имеют тенденцию выходить из строя в очень неприятное время. Даже если операционная система предоставляет предупреждения о низком энергопотреблении и автоматические спящие режимы, на многих моделях слишком легко мгновенно вывести аккумулятор, если устройство уронили или неправильно использовали. Кроме того, многие устройства используют съемные накопители, которые имеют тенденцию сниматься и исчезать в неудобное время. Во всех случаях основной защитой от поврежденной базы данных является процедура синхронизации хранилища и ведения журнала.

Сбои хранилища и повреждение базы данных могут быть особенно разрушительными в мобильной или встроенной среде. Поскольку мобильные платформы, как правило, более закрыты для пользователя, конечному пользователю часто бывает трудно выполнить резервное копирование и восстановление данных из отдельных приложений, даже если они достаточно дисциплинированы, чтобы регулярно выполнять резервное копирование своих данных. Наконец, ввод данных на мобильных устройствах часто выполняется медленно и обременительно, что делает перспективу ручного восстановления долгой и нежелательной. Мобильные приложения должны быть максимально простыми и терпимыми к ошибкам. Во многих случаях потеря данных клиента приводит к потере клиента.

Другие источники

Помимо специальных обработчиков памяти и устройств хранения, большинство других проблем сводятся к осознанию ограничений вашей платформы и контролю за использованием ресурсов. Если вы готовите пользовательскую сборку SQLite для своего приложения, вам следует потратить некоторое время, чтобы просмотреть все доступные директивы компилятора и посмотреть, есть ли какие-либо значения по умолчанию, которые вы хотите изменить, или какие-либо функции, которые вы, возможно, захотите отключить (см. приложение A) . Отключение неиспользуемых функций может еще больше уменьшить объем кода и памяти. Отключение некоторых функций также обеспечивает незначительное повышение производительности.

Также рекомендуется прочитать доступные операторы PRAGMA и посмотреть, есть ли какие-либо дополнительные параметры конфигурации для настройки поведения SQLite для вашей конкретной среды. Команды PRAGMA также можно использовать для динамической настройки использования ресурсов. Например, можно временно увеличить размер кэша для операции с интенсивным вводом-выводом, если это делается в то время, когда вы знаете, что доступно больше памяти. Затем размер кеш-памяти может быть уменьшен, что позволит использовать память в другом месте приложения.

Поддержка iPhone

Когда впервые были выпущены iPhone и iPod touch, Apple активно пропагандировала использование SQLite. Библиотека SQLite была предоставлена как системная структура и была хорошо документирована вместе с примерами кода в SDK.

С выпуском версии 3.0 Apple сделала свою систему Core Data доступной для iPhone OS. Core Data был доступен на платформе Macintosh в течение ряда лет и предоставляет высокоуровневую структуру абстракции данных, которая предлагает интегрированные инструменты проектирования и поддержку времени выполнения для решения сложных задач управления данными. В отличие от SQLite, модель Core Data не является строго реляционной по своей природе.

Теперь, когда на их мобильной платформе доступна библиотека более высокого уровня, Apple поощряет людей переходить на Core Data. Большая часть документации и примеров кода SQLite была удалена из SDK, и инфраструктура системного уровня больше не доступна. Однако, поскольку Core Data использует SQLite на уровне хранения, для использования по-прежнему доступна стандартная системная библиотека SQLite. Также относительно легко скомпилировать версии библиотеки SQLite для конкретных приложений. Это необходимо, если вы хотите воспользоваться некоторыми из новейших функций, поскольку системная версия SQLite часто отстает на несколько версий.

Core Data имеет ряд существенных преимуществ. Apple предоставляет инструменты разработки, которые позволяют разработчику быстро определять свои требования к данным и отношения. Это может сократить время разработки и сэкономить код. Пакет Core Data также хорошо интегрирован в текущие системы Mac OS X, что позволяет легко перемещать данные между платформами.

Несмотря на все преимущества, которые предоставляет Core Data, все же есть ситуации, когда имеет смысл использовать SQLite напрямую. Наиболее очевидное соображение - если ваши потребности в разработке выходят за рамки платформ Apple. В отличие от Core Data, библиотека SQLite доступна практически на любой платформе, что позволяет перемещать файлы данных и получать к ним доступ практически в любом месте на любой платформе. Core Data также использует другую модель хранения и извлечения, чем SQLite. Если ваше приложение особенно хорошо подходит для реляционной модели, прямой доступ SQL-запроса к уровню хранения данных может иметь преимущества. Непосредственное использование библиотеки SQLite также устраняет ряд уровней абстракции из дизайна приложения. Хотя это может привести к более подробному коду, это также может привести к повышению производительности, особенно с большими наборами данных.

Как и многие другие инженерные решения, оба подхода имеют свои преимущества и недостатки. Если предположить, что ограничения платформы не вызывают беспокойства, Core Data может предоставить очень быстрое решение для умеренно простых систем. С другой стороны, SQLite обеспечивает лучшую кроссплатформенную совместимость (как в коде, так и в данных) и позволяет напрямую манипулировать сложными моделями данных. Если вы не зависите от последней версии SQLite, вы даже можете уменьшить размер своего приложения, используя существующую системную библиотеку SQLite. Какой набор факторов имеет для вас большее значение, вероятно, будет зависеть от требований вашей платформы и сложности вашей модели данных.

Другие среды

Для ряда сред смартфонов требуется, чтобы разработка приложений выполнялась на Java или другом подобном языке. Эти системы часто не предоставляют компиляторов C и ограничивают возможность развертывания чего-либо, кроме байт-кода. Хотя большинство этих платформ предоставляют настраиваемые оболочки для системных библиотек SQLite, эти оболочки часто несколько ограничены. Обычно системные библиотеки отстают на несколько версий, а оболочки Java часто ограничиваются вызовами основных основных функций.

Хотя это может ограничить возможность настройки сборки SQLite и использования расширенных функций продукта SQLite, эти библиотеки по-прежнему обеспечивают полный доступ к уровню SQL и всем функциям, которые с ним связаны, включая ограничения, триггеры и транзакции. Чтобы обойти некоторые ограничения (например, отсутствие пользовательских функций), иногда может потребоваться перенести часть бизнес-логики в приложение. Лучше всего это сделать, разработав уровень доступа в приложении, который централизует все функции базы данных. Централизация позволяет коду приложения последовательно обеспечивать соблюдение любых ограничений конструкции базы данных, даже если база данных не в состоянии сделать это в полной мере. Также неплохо включить какой-либо тип функции проверки, которая может сканировать базу данных, выявляя (и, надеюсь, исправляя) любые проблемы.

Дополнительные расширения

Помимо описанных здесь интерфейсов и модулей, для SQLite доступно множество других расширений и сторонних пакетов. Некоторые из них представляют собой простые, но полезные расширения, например полный набор математических функций. Лучший способ найти их - поискать в Интернете или задать вопрос в списке рассылки пользователя SQLite. Вы также можете начать с просмотра http://sqlite.org/contrib/ списка некоторых из старых кодов.

В дополнение к расширениям баз данных доступно также несколько инструментов и менеджеров баз данных, специфичных для SQLite. В дополнение к оболочке командной строки существует несколько интерфейсов GUI. Одним из наиболее популярных является SQLite Manager, расширение Firefox, доступное по адресу http://code.google.com/p/sqlite-manager/.

Также существует небольшое количество коммерческих расширений для SQLite. Как уже говорилось, для некоторых драйверов баз данных требуется коммерческая лицензия. Hwaci, Inc. (компания, ответственная за разработку SQLite) предлагает два коммерческих расширения. Расширение SQLite Encryption Extension (SEE) шифрует страницы базы данных по мере их записи на диск, эффективно шифруя любую базу данных. Расширение Compressed and Encrypted Read-Only Database (CEROD) идет дальше, сжимая страницы базы данных. Сжатие уменьшает размер файла базы данных, но также делает базу данных доступной только для чтения. Это расширение может быть полезно для распространения лицензионных архивов данных или справочных материалов. Для получения дополнительной информации об этих расширениях см. http://www.sqlite.org/support.html.

Глава 9
Функции и расширения SQL

SQLite позволяет разработчику расширять среду SQL, создавая собственные функции SQL. Хотя эти функции используются в операторах SQL, код для реализации функции написан на C.

SQLite поддерживает три типа пользовательских функций. Первый тип - простые скалярные функции. Они принимают некоторый набор параметров и возвращают одно значение. Примером может служить встроенная функция abs(), которая принимает один числовой параметр и возвращает абсолютное значение этого числа.

Второй тип функций - это агрегатная функция или агрегатор. Это функции SQL, такие как sum() или avg(), которые используются вместе с предложениями GROUP BY для суммирования или иного агрегирования серии значений в окончательный результат.

Последний тип пользовательской функции - это сопоставление. Параметры сортировки используются для определения настраиваемого порядка сортировки для индекса или предложения ORDER BY. Концептуально функции сопоставления довольно просты: они принимают два текстовых значения и возвращают состояние больше, меньше или равно. На практике сопоставления могут стать довольно сложными, особенно при работе со строками на естественном языке.

В этой главе также будет рассмотрено, как упаковать набор настраиваемых функций в расширение SQLite. Расширения - это стандартный способ упаковки настраиваемых функций, агрегатов, сопоставлений, виртуальных таблиц (см. главу 10) или любых других настраиваемых функций. Расширения - это удобный и стандартизированный способ объединения наборов связанных функций или настроек в библиотеки функций SQL.

Расширения могут быть статически связаны с приложением или могут быть встроены в загружаемые расширения. Загружаемые расширения действуют как «плагины» для библиотеки SQLite. Загружаемые расширения - это особенно полезный способ загрузки ваших пользовательских функций в sqlite3, предоставляющий возможность тестировать запросы или отлаживать проблемы в той же среде SQL, что и в вашем приложении.

Исходный код примеров, приведенных в этой главе, можно найти в книге для скачивания. Скачивание исходного кода значительно упростит создание примеров и их опробование. См. «Пример загрузки кода» для получения дополнительной информации о том, где найти исходный код (локальная ссылка: UsingSQLiteCode.tar.gz).

Скалярные функции

Структура и назначение скалярных функций SQL аналогичны функциям C или традиционным математическим функциям. Вызывающий предоставляет ряд параметров функции, а функция вычисляет и возвращает значение. Иногда эти функции являются чисто функциональными (в математическом смысле), поскольку они вычисляют результат, основываясь исключительно на параметрах без какого-либо внешнего влияния. В других случаях функции носят более процедурный характер и вызываются для вызова определенных побочных эффектов.

Тело функции может делать практически все, что вы хотите, включая вызовы в другие библиотеки. Например, вы можете написать функцию, которая позволяет SQLite отправлять электронную почту или запрашивать статус веб-сервера с помощью функций SQL. Ваш код также может взаимодействовать с базой данных и запускать собственные запросы.

Хотя скалярные функции могут принимать несколько параметров, они могут возвращать только одно значение, например целое число или строку. Функции не могут возвращать строки (серию значений), а также не могут возвращать набор результатов со строками и столбцами.

Однако скалярные функции по-прежнему можно использовать для обработки наборов данных. Рассмотрим этот оператор SQL:

SELECT format( name ) FROM employees;

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

Регистрация функций

Чтобы создать пользовательскую функцию SQL, вы должны привязать имя функции SQL к указателю функции C. Функция C действует как обратный вызов. Каждый раз, когда механизму SQL требуется вызвать названную функцию SQL, вызывается зарегистрированный указатель функции C. Это дает возможность оператору SQL вызывать написанную вами функцию C.

Эти функции позволяют создавать и связывать имя функции SQL с указателем функции C:

int sqlite3_create_function( sqlite3 *db, const char *func_name, int num_param, int text_rep, void *udp, func_ptr, step_func, final_func ) int sqlite3_create_function16( sqlite3 *db, const void *func_name, int num_param, int text_rep, void *udp, func_ptr, step_func, final_func )

Создает новую функцию SQL в соединении с базой данных. Первый параметр - это соединение с базой данных. Второй параметр - это имя функции в виде строки в кодировке UTF-8 или UTF-16. Третий параметр - это количество ожидаемых параметров функции SQL. Если это значение отрицательное, количество ожидаемых параметров является переменным или неопределенным. Четвертое - это ожидаемое представление текстовых значений, передаваемых в функцию, и может быть одним из SQLITE_UTF8, SQLITE_UTF16, SQLITE_UTF16BE, SQLITE_UTF16LE или SQLITE_ANY. За ним следует указатель пользовательских данных.

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

SQLite позволяет перегрузить имена функций SQL в зависимости от количества параметров и текстового представления. Это позволяет связать несколько функций C с одним и тем же именем функции SQL. Вы можете использовать эту возможность перегрузки для регистрации различных реализаций C одной и той же функции SQL. Это может быть полезно для эффективной обработки различных кодировок текста или для обеспечения различного поведения в зависимости от количества параметров.

Вам не нужно регистрировать несколько кодировок текста. Когда библиотеке SQLite необходимо выполнить вызов функции, она попытается найти зарегистрированную функцию с соответствующим текстовым представлением. Если она не может найти точное совпадение, она преобразует любые текстовые значения и вызовет одну из других доступных функций. Значение SQLITE_ANY указывает, что функция готова принимать текстовые значения в любой возможной кодировке.

Вы можете обновить или переопределить функцию, просто перерегистрировав ее с другим указателем на функцию. Чтобы удалить функцию, вызовите sqlite3_create_function_xxx() с тем же именем, количеством параметров и текстовым представлением, но передайте NULL для всех указателей функций. К сожалению, нет способа узнать, зарегистрировано ли имя функции или нет, кроме как самостоятельно отслеживать. Это означает, что нет никакого способа отличить действие создания от действия переопределения.

Допускается создание новой функции в любое время. Однако существуют ограничения на то, когда вы можете изменить или удалить функцию. Если в соединении с базой данных есть какие-либо подготовленные операторы, которые в настоящее время выполняются (sqlite3_step() был вызван хотя бы один раз, а sqlite3_reset() - нет), вы не можете переопределить или удалить пользовательскую функцию, вы можете только создать новую. Любая попытка переопределить или удалить функцию вернет SQLITE_BUSY.

Если в данный момент нет выполняемых операторов, вы можете переопределить или удалить пользовательскую функцию, но это сделает недействительными все подготовленные в данный момент операторы (как и любое изменение схемы). Если операторы были подготовлены с помощью sqlite3_prepare_v2(), они автоматически представят себя при следующем использовании. Если они были подготовлены с помощью исходной версии sqlite3_prepare(), любое использование оператора вернет ошибку SQLITE_SCHEMA.

Фактическая функция C, которую вам нужно написать, выглядит так:

void custom_scalar_function( sqlite3_context *ctx, int num_values, sqlite3_value **values )

Это прототип функции C, используемой для реализации специальной скалярной функции SQL. Первый параметр - это структура sqlite3_context, которую можно использовать для доступа к указателю пользовательских данных, а также для установки результата функции. Второй параметр - это количество значений параметра, присутствующих в третьем параметре. Третий параметр - это массив указателей sqlite3_value.

Второй и третий параметры (int num_values, sqlite3_value **values) работают вместе очень похоже на традиционные основные параметры C (int argc, char **argv).

В многопоточном приложении разные потоки могут одновременно вызывать вашу функцию. Таким образом, пользовательские функции должны быть потокобезопасными.

Большинство пользовательских функций следуют довольно стандартному шаблону. Во-первых, вам нужно изучить параметры sqlite3_value, чтобы проверить их типы и извлечь их значения. Вы также можете извлечь указатель пользовательских данных, переданный в sqlite3_create_function_xxx(). Затем ваш код может выполнять любые необходимые вычисления или процедуры. Наконец, вы можете установить возвращаемое значение функции или вернуть состояние ошибки.

Извлечение параметров

Параметры функции SQL передаются в вашу функцию C в виде массива структур sqlite3_value. Каждая из этих структур содержит одно значение параметра.

Чтобы извлечь рабочие значения C из структур sqlite3_value, необходимо вызвать одну из функций sqlite3_value_xxx(). Эти функции очень похожи на функции sqlite3_column_xxx() по использованию и дизайну. Единственное существенное отличие состоит в том, что эти функции принимают один указатель sqlite3_value, а не подготовленный оператор и индекс столбца.

Как и их аналоги в столбцах, функции значений будут пытаться автоматически преобразовать значение в любой запрошенный тип данных. Процесс и правила преобразования такие же, как и в функциях sqlite3_column_xxx(). См. Таблицу 7-1 для получения более подробной информации.

const void* sqlite3_value_blob( sqlite3_value *value )
Извлекает и возвращает указатель на BLOB.
double sqlite3_value_double( sqlite3_value *value )
Извлекает и возвращает значение с плавающей запятой двойной точности.
int sqlite3_value_int( sqlite3_value *value )
Извлекает и возвращает 32-разрядное целое число со знаком. Возвращаемое значение будет обрезано (без предупреждения), если значение параметра содержит целочисленное значение, которое не может быть представлено только 32 битами.
sqlite3_int64 sqlite3_value_int64( sqlite3_value *value )
Извлекает и возвращает 64-битное целое число со знаком.
const unsigned char* sqlite3_value_text( sqlite3_value *value )
Извлекает и возвращает текстовое значение в кодировке UTF-8. Значение всегда будет обнулено. Обратите внимание, что возвращаемый указатель char не имеет знака и, вероятно, потребует приведения. Указатель также может иметь значение NULL, если требовалось преобразование типа.
const void* sqlite3_value_text16( sqlite3_value *value ) const void* sqlite3_value_text16be( sqlite3_value *value ) const void* sqlite3_value_text16le( sqlite3_value *value )
Извлекает и возвращает строку в кодировке UTF-16. Первая функция возвращает строку в собственном порядке байтов машины. Две другие функции будут возвращать строку, которая всегда закодирована с прямым или обратным порядком байтов. Значение всегда будет заканчиваться нулем. Указатель также может иметь значение NULL, если требовалось преобразование типа.

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

int sqlite3_value_type( sqlite3_value *value )
Возвращает собственный тип данных значения. Возвращаемое значение может быть одним из SQLITE_BLOB, SQLITE_INTEGER, SQLITE_FLOAT, SQLITE_TEXT или SQLITE_NULL. Это значение может измениться или стать недействительным, если происходит преобразование типа.
int sqlite3_value_numeric_type( sqlite3_value *value )

Эта функция пытается преобразовать значение в числовой тип (SQLITE_FLOAT или SQLITE_INTEGER). Если преобразование может быть выполнено без потери данных, то преобразование выполняется и возвращается тип данных нового значения. Если преобразование не может быть выполнено, значение не будет преобразовано, и будет возвращен исходный тип данных значения. Это может быть любое значение, возвращаемое функцией sqlite3_value_type().

Основное различие между этой функцией и простым вызовом sqlite3_value_double() или sqlite3_value_int() заключается в том, что преобразование будет происходить только в том случае, если оно имеет смысл и не приведет к потере данных. Например, sqlite3_value_double() преобразует NULL в значение 0.0, а эта функция - нет. Точно так же sqlite3_value_int() преобразует первую часть строки '123xyz' в целое число 123, игнорируя завершающий 'xyz'. Однако эта функция не будет работать, потому что конечный 'xyz' не может быть понят в числовом контексте.

int sqlite3_value_bytes( sqlite3_value *value )
Возвращает количество байтов в BLOB или строке в кодировке UTF-8. При возврате размера текстового значения размер будет включать нулевой терминатор.
int sqlite3_value_bytes16( sqlite3_value *value )
Возвращает количество байтов в строке в кодировке UTF-16, включая нулевой признак конца.

Как и в случае с функциями sqlite3_column_xxx(), любые возвращаемые указатели могут стать недействительными, если другой вызов sqlite3_value_xxx() выполняется для той же структуры sqlite3_value. Точно так же преобразование данных может происходить для текстовых типов данных при вызове sqlite3_value_bytes() или sqlite3_value_bytes16(). В общем, вы должны следовать тем же правилам и практикам, что и при использовании функций sqlite3_column_xxx(). См. «Столбцы результатов» для получения более подробной информации.

Помимо параметров функции SQL, параметр sqlite3_context также несет полезную информацию. Эти функции могут использоваться для извлечения либо соединения с базой данных, либо указателя данных пользователя, который использовался для создания функции.

void* sqlite3_user_data( sqlite3_context *ctx )
Извлекает указатель пользовательских данных, который был передан в sqlite3_create_function_xxx() при регистрации функции. Имейте в виду, что этот указатель используется для всех вызовов этой функции в этом соединении с базой данных.
sqlite3* sqlite3_context_db_handle( sqlite3_context *ctx )
Возвращает соединение с базой данных, которое использовалось для регистрации этой функции.

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

Возврат результатов и ошибок

После того, как функция извлекла и проверила свои параметры, она может приступить к работе. Когда результат вычислен, его нужно передать обратно в механизм SQLite. Это делается с помощью одной из функций sqlite3_result_xxx(). Эти функции устанавливают значение результата в структуре функции sqlite3_context.

Установка значения результата - единственный способ, которым ваша функция может сообщить механизму SQLite об успешном или неудачном вызове функции. Сама функция C имеет тип возвращаемого значения void, поэтому любой результат или ошибка должны быть переданы обратно через структуру контекста. Обычно одна из функций sqlite3_result_xxx() вызывается непосредственно перед вызовом return в вашей функции C, но допустимо устанавливать новый результат несколько раз на протяжении всей функции. Однако будет возвращен только последний результат.

Функции sqlite3_result_xxx() очень похожи на функции sqlite3_bind_xxx() по использованию и дизайну. Основное отличие состоит в том, что эти функции принимают структуру sqlite3_context, а не подготовленный оператор и индекс параметра. Функция может возвращать только один результат, поэтому любой вызов функции sqlite3_result_xxx() переопределит значение, установленное предыдущим вызовом.

void sqlite3_result_blob( sqlite3_context* ctx, const void *data, int data_len, mem_callback )
Кодирует буфер данных как результат BLOB.
void sqlite3_result_double( sqlite3_context *ctx, double data )
В результате кодирует 64-битное значение с плавающей запятой.
void sqlite3_result_int( sqlite3_context *ctx, int data )
В результате кодирует 32-битное целое число со знаком.
void sqlite3_result_int64( sqlite3_context *ctx, sqlite3_int64 data )
В результате кодирует 64-битное целое число со знаком.
void sqlite3_result_null( sqlite3_context *ctx )
В результате кодирует SQL NULL.
void sqlite3_result_text( sqlite3_context *ctx, const char *data, int data_len, mem_callback )
В результате кодирует строку в кодировке UTF-8.
void sqlite3_result_text16( sqlite3_context *ctx, const void *data, int data_len, mem_callback ) void sqlite3_result_text16be( sqlite3_context *ctx, const void *data, int data_len, mem_callback ) void sqlite3_result_text16le( sqlite3_context *ctx, const void *data, int data_len, mem_callback )
В результате кодирует строку в кодировке UTF-16. Первая функция используется для строки в собственном байтовом формате, а две последние функции используются для строк, которые явно закодированы как big-endian или little-endian, соответственно.
void sqlite3_result_zeroblob( sqlite3_context *ctx, int length )
В результате кодирует BLOB. BLOB будет содержать указанное количество байтов, и каждый байт будет установлен в ноль (0x00).
void sqlite3_result_value( sqlite3_context *ctx, sqlite3_value *result_value )

В результате кодирует sqlite3_value. Создается копия значения, поэтому нет необходимости беспокоиться о том, чтобы параметр sqlite3_value оставался стабильным между этим вызовом и фактическим завершением вашей функции.

Эта функция принимает как защищенные, так и незащищенные объекты значений. Вы можете передать в эту функцию один из параметров sqlite3_value, если хотите вернуть один из входных параметров функции SQL. Вы также можете передать значение, полученное в результате вызова, в sqlite3_column_value().

Установка значения BLOB или текста требует того же типа управления памятью, что и эквивалентные функции sqlite3_bind_xxx(). Последний параметр этих функций - указатель обратного вызова, который должным образом освободит и выпустит указанный буфер данных. Вы можете передать ссылку на sqlite3_free() напрямую (при условии, что буферы данных были выделены с помощью sqlite3_malloc()), или вы можете написать свой собственный менеджер памяти (или оболочку). Вы также можете передать один из флагов SQLITE_TRANSIENT или SQLITE_STATIC. См. «Значения привязки» для подробностей о том, как можно использовать эти флаги.

Помимо кодирования определенных типов данных, вы также можете вернуть статус ошибки. Это может быть использовано для обозначения проблемы использования (например, неправильного количества параметров) или проблемы среды, например нехватки памяти. Возврат кода ошибки приведет к тому, что SQLite прервет текущий оператор SQL и вернет ошибку обратно в приложение через код возврата sqlite3_step() или одной из вспомогательных функций.

void sqlite3_result_error( sqlite3_context *ctx, const char *msg, int msg_size ) void sqlite3_result_error16( sqlite3_context *ctx, const void *msg, int msg_size )
Устанавливает код ошибки на SQLITE_ERROR и устанавливает сообщение об ошибке в предоставленную строку в кодировке UTF-8 или UTF-16. Создается внутренняя копия строки, поэтому приложение может освободить или изменить строку, как только эта функция вернется. Последний параметр указывает размер сообщения в байтах. Если строка заканчивается нулем и последний параметр отрицательный, размер строки вычисляется автоматически.
void sqlite3_result_error_toobig( sqlite3_context *ctx )
Указывает, что функция не смогла обработать текст или значение BLOB из-за своего размера.
void sqlite3_result_error_nomem( sqlite3_context *ctx )
Указывает, что функция не может быть завершена из-за невозможности выделить необходимую память. Эта специализированная функция предназначена для работы без выделения дополнительной памяти. Если вы столкнулись с ошибкой выделения памяти, просто вызовите эту функцию, и ваша функция вернется.
void sqlite3_result_error_code( sqlite3_context *ctx, int code )
Устанавливает конкретный код ошибки SQLite. Не устанавливает и не изменяет сообщение об ошибке.

Можно вернуть как настраиваемое сообщение об ошибке, так и конкретный код ошибки. Сначала вызовите sqlite3_result_error() (или sqlite3_result_error16()), чтобы установить сообщение об ошибке. Это также установит код ошибки на SQLITE_ERROR. Если вам нужен другой код ошибки, вы можете вызвать sqlite3_result_error_code(), чтобы заменить общий код ошибки чем-то более конкретным, оставив сообщение об ошибке нетронутым. Просто имейте в виду, что sqlite3_result_error() всегда будет устанавливать код ошибки на SQLITE_ERROR, поэтому вы должны установить сообщение об ошибке, прежде чем устанавливать конкретный код ошибки.

Пример

Вот простой пример, который предоставляет функцию SQLite C API sqlite3_limit() среде SQL как функцию SQL sql_limit(). Эта функция используется для настройки различных ограничений, связанных с подключением к базе данных, таких как максимальное количество столбцов в наборе результатов или максимальный размер значения BLOB.

Вот краткое введение в C-функцию sqlite3_limit(), которую можно использовать для настройки мягких ограничений среды SQLite:

int sqlite3_limit( sqlite3 *db, int limit_type, int limit_value )
Для данного соединения с базой данных это устанавливает ограничение, на которое ссылается второй параметр, равным значению, указанному в третьем параметре. Возвращается старый лимит. Если новое значение отрицательное, предельное значение останется неизменным. Это можно использовать для проверки существующего лимита. Мягкий предел не может быть повышен выше жесткого предела, который устанавливается во время компиляции.

Для получения более подробной информации о sqlite3_limit() см. sqlite3_limit() в приложении G. Вам не нужно полностью понимать, как работает этот вызов API, чтобы понять эти примеры.

Хотя функция sqlite3_limit() является хорошим примером, возможно, это не та вещь, которую вы хотели бы использовать для языка SQL в реальном приложении. На практике раскрытие этого вызова C API на уровне SQL вызывает некоторые проблемы безопасности. Любой, кто может выполнять произвольные вызовы SQL, будет иметь возможность изменять мягкие ограничения SQLite. Это может быть использовано для некоторых типов атак типа «отказ в обслуживании» путем повышения или понижения пределов до крайних значений.

sql_set_limit

Чтобы вызвать функцию sqlite3_limit(), нам нужно определить параметры limit_type и value. Для этого потребуется функция SQL, которая принимает два параметра. Первым параметром будет тип ограничения, выраженный в виде текстовой константы. Второй параметр будет новым пределом. Функцию SQL можно вызвать следующим образом, чтобы установить новый предел глубины выражения:

SELECT sql_limit( 'EXPR_DEPTH', 400 );

Функция C, реализующая функцию SQL sql_limit(), состоит из четырех основных частей. Первая задача - проверить, является ли первый параметр функции SQL (переданный как values[0]) текстовым значением. Если это так, функция извлекает текст до указателя str:

static void sql_set_limit( sqlite3_context *ctx, int
                      num_values, sqlite3_value **values )
{
    sqlite3      *db = sqlite3_context_db_handle( ctx );
    const char   *str = NULL;
    int           limit = -1, val = -1, result = -1;

    /* verify the first param is a string and extract pointer */
    if ( sqlite3_value_type( values[0] ) == SQLITE_TEXT ) {
        str = (const char*) sqlite3_value_text( values[0] );
    } else {
    sqlite3_result_error( ctx, "sql_limit(): wrong parameter type", -1 );
    return;
}

Затем функция проверяет, что второй параметр SQL (values[1]) является целочисленным значением, и извлекает его в переменную val:

/* verify the second parameter is an integer and extract value */
if ( sqlite3_value_type( values[1] ) == SQLITE_INTEGER ) {
    val = sqlite3_value_int( values[1] );
} else {
    sqlite3_result_error( ctx, "sql_limit(): wrong parameter type", -1 );
    return;
}

Хотя наша функция SQL использует текстовое значение, чтобы указать, какой предел мы хотели бы изменить, для функции C sqlite3_limit() требуется предопределенное целочисленное значение. Нам нужно декодировать текстовое значение str в целочисленное предельное значение. Я покажу код для decode_limit_str() чуть позже:

/* translate string into integer limit */
limit = decode_limit_str( str );
if ( limit == -1 ) {
    sqlite3_result_error( ctx, "sql_limit(): unknown limit type", -1 );
    return;
}

После проверки двух параметров нашей функции SQL, извлечения их значений и перевода индикатора ограничения текста в правильное целочисленное значение мы, наконец, вызываем sqlite3_limit(). Результат устанавливается как значение результата функции SQL, и функция возвращает:

    /* call sqlite3_limit(), return result */
    result = sqlite3_limit( db, limit, val );
    sqlite3_result_int( ctx, result );
    return;
}

Функция decode_limit_str() очень проста и просто ищет предопределенный набор текстовых значений:

int decode_limit_str( const char *str )
{
    if ( str == NULL ) return -1;
    if ( !strcmp( str, "LENGTH"          ) ) return SQLITE_LIMIT_LENGTH;
    if ( !strcmp( str, "SQL_LENGTH"      ) ) return SQLITE_LIMIT_SQL_LENGTH;
    if ( !strcmp( str, "COLUMN"          ) ) return SQLITE_LIMIT_COLUMN;
    if ( !strcmp( str, "EXPR_DEPTH"      ) ) return SQLITE_LIMIT_EXPR_DEPTH;
    if ( !strcmp( str, "COMPOUND_SELECT" ) ) return SQLITE_LIMIT_COMPOUND_SELECT;
    if ( !strcmp( str, "VDBE_OP"         ) ) return SQLITE_LIMIT_VDBE_OP;
    if ( !strcmp( str, "FUNCTION_ARG"    ) ) return SQLITE_LIMIT_FUNCTION_ARG;
    if ( !strcmp( str, "ATTACHED"        ) ) return SQLITE_LIMIT_ATTACHED;
    if ( !strcmp( str, "LIKE_LENGTH"     ) ) return SQLITE_LIMIT_LIKE_PATTERN_LENGTH;
    if ( !strcmp( str, "VARIABLE_NUMBER" ) ) return SQLITE_LIMIT_VARIABLE_NUMBER;
    if ( !strcmp( str, "TRIGGER_DEPTH"   ) ) return SQLITE_LIMIT_TRIGGER_DEPTH;
    return -1;
}

Имея эти две функции, мы можем создать SQL-функцию sql_limit(), зарегистрировав указатель на C-функцию sql_set_limit().

sqlite3_create_function( db, "sql_limit", 2, SQLITE_UTF8,
                         NULL, sql_set_limit, NULL, NULL );

Параметры этой функции включают соединение с базой данных (db), имя функции SQL (sql_limit), необходимое количество параметров (2), ожидаемую кодировку текста (UTF-8), указатель данных пользователя (NULL) и, наконец, указатель функции C, реализующей эту функцию (sql_set_limit). Последние два параметра используются только при создании агрегатных функций и имеют значение NULL.

После создания функции SQL мы можем теперь управлять пределами нашей среды SQLite, вводя команды SQL. Вот несколько примеров того, как могла бы выглядеть SQL-функция sql_limit(), если бы мы интегрировали ее в инструмент sqlite3 (мы увидим, как это сделать с помощью загружаемого расширения, позже в этой главе).

Во-первых, мы можем найти текущий предел COLUMN, передав новое значение ограничения -1:

sqlite> SELECT sql_limit( 'COLUMN', -1 );
2000

Мы проверяем, что функция работает правильно, устанавливая максимальное ограничение столбца равным двум, а затем генерируя результат с тремя столбцами. Когда мы устанавливаем новое значение, возвращается предыдущее предельное значение:

sqlite> SELECT sql_limit( 'COLUMN', 2 );
2000
sqlite> SELECT 1, 2, 3;
Error: too many columns in result set

Из ошибки видно, что мягкий предел установлен правильно, то есть наша функция работает.

Одна вещь, которая может вас заинтересовать, - это количество значений параметра. Хотя функция sql_set_limit() тщательно проверяет типы параметров, на самом деле она не проверяет, что num_values равно двум. В этом случае это не обязательно, так как он был зарегистрирован с помощью sqlite3_create_function() с обязательным количеством параметров, равным двум. SQLite даже не будет вызывать нашу функцию sql_set_limit(), если у нас нет ровно двух параметров:

sqlite> SELECT sql_limit( 'COLUMN', 2000, 'extra' );
Error: wrong number of arguments to function sql_limit()

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

sql_get_limit

Хотя наличие фиксированного количества параметров упрощает код проверки, может быть полезно предоставить версию с одним параметром, которую можно использовать для поиска текущего значения. Это можно сделать несколькими способами. Во-первых, мы можем определить вторую функцию C, называемую sql_get_limit(). Эта функция будет такой же, как sql_set_limit(), но с удалением второго блока кода:

/* remove this block of code from a copy of */
/* sql_set_limit() to produce sql_get_limit() */
if ( sqlite3_value_type( values[1] ) == SQLITE_INTEGER ) {
    val = sqlite3_value_int( values[1] );
} else {
    sqlite3_result_error( ctx, "sql_limit(): wrong parameter type", -1 );
    return;
}

После удаления этого кода функция никогда не будет декодировать второй параметр функции SQL. Поскольку val инициализируется значением -1, это фактически делает каждый вызов вызовом запроса.

Регистрируем каждую из этих функций отдельно:

sqlite3_create_function( db, "sql_limit", 1,
        SQLITE_UTF8, NULL, sql_get_limit, NULL, NULL );
sqlite3_create_function( db, "sql_limit", 2,
        SQLITE_UTF8, NULL, sql_set_limit, NULL, NULL );

Эта двойная регистрация перегружает имя функции SQL sql_limit(). Перегрузка разрешена, потому что два вызова sqlite3_create_function() имеют разное количество требуемых параметров. Если функция SQL sql_limit() вызывается с одним параметром, то вызывается функция языка C sql_get_limit(). Если для функции SQL предоставлены два параметра, то вызывается функция C sql_set_limit().

sql_getset_limit

Хотя две функции C sql_get_limit() и sql_set_limit() обеспечивают правильную функциональность, большая часть их кода одинакова. Вместо того, чтобы иметь две функции, было бы проще объединить эти две функции в одну функцию, которая может работать с одним или двумя параметрами и способна как получать, так и устанавливать предельное значение.

Эту комбинированную функцию sql_getset_limit() можно создать, взяв исходную функцию sql_set_limit() и изменив второй раздел. Вместо того, чтобы устранять его, как мы это сделали при создании sql_get_limit(), мы просто заключим его в оператор if, поэтому второй раздел (который извлекает второй параметр функции SQL) запускается только в том случае, если у нас есть два параметра:

/* verify the second parameter is an integer and extract value */
if ( num_values == 2 ) {
    if ( sqlite3_value_type( values[1] ) == SQLITE_INTEGER ) {
        val = sqlite3_value_int( values[1] );
    } else {
        sqlite3_result_error( ctx, "sql_limit(): wrong parameter type", -1 );
        return;
    }
}

Мы регистрируем одну и ту же функцию sql_getset_limit() C для обоих счетчиков параметров:

sqlite3_create_function( db, "sql_limit", 1,
        SQLITE_UTF8, NULL, sql_getset_limit, NULL, NULL );
sqlite3_create_function( db, "sql_limit", 2,
        SQLITE_UTF8, NULL, sql_getset_limit, NULL, NULL );

Для этой конкретной задачи это, вероятно, лучший выбор. SQLite проверит, что функция SQL sql_limit() имеет ровно один или два параметра, прежде чем вызывать нашу функцию C, которая может легко справиться с любым из этих двух случаев.

sql_getset_var_limit

Если по какой-то причине вам не нравится идея регистрировать одну и ту же функцию дважды, мы также можем заставить SQLite игнорировать счетчик параметров и вызывать нашу функцию, несмотря ни на что. Это оставляет нам проверку правильности подсчета параметров. Для этого мы бы начали с функции sql_getset_limit() и изменили ее на sql_getset_var_limit(), добавив этот блок вверху функции:

if ( ( num_values < 1 )||( num_values > 2 ) ) {
    sqlite3_result_error( ctx, "sql_limit(): bad parameter count", -1 );
    return;
}

Регистрируем только одну версию. Передавая необходимое количество параметров, равное -1, мы сообщаем механизму SQLite, что готовы принять любое количество параметров:

sqlite3_create_function( db, "sql_limit", -1, SQLITE_UTF8,
        NULL, sql_getset_var_limit, NULL, NULL );

Хотя это работает, я по-прежнему предпочитаю версию sql_getset_limit(). Регистрация дает понять, какие версии функции считаются действительными, а код функции достаточно четкий и компактный.

Подсчет параметров полностью произвольной формы обычно используется такими элементами, как встроенная функция coalesce(). Функция coalesce() примет любое количество параметров (больше одного) и вернет первое значение в списке, отличное от NULL. Поскольку вы можете передать от двух до дюжины или более параметров, нецелесообразно регистрировать каждую возможную конфигурацию, и лучше просто позволить функции самостоятельно управлять параметрами.

С другой стороны, что-то вроде sql_getset_limit() действительно может принимать только две конфигурации: один или два параметра. В этом случае мне проще явно зарегистрировать оба счетчика параметров и позволить SQLite выполнить проверку моих параметров за меня.

Агрегатные функции

Агрегатные функции используются для сворачивания значений из группы строк в одно значение результата. Это можно сделать со всей таблицей, как это обычно бывает с агрегатной функцией count(*), или с группировкой строк из предложения GROUP BY, как это обычно бывает с чем-то вроде avg() или sum(). Агрегатные функции используются для суммирования или агрегирования всех значений отдельных строк в одно репрезентативное значение.

Определение агрегатов

Агрегатные функции SQL создаются с использованием той же функции sqlite3_create_function_xxx(), которая используется для создания скалярных функций (см. «Скалярные функции»). При определении скалярной функции вы передаете указатель функции C в шестом параметре и устанавливаете седьмой и восьмой параметр в NULL. При определении агрегатной функции шестой параметр устанавливается в NULL (указатель скалярной функции), а седьмой и восьмой параметры используются для передачи двух указателей функций C.

Первая функция C - это «пошаговая» функция. Она вызывается один раз для каждой строки в совокупной группе. Она действует аналогично скалярной функции, за исключением того, что не возвращает результата (однако может возвращать ошибку).

Вторая функция C - это функция «финализации». После того, как все строки SQL пройдены, вызывается функция finalize для вычисления и установки окончательного результата. Функция finalize не принимает никаких параметров SQL, но отвечает за установку значения результата.

Две функции C работают вместе, чтобы реализовать агрегатную функцию SQL. Рассмотрим встроенный агрегат avg(), который вычисляет среднее числовое значение всех строк в столбце. Каждый вызов функции step извлекает значение SQL для этой строки и обновляет как промежуточную сумму, так и количество строк. Функция finalize делит итог на количество строк и устанавливает значение результата агрегатной функции.

Функции C, используемые для реализации агрегата, определяются следующим образом:

void user_aggregate_step( sqlite3_context *ctx, int num_values, sqlite3_value **values )
Прототип определяемой пользователем агрегатной пошаговой функции. Эта функция вызывается один раз для каждой строки агрегированного вычисления. Прототип аналогичен скалярной функции, и все параметры имеют одинаковое значение. Функция step не должна устанавливать значение результата с помощью sqlite3_result_xxx(), но она может установить ошибку.
void user_aggregate_finalize( sqlite3_context *ctx )
Прототип определяемой пользователем функции завершения агрегата. Эта функция вызывается один раз в конце агрегации, чтобы произвести окончательный расчет и установить результат. Эта функция должна устанавливать значение результата или условие ошибки.

Большинство правил перегрузки функций SQL, которые применяются к скалярным функциям, также применимы к агрегатным функциям. Более одного набора функций C могут быть зарегистрированы под одним и тем же именем функции SQL, если используются разные подсчеты параметров или текстовые кодировки. Однако это реже используется с агрегатами, поскольку большинство агрегатных функций основаны на числах, и большинство агрегатов принимают только один параметр.

Также можно зарегистрировать как скалярную, так и агрегатную функции под одним и тем же именем, если количество параметров различается. Например, встроенные функции SQL min() и max() доступны как скалярные функции (с двумя параметрами), так и как агрегатные функции (с одним параметром).

Функции step и finalize можно смешивать и сопоставлять - они не всегда должны быть уникальными парами. Например, встроенные агрегаты sum() и avg() используют одну и ту же пошаговую функцию, поскольку оба агрегата должны вычислять промежуточную сумму. Единственное различие между этими агрегатами - это функция finalize. Функция finalize для sum() просто возвращает общий итог, а функция finalize для avg() сначала делит итог на количество строк.

Контекст агрегатов

Агрегатным функциям обычно требуется нести большое количество состояний. Например, встроенный агрегат avg() должен отслеживать промежуточную сумму, а также количество обработанных строк. Каждый вызов функции step, а также функции finalize требует доступа к некоторому общему блоку памяти, который содержит все значения состояния.

Хотя агрегатные функции могут вызывать sqlite3_user_data() или sqlite3_context_db_handle(), вы не можете использовать указатель пользовательских данных для хранения данных агрегированного состояния. Указатель пользовательских данных используется всеми экземплярами данной агрегатной функции. Если одновременно активны несколько экземпляров агрегатной функции (например, SQL-запрос, который усредняет более одного столбца), каждому экземпляру агрегата требуется частная копия данных агрегированного состояния или различные агрегированные вычисления будут перемешаны.

К счастью, есть простое решение. Поскольку почти каждая агрегатная функция требует каких-либо данных о состоянии, SQLite позволяет прикрепить блок данных к каждому конкретному агрегатному экземпляру.

void* sqlite3_aggregate_context( sqlite3_context *ctx, int bytes )

Эта функция может быть вызвана внутри функции агрегированного шага или функции завершения. Первый параметр - это структура sqlite3_context, переданная в функцию step или finalize. Второй параметр представляет количество байтов.

При первом вызове этой функции в конкретном экземпляре агрегата функция выделяет блок памяти подходящего размера, обнуляет его и присоединяет к контексту агрегата перед возвратом указателя. Эта функция будет возвращать тот же блок памяти при последующих вызовах функций step и finalize. Блок памяти автоматически освобождается, когда агрегат выходит за пределы области видимости.

Используя этот вызов API, вы можете заставить механизм SQLite автоматически выделять и освобождать ваши данные агрегированного состояния для каждого экземпляра. Это позволяет нескольким экземплярам вашей агрегированной функции быть активными одновременно без каких-либо дополнительных действий с вашей стороны.

Обычно одно из первых действий, выполняемых функцией step или finalize, - это вызов sqlite3_aggregate_context(). Например, рассмотрим эту упрощенную версию суммы:

void simple_sum_step( sqlite3_context *ctx, int num_values, sqlite3_value **values )
{
    double *total = (double*)sqlite3_aggregate_context( ctx, sizeof( double ) );
    *total += sqlite3_value_double( values[0] );
}

void simple_sum_final( sqlite3_context *ctx )
{
    double *total = (double*)sqlite3_aggregate_context( ctx, sizeof( double ) );
    sqlite3_result_double( ctx, *total );
}

/* ...inside an initialization function... */
    sqlite3_create_function( db, "simple_sum", 1, SQLITE_UTF8, NULL,
            NULL, simple_sum_step, simple_sum_final );

В этом случае мы выделяем достаточно памяти только для хранения значения с плавающей запятой двойной точности. Большинство агрегатных функций выделяют структуру C с любыми полями, необходимыми для вычисления агрегата, но все работает одинаково. При первом вызове simple_sum_step() вызов sqlite3_aggregate_context() выделит достаточно памяти для хранения значения double и обнулит его. Последующие вызовы simple_sum_step(), которые являются частью одного и того же вычисления агрегации (имеют тот же sqlite3_context), будут возвращать тот же блок памяти, что и simple_sum_final().

Поскольку sqlite3_aggregate_context() может потребоваться выделить память, также рекомендуется убедиться, что возвращаемое значение не равно NULL. Приведенный выше код в функциях step и finalize действительно должен выглядеть примерно так:

double *total = (double*)sqlite3_aggregate_context( ctx, sizeof( double ) );
if ( total == NULL ) {
    sqlite3_result_error_nomem( ctx );
    return;
}

Единственное предостережение при использовании sqlite3_aggregate_context() заключается в правильной работе с инициализацией структуры данных. Поскольку структура данных контекста автоматически выделяется и обнуляется при первом вызове, нет очевидного способа определить разницу между вновь выделенной структурой и структурой, которая была выделена в предыдущем вызове вашей функции step.

Если состояние «все нули» по умолчанию для вновь выделенного контекста не подходит, и вам нужно каким-то образом инициализировать агрегированный контекст, вам нужно будет включить какой-либо тип флага инициализации. Например:

typedef struct agg_state_s {
    int    init_flag;
    /* other fields used by aggregate... */
} agg_state;

Агрегатные функции могут использовать этот флаг, чтобы определить, нужно ли инициализировать агрегированные данные контекста или нет:

agg_state *st = (agg_state*)sqlite3_aggregate_context( ctx, sizeof( agg_state ) );
/* ...return nonmem error if st == NULL... */
if ( st->init_flag == 0 ) {
    st->init_flag = 1;
    /* ...initialize the rest of agg_state... */
}

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

Обязательно проверьте флаг инициализации как в пошаговой функции, так и в функции финализации. Бывают случаи, когда функция finalize может быть вызвана без предварительного вызова функции step, а функция finalize должна правильно обрабатывать эти случаи.

Пример

В качестве более подробного примера рассмотрим агрегированный средневзвешенный показатель. Хотя большинство агрегатов принимают только один параметр, наша агрегатная функция wtavg() принимает два. Первым параметром будет любое числовое значение, которое мы пытаемся усреднить, а вторым необязательным параметром будет вес для этой строки. Если строка имеет вес два, ее значение будет считаться вдвое более важным, чем строка с весом только один. Средневзвешенное значение берется путем суммирования произведения значений и весов и деления на сумму весов.

Говоря языком SQL, если наша функция wtavg() используется следующим образом:

SELECT wtavg( data, weight ) FROM ...

Результат должен быть примерно таким:

SELECT ( sum( data * weight ) / sum( weight ) ) FROM ...

Основное отличие состоит в том, что наша функция wtavg() должна быть немного более интеллектуальной в обработке недопустимых значений веса (например, NULL) и назначать им значение веса 1.0.

Чтобы отслеживать общие значения данных и значения общего веса, нам необходимо определить агрегированную структуру контекстных данных. Это будет содержать данные о состоянии для нашего агрегата. Единственное место, где упоминается эта структура, - это две агрегатные функции, поэтому нет необходимости помещать ее в отдельный файл заголовка. Его можно определить в коде вместе с двумя функциями:

typedef struct wt_avg_state_s {
    double   total_data;  /* sum of (data * weight) values */
    double   total_wt;    /* sum of weight values */
} wt_avg_state;

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

В этом примере я сделал второй параметр агрегатной функции (значение веса) необязательным. Если указан только один параметр, предполагается, что все веса равны единице, что приводит к традиционному среднему значению. Однако она все равно будет отличаться от встроенной функции avg(). Встроенная функция SQLite avg() следует стандарту SQL в отношении ввода и обработки NULL, что может быть не тем, что вы сначала предполагали. (Подробнее см. avg() в приложении E). Наш wtavg() немного проще. Помимо того, что всегда возвращается значение double (даже если результат может быть выражен как целое число), он просто игнорирует любые значения, которые не могут быть легко преобразованы в число.

Во-первых, ступенчатая функция. Она обрабатывает каждую строку, складывая продукты с ценным весом, а также общее значение веса:

void wt_avg_step( sqlite3_context *ctx, int num_values, sqlite3_value **values )
{
    double        row_wt = 1.0;
    int           type;
    wt_avg_state  *st = (wt_avg_state*)sqlite3_aggregate_context( ctx,
                                         sizeof( wt_avg_state ) );
if ( st == NULL ) {
    sqlite3_result_error_nomem( ctx );
    return;
}

/* Extract weight, if we have a weight and it looks like a number */
if ( num_values == 2 ) {
    type = sqlite3_value_numeric_type( values[1] );
    if ( ( type == SQLITE_FLOAT )||( type == SQLITE_INTEGER ) ) {
        row_wt = sqlite3_value_double( values[1] );
    }
}

/* Extract data, if we were given something that looks like a number. */
type = sqlite3_value_numeric_type( values[0] );
if ( ( type == SQLITE_FLOAT )||( type == SQLITE_INTEGER ) ) {
    st->total_data += row_wt * sqlite3_value_double( values[0] );
    st->total_wt += row_wt;
}

Наша пошаговая функция использует sqlite3_value_numeric_type(), чтобы попытаться без потерь преобразовать значения параметров в числовой тип. Если преобразование возможно, мы всегда преобразуем значения в числа с плавающей запятой двойной точности, просто для простоты. Этот подход означает, что функция будет правильно работать с текстовыми представлениями чисел (такими как строка '153'), но будет игнорировать другие типы данных и другие строки.

В этом случае функция не сообщает об ошибке, она просто игнорирует значение. Если вес не может быть преобразован, предполагается, что он равен единице. Если значение данных не может быть преобразовано, строка пропускается.

Когда у нас есть итоги, нам нужно вычислить окончательный ответ и вернуть результат. Это делается в функции finalize, что довольно просто. Главное, о чем нужно побеспокоиться, - это возможность деления на ноль:

void wt_avg_final( sqlite3_context *ctx )
{
    double        result = 0.0;
    wt_avg_state  *st = (wt_avg_state*)sqlite3_aggregate_context( ctx,
                                         sizeof( wt_avg_state ) );
    if ( st == NULL ) {
        sqlite3_result_error_nomem( ctx );
        return;
    }

    if ( st->total_wt != 0.0 ) {
        result = st->total_data / st->total_wt;
    }
    sqlite3_result_double( ctx, result );
}

Чтобы использовать наш агрегат, код нашего приложения должен зарегистрировать эти две функции с подключением к базе данных с помощью sqlite3_create_function(). Поскольку агрегат wtavg() предназначен для приема одного или двух параметров, мы зарегистрируем его дважды:

sqlite3_create_function( db, "wtavg", 1, SQLITE_UTF8, NULL,
        NULL, wt_avg_step, wt_avg_final );
sqlite3_create_function( db, "wtavg", 2, SQLITE_UTF8, NULL,
        NULL, wt_avg_step, wt_avg_final );

Вот несколько примеров запросов из командной оболочки sqlite3. Предполагается, что мы интегрировали наш настраиваемый агрегат в код sqlite3 (пример различных способов сделать это приведен ниже в этой главе):

sqlite> SELECT class, value, weight FROM t;

class       value       weight
----------  ----------  ----------
1           3.4         1.0
1           6.4         2.3
1           4.3         0.9
2           3.4         1.4
3           2.7         1.1
3           2.5         1.1

Во-первых, мы можем попробовать что-то только с одним параметром. Это будет использовать вес по умолчанию 1.0 для каждой строки, что приведет к традиционному вычислению среднего значения:

sqlite> SELECT class, wtavg( value ) AS wtavg, avg( value ) AS avg
   ...>   FROM t GROUP BY 1;

class       wtavg       avg
----------  ----------  ----------
1           4.7         4.7
2           3.4         3.4
3           2.6         2.6

И напоследок пример расчета полного средневзвешенного значения:

sqlite> SELECT class, wtavg( value, weight ) AS wtavg, avg( value ) AS avg
   ...>   FROM t GROUP BY 1;

class       wtavg             avg
----------  ----------------  ----------
1           5.23571428571428  4.7
2           3.4               3.4
3           2.6               2.6

В случае class = 1 мы видим явную разницу, где тяжелый вес 6.4 показывает среднее значение выше. Для class = 2 есть только одно значение, поэтому взвешенные и невзвешенные средние совпадают (само значение). В случае class = 3 веса одинаковы для всех значений, поэтому, опять же, среднее значение совпадает с невзвешенным средним.

Функции сопоставления

Сопоставления используются для сортировки текстовых значений. Их можно использовать с предложениями ORDER BY или GROUP BY или для определения индексов. Вы также можете назначить сопоставление столбцу таблицы, чтобы любая операция индексации или упорядочения, применяемая к этому столбцу, автоматически использовала определенное сопоставление. Помимо всего прочего, SQLite всегда будет сортировать по типу данных. Первыми всегда будут NULL, за которыми следует сочетание целочисленных и числовых значений с плавающей запятой в их естественном порядке сортировки. После чисел идут текстовые значения, за которыми следуют BLOB.

Большинство типов имеют четко определенный порядок сортировки. Типы NULL не имеют значений, поэтому их нельзя отсортировать. Числовые типы используют свой естественный числовой порядок, а BLOBы всегда сортируются с использованием двоичных сравнений. Что интересно, так это когда дело доходит до текстовых значений.

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

В дополнение к параметрам сортировки BINARY по умолчанию, SQLite включает встроенные параметры сортировки NOCASE и RTRIM, которые можно использовать с текстовыми значениями. Параметры сортировки NOCASE игнорируют регистр символов в целях сортировки 7-битного ASCII и будут рассматривать выражение 'A' == 'a' как истинное. Однако он не считает 'Ä' == 'ä' истинным и не считает 'Ä' == 'A' истинным, поскольку представления этих символов выходят за рамки стандарта ASCII. Параметры сортировки RTRIM (обрезка по правому краю) аналогичны параметрам сортировки BINARY по умолчанию, только игнорируют завершающие пробелы (то есть пробелы с правой стороны значения).

Хотя эти встроенные параметры сортировки предлагают некоторые основные параметры, бывают случаи, когда требуется сложный порядок сортировки. Это особенно верно, когда вы попадаете в представления Unicode для языков, которые не могут быть представлены с помощью простой 7-битной кодировки ASCII. Вам также может потребоваться специальная функция сортировки, которая сортирует по целым словам или группам символов, если вы храните что-то, кроме текста на естественном языке. Например, если вы храните последовательности генов в виде текстовых данных, вам может потребоваться пользовательская функция сортировки для этих данных.

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

Регистрация сопоставления

Чтобы определить настраиваемое сопоставление, приложение должно зарегистрировать функцию сравнения под именем сопоставления. Каждый раз, когда движку базы данных нужно отсортировать что-то в соответствии с этим сопоставлением, он использует функцию сравнения для определения требуемого порядка. Вам нужно будет повторно зарегистрировать параметры сортировки для каждого подключения к базе данных, которое требует этого.

Есть три вызова API, которые можно использовать для регистрации функции сравнения параметров сортировки:

int sqlite3_create_collation( sqlite3 *db, const char *name, int text_rep, void *udp, comp_func ) int sqlite3_create_collation16( sqlite3 *db, const void *name, int text_rep, void *udp, comp_func )

Регистрирует функцию сравнения параметров сортировки с подключением к базе данных. Первый параметр - это соединение с базой данных. Второй параметр - это имя настраиваемого сопоставления, закодированного как строка UTF-8 или UTF-16. Третий параметр - это строковая кодировка, которую ожидает функция сравнения, и может быть одним из SQLITE_UTF8, SQLITE_UTF16, SQLITE_UTF16BE, SQLITE_UTF16LE или SQLITE_UTF16_ALIGNED (собственный UTF-16, выровненный по 16-битной памяти). Четвертый параметр - это общий указатель пользовательских данных, который передается вашей функции сравнения. Последний параметр - это указатель функции на вашу функцию сравнения (прототип этой функции приведен ниже).

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

int sqlite3_create_collation_v2( sqlite3 *db, const char *name, int text_rep, void *udp, comp_func, dest_func )

Эта функция аналогична sqlite3_create_collation() с одним дополнительным параметром. Дополнительный шестой параметр - это необязательный указатель на функцию, ссылающийся на функцию очистки, которая вызывается при уничтожении сопоставления (прототип этой функции приведен ниже). Это позволяет сопоставлению высвободить любые ресурсы, связанные с сопоставлением (например, указатель пользовательских данных). Указатель на функцию NULL может быть передан, если функция уничтожения не требуется.

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

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

Формат указателей пользовательских функций приведен ниже.

int user_defined_collation_compare( void* udp, int lenA, const void *strA, int lenB, const void *strB )

Это тип функции определяемой пользователем функции сравнения параметров сортировки. Первый параметр - это указатель пользовательских данных, переданный в sqlite3_create_collation_xxx() в качестве четвертого параметра. Следующие параметры передают указатели длины и буфера для двух строк. Строки будут в любой кодировке, определенной функцией регистрации. Вы не можете предполагать, что строки заканчиваются нулем.

Возвращаемое значение должно быть отрицательным, если строка A меньше строки B (то есть, A сортирует до B), 0, если строки считаются равными, и положительным, если строка A больше, чем B (A сортирует после B). По сути, возвращаемое значение - это порядок A минус B.

void user_defined_collation_destroy( void *udp )
Это тип функции определяемой пользователем функции уничтожения сопоставления. Единственный параметр - это указатель пользовательских данных, переданный в качестве четвертого параметра sqlite3_cre ate_collation_v2().

Хотя функции сопоставления имеют доступ к указателю пользовательских данных, у них нет указателя sqlite3_context. Это означает, что нет никакого способа сообщить об ошибке движку SQLite. Таким образом, если у вас есть сложная функция сопоставления, вы должны попытаться устранить как можно больше источников ошибок. В частности, это означает, что рекомендуется заранее выделить любые рабочие буферы, которые могут вам понадобиться, поскольку нет возможности прервать сравнение, если распределение памяти не выполнено. Поскольку функция сопоставления на самом деле представляет собой простое сравнение, ожидается, что она будет работать и каждый раз давать ответ.

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

Пример сопоставления

Вот простой пример определяемой пользователем сортировки. В этом примере мы определяем параметры сортировки STRINGNUM, которые можно использовать для сортировки строковых представлений числовых значений.

Если они не одинаковой длины, строковые представления чисел часто сортируются странным образом. Например, используя стандартные правила сортировки текста, строка '485' будет отсортирована перед строкой '73', потому что символ '4' сортируется перед символом '7', так же, как символ 'D' сортируется перед символом 'G'. Чтобы было ясно, это текстовые строки, состоящие из символов, которые представляют собой цифры, а не фактические числа.

При сопоставлении предпринимается попытка преобразовать эти строки в числовое представление, а затем использовать это числовое значение для сортировки. Используя это сопоставление, строка '485' будет отсортирована после '73'. Для простоты мы будем иметь дело только с целочисленными значениями:

int col_str_num( void *udp,
    int lenA, const void *strA,
    int lenB, const void *strB )
{
    int valA = col_str_num_atoi_n( (const char*)strA, lenA );
    int valB = col_str_num_atoi_n( (const char*)strB, lenB );

    return valA - valB;
}

static int col_str_num_atoi_n( const char *str, int len )
{
    int total = 0, i;
    for ( i = 0; i < len; i++ ) {
        if ( ! isdigit( str[i] ) ) {
            break;
        }
        total *= 10;
        total += digittoint( str[i] );
    }
    return total;
}

Сопоставление пытается преобразовать каждую строку в целочисленное значение с помощью нашей пользовательской функции col_str_num_atoi_n(), а затем сравнивает числовые результаты. Функция col_str_num_atoi_n() очень похожа на стандартную функцию C atoi() с той разницей, что она принимает параметр максимальной длины. Это необходимо в данном случае, поскольку строки, переданные в наши сопоставления, могут не иметь завершающего нуля.

Мы бы зарегистрировали это сопоставление с помощью SQLite следующим образом:

sqlite3_create_collation( db, "STRINGNUM", SQLITE_UTF8, NULL, col_str_num );

Поскольку стандартная функция C isdigit() не поддерживает Unicode, наша функция сортировки с сопоставлением будет работать только со строками, ограниченными 7-битным ASCII.

Тогда у нас может быть SQL, который выглядит так:

sqlite> CREATE TABLE t ( s TEXT );
sqlite> INSERT INTO t VALUES ( '485' );
sqlite> INSERT INTO t VALUES ( '73' );
sqlite> SELECT s FROM t ORDER BY s;
485
73
sqlite> SELECT s FROM t ORDER BY s COLLATE STRINGNUM;
73
485

Также можно было бы навсегда связать нашу сортировку с определенным столбцом таблицы, включив сортировку в определение таблицы. См. CREATE TABLE в приложении C для более подробной информации.

Расширения SQLite

Хотя пользовательские функции являются очень мощной функцией, они также могут создавать нежелательные зависимости между файлами базы данных и пользовательскими средами SQLite. Если база данных использует настраиваемую сортировку в определении таблицы или настраиваемую функцию в определении представления, то эта база данных не может быть открыта никаким приложением (включая sqlite3), в котором не определены все надлежащие настраиваемые функции.

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

Сложности возникают в том случае, если вам нужно открыть файлы базы данных в приложении общего назначения, таком как оболочка командной строки sqlite3 или один из сторонних менеджеров баз данных с графическим интерфейсом пользователя. Без какого-либо способа принести с собой свои настраиваемые функции и возможности, ваш единственный выбор - объединить код настраиваемой функции с источником любых утилит, которые вам требуются, и создать версии для конкретного сайта, поддерживающие вашу среду SQL. В большинстве случаев это не очень практично, особенно если исходный код утилиты недоступен.

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

Статические расширения можно создавать и связывать непосредственно с приложением, в отличие от статической библиотеки C. Загружаемые расширения действуют как внешние библиотеки или «плагины» для механизма SQLite. Если вы создаете свое расширение как внешнее загружаемое расширение, вы можете загрузить расширение (почти) в любую среду SQLite, сделав свои пользовательские функции и среду SQL доступными для sqlite3 или любого другого менеджера баз данных.

В обоих случаях расширения - удобный способ упаковать набор связанных функций в одну развертываемую единицу. Это особенно полезно, если вы пишете библиотеку поддержки SQL, которая используется большим количеством приложений, или если вы пишете интерфейс SQLite для существующей библиотеки. Структурирование кода как расширения также предоставляет стандартный способ распространения набора настраиваемых функций среди других пользователей SQLite. Предоставляя свой код в качестве расширения, каждый разработчик может выбрать создание и интеграцию расширения в соответствии со своими потребностями, не беспокоясь о формате или дизайне кода расширения.

Даже если вы планируете статически связывать все свои пользовательские функции и код функции непосредственно в своем приложении, упаковка кода как расширения все равно имеет большое значение. Написав расширение, вы не потеряете возможность создавать и статически связывать свое расширение непосредственно с вашим приложением, но вы получаете возможность создавать внешний загружаемый модуль.

Наличие вашей расширенной среды в виде загружаемого модуля позволяет вам воссоздать среду SQL вашего приложения с помощью инструмента командной строки sqlite3 или любого другого менеджера баз данных общего назначения. Это открывает возможность интерактивного изучения файлов базы данных, чтобы разрабатывать и тестировать запросы, отлаживать проблемы и отслеживать проблемы поддержки клиентов. Уже одно это является веской причиной рассмотреть возможность написания всех ваших пользовательских функций в виде загружаемых расширений, даже если вы никогда не планируете выпускать или распространять автономные загружаемые расширения.

Архитектура расширений

Расширения - это не что иное, как стиль упаковки вашего кода. Вызовы API SQLite, используемые для регистрации и создания пользовательских обработчиков функций, агрегатных обработчиков, сопоставлений или других настраиваемых функций, полностью не изменяются в расширении. Единственное различие заключается в процессе инициализации, который создает и связывает ваши пользовательские функции C с подключением к базе данных. Процесс сборки также немного отличается, в зависимости от того, хотите ли вы создать статически связанное расширение или динамически загружаемое расширение, но оба типа расширений могут быть построены из одного и того же исходного кода.

Архитектура расширений направлена на обеспечение корректной работы динамически загружаемых расширений на нескольких платформах. Самая большая проблема для архитектуры динамических расширений - убедиться, что загружаемое расширение имеет доступ к SQLite API. Если не вдаваться в подробности того, как компоновщик среды выполнения работает в разных операционных системах, основная проблема заключается в том, что код, скомпилированный в расширение и загруженный во время выполнения, может быть не в состоянии разрешить зависимости ссылок из загружаемого расширения обратно в приложение, в котором находится библиотека SQLite.

Чтобы избежать этой проблемы, при инициализации расширения ему передается большая структура данных, которая содержит указатель функции C на каждую функцию в SQLite API. Вместо прямого вызова функций SQLite расширение будет разыменовывать требуемый указатель функции и использовать его. Это дает возможность разрешать любые вызовы библиотеки SQLite вне зависимости от компоновщика. Хотя это не полностью требуется для статического расширения, механизм одинаково хорошо работает как со статическими, так и с динамическими расширениями.

К счастью, детали того, как работает эта большая структура данных, хорошо скрыты от разработчика с помощью альтернативного файла заголовка и нескольких макросов препроцессора. Эти макросы полностью скрывают всю проблему компоновщика и указателя на функцию, но с одним ограничением: весь код расширения, который выполняет вызовы в API SQLite, должен находиться в одном файле вместе с функцией инициализации расширения. Этот код может обращаться к другим файлам и другим библиотекам, пока этот «другой код» не выполняет никаких прямых вызовов какой-либо функции sqlite3_xxx().

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

На практике это редко бывает значительным ограничением. Хранение ваших пользовательских расширений SQLite в их собственных файлах, вне кода вашего приложения, - это естественный способ организовать ваш код. Большинство расширений SQLite состоят из нескольких сотен строк или меньше, особенно если они просто действуют как связующий слой между SQLite и какой-либо другой библиотекой. Это может сделать их большими, но обычно не такими большими, что они становятся неуправляемыми как единый файл.

Дизайн расширения

Чтобы написать расширение, нам нужно использовать файл заголовка расширения. Вместо более распространенного файла sqlite.h расширение использует файл sqlite3ext.h:

#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1;  /* required by SQLite extension header */

Заголовок расширения SQLite определяет два макроса. Первым из них является SQLITE_EXTENSION_INIT1, и на него следует ссылаться в верхней части файла C, содержащего источник расширения. Этот макрос определяет переменную в файловой области, которая содержит указатель на большую структуру API.

Каждое расширение должно определять точку входа. Это действует как функция инициализации расширения. Функция точки входа выглядит так:

int ext_entry_point( sqlite3 *db, char **error, const sqlite3_api_routines *api )

Это прототип точки входа расширения. Первый параметр - это соединение с базой данных, загружающее это расширение. Второй параметр может использоваться для передачи ссылки на сообщение об ошибке, если расширение не может правильно инициализировать себя. Последний параметр используется для передачи блока указателей функций, чтобы помочь в процессе связывания. Мы скоро увидим, как это используется.

Эта функция вызывается механизмом SQLite при загрузке статического или динамического расширения. Обычно эта функция создает и регистрирует любые настраиваемые функции или другие настраиваемые расширения с подключением к базе данных.

Точка входа выполняет две основные задачи. Первое задание - завершить процесс инициализации, вызвав второй макрос расширения. Это должно быть сделано в качестве первого бита кода в точке входа (макрос разворачивается в строку кода, поэтому, если вы работаете на чистом C, вам нужно будет поместить любые переменные области действия перед макросом инициализации). Это необходимо сделать до того, как будут выполнены какие-либо вызовы sqlite3_xxx(), иначе приложение выйдет из строя:

int ext_init( sqlite3 *db, char **error, const sqlite3_api_routines *api )
{
    /* local variable definitions */
    SQLITE_EXTENSION_INIT2(api);
    /* ... */
}

Этот макрос - единственный раз, когда вам нужно напрямую ссылаться на параметр api. После того, как функция ввода завершит инициализацию API расширения, она может приступить к своему второму основному заданию, которое заключается в регистрации любых без исключения пользовательских функций или возможностей, предоставляемых этим расширением.

В отличие от многих функций, название функции точки входа имеет большое значение. Когда загружается динамическое расширение, SQLite должен попросить компоновщик среды выполнения вернуть указатель функции на функцию точки входа. Для этого необходимо знать имя точки входа.

Как мы увидим, когда мы посмотрим на функции динамической загрузки, по умолчанию SQLite будет искать точку входа с именем sqlite3_extension_init(). Теоретически это хорошее имя функции, поскольку оно позволяет загружать динамическое расширение, даже если все, что вы знаете, - это имя файла.

Хотя одно и то же приложение может загружать несколько динамических расширений, даже если они имеют одно и то же имя точки входа, это не относится к статически связанным расширениям. Если вам нужно статически связать более одного расширения с вашим приложением, точки входа должны иметь уникальные имена, иначе компоновщик не сможет правильно связать расширения.

В результате принято называть точку входа чем-то уникальным для расширения, но довольно легко документируемым и запоминающимся. Точка входа часто имеет то же имя, что и само расширение, возможно, с суффиксом _init. Пример расширения, который мы рассмотрим, называется sql_trig, поэтому хорошим выбором для точки входа будет sql_trig() или sql_trig_init().

Пример расширения: sql_trig

Для нашего примера расширения мы создадим пару функций SQL, которые предоставляют некоторые простые тригонометрические функции из стандартной математической библиотеки C. Поскольку это всего лишь пример, мы будем создавать только две функции SQL, но вы можете использовать ту же базовую технику для создания функций SQL почти для каждой функции в стандартной математической библиотеке.

Первая половина нашего исходного файла sql_trig.c содержит две функции, которые мы будем определять в нашем примере расширения. Сами функции довольно просты: они извлекают одно число с плавающей запятой двойной точности, конвертируют градусы в радианы, а затем возвращают результат из математической библиотеки. Я также показал верхнюю часть файла с необходимыми операторами #include и макросами инициализации:

/* sql_trig.c */

#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1;

#include <stdlib.h>

/* this bit is required to get M_PI out of MS headers */
#if defined( _WIN32 )
#define _USE_MATH_DEFINES
#endif /* _WIN32 */

#include <math.h>

static void sql_trig_sin( sqlite3_context *ctx, int num_values, sqlite3_value **values )
{
    double a = sqlite3_value_double( values[0] );
    a = ( a / 180.0 ) * M_PI;   /* convert from degrees to radians */
    sqlite3_result_double( ctx, sin( a ) );
}

static void sql_trig_cos( sqlite3_context *ctx, int num_values, sqlite3_value **values )
{
    double a = sqlite3_value_double( values[0] );
    a = ( a / 180.0 ) * M_PI;   /* convert from degrees to radians */
    sqlite3_result_double( ctx, cos( a ) );
}

Вы заметите, что они объявлены как static функции. Если сделать их static, они будут скрыты от компоновщика, что устранит любые возможные конфликты имен между этим расширением и другими расширениями. Пока точка входа расширения находится в том же файле (который, как мы уже обсуждали, требуется по другим причинам), точка входа по-прежнему сможет правильно зарегистрировать эти функции. Объявление этих функций статическими не является строго обязательным, но это хорошая практика и может устранить конфликты связывания.

Затем нам нужно определить нашу точку входа. Вот вторая часть файла sql_trig.c:

int sql_trig_init( sqlite3 *db, char **error, const sqlite3_api_routines *api )
{
    SQLITE_EXTENSION_INIT2(api);

    sqlite3_create_function( db, "sin", 1,
            SQLITE_UTF8, NULL, sql_sin, NULL, NULL );
    sqlite3_create_function( db, "cos", 1,
            SQLITE_UTF8, NULL, sql_cos, NULL, NULL );

    return SQLITE_OK;
}

Эту функцию точки входа не следует объявлять static. Как статический компоновщик (в случае статического расширения), так и динамический компоновщик (в случае загружаемого расширения) должны иметь возможность найти функцию точки входа для правильной работы расширения. Если сделать функцию static, она будет скрыта от компоновщика.

Эти два блока кода составляют весь наш исходный файл sql_trig.c. Давайте посмотрим, как создать этот файл как статическое расширение или как динамически загружаемое расширение.

Сборка и интеграция статических расширений

Чтобы статически связать расширение с приложением, вы можете просто встроить исходный файл расширения в приложение, как и любой другой файл .c. Если код вашего приложения содержался в файле application.c, вы могли бы создать и связать наш пример расширения sql_trig с помощью команд, показанных здесь.

В случае большинства систем Linux, Unix и Mac OS X наш пример триггера требует, чтобы мы явно связали математическую библиотеку (libm). В некоторых случаях также требуется стандартная библиотека C (libc). Windows включает математические функции в стандартные библиотеки времени выполнения, поэтому связывание в математической библиотеке не требуется.

Системы Unix и Mac OS X (с математической библиотекой):

$ gcc -o application application.c sqlite3.c sql_trig.c -lm

Системы Windows, использующие компилятор Visual Studio:

> cl /Feapplication application.c sqlite3.c sql_trig.c

Эти команды должны создать исполняемый файл с именем application (или application.exe в Windows).

Простое объединение кода вместе не позволяет волшебным образом интегрировать его в SQLite. Вам по-прежнему необходимо, чтобы SQLite знал о расширении, чтобы библиотека SQLite могла правильно инициализировать расширение:

int sqlite3_auto_extension( entry_point_function );

Регистрирует функцию точки входа расширения в библиотеке SQLite. Как только это будет сделано, SQLite автоматически вызовет функцию точки входа расширения для каждого открытого соединения с базой данных. Единственный параметр - это указатель функции на точку входа.

Эта функция работает только со статически связанными расширениями и не работает с динамически загружаемыми расширениями. Эту функцию можно вызывать столько раз, сколько необходимо для регистрации необходимого количества уникальных точек входа.

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

Единственная странность в sqlite3_auto_extension() - это объявление функции точки входа. Вызов API автоматического расширения объявляет, что указатель функции имеет тип void entry_point (void). Это определяет функцию, которая не принимает параметров и не возвращает значения. Как мы уже видели, фактическая точка входа в расширение имеет немного более сложный прототип.

Код, который фактически вызывает расширение, сначала приводит предоставленный указатель функции к правильному типу, поэтому тот факт, что типы не совпадают, является проблемой только для установки указателя. Расширения обычно не имеют файлов заголовков, поскольку функция точки входа обычно является единственным элементом заголовка. Чтобы все заработало, вы можете либо предоставить правильный прототип для точки входа, а затем привести обратно к тому, что ожидает API, либо вы можете просто неправильно объявить прототип функции и позволить компоновщику согласовать вещи. Чистый C не проверяет типы параметров функции при связывании, поэтому это будет работать, даже если это не самый элегантный подход.

Вот как может выглядеть правильный прототип с приведением в коде нашего приложения:

/* declare the (correct) function prototype manually */
int sql_trig_init( sqlite3 *db, char **error, const sqlite3_api_routines *api );

/* ... */
    sqlite3_auto_extension( (void(*)(void))sql_trig_init ); /* needs cast */
/* ... */

Или, если вы работаете на чистом C, вы можете просто объявить другой прототип:

/* declare the (wrong) function prototype manually */
void sql_trig_init(void);
/* ... */
    sqlite3_auto_extension( sql_trig_init );
/* ... */

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

Если вам нужен быстрый практический пример того, как добавить статическое расширение к существующему приложению, мы можем добавить наше расширение sql_trig в оболочку sqlite3 с минимальным количеством изменений. Нам понадобится наш файл sql_trig.c, который содержит две триггерные функции SQL, а также функцию ввода sql_trig_init(). Нам также понадобится исходный код shell.c для приложения командной строки sqlite3.

Во-первых, нам нужно добавить несколько хуков инициализации в исходный код sqlite3. Сделайте копию файла shell.c как shell_trig.c. Откройте новую копию и найдите фразу «int main (», чтобы быстро найти начальную точку приложения. Прямо перед основной функцией в области глобального файла добавьте прототип для нашей точки входа sql_trig_init():

/* ... */
void sql_trig_init(void);  /* insert this line */

int main(int argc, char **argv){
/* ... */

Затем внутри существующей функции main() найдите вызов «open_db (», чтобы найти подходящее место для вставки нашего кода. Прямо перед небольшим блоком кода (и комментариями), содержащим первый вызов open_db(), добавьте эту строку:

sqlite3_auto_extension( sql_trig_init );

С этими двумя изменениями вы можете сохранить и закрыть файл shell_trig.c. Затем мы можем перекомпилировать наш модифицированный исходный код shell_trig.c в специальную утилиту sqlite3trig, в которую встроено наше расширение.

Unix/Linux и Mac OS X:

$ gcc -o sqlite3trig sqlite3.c shell_trig.c sql_trig.c -lm

Windows:

> cl /Fesqlite3trig sqlite3.c shell_trig.c sql_trig.c

В наше новое приложение sqlite3trig теперь встроено наше расширение, и наши функции доступны из любой базы данных, которая открывается с помощью нашей модифицированной утилиты sqlite3trig:

$ ./sqlite3trig
SQLite version 3.X.XX
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> SELECT sin( 30 );
0.5
sqlite> SELECT cos( 30 );
0.866025403784439

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

Использование загружаемых расширений

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

Загружаемые файлы расширений в основном представляют собой общие библиотеки в любом формате, подходящем для платформы. Загружаемые расширения упаковывают скомпилированный код в формат, который операционная система может загрузить и связать с вашим приложением во время выполнения. Таблица 9-1 содержит сводку соответствующих форматов файлов на разных платформах.

Таблица 9-1. Сводка загружаемого формата файла расширения

Платформа Тип файла Расширение файла по умолчанию
Linux и большинство Unix Общие объектные файлы .so
Mac OS X Динамическая библиотека .dylib
Windows Динамически подключаемая библиотека .DLL

Загружаемые расширения поддерживаются не на всех платформах. Загружаемые расширения зависят от операционной системы, имеющей хорошо поддерживаемый компоновщик среды выполнения, и не все портативные и встроенные устройства предлагают такой уровень поддержки. В общем, если платформа поддерживает какой-либо тип динамической или разделяемой библиотеки для использования в приложениях, существует разумная вероятность того, что загружаемый интерфейс расширения будет доступен. Если платформа не поддерживает динамические или разделяемые библиотеки, вы можете быть ограничены статически связанными расширениями. Однако для большинства встроенных сред это не является серьезным ограничением.

Хотя форматы файлов и расширения зависят от платформы, нередко можно выбрать собственное расширение файла, которое используется на всех поддерживаемых вами платформах. Использование общего расширения файла не требуется, но оно может упростить кроссплатформенный код C или SQL, который отвечает за загрузку расширений. Как и файлы базы данных, для загружаемого расширения SQLite нет официального расширения, но иногда используется .sqlite3ext. Это то, что я буду использовать в наших примерах.

Создание загружаемых расширений

Как правило, создание загружаемого расширения аналогично созданию динамической или общей библиотеки. Код сначала должен быть скомпилирован в объектный файл (файл .o или .obj), и этот файл должен быть упакован в общую или динамическую библиотеку. Процесс создания объектного файла одинаков как для статических, так и для динамических библиотек. Вы можете создать объектный файл напрямую с помощью одной из этих команд.

Mac OS X и Unix / Linux:

$ gcc -c sql_trig.c

Windows:

> cl /c sql_trig.c

Когда у вас есть объектный файл, его необходимо преобразовать в динамическую или общую библиотеку с помощью компоновщика. Команды для этого немного больше зависят от платформы.

Во-первых, команда Unix и Linux, которая создает общий объектный файл и связывает его со стандартной математической библиотекой:

$ ld -shared -o sql_trig.sqlite3ext sql_trig.o -lm

Mac OS X, в которой используются динамические библиотеки, а не файлы общих объектов:

$ ld -dylib -o sql_trig.sqlite3ext sql_trig.o -lm

И, наконец, Windows, где нам нужно создать файл DLL. В этом случае нам нужно указать компоновщику, какие символы мы хотим экспортировать. Для расширения нужно экспортировать только точку входа, поэтому мы просто включаем ее в командную строку:

> link /dll /out:sql_trig.sqlite3ext /export:sql_trig_init sql_trig.obj

Вы можете протестировать свое динамическое расширение в sqlite3 с помощью команды .load. Команда принимает два параметра. Первый - это имя файла вашего загружаемого расширения, а второй - имя функции точки входа:

$ sqlite3
SQLite version 3.X.XX
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> SELECT sin( 60 );
Error: no such function: sin
sqlite> .load sql_trig.sqlite3ext sql_trig_init
sqlite> SELECT sin( 60 );
0.866025403784439

Как видите, когда мы впервые запускаем sqlite3, он не знает ни о нашем расширении, ни о функциях SQL, которые он содержит. Команда .load используется для динамической загрузки расширения. После загрузки наши настраиваемые триггерные функции доступны без необходимости перекомпилировать или перестраивать утилиту sqlite3.

Безопасность загружаемых расширений

Есть некоторые незначительные проблемы безопасности, связанные с загружаемыми расширениями. Поскольку расширение может содержать практически любой код, загружаемое расширение может использоваться для переопределения значений приложения для среды SQLite. В частности, если приложение использует функцию авторизации для защиты от определенных типов запросов или модификаций, загружаемое расширение может очистить функцию обратного вызова авторизации, исключив любой шаг авторизации (подробнее см. sqlite3_set_authorizer() в приложении G).

Чтобы предотвратить эту и другие возможные проблемы, приложение должно явно разрешить возможность загрузки внешних расширений. Это нужно делать каждый раз, когда устанавливается соединение с базой данных.

int sqlite3_enable_load_extension( sqlite3 *db, int onoff )

Включает или отключает возможность загрузки динамических расширений. Загружаемые расширения по умолчанию отключены.

Первый параметр - это устанавливаемое соединение с базой данных. Второй параметр должен иметь значение true (ненулевое значение), чтобы включить расширения, или false (ноль), чтобы отключить их. Эта функция всегда возвращает SQLITE_OK.

Большинство приложений общего назначения, включая оболочку sqlite3, автоматически включают загружаемые расширения для каждой открытой базы данных. Если ваше приложение поддерживает загружаемые расширения, вам также необходимо включить это. Загрузка расширения должна быть включена для каждого соединения с базой данных, каждый раз, когда соединение с базой данных открывается.

Загрузка загружаемых расширений

Есть два способа загрузить расширение. Один - через вызов C API, а другой - через функцию SQL, которая вызывает тот же код, что и функция C API. В обоих случаях вы указываете имя файла и, необязательно, имя функции точки входа.

int sqlite3_load_extension( sqlite3 *db, const char *ext_name, const char *entry_point, char **error )

Пытается загрузить загружаемое расширение и связать его с данным подключением к базе данных. Первый параметр - это соединение с базой данных, которое нужно связать с этим расширением. Второй параметр - это имя файла расширения. Третий параметр - это имя функции точки входа. Если имя точки входа NULL, используется точка входа sqlite3_extension_init. Четвертый параметр используется для передачи сообщения об ошибке, если что-то пойдет не так. Этот строковый буфер должен быть освобожден с помощью sqlite3_free(). Этот последний параметр является необязательным и может иметь значение NULL.

Это вернет либо SQLITE_OK, чтобы указать, что расширение было загружено и функция инициализации была успешно вызвана, либо он может вернуть SQLITE_ERROR, чтобы указать, что что-то пошло не так. Если возвращается условие ошибки, может быть или не быть допустимой строки ошибки.

Эта функция обычно вызывается, как только открывается соединение с базой данных, до подготовки каких-либо операторов. Хотя вызов sqlite3_load_extension() в любое время является законным, любые вызовы API, сделанные точкой входа расширения и функцией инициализации, подчиняются стандартным ограничениям. В частности, это означает, что любые вызовы sqlite3_create_function(), сделанные функцией точки входа расширения, не смогут переопределить или удалить функцию, если есть какие-либо выполняющиеся операторы SQL.

Другой способ загрузить загружаемое расширение - использовать встроенную функцию SQL load_extension().

load_extension( 'ext_name' ) load_extension( 'ext_name', 'entry_point' )
Эта функция SQL загружает расширение с заданным именем файла. Если указано имя точки входа, оно используется как функция инициализации. В противном случае будет использовано имя sqlite3_extension_init.

Эта функция аналогична вызову sqlite3_load_extension() в языке C, но с одним существенным ограничением. Поскольку это функция SQL, при ее вызове по определению будет выполняться инструкция SQL при загрузке расширения. Это означает, что любое расширение, загруженное функцией SQL load_extension(), не сможет переопределить или удалить пользовательскую функцию, включая специализированный набор функций like().

Чтобы избежать этой проблемы при тестировании загружаемых расширений в оболочке sqlite3, используйте команду .load. Это обеспечивает прямой доступ к вызову C API, позволяя обойти ограничения функции SQL. См. .load в приложении B для более подробной информации.

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

Единственный способ полностью выгрузить загружаемое расширение - закрыть соединение с базой данных.

Несколько точек входа

Хотя большинство расширений имеют только одну функцию точки входа, ничто не говорит о том, что это должно быть правдой. Вполне допустимо определять несколько точек входа в одном расширении - просто убедитесь, что каждая из них вызывает SQLITE_EXTENSION_INIT2().

Для управления количеством импортируемых функций можно использовать несколько точек входа. Например, если у вас очень большое расширение, которое определяет значительное количество функций в нескольких разных подкатегориях, вы, вероятно, определите одну основную точку входа, которая импортирует каждое расширение, агрегирование, сопоставление и другие функции одним вызовом. Вы также можете определить точку входа для каждой подкатегории функциональности или одну точку входа для всех функций, одну для всех сопоставлений и т. д. Вы также можете определить одну точку входа для привязки функций UTF-8, а другую для UTF-16.

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

Даже если ваше расширение невелико и на самом деле не оправдывает множественные точки входа, второе может быть удобно. Некоторые расширения определяют «чистую» точку входа, например, sql_trig_clear(). Обычно это очень похоже на функцию точки входа _init(), но вместо того, чтобы связывать все указатели функций в соединение с базой данных, она будет связывать все указатели NULL. Это эффективно «выгружает» расширение из среды SQL или, по крайней мере, удаляет все созданные им функции. Файл расширения может все еще находиться в памяти, но функции SQL больше не доступны для этого соединения с базой данных. Единственное, что следует помнить о точке входа _clear(), - это то, что ее нельзя вызвать во время выполнения оператора SQL из-за правил переопределения/удаления для таких функций, как sqlite3_create_function().

Краткое содержание главы

Пользовательские функции, агрегаты и сопоставления могут быть чрезвычайно мощным средством расширения среды SQL в соответствии с вашими потребностями и дизайном. Расширения обеспечивают относительно безболезненный способ модульности и разделения этих пользовательских функций. Это упрощает тестирование, поддержку и распространение кода расширения.

В следующей главе мы рассмотрим одну из наиболее эффективных настроек SQLite: виртуальные таблицы. Виртуальные таблицы позволяют разработчику объединить среду SQLite практически с любым источником данных. Как и другие настраиваемые функции, самый простой способ написать виртуальную таблицу - использовать расширение.

Глава 10
Виртуальные таблицы и модули

В этой главе рассматривается, как использовать и писать виртуальные таблицы. Виртуальная таблица - это настраиваемое расширение SQLite, которое позволяет разработчику определять структуру и содержимое таблицы с помощью кода. Для механизма базы данных виртуальная таблица выглядит как любая другая таблица - виртуальную таблицу можно запрашивать, обновлять и управлять ею с помощью тех же команд SQL, которые используются в других таблицах. Основное различие заключается в том, откуда берутся данные таблицы. При обработке обычной таблицы библиотека SQLite может обращаться к файлу базы данных для получения значений строк и столбцов. В случае виртуальной таблицы библиотека SQLite обращается к вашему коду для выполнения этих функций и возврата значений данных. Ваши функции, в свою очередь, могут собирать или вычислять запрошенные данные из любых источников, которые вы хотите.

Разработка реализации виртуальной таблицы, известной как модуль SQLite, - довольно продвинутая функция. Эта глава должна дать вам хорошее представление о том, на что способны виртуальные таблицы, и основы написания собственного модуля. Мы рассмотрим код двух разных модулей. Первый довольно простой, он предоставляет некоторые внутренние данные SQLite в виде таблицы. Во втором примере будет разрешен доступ только для чтения к стандартным журналам сервера Apache httpd.

Эта глава должна стать хорошей отправной точкой. Однако, если вы обнаружите, что вам нужно написать более надежный модуль, вам, возможно, придется немного углубиться в документацию по разработке, которую можно найти по адресу http://www.sqlite.org/vtab.html. Я также предлагаю взглянуть на исходный код некоторых других модулей (включая те, которые поставляются с SQLite), чтобы лучше понять, как работают расширенные функции. Модули достаточно сложные, поэтому иногда проще изменить существующий модуль, чем реализовывать все с нуля.

Как и в предыдущей главе, этот тип продвинутого развития лучше всего изучать вручную. Чтобы помочь в этом, полный исходный код обоих примеров доступен в книге для загрузки. См. «Пример загрузки кода» для получения более подробной информации.

Введение в модули

Виртуальные таблицы обычно используются для связи ядра базы данных SQLite с альтернативным источником данных. Есть две основные категории виртуальных таблиц: внутренние и внешние. Нет никаких различий в реализации между категориями, они просто дают приблизительный способ определения функциональности модуля.

Внутренние модули

Внутренние модули автономны в базе данных. То есть виртуальная таблица действует как причудливый интерфейс для более традиционных таблиц базы данных, которые создаются и обслуживаются модулем виртуальной таблицы. Эти внутренние таблицы иногда называют теневыми таблицами (shadow tables). Что наиболее важно, все данные, используемые модулем, по-прежнему хранятся в файле базы данных. Эти типы модулей обычно предоставляют специализированный тип индексирования или функции поиска, который плохо подходит для собственных индексов базы данных. Для эффективной работы внутренних виртуальных таблиц может потребоваться несколько теневых таблиц.

Два самых больших модуля, включенных в дистрибутив SQLite (FTS3 и R*Trees), являются модулями внутреннего стиля. Оба этих модуля создают и настраивают несколько стандартных таблиц для хранения и индексирования данных, которые им было предложено поддерживать.

Обычно внутренние модули используются для улучшения или расширения возможностей обработки данных в базе данных. В большинстве случаев внутренняя виртуальная таблица не делает того, что разработчик SQL не мог бы сделать самостоятельно, модуль просто упрощает или ускоряет работу (или и то, и другое). Внутренние модули часто играют роль абстрактного «интеллектуального представления», которое предлагает оптимизированные шаблоны доступа к определенным типам данных или определенным структурам данных. И модуль полнотекстового поиска, и модуль R*Tree являются яркими примерами модулей, обеспечивающих узкоспециализированный поиск по определенным типам и структурам данных.

Внешние модули

Другая основная категория модулей - внешние модули. Это модули, которые взаимодействуют с некоторыми типами внешних источников данных. Этот источник данных может быть таким же простым, как внешний файл. Например, модуль может предоставить файл CSV или Excel в виде таблицы SQL в базе данных. Таким образом можно открыть практически любой структурированный файл. Внешний модуль также может использоваться для представления других источников данных механизму базы данных SQLite. Фактически вы могли бы написать модуль SQLite, который предоставлял бы таблицы из базы данных MySQL механизму базы данных SQLite. Или, для чего-то более необычного, запросите SELECT ip FROM dns WHERE hostname = 'www.oreilly.com' и обработайте DNS-запрос. Внешние модули могут получиться довольно экзотическими.

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

Конечно, вы можете использовать внешний модуль в качестве импортера, скопировав данные из виртуальной таблицы в стандартную таблицу с помощью оператора INSERT...SELECT. Если модуль имеет полную поддержку чтения/записи, вы даже можете использовать его в качестве экспортера, копируя данные из базы данных в виртуальную таблицу. Используя эту технику, я видел случаи, когда SQLite использовался как «универсальный переводчик» для нескольких различных форматов внешних данных. Написав модуль виртуальной таблицы, который может взаимодействовать с каждым форматом файла, вы можете легко и быстро перемещать данные между поддерживаемыми форматами.

Примеры модулей

Чтобы объяснить, как работают модули, мы рассмотрим два примера. Первый пример - это очень простой внутренний модуль, который представляет вывод команды PRAGMA database_list в виде полноценной таблицы. Это позволяет выполнять запросы SELECT (включая ограничения WHERE) для текущего списка базы данных. Хотя этот модуль предназначен только для чтения и чрезвычайно прост, он должен послужить хорошим первым введением в модульную систему.

Второй пример более подробный. Мы рассмотрим создание внешнего модуля, который будет предоставлять журналы httpd-сервера Apache механизму базы данных. Это позволяет веб-мастеру запускать SQL-запросы непосредственно к файлу журнала (включая активный файл журнала) без необходимости сначала импортировать данные в традиционную таблицу базы данных.

SQL для чего угодно

Как мы увидим на примере файла журнала веб-сервера, разработка внешнего модуля SQLite может быть простым способом предоставления общих служб поиска и отчетов для произвольных форматов данных. Что касается журналов веб-сервера, у многих администраторов серверов есть набор сценариев и утилит, которые они используют для анализа файлов журналов. Хотя они могут работать достаточно хорошо для четко определенных задач, такие сценарии часто требуют значительных модификаций кода для изменения параметров поиска или форматов отчетов. Это может затруднить изменение пользовательских сценариев и сделать их несколько негибкими.

Анализ журнала веб-сервера - достаточно распространенная проблема, поэтому для загрузки доступны несколько чрезвычайно мощных пакетов общего назначения. Некоторые из этих пакетов довольно надежны и впечатляют, но для их эффективного использования требуется понимание и опыт работы с пакетом и инструментами, которые он предоставляет.

С помощью модуля внешних данных вы можете просто прикрепить механизм SQLite непосредственно к своим файлам журналов, чтобы журналы отображались в виде большой (и постоянно обновляющейся) таблицы. Как только это будет сделано, вы получите в свое распоряжение всю мощь механизма реляционной базы данных. Лучше всего то, что все запросы и поиски определяются на языке SQL, который уже знают многие веб-администраторы. Создание отчетов становится несложным, и в сочетании с утилитой командной строки sqlite3 модуль позволит в реальном времени взаимодействовать с данными журнала в реальном времени. Это позволяет системному администратору, столкнувшемуся с проблемами безопасности или производительности, быстро формулировать и выполнять произвольный поиск и сводные отчеты в интерактивном режиме на языке и в среде, которые им уже удобно использовать.

Это одно из наиболее убедительных применений виртуальных таблиц. Хотя существует множество приложений, которые могут использовать преимущества настраиваемых форматов индексов и улучшенных функций поиска, предлагаемых некоторыми виртуальными таблицами, настоящая магия происходит с внешними модулями. Возможность интеграции любого обычного источника данных в полноценную среду SQL делает чрезвычайно мощным и функциональным инструментом, особенно в тех случаях, когда необходимо напрямую взаимодействовать с данными в реальном времени.

В следующий раз, когда вы подумаете о том, чтобы собрать вместе несколько скриптов для сканирования или фильтрации структурированного источника данных, спросите себя, насколько сложно было бы вместо этого написать модуль SQLite. Модули, безусловно, сложно писать, но если у вас есть рабочий модуль, вы также получаете в свои руки всю мощь языка SQL.

Модуль API

API виртуальных таблиц - один из наиболее продвинутых API SQLite. В частности, он очень редко ведет вас за руку и часто обманывает тех, кто делает предположения. Функции, которые вам нужно написать, часто требуются для выполнения очень определенного набора операций. Если вы не выполните одно из этих действий или забудете инициализировать поле структуры данных, результатом может быть ошибка шины или ошибка сегментации.

Однако эта улица идет в два направления. В то время как ядро SQLite ожидает от вас выполнения своей работы, оно всегда выполняет свою работу предсказуемым и задокументированным образом. Большая часть этого кода работает довольно глубоко в ядре SQLite, и SQLite выполняет серьезную работу по защите вашего кода от странного поведения пользователя. Например, ни один из примеров кода не проверяет значения параметра NULL, так как вы можете быть уверены, что SQLite никогда не позволит передать указатель базы данных NULL (или какой-либо не менее важный параметр) в вашу функцию.

Реализация модуля виртуальной таблицы немного похожа на разработку агрегатной функции, только намного сложнее. Вы должны написать серию функций, которые вместе определяют поведение модуля. Затем этот блок функций регистрируется под именем модуля.

int sqlite3_create_module( sqlite3 *db, const char *name, const sqlite3_module *module, void *udp )
Создает и регистрирует модуль виртуальной таблицы с подключением к базе данных. Второй параметр - это имя модуля. Третий параметр - это блок указателей на функции, реализующий виртуальную таблицу. Этот указатель должен оставаться действительным, пока библиотека SQLite не будет закрыта. Последний параметр - это общий указатель пользовательских данных, который передается некоторым функциям модуля.
int sqlite3_create_module_v2( sqlite3 *db, const char *name, const sqlite3_module *p, void *udp, destroy_callback )
Версия этой функции v2 идентична исходной функции, за исключением дополнительного пятого параметра. Эта версия добавляет уничтожение обратного вызова в форме void callback(void *udp). Эта функция может использоваться для освобождения или иной очистки указателя пользовательских данных и вызывается, когда весь модуль выгружается. Это делается при выключении базы данных или при регистрации нового модуля с тем же именем вместо этого. Указатель на функцию уничтожения является необязательным и может иметь значение NULL.

Указатели функций передаются через структуру sqlite3_module. Основная причина этого в том, что существует около 20 функций, которые определяют виртуальную таблицу. Все эти функции, кроме некоторых, являются обязательными.

Модуль определяет конкретный тип виртуальной таблицы. После успешной регистрации модуля необходимо создать фактический экземпляр таблицы этого типа с помощью команды SQL CREATE VIRTUAL TABLE. В одной базе данных может быть несколько экземпляров виртуальных таблиц одного типа. Одна база данных может также иметь разные типы виртуальных таблиц, если все модули правильно зарегистрированы.

Синтаксис команды CREATE VIRTUAL TABLE выглядит примерно так:

CREATE VIRTUAL TABLE table_name USING module_name( arg1, arg2, ... )

Виртуальная таблица имеет имя, как и любая другая таблица. Чтобы определить таблицу, вы должны указать имя модуля и любые аргументы, которые требуются модулю. Блок аргументов является необязательным, и точное значение аргументов зависит от реализации отдельных модулей. Модуль отвечает за определение фактической структуры (имен и типов столбцов) таблицы. Аргументы не имеют предопределенной структуры и не обязательно должны быть действительными выражениями SQL или определениями столбцов. Каждый аргумент передается модулю как буквальное текстовое значение, с обрезкой только начального и конечного пробелов. Все остальное, включая пробелы в аргументе, передается как одно текстовое значение.

Вот краткий обзор различных функций, которые определяются структурой sqlite3_module. Когда мы рассмотрим примеры модулей, мы вернемся к ним по очереди более подробно. Функции модуля разделены на три грубые группы. Первый набор функций работает с экземплярами таблиц. Второй набор включает функции, которые просматривают таблицу и возвращают значения данных. Последняя группа функций связана с реализацией управления транзакциями. Чтобы реализовать модуль виртуальной таблицы, вам нужно будет написать функцию C, которая выполняет каждую из этих задач.

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

xCreate()
Обязательная. Вызывается при первом создании экземпляра виртуальной таблицы с помощью команды CREATE VIRTUAL TABLE.
xConnect()
Обязательная, но часто то же самое, что и xCreate(). Очень похожа на xCreate(), она вызывается, когда загружается база данных с существующим экземпляром виртуальной таблицы. Вызывается один раз для каждого экземпляра таблицы.
xDisconnect()
Обязательная. Вызывается, когда база данных, содержащая экземпляр виртуальной таблицы, отключается или закрывается. Вызывается один раз для каждого экземпляра таблицы.
xDestroy()
Обязательная, но часто то же самое, что и xDisconnect(). Очень похожа на xDisconnect(), она вызывается, когда экземпляр виртуальной таблицы уничтожается с помощью команды DROP TABLE.
xBestIndex()
Обязательная. Вызывается, иногда несколько раз, когда ядро базы данных готовит инструкцию SQL, которая включает виртуальную таблицу. Эта функция используется, чтобы определить, как лучше всего оптимизировать поиск и запросы, выполняемые по таблице. Эта информация помогает оптимизатору понять, как добиться максимальной производительности из таблицы.
xUpdate()
По желанию. Вызывается для изменения (INSERT, UPDATE или DELETE) строки та