Сайт вебмастера

База SQLite и основы SQL - 7. Типы данных

14-11-2021Время чтения ~ 7 мин.SQL 5400

Затронем вопрос о типах данных, которые можно хранить в базе. Тип поля мы указываем в момент создания таблицы и считается, что чем точнее будет тип подобран к данным, тем лучше. В других СУБД типы данных играют очень важную роль, поскольку используется строгая типизация. Например тип поля TINYINT будет хранить число в размере одного байта. Когда таблица создаётся, то в ней резервируется место под необходимый объём данных. Именно поэтому, если поле будет содержать числа в один байт, то очевидно, нет смысла указывать INTEGER, поскольку это зарезервирует в базе больше места для поля (4 байта).

Если вы уже знакомы с другими базами, то знаете, что обычно там предлагается довольно много разных типов данных. Например в MySQL их что-то больше 30. Такая ситуация из-за строгой типизации, когда в числовое поле нельзя будет добавить данные другого типа (кроме, конечно, совместимых).

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

В SQLite всего 5 типов данных:

  • NULL — означает что-то вроде «пусто»;
  • INTEGER — целые числа со знаком;
  • REAL — числа с запятой;
  • TEXT — любой текст;
  • BLOB — данные, которые хранятся в том виде, как были добавлены, например содержимое файла.

SQLite сама решает в каком подтипе хранить данные. Например INTEGER может занимать от 1 до 8 байт в зависимости от реального содержимого.

Есть ещё NUMERIC, но о нём отдельно.

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

CREATE TABLE tb (
   id INTEGER PRIMARY KEY AUTOINCREMENT, 
   content
);
 
INSERT INTO tb(content) VALUES (1); -- INTEGER
INSERT INTO tb(content) VALUES ('1'); -- TEXT
INSERT INTO tb(content) VALUES ('a'); -- TEXT
INSERT INTO tb(content) VALUES (1.2); -- REAL
INSERT INTO tb(content) VALUES ('1.2'); -- TEXT
INSERT INTO tb(content) VALUES (NULL); -- NULL

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

SELECT content, typeof(content) FROM tb;
 
content   typeof(content)
-------------------------
1         integer
1         text
a         text
1.2       real
1.2       text
NULL      null

Как видим никаких чудес — данные ровно в том виде, как и были добавлены. Если у поля не указывать тип данных, то SQLite сама определит с чем имеет дело.

Преобразование типов данных

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

Тот же самый пример, только задаём поле INTEGER.

CREATE TABLE tb (
   id INTEGER PRIMARY KEY AUTOINCREMENT, 
   content INTEGER
);
 
INSERT INTO tb(content) VALUES (1); -- INTEGER
INSERT INTO tb(content) VALUES ('1'); -- TEXT
INSERT INTO tb(content) VALUES ('a'); -- TEXT
INSERT INTO tb(content) VALUES (1.2); -- REAL
INSERT INTO tb(content) VALUES ('1.2'); -- TEXT
INSERT INTO tb(content) VALUES (NULL); -- NULL
 
SELECT content, typeof(content) FROM tb;
 
content   typeof(content)
-------------------------
1         integer
1         integer
a         text
1.2       real
1.2       real
NULL      null

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

Казалось бы радужная картина, но мы все равно стараемся всегда указывать тип данных поля. И главная причина в том, что в SQL-запросах мы часто используем сравнение (WHERE или ON), которое будет корректно работать только с единым типом данных. Потому, что если сравнивать число 3 со строкой «3», то неизвестно какой будет результат сравнения.

Чтобы избежать такой неопределенности, мы и задаём тип данных.

Тип NULL

Тип NULL есть и в других базах — означает «пусто». С NULL нужно просто запомнить несколько правил, чтобы потом не ошибаться.

В первую очередь NULL не равен 0, пустой строке или false. NULL не равен вообще ничему. Более того один NULL не равен другому NULL.

В базах, если поле содержит NULL, то говорят, что это пустое значение.

Чтобы работать с NULL в условиях используют команду IS NULL.

SELECT content, typeof(content) 
FROM tb 
WHERE content IS NULL;
 
content   typeof(content)
-------------------------
NULL      null

Противоположная команда — IS NOT NULL, наоборот проверяет условие «если не NULL»:

SELECT content, typeof(content) 
FROM tb 
WHERE content IS NOT NULL;
 
content   typeof(content)
-------------------------
1         integer
1         integer
a         text
1.2       real
1.2       real

