воскресенье, 3 февраля 2019 г.

Оптимизация работы с MySQL


Оптимизация работы с MySQL

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

Проблемы при использовании MySQL можно разделить на следующие три группы(в порядке значимости):
  1. Неиспользование или неправильное использование индексов.
  2. Неправильная структура БД.
  3. Неправильные \ неоптимальные SQL запросы.

Остановимся на каждой из этих групп подробнее.

Использование индексов.
Неиспользование или неправильное использование индексов — это то, что чаще всего замедляет запросы. Для тех, кто мало знаком с механизмом работы индексов или еще не читал об этом в мануале, очень советую почитать.
Советы по использованию индексов:
  • Не нужно индексировать все подряд. Довольно часто, не понимая смысла, люди просто индексируют все поля таблицы. Индексы ускоряют выборки, но замедляют вставки и обновления строк, поэтому выбор каждого индекса должен быть осмыслен.
  • Один из основных параметров, характеризующий индекс — селективность(selectivity) — количество разных элементов в индексе. Нет смысла индексировать поле, в котором два-три возможных значения. Пользы от такого индекса будет мало.
  • Выбор индексов должен начинаться с анализа всех запросов к данной таблице. Очень часто после такого анализа вместо трех-четырех индексов можно сделать один составной. 
  • При использовании составных индексов порядок полей в индексе имеет определяющее значение.
  • Не забывайте про покрывающие(covering) индексы. Если все данные в запросе могут быть получены из индекса, то MySQL не будет обращаться непосредственно к таблице. Подобные запросы будут выполняться очень быстро. Например для запроса SELECT name FROM user WHERE login=«test» при наличии индекса (login, name) обращения к таблице не потребуется. Порой имеет смысл добавить в составной индекс дополнительное поле, которое сделает индекс покрывающим и ускорит запросы.
  • Для индексов по строкам часто достаточно индексировать лишь часть строки. Это может значительно уменьшить размер индекса. 
  • Если % стоит в начале LIKE(SELECT * FROM table WHERE field LIKE '%test') индексы использоваться не будут.
  • FULLTEXT индекс используется только с синтаксисом MATCH … AGAINST.

Структура БД.
Грамотно спроектированная БД — залог быстрой и эффективной работы с базой. С другой стороны, плохо продуманная БД — это всегда головная боль для разработчиков.
Советы по проектированию БД:
  • Используйте минимально возможные типы данных. Чем больше тип данных, тем больше таблица, тем больше обращений к дискам нужно для получения данных. Используйте очень удобную процедуру: SELECT * FROM table_name PROCEDURE ANALYSE(); для определения минимально возможных типов данных.
  • На этапе проектирования соблюдайте нормальные формы. Часто программисты прибегают к денормализации уже на этом этапе. Однако в большинстве случаев в начале проекта далеко не очевидно чем это может вылиться. Денормализовать таблицу гораздо проще, чем страдать от неоптимально денормализованной. Да и JOIN порой работает быстрее, чем неверно денормализованные таблицы.
  • Не используйте NULL столбцы кроме случаев, когда они вам осознанно нужны.
SQL запросы.
  • Избегайте запросов в цикле. SQL — язык множеств и к написанию запросов нужно подходить не языком функций, а языком множеств.
  • Избегайте * (звездочки) в запросах. Не поленитесь перечислить именно те поля, которые вы выбираете. Это сократит количество выбираемых и пересылаемых данных. Кроме этого, не забывайте про покрывающие индексы. Даже если вы действительно выбираете все поля в таблице, лучше их перечислить. Во-первых, это повышает читабельность кода. При использовании звездочек невозможно узнать какие поля есть в таблице без заглядывания в нее. Во-вторых, сегодня в вашей таблице пять INT столбцов, а через месяц добавилось еще одно TEXT и BLOB, а звездочка как была, так и осталась.
  • При постраничном выборе для получения общего количества записей используйте SQL_CALC_FOUND_ROWS и SELECT FOUND_ROWS(); При использовании SQL_CALC_FOUND_ROWS MySQL кеширует выбранное количество строк(до применения LIMIT) и при SELECT FOUND_ROWS() только отдает это закешированное значение без необходимости повторного выполнения запроса.
  • Не забывайте, что у INSERT есть синтаксис для множественной вставки. Один запрос будет выполняться на порядок быстрее, чем множество запросов в цикле.
  • Используйте LIMIT там, где вам не нужны все данные.
  • Используйте INSERT… ON DUPLICATE KEY UPDATE… вместо выборки и INSERT или UPDATE после нее, а также часто вместо REPLACE.
  • Не забывайте про замечательную функцию GROUP_CONCAT. Она может выручить при сложных запросах.

суббота, 2 февраля 2019 г.

Хранение древовидных структур в Базах данных

Хранение древовидных структур в Базах данных

