1.2.4. Ограничения значений полей в SQL

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

А что если в нашем списке добавить поле для хранения пола. Что можно записать в такое поле? Конечно же, достаточно одной буквы «М» или «Ж», другого пола пока не придумали :). А что если пользователь случайно введет букву «Ь»? Это не смешно, а вполне реальная ситуация. Посмотрите на клавиатуру и увидите, что на ней две буквы «М», просто одна английская, а другая русская. На одной клавише с английской буквой «М» находиться «Ь». Начинающие пользователи нередко путаются и если не проверят результат своего ввода, то корректность данных нарушается.

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

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

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

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

Следующий пример, показывает, как создаются поля, в которых запрещаются пустые значения:

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 dDate datetime DEFAULT (getdate()) NULL,
 vcName varchar(50) NOT NULL
)

В данном случае указано, что поле id и vcName не могут содержать нулевые значения. Пользователь обязательно должен указать хоть что-нибудь, иначе изменения не будут приняты, и сервер сгенерирует ошибку. А вот поле dDate может содержать нулевое значение NULL.

Что не корректно в этом примере? Хотя нет, некорректного ничего нет, но на первый взгляд есть одна глупость. Для поля dDate мы разрешили нулевое значение, но это бессмысленно. Дело в том, что у этого поля есть значение по умолчанию, а значит, если в поле будет попытка записать нулевое значение, оно будет автоматически заменено на значение по умолчанию. Получается, что указывать NULL бессмысленно, если для поля установлено значение по умолчанию и нулевого значения просто не будет.

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

Ограничения NULL и NOT NULL являются не жесткими и некоторые специалисты даже не относят их к ограничениям, хотя, по своей сути они такими являются. Более жесткие ограничения задаются оператором CHECK. Рассмотрим этот оператор на примере. Допустим, что нам нужно создать список хозяев квартир. Для этого нам понадобиться ключевое поле ("id"), имя хозяина ("vcName") и номер квартиры ("iApartment"). Для квартиры вполне логичным будет сделать ограничение ввода от 1 до 1000. Квартир с отрицательными номерами и нулевыми значениями не бывает (по крайней мере, в моем городе), да и более 1000 у нас не бывает, я даже не видел квартиры, с номером более 400. Поэтому логично будет запретить ввод явно некорректных данных с помощью ограничения. Итак, посмотрим на следующий запрос создания таблицы:

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 vcName varchar(50) NOT NULL,
 iApartment int CHECK 
    (iApartment>0 and iApartment<1000)
)

После указания имени и типа поля "iApartment" указано ключевое слово CHECK, после которого в круглых скобках необходимо указать ограничения для данного поля. В нашем примере в качестве ограничения выступает:

iApartment>0 and iApartment<1000

Это значит, что значение в поле iApartment должно быть более 0 и в то же время меньше 1000. Объединение происходит с помощью ключевого слова and, то есть «и». Это значит, что оба условия должны быть выполнены. Бывают случаи, когда необходимо, чтобы хотя бы одно из условий было выполнено, тогда их объединяют оператором or. Например, нужно чтобы в поле заносилось значение или меньше нуля, или больше 1000. Значения в промежутке 1 – 1000 указывать нельзя. В этом случае, сравнение выглядело бы:

iApartment<0 or iApartment>1000

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

INSERT INTO TestTable(vcName, iApartment)
VALUES('Корректная запись', 10)

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

INSERT INTO TestTable(vcName, iApartment)
VALUES('Ошибочная запись', 10000)

В ответ на это, перед нами должно появиться окно с сообщением об ошибке:

INSERT statement conflicted with COLUMN CHECK constraint 'CK__TestTable__iApar__22AA2996'. The conflict occurred in database 'TestDatabase', table 'TestTable', column 'iApartment'.

The statement has been terminated.

Оператор INSERT конфликтует с COLUMN CHECK ограничением 'CK__TestTable__iApar__22AA2996'. Конфликт обнаружен в базе данных 'TestDatabase', таблица 'TestTable', колонка 'iApartment'.

Выполнение было прервано.

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

Если с помощью Enterprise Manager открыть свойства таблицы TestDatabase, то в появившемся окне на закладке Check Constraints в выпадающем списке Selected constraint выбрать ограничение, которое мы увидели в сообщении (CK__TestTable__iApar__22AA2996), а в окне Constraint expression появится описание ограничения.

Напоследок, выполним запрос, который покажет содержимое таблицы:

SELECT * 
FROM TestTable

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

Самое неприятное в этом примере – это имя ограничения, которое было назначено автоматически - CK__TestTable__iApar__22AA2996. С такими именами очень неудобно работать, намного приятнее видеть читаемые имена и со смыслом. Чтобы задать имя ограничению, необходимо использовать конструкцию:

CONSTRAINT имя CHECK (ограничения)
Внимание

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

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

CONSTRAINT Имя ограничения CHECK (Ограничение)

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