На практике значения NULL лучше всего избегать, поскольку его неопределённость может создавать трудности в анализе полученных данных. Поэтому, когда создаётся таблица, часто можно встретить инструкцию NOT NULL, которая фактически запрещает создание NULL-значений. Вместо этого используется какое-то дефолтное значение. Например для INTEGER это может быть 0, а для текста пустая строка «''»:

CREATE TABLE tb1 (
   id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
   i INTEGER NOT NULL DEFAULT 0,
   t TEXT NOT NULL DEFAULT '',
   r REAL NOT NULL DEFAULT 0.1
);

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

Тип INTEGER

Целые числа могут быть со знаком и без, а также быть малыми и большими. В зависимости от реального содержимого, SQLite сама решит сколько байт выделить под хранение числа: 1, 2, 3, 4, 6 или 8 байт. То есть максимальное число будет состоять из 20 цифр. В общем хватит для большинства задач.

Тип REAL

Это десятичные дроби или числа с запятой. SQLite такие числа хранит в 8 байтах с плавающей запятой. Наверное тоже хватит для большинства задач. Если требуется большая точность, то такое число наверное стоит хранить как TEXT, чтобы потом использовать в функциях с повышенной точностью, например BC Math в PHP (они как раз работают со строками).

Тип TEXT

Текст хранится в кодировке UTF-8, UTF-16BE или UTF-16LE. То есть мы вообще не задумываемся о кодировках и прочих преобразованиях.

Кстати именно это и является причиной, почему не работает регистронезависимый поиск LIKE.

Что касается занимаемого места, то оно зависит от реального содержимого. Нам не нужно об этом задумываться.

Тип BLOB

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

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

Как хранить булево значение

В SQLite нет булевского типа данных, поэтому предлагается использовать обычный INTEGER, где 1 — это true, а 0 — false. Это по сути программистский тип BOOL. Просто и сердито.

Как хранить дату

Отсутствия типа для хранения даты, вначале немного обескураживает, особенно, если вы знакомы с другими СУБД. Но потом понимаешь, что любая дата это не что иное, как метка времени и у неё есть только три формата:

  • строка вида YYYY-MM-DD HH:MM:SS.SSS (стандарт ISO8601) — тип TEXT
  • целое число в Unix Time — (число секунд с 1970 года) — тип INTEGER
  • дробное число — юлианские дни (там используются дробь, как часть суток) — тип REAL

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

Что интересно, функции PHP рассчитаны точно на такие же метки времени.

Кроме всего этого, в SQLite есть несколько функций для работы с датами.

SELECT date('now'); -- текущая дата
SELECT time('now'); -- текущее время
SELECT datetime('now'); -- текущие дата и время
SELECT julianday('now'); -- текущий юлианский день

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

Если дата не используется явно в условиях sql-запросов, то её удобно хранить в виде числа Unix Time. Если же дата где-то выводится, то лучше выбрать строковое представление в полном формате, включая часовой пояс. Либо в виде «Y-m-d H:i:s» для UTC. Тогда преобразовать любую дату к нужному часовому поясу достаточно просто с помощью php-функций вроде strtotime().

Совместимость типов данных

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

  • INTEGER — это INT INTEGER TINYINT SMALLINT MEDIUMINT BIGINT UNSIGNED BIG INT INT2 INT8
  • TEXT — это CHARACTER(20) VARCHAR(255) VARYING CHARACTER(255) NCHAR(55) NATIVE CHARACTER(70) NVARCHAR(100) TEXT * CLOB
  • REAL — это REAL DOUBLE DOUBLE PRECISION * FLOAT
  • BLOB — так и будет BLOB
  • NUMERIC — это NUMERIC DECIMAL(10,5) BOOLEAN DATE DATETIME

Тип данных NUMERIC можно считать универсальным типом, поскольку может преобразоваться к любому из 5 типов данных. Если это будет строка, но в виде корректного числа, то она будет преобразована в INTEGER. Если это строка, где десятичное число, то будет преобразование в REAL. Но при этом, если после преобразования получится целое число, то это будет уже INTEGER. Типы NULL и BLOB остаются без изменений. Остальное будет преобразовано в TEXT (без ограничений длины).

Из-за таких особенностей не стоит использовать NUMERIC в своих запросах, лучше сразу указывать подходящий тип.

Если вы пишите sql-запросы для разных СУБД, то в общем-то нет сложностей использовать подходящий тип для другой базы. Например для даты можно использовать DATETIME и хранить там форматированную строку даты. SQLite автоматом преобразует такой тип к TEXT (через NUMERIC).

Похожие записи