Автор:Maxim Matyukhin 
Описание хранения древовидных структур в базах данных по методу вложенных множеств (Nested Sets)
Думаю с проблемой хранения деревьев в MySQL сталкивались многие и мне не нужно объяснять сколько при этом возникает проблем. В данной статье я хочу рассказать об одном из методов хранения деревьев под названием "Вложенные множества" (Nested Sets). Для начала описание метода. Взгляните на рисунок ниже (рисую я плохо):

На нем представлено дерево, описанное по всем правилам метода "Вложенных множеств".
Квадратами обозначены узлы дерева, красные цифры в центре узлов - просто уникальные идентификаторы узла, а цифры в углах - это левое и правое смещения. Именно в этих двух цифрах - левом и правом смещении заложена вся информация о дереве. И если информацию о смещениях занести в базу данных, то работа с деревом намного упрощается.
Обратите внимание на то, в каком порядке проставлены эти смещения. Если мысленно пройтись по порядку от 1 до 22, то вы обойдете все узлы дерева слева направо. Фактически это путь обхода всех узлов дерева слева направо. Опишу правила по которым эти смещения расставляются:
  • Начинать обход дерева нужно с корневого узла и в нем же заканчивать.
  • При самом первом входе в узел нужно "оставить" цифру в его левом углу и при последнем выходе из узла нужно оставить цифру в правом углу.
  • Все цифры расставляются, начиная с 1.
  • Последняя цифра должна быть в 2 раза больше количества узлов дерева (потому что в каждом узле оставляем по 2 по указанным выше правилам)
Данная статья предназначена прежде всего для PHP-программистов. Поэтому я не буду вдаваться в подробности как "вручную" организовать такое дерево в mysql-таблице, поскольку для этого есть готовый класс - CDBTree. Скачать его можно с http://dev.e-taller.net/dbtree/

На этом теорию заканчивается и начинается практика. Предположим нужно сделать каталог ресурсов с категориями неограниченой вложенности. При этом перед программистом возникает несколько микро-задач:
  • Нужно сделать вывод дерева категорий
  • Нужно получить список ВСЕХ подкатегорий для указанной категории
  • Нужно получить список подкатегорий, уровень вложенности которых на единицу больше уровня вложенности указанной категории
  • Нужно получить список всех родительских категорий для указанной категории (для посторения пути к текущей категории)
Замечу, что класс CDBTree еще хранит в таблице уровень вложенности для данного узла. Это факт облегчит нам решение задачи номер 3 Для начала создайте таблицу:
CREATE TABLE категории (
cid int (10) без знака NOT NULL auto_increment,
title varchar (128) NOT NULL по умолчанию '',
cleft int (10) без знака NOT NULL по умолчанию '0',
cright int (10) unsigned NOT NULL по умолчанию '0',
clevel int (10) без знака NOT NULL по умолчанию '0',
ПЕРВИЧНЫЙ КЛЮЧ (cid),
КЛЮЧ расщелина (расщелина, крит, клевел)
) ТИП = MyISAM;
                  
Данная таблица представлена только для примеров. В конце статьи я приведу слова автора класса по поводу организации таблиц для хранения деревьев. А теперь выполните следующий скрипт:
<? /*
   Работа с деревом по алгоритму Nested Sets.
   Подготовка таблицы к работе.
   Используется класс dbtree.php
   Взять его можно: http://dev.e-taller.net/dbtree/

    ---------------------
    Author: Maxim Matykhin.
    mailto: max@webscript.ru
*/
$table="categories"// таблица категорий $id_name="cid";     // имя поля первичного ключа $field_names = array( // имена полей таблицы
   
'left' => 'cleft',
   
'right'=> 'cright',
   
'level'=> 'clevel',
);

require_once 
"cd.php";
require_once 
"dbtree.php";$dbh=new CDataBase("dbname""localhost""root""password"); $Tree = new CDBTree($dbh$table$id_name$field_names); // создаем "корневую" запись (см. пояснения к статье) $id=$Tree->clear(array("title"=>"Каталог ресурсов"));$level_2=array(); $level_2[0]=$Tree->insert($id,array("title"=>"Программирование")); $level_2[1]=$Tree->insert($id,array("title"=>"Новости")); $level_2[2]=$Tree->insert($id,array("title"=>"Сопрт")); $level_2[3]=$Tree->insert($id,array("title"=>"Разное"));// теперь создадим несколько записей третьего уровня $level_3=array(); $level_3[0]=$Tree->insert($level_2[0],array("title"=>"PHP")); $level_3[1]=$Tree->insert($level_2[0],array("title"=>"Perl")); $level_3[2]=$Tree->insert($level_2[0],array("title"=>"Delphi"));$level_3[3]=$Tree->insert($level_2[1],array("title"=>"Криминал"));$level_3[4]=$Tree->insert($level_2[2],array("title"=>"Футбол")); $level_3[5]=$Tree->insert($level_2[2],array("title"=>"Шахматы"));$level_3[6]=$Tree->insert($level_2[3],array("title"=>"Медицина")); $level_3[7]=$Tree->insert($level_2[3],array("title"=>"Экология")); $level_3[8]=$Tree->insert($level_2[3],array("title"=>"Промышленность"));
                                               