Давайте удалим старую таблицу, а создадим новую с еще одним полем – dDate типа datetime. Для этого поля поставим ограничение, которое позволит вводить только даты, которые меньше текущей:

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 vcName varchar(50) NOT NULL,
 iApartment int,
 dDate datetime,
 CONSTRAINT check_iApartment CHECK 
    (iApartment>0 and iApartment<1000),
 CONSTRAINT check_dDate CHECK 
    (dDate<getdate())
)

Как видите, в этом примере ограничения описываются через запятую, так же, как и поля. После ключевого слова CONSTRAINT указывается имя ограничения. Из своей практики хочу порекомендовать называть их в виде check_имя, где имя – имя поля, которое проверяется ограничением. После этого указывается само ограничение точно также как и в предыдущем примере.

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

При создании ограничения, можно использовать многие операторы сравнения языка SQL. Например, в SQL есть очень удобный оператор IN. С его помощью можно задать возможные значения для поля, которые оно может принимать. Например, вам нужно в таблице ограничить ввода данных в поле содержащую такую информацию как пол человека. В этом случае, можно разрешить ввод букв "М" и "Ж" следующим образом:

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 vcName varchar(50) NOT NULL,
 cPol char(1),
 CONSTRAINT check_cPol CHECK 
    (cPol IN ('М', 'Ж'))
)

В данном случае, ограничение выглядит следующим образом: cPol IN ('М', 'Ж'). Оператор IN означает, что поле может принимать любые значения, перечисленные в круглых скобках. В нашем случае указано две строки 'М' и 'Ж'. Другие буквы вносить в поле нельзя. Конечно же, то же самое можно было бы написать и следующим образом:

(cPol = 'М' or cPol = 'Ж')

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

Очень мощных возможностей можно добиться, используя в ограничении оператора LIKE. Например, вы хотите, чтобы поле для хранения телефонного номера содержало номер в формате (ХХХ) ХХХ-ХХ-ХХ, где Х – это любая цифра. Для реализации примера такого ограничения создадим новую таблицу с полем "vcPhonenumber":

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 vcPhonenumber varchar(50) NOT NULL
 CONSTRAINT check_vcPhonenumber CHECK
 (vcPhonenumber LIKE 
   '([0-9][0-9][0-9]) [0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]')
)

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

([0-9][0-9][0-9]) [0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]

В квадратных скобках мы указываем возможный диапазон символа. В данном случае, диапазон от 0 до 9. Если заменить все [0-9] на Х, то мы получим искомый шаблон (ХХХ) ХХХ-ХХ-ХХ. Следующий пример запроса добавляет в таблицу номер телефона (085) 880-08-00:

INSERT INTO TestTable(vcPhonenumber)
VALUES('(085) 880-08-00')

Операция пройдет успешно. Но если убрать пробел, или любой из символов ( или -, или заменить какую-либо цифру буквой, то операция завершиться неудачей:

INSERT INTO TestTable(vcPhonenumber)
VALUES('(095)8800800')

В ответ на это мы увидим сообщение:

INSERT statement conflicted with COLUMN CHECK constraint 'check_vcPhonenumber'. The conflict occurred in database 'TestDatabase', table 'TestTable', column 'vcPhonenumber'.

The statement has been terminated.

Смысл сообщения в следующем: «Оператор INSERT конфликтует с ограничением COLUMN CHECK с именем check_vcPhonenumber. Конфликт появился в базе данных TestDatabase, таблице TestTable, колонке vcPhoneNumber. Выполнение оператора прервано».

Следующий пример задает шаблон для ввода даты:

CREATE TABLE TestTable 
(
 id int DEFAULT 1 NOT NULL,
 vcDate varchar(50) NOT NULL
 CONSTRAINT check_vcDate CHECK
 (vcDate LIKE 
    '[0-3][0-9].[0-1][0-9].[0-9][0-9][0-9][0-9]')
)

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

Первая часть (до точки) определяет число месяца. Первая цифра числа может принимать значения от 0 до 3, а вторая цифра от 0 до 9. Это значит, что пользователь может ввести числа от 01 до 39. Числа от 32 до 39 заранее являются не корректными. При указании месяца пользователь также может указать неверное значение, потому что это значение мы ограничили значениями от 01 до 19.

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

CONSTRAINT Имя ограничения UNIQUE (Поле или список полей)

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

CREATE TABLE Names
(
 idName int, 
 vcName varchar(50),
 vcLastName varchar(50),
 vcSurName varchar(50),
 dBirthDay datetime, 
 CONSTRAINT cn_unique UNIQUE (idName)
)

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

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

CREATE TABLE Names
(
 idName int , 
 vcName varchar(50),
 vcLastName varchar(50),
 vcSurName varchar(50),
 dBirthDay datetime, 
 CONSTRAINT cn_unique UNIQUE (vcName, vcLastName, 
    vcSurName, dBirthDay)
)

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

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

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

Пишите мне