// и для некоторых сделаем четвертый уровень $Tree->insert($level_3[0],array("title"=>"PEAR")); $Tree->insert($level_3[8],array("title"=>"Металлургия")); $Tree->insert($level_3[6],array("title"=>"Морги"));
echo 
"Таблица заполнена."?>
Данный пример просто заполняет таблицу (просто чтобы следующие примеры потестить на ней).
Думаю код здесь ясен. Наверное стоит лишь объяснить строку:
<? $id=$Tree->clear(array("title"=>"Каталог ресурсов")); ?>
Данная строка вставляет корневой узел в таблицу. Корневой узел должен быть один. Также следует заметить, что этот метод очищает таблицу, перед вставкой, так что будьте осторожны.
Теперь давайте выведем дерево. Для этого достаточно просто вывести все элементы, отсортировав их по cleft:
<?
$query
="SELECT * FROM ".$table." ORDER BY cleft ASC"$result=$dbh->query($query);
while(
$row $dbh->fetch_array($result))
{
   echo 
str_repeat("&nbsp;",6*$row['clevel']).$row['title']."<br>";
?>
В данном случае для расчета величины отступа используется значение clevel. Теперь посмотрим, как получить все подкатегории. Это можно сделать либо с помощью метода enumChildrenAll($cid); либо написав правильный SQL-запрос. Предположим есть категория параметры которой $cleft, $cright и $clevel. Тогда запрос, получающий все дочерние узлы, выглядит так:
ВЫБРАТЬ
чид,
заглавие, 
clevel
ОТ
категории
ГДЕ
расщелина между $ расщелиной и $ cright 
ПОРЯДОК РАССЫЛКИ
Выполнив этот запрос, вы получите все подкатегории для указанной категории Метод enumChildrenAll($cid); также возвращает подкатегории, но он возвращает только их идентификаторы, и нет встроенных методов для того чтобы он возвращал дополнительные поля (конечно же вы можете внести изменения в класс).
Для получения списка подкатегорий, уровень вложенности которых на единицу больше уровня вложенности указанной категории происходит.
ВЫБРАТЬ
чид,
заглавие, 
clevel
ОТ
категории
ГДЕ
расщелина между $ расщелиной и $ кратом и
Clevel = $ Clevel + 1
ПОРЯДОК РАССЫЛКИ
 
И последнее - определение родительских категорий.
Запрос должен выглядеть так:
ВЫБРАТЬ
чид,
заглавие, 
clevel
ОТ
категории
ГДЕ
расщелина <$ расщелина И
cright> $ cright 
ПОРЯДОК РАССЫЛКИ
                  
В классе CDBTree для этого есть метод enumPath($cid).
Теперь об рекомендациях автора класса:
Опыт показывает, что структуру с обходом дерева лучше хранить отдельно от данных, т.к. в этом случае при обновлении таблицы очень долго обновляются индексы, да и данные могут сделать невозможным формат записи фиксированной длины, что тоже кардинально скажется на скорости. Самой оптимальной структурой по-моему будет:
CREATE TABLE категории (
cat_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
cat_left INT UNSIGNED NOT NULL,
cat_right INT UNSIGNED NOT NULL,
cat_level INT UNSIGNED NOT NULL,
ПЕРВИЧНЫЙ КЛЮЧ (cat_id)
KEY (cat_left, cat_right, cat_level)
);
В итоге MySQL за многими запросами даже не будет обращаться к файлу с данными. Ему будет хватать файла с индексами. А когда уже пройдёт всё фильтрование в таблице с деревом, по примари-кею быстро произойдёт линкование с остальными данными. Также добавлю, что автор класса рекомендует использовать его (класс) только для добавления/удаления узлов в дерево, а для получения узлов писать SELECT-запросы самому.
Описание методов класса CDBTree
CDBTree (& $ DB, $ tableName, $ itemId, $ fieldNames = array ())
конструктор
Параметры:
  • &$DB - ссылка на объект класса CDatabase (этот класс лежит в архиве с dbtree.php);
  • $tableName - имя таблицы, в которой лежит "дерево";
  • $itemID - имя первичного ключа таблицы в которой лежит "дерево";
  • $fieldNames - массив с именами полей таблицы
функция getElementInfo ($ ID)
Возвращает массив с информацией о об элементе с указанным $ID (параметры left, right, level).
очистка функции ($ data = array ())
данная функция очищает таблицу и вставляет в нее корневой узел дерева.
В массиве $data должна быть информация в виде array("db_field"=>"value", ...)
Поля left, right и level будут вставлены автоматически.
обновление функции ($ ID, $ data)
этот метод используется для изменения информации об указанном элементе. Параметры:
  • $ID - идентификатор узла
  • $data - массив с обновленными данными. (параметры left, right и level вставлять не нужно)
вставка функции ($ ID, $ data)
Метод для вставки узла в дерево. Параметры:
  • $ID - идентификатор родительского узла
  • $data - массив с обновленными данными. (параметры left, right и level вставлять не нужно)
функция удаления ($ ID)
Удаляет указанный узел не удаляя его "потомков";
функция deleteAll ($ ID)
Удаляет указанный узел вместе с его "потомков";
функция enumChildrenAll ($ ID)
Определяет всех "потомков" для указанного узла
функция enumChildren ($ ID, $ start_level = 1, $ end_level = 1)
Определяет потомков для указанного узла
  • $start_level - начальный уровень вложенности узла с которого нужно искать "потомков";
  • $end_level - конечный уровень вложенности узла до которого нужно искать "потомков";
    Если $end_level не указан, (равен 0) то ищутся все узлы "глубже" $start_level
Функция enumPath ($ ID, $ showRoot = false)
Определяет всех родителей для указанного узла.
  • $showRoot - true, если нужно выводить корневой элемент.
Последние три описанные функции формируют SQL-запрос таким образом, что выводятся только идентификаторы узла.
Остальные методы мне использовать не приходилось. Поэтому описывать их не буду. В последней версии класса появился метод MoveAll для перемещения узлов в дереве, но пока он работает с багами.
Ссылки по теме:
  • http://dev.e-taller.net/dbtree - здесь лежит сам DBTree и некоторые статьи по хранению деревьев в БД

Рекурсивные SQL запросы


Рекурсивные SQL запросы

  • SQL
Рекурсивны SQL запросы являются одним из способов решения проблемы дерева и других проблем, требующих рекурсивную обработку. Они были добавлены в стандарт SQL 99. До этого они уже существовали в Oracle. Несмотря на то, что стандарт вышел так давно, реализации запоздали. Например, в MS SQL они появились только в 2005-ом сервере.

Рекурсивные запросы используют довольно редко, прежде всего, из-за их сложного и непонятного синтаксиса:
with [recursive] <имя_алиаса_запроса> [ (<список столбцов>) ]
as (<запрос>)
<основной запрос>

В MS SQL нет ключевого слова recursive, а в остальном все тоже самое. Такой синтаксис поддерживается в DB2, Sybase iAnywhere, MS SQL и во всех базах данных, которые поддерживают стандарт SQL 99.

Проще разобрать на примере. Предположим, есть таблица:
create table tree_sample (
  id integer not null primary key,
  id_parent integer foreign key references tree_sample (id),
  nm varchar(31) )


id – идентификатор
id_parent – ссылка на родитель
nm – название.

Для вывода дерева:
with recursive tree (nm, id, level, pathstr)
as (select nm, id, 0, cast('' as text)
   from tree_sample
   where id_parent is null
union all
   select tree_sample.nm, tree_sample.id, t.level + 1, tree.pathstr + tree_sample.nm
   from tree_sample
     inner join tree on tree.id = tree_sample.id_parent)
select id, spacelevel ) + nm as nm
from tree
order by pathstr


Этот пример выведет дерево по таблице с отступами. Первый запрос из tree_sample этот запрос выдаст все корни дерева. Второй запрос соединяет между собой таблицу tree_sample и tree, которая определяется этим же запросом. Этот запрос дополняет таблицу узлами дерева.

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

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

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

пятница, 1 февраля 2019 г.

Введение в SQLite

Введение в SQLite

Автор перевода: Ожегов Денис
Для кого эта статья
Данная статья предназначается PHP программистам, проявившим интерес к расширению SQLite. Статья представит читателю функциональность, предлагаемую расширением PHP SQLite, а также рассмотрит преимущества SQLite в сравнении с другими БД. Предполагается, что читатель знаком с основами PHP и SQL. Некоторый опыт работы с MySQL или PostgreSQL , будет способствовать лучшему пониманию того, о чём пойдёт речь.
Введение
В последнее время, вы, возможно, слышали о новом расширении для PHP: SQLite. Есть много причин, по которым SQLite может показаться лучшим достижением с тех пор, как научились резать хлеб. SQLite предлагает добротный SQL интерфейс к нерелятивистской базе данных и создаёт элегантную альтернативу громоздким интерфейсам других баз данных без потери функциональности или скорости, как можно было бы ожидать. Мы рассмотрим в статье это удивительное расширение и, будем надеяться, нам удастся подтвердить те преимущества, о которых, возможно, вы наслышаны.
Что такое SQLite?
SQLite – это встраиваемая библиотека в которой реализовано многое из стандарта SQL 92. Её притязанием на известность является как собственно сам движок базы, так и её интерфейс (точнее его движок) в пределах одной библиотеки, а также возможность хранить все данные в одном файле. Я отношу позицию функциональности SQLite где-то между MySQL и PostgreSQL. Однако, на практике, SQLite не редко оказывается в 2-3 раза (и даже больше) быстрее. Такое возможно благодаря высокоупорядоченной внутренней архитектуре и устранению необходимости в соединениях типа «сервер-клиент» и «клиент-сервер».
Всё это, собранное в один пакет, лишь немногим больше по размеру клиентской части библиотеки MySQL, является впечатляющим достижением для полноценной базы данных. Используя высоко эффективную инфраструктуру, SQLite может работать в крошечном объёме выделяемой для неё памяти, гораздо меньшем, чем в любых других системах БД. Это делает SQLite очень удобным инструментом с возможностью использования практически в любых задачах возлагаемых на базу данных.
Что мне проку от SQLite?
Помимо скорости и эффективности у SQLite есть ряд других преимуществ, которые делают её идеальным решением для многих задач. Так как база данных SQLite по сути своей – обычные файлы, нет ни какой необходимости в дополнительных средствах администрирования требующих много времени на создание сложной структуры прав доступа для защиты пользовательских баз данных. Всё это уже автоматически поддерживается организацией прав доступа в самой файловой системе, это также подразумевает (в области ограничения пространства), что не требуется ни каких специальных правил для отслеживания заполнения дискового пространства пользователем. Преимущество для пользователей в том, что есть возможность создать такое количество баз данных, какое они себе пожелают плюс абсолютный контроль над всеми этими базами данных.
Факт, что база данных – это единственный файл, делает её легко переносимой. SQLite к тому же, устраняет необходимость в запуске дополнительных служебных процессов (daemons), которые могли бы «отъедать» значительное количество памяти и других ресурсов, даже в случае умеренного использования базы данных.
Расширение SQLite
В качестве новейшего расширения БД, SQLite посчастливилось быть свободным от кода для обратной совместимости, в противоположность более старым расширениям, таким как MySQL , которые обязаны поддерживать устаревшую функциональность по причинам обратной совместимости. Это также позволяет новому расширению использовать самые новейшие разработки в PHP для достижения самого высокого уровня исполнения и функциональности. Разработчики облегчили задачу пользователям, создав расширение лёгким для перехода к нему от других систем БД, оставив интерфейс похожим на те, что уже были реализованы в PHP.
SQLite также поддерживает гибкий механизм для передачи ресурсов базы данных в процедурные интерфейсы, делая это одинаково лёгким для перехода из MySQL, где ресурс передаётся последним, и PostgreSQL, где он передаётся первым.
SQLite также отличает мощный объектно-ориентированный интерфейс, который может быть использован для эффективного извлечения данных из базы, избавляя вас от необходимости выполнения вашей собственной оболочки для процедурного интерфейса. Как показано в примере ниже, объектно-ориентированный интерфейс также позволяет вам избежать передачи всех ресурсов кучей.
<?php
// создаём новую базу (OO интерфейс) $db = new sqlite_db("db.sqlite");
// создаём таблицу foo и вставляем что-нибудь для примера $db->query("BEGIN;
        CREATE TABLE foo(id INTEGER PRIMARY KEY, name CHAR(255));
        INSERT INTO foo (name) VALUES('Ilia');
        INSERT INTO foo (name) VALUES('Ilia2');
        INSERT INTO foo (name) VALUES('Ilia3');
        COMMIT;"
);
// выполняем запрос $result $db->query("SELECT * FROM foo"); // проходим в цикле по ячейкам while ($result->hasMore()) {
    
// получаем текущую ячейку
    
$row $result->current();     
    
print_r($row); // переходим к следующей ячейке
    
$result->next();
}
// нет особой необходимости, так как PHP сам разорвёт соединение unset($db);
?>
Установка SQLite
В PHP 5.0 установка SQLite имеет свою особенность, так как и расширение и библиотека связаны вместе, поэтому всё, что вам необходимо сделать – это добавить –with-sqlite в строке конфигурирования. Я бы ещё порекомендовал установить SQLite, но только в случае если речь об откомпилированном бинарном файле, что позволит вам открывать базу и управлять ею без использования PHP. Это весьма полезно для отладки и выполнения различных одновременных команд, а также полезно для тестирования кода запросов. В будущем, довольно часто вы сможете обнаружить, что связанная библиотека SQLite немного "устарела", поэтому связка вашего PHP с внешней библиотекой позволит вам извлечь выгоду из последних исправлений и нововведений SQLite. Это также позволит вам в будущем обновлять вашу SQLite без перекомпиляции PHP.
Чтобы собрать расширение SQLite в качестве внешней библиотеки, просто используйте –with-sqlite=/path/to/lib/.
Мне также следует упомянуть, что расширение SQLite проходит исчерпывающую серию тестов, как для объектно-ориентированного, так и для процедурного интерфейсов. Тестируются каждая отдельная функция, и каждый метод поддерживаемый SQLite. Это великолепный источник примеров не только того, как работает каждый метод и каждая функция SQLite, но и ожидаемого вывода, позволяющего вам видеть конечный результат каждой операции.
Использование SQLite
Процедурный интерфейс к SQLite почти такой же, как у MySQL и других расширений БД. По большей части переход к SQLite потребует только изменить mysql/pq/etc… префикс функции на sqlite.
<?php // создаём новую базу (процедурный интерфейс) $db sqlite_open("db.sqlite");
// создаём таблицу foo sqlite_query($db"CREATE TABLE foo (id INTEGER PRIMARY KEY, name CHAR(255))");
// добавляем что-нибудь для примера sqlite_query($db"INSERT INTO foo (name) VALUES ('Ilia')"); sqlite_query($db"INSERT INTO foo (name) VALUES ('Ilia2')"); sqlite_query($db"INSERT INTO foo (name) VALUES ('Ilia3')");
// выполняем запрос $result sqlite_query($db"SELECT * FROM foo"); // проходим в цикле выборкой по ячейкам while ($row sqlite_fetch_array($result)) {
    
print_r($row);
    
 /* каждый результат будет выглядеть примерно так
    Array
    (
        [0] => 1
        [id] => 1
        [1] => Ilia
        [name] => Ilia
    )
*/ 
}
// закрываем соединение с базой sqlite_close($db);
?>
Собственно значительные отличия между SQLite и другими базами данных находятся в самом движке. В отличие от других БД в SQLite нет привязки к типам; все данные сохраняются как строки оканчивающиеся символом NULL, что лучше, чем двоичное представление данных в столбцах специального типа. По причине совместимости SQLite пока поддерживает тип спецификации в конструкциях CREATE TABLE, например, такой как INT, CHAR, FLOAT, TEXT и тому подобные, но реально их не использует. Внутри базы, SQLite только делает различие между строковыми и целочисленными данными во время сортировки. Поэтому, если вы не собираетесь сортировать данные, вы можете обойтись без указания специального типа полей при создании таблиц в SQLite.
"Безтиповая природа" SQLite делает сортировку и сопоставление данных в некотором роде медленнее, так как каждый раз SQLite будет вынуждена определять тип данных и применять либо строковый механизм сортировки/сравнения либо числовой. SQL таблицы часто требуют автоматически присваиваемый ключ для быстрого доступа к ячейкам, подразумевая возврат ссылки на последнюю добавленную ячейку. Для SQLite этот синтаксис, мягко говоря, бесполезен. Чтобы создать такую таблицу, вам понадобится объявить поле как INTEGER PRIMARY KEY, что более удобно чем указание специализированного типа или присваивание дополнительных свойств, которые указывают на то, что поле является автоинкрементным.
Связанные запросы
Как вы можете ожидать, SQLite несёт в себе много новых особенностей улучшающих исполнение и расширяющих функциональность. Одна из таких особенностей – возможность выполнять связанные запросы, которые подразумевают выполнение множественных запросов через функцию выполнения одного запроса. Это снижает количество задействованных функций PHP и таким образом увеличивает скорость работы скрипта. А также позволяет вам легко группировать блоки запросов внутри транзакций, улучшая выполнение в дальнейшем. Такая возможность может быть значительным фактором во время выполнения множественных обращений на запись в базу. Однако существует несколько пикантных особенностей, о которых не следует забывать.
Если какой-либо запрос в SQLite использует что-то введённое пользователем напрямую, вам следует предпринять дополнительные меры предосторожности для проверки такого ввода, чтобы пресечь нежелательный запрос. В противоположность MySQL, где такое может повлечь за собой лишь недоразумение при запросе к БД и выдать ошибку, в SQLite это позволит атакующему выполнить какой-нибудь запрос на вашем сервере с возможными плачевными последствиями. Если вы добавляете записи через блок запросов и хотели бы возвращать идентификатор, то sqlite_last_insert_rowid() хорошо справится с такой задачей, но вернёт идентификатор только последней записи. С другой стороны, при попытке определить какое количество ячеек было изменено, используя sqlite_changes(), мы получим результат, содержащий общее число ячеек изменённых всеми выполненными запросами. Если ваш блок запросов содержит SELECT, убедитесь что это самый первый запрос, в противном случае ваша окончательная выборка не будет содержать ячеек, возвращаемых этим запросом.
<?php
// создаём новую базу только в памяти $db = new sqlite_db(":memory:"); // создаём таблицу из двух столбцов bar и добавляем в неё 2 ячейки
/* Чтобы улучшить выполнение, весь блок запроса сгруппирован внутри транзакции. */ 
$db->query("BEGIN;
        CREATE TABLE bar ( id INTEGER PRIMARY KEY, id2 );
        INSERT INTO bar (id2) VALUES(1);
        INSERT INTO bar (id2) VALUES(2);
        COMMIT;"
); // напечатает "2 insert queries" echo $db->changes()." insert queries\n"// напечатает: "last inserted row id: 2" echo "last inserted row id: ".$db->last_insert_rowid();
?>
Новые функции
Помимо своих новых внутренних особенностей, SQLite предлагает ряд новых функций, которые упрощают и ускоряют извлечение данных из базы.
<?php

$db 
= new sqlite_db("db.sqlite"); /* Выполняем запрос с выборкой всех ячеек в ассоциативный массив */ $result_array $db->array_query("SELECT * FROM foo"SQLITE_ASSOC); print_r($result_array);
?>
Это позволяет и выполнять запрос и извлекать данные одним вызовом функции, сводя на нет всю лишнюю работу PHP. Сам по себе PHP скрипт упрощается, за счёт использования всего одной функции там, где иначе бы Вам пришлось использовать несколько функций для извлечения данных работающих в цикле. В тех случаях, когда возвращается результат только одного столбца, можно использовать sqlite_single_query(), которая мгновенно возвращает строку или массив строк в зависимости от числа ячеек, из которых извлекается информация.
<?php

$db 
sqlite_open("db.sqlite"); // Получаем идентификатор столбца (в качестве строки)$id sqlite_single_query($db"SELECT id FROM foo WHERE name='Ilia'"); var_dump($id); //string(1)

// В случае, если совпадений больше одного, результатом будет массив 
$ids sqlite_single_query($db"SELECT id FROM foo WHERE name LIKE 'I%'"); var_dump($ids); // array(3)
?>
Как и в случае с другими особенностями, вы можете этим пользоваться, но вам не следует этим злоупотреблять. В случае выборки всех данных, запросом в один проход, вы должны помнить, что все результаты будут сохраняться в памяти. Если результат выборки содержит большое количество данных, цена затрат на память безусловно сведёт на нет все преимущества полученные путём снижения числа вызовов функций. Следовательно, вам нужно приберечь использование этих функций для случаев выборки небольшого количества данных.
Итераторы в SQLite
В PHP 5.0 есть другой способ выборки данных запросом с использованием итераторов.
<?php

$db 
= new sqlite_db("db.sqlite"); // уменьшаем использование памяти, выполняя запрос без кэширования $res $db->unbuffered_query("SELECT * FROM foo");
foreach (
$res as $row) { // получаем результат в цикле
        // Код вывода
        
print_r($row);
}
?>
Получение результатов в цикле работает точно так же, как и проход по массиву в цикле через foreach(), за исключением того, что в это время у вас нет доступа к ключам и значение представляет собой массив, содержащий данные в особых ячейках. Так как итераторы это не функции, а внутренние указатели в самом движке, они требуют гораздо меньше ресурсов по сравнению с функциями sqlite_fetch_*(), и не требуют, чтобы результаты кэшировались в памяти. Конечный результат будет чрезвычайно быстрым, это более простой и доступный метод извлечения данных. Нет ни каких побочных эффектов в использовании итераторов объектов в SQLite, и когда бы вам ни понадобилось получить результат в виде множества строк, вам нужно обязательно помнить о такой возможности.
Вспомогательные функции
SQLite также отличает ряд вспомогательных функций, которые могут пригодится во время работы с базой данных. Одна из таких функций, sqlite_num_fields(), может быть использована для определения количества столбцов в специальных результатах выборки. Как вариант, если вы собираетесь извлекать данные, просто используйте count() с первым результатом, которая вернёт вам то же самое число. Если извлечены как строковый, так и числовой ключи, вам понадобится отделить результат, поскольку в результирующем массиве будет столько вхождений сколько существует полей в таблице. Это число может быть важно, если вашему скрипту необходимо получить названия полей внутри какой-то таблицы. В таком случае вы могли бы использовать sqlite_field_name() в цикле, для доступа к этой информации, как показано в примере ниже.
<?php

$db 
= new sqlite_db("db.sqlite"); $res $db->unbuffered_query("SELECT * FROM foo LIMIT 1"); // получаем количество полей $n_fields $res->num_fields();
while (
$i $n_fields) {
    
 // извлекаем отдельные поля$field_name $res->field_name($i++);
    echo 
$field_name."\n";
}
?>
Конечно, это не идеальный способ для получения имён столбцов из таблицы, просто по тому, что он окажется неудачным в случае, когда таблица не содержит ни одной ячейки, а также по тому, что это потребует от вас извлечения данных, которые вы не собираетесь использовать. Гораздо лучшим решением будет использование функции sqlite_fetch_column_types(), которая получает столбцы вместе с их типами из таблицы не зависимо от того, есть ли там данные или нет.
Преимущество буферизации
В большинстве случаев, по причинам выполнения или использования памяти, вы не захотите использовать запросы без кеширования. Это может привести к небольшой потери функциональности, что может быть необходимо в определённых случаях, для которых запросы без кеширования не всегда самый лучший выбор.
Например, предположим, вы хотели бы выяснить реальное количество ячеек, выбранное Вашим запросом. В запросе без кеширования, вам придётся делать выборку каждой отдельной ячейки, прежде чем вы сможете её определить. Кешированные же запросы – более простой способ, заключающийся в использовании функции sqlite_num_rows(), которая легко получит эту информацию из результата выборки. Запросы без кеширования также ограничены в прямой выборке данных, подразумевая, что вы обязаны выбрать данные из всех ячеек за один проход. В кешированных запросах такого ограничения нет, вы можете использовать sqlite_seek() для смещения на любую ячейку и выборки из неё данных, или даже для доступа к ячейкам в обратном порядке, если это необходимо.
<?php

$db 
= new sqlite_db("db.sqlite"); $res $db->query("SELECT * FROM foo"); $n_rows $res->num_rows(); // получаем число ячеек выборки $res->seek($n_rows 1); // переходим к позиции следующей ячейки
// извлечение данных в обратном порядке 
do {
    
$data $res->current(SQLITE_ASSOC); // получаем данные ячейки
    
print_r($data);
}
while (
$res->hasPrev()&& $res->prev()); // и так до первой ячейки
?>
Пользовательские функции
Одна из самых интересных возможностей в том, что расширение SQLite предоставляет таблицу для возможности создания своих собственных функций используемых в пределах SQL. Это возможно благодаря тому, что SQLite содержит в одной библиотеке связанной с PHP, как интерфейс, так и сам движок базы. Используя sqlite_create_function() вы можете создавать функции, которые в дальнейшем могут быть применены к результату выборки или использованы внутри какого-нибудь условия WHERE.
<?php /* определяем разницу между строкой введённой пользователем и строкой из базы исходя из символов содержащихся в строке */ function char_compare($db_str$user_str) {
    return 
similar_text($db_str$user_str);
}
$db = new sqlite_db("db.sqlite");
/* Создаём функцию char_compare() внутри SQLite на основании нашей PHP функции, char_compare(). 3-ий параметр указывает количество аргументов необходимое функции */ $db->create_function('char_compare''char_compare'2);
         
/* Выполняем запрос, где char_compare() используется для сравнения между name и указанной строкой */ $res $db->array_query("SELECT name, char_compare(name, 'Il2') AS sim_index FROM foo"SQLITE_ASSOC);
print_r($res);
?>
Возможность использования PHP в SQL позволяет вам упростить данный скрипт, делая его доступным большей аудитории разработчиков. Это позволяет использовать PHP в качестве движка шаблонов, который будет вставлять HTML код в данные из базы. Во многих случаях это может упростить код так сильно, что не возникнет необходимости в использовании дополнительного движка шаблонов. Помимо упрощения кода, это также улучшает выполнение программы и снижает затраты скрипта на память вследствие того, что нет ни каких действий с данными, происходящими в пользовательском пространстве.
Не забывайте, что в том случае, если данные, с которыми вы работаете, могут содержать двоичный код, вам следует использовать функцию sqlite_udf_decode_binary() для перевода их из внутренней кодировки, в которой они хранятся в SQLite, прежде чем работать с ними. Если вы так поступили, вам понадобится закодировать двоичные данные, используя sqlite_udf_encode_binary() для того, чтобы быть уверенным, что данные не будут повреждены при следующем к ним доступе.
Подведём итоги
Теперь, посмотрев как работает SQLite, и что она может предложить, вы, возможно, обратите на неё внимание для использования в своих текущих или будущих приложениях. Надеюсь, этот краткий обзор дал вам некоторое представление о функциональности SQLite, и не развеял ничего из того, что вы слышали о ней хорошего.
Как и любой инструмент, SQLite имеет свои сильные и слабые стороны. Будучи идеальным решением, для приложений небольших и/или предназначенных главным образом для считывания данных, она не очень хорошо подходит для крупномасштабных приложений, часто выполняющих запись в базу. Это ограничение – следствие того, что архитектура SQLite основана на одном единственном файле, которая не предусматривает распределения данных на многократно создаваемые файлы, а также использует блокировку на запись.
Об авторе
Ilia Alshanetsky занимается разработкой web-приложений более 7 лет, главным образом на PHP. Последние несколько лет он является активным участником в разработке PHP и соавтором ряда расширений, в том числе SQLite. В настоящее время Ilia управляет своей собственной компанией “Advanced Internet Designs Inc.”, которая специализируется на разработке и поддержке FUDforum, форума с открытым кодом.

С ним можно связаться по e-mail: ilia{@}prohost.org