Оператор SELECT осуществляет выборку из базы данных и имеет наиболее сложную структуру среди всех операторов языка SQL. Практически любой пользователь баз данных в состоянии написать простейший оператор SELECT типа
SELECT * FROM PC;
который осуществляет выборку всех записей из объекта БД табличного типа с именем PC. При этом столбцы и строки результирующего набора не упорядочены. Чтобы упорядочить поля результирующего набора, их следует перечислить через запятую в нужном порядке после слова SELECT:
SELECT price, speed, hd, ram, cd, model, code
FROM Pc;
Ниже приводится результат выполнения этого запроса.
price |
speed |
hd |
ram |
cd |
model |
code |
---|---|---|---|---|---|---|
600.0 |
500 |
5 |
64 |
12x |
1232 |
1 |
850.0 |
750 |
14 |
128 |
40x |
1121 |
2 |
600.0 |
500 |
5 |
64 |
12x |
1233 |
3 |
850.0 |
600 |
14 |
128 |
40x |
1121 |
4 |
850.0 |
600 |
8 |
128 |
40x |
1121 |
5 |
950.0 |
750 |
20 |
128 |
50x |
1233 |
6 |
400.0 |
500 |
10 |
32 |
12x |
1232 |
7 |
350.0 |
450 |
8 |
64 |
24x |
1232 |
8 |
350.0 |
450 |
10 |
32 |
24x |
1232 |
9 |
350.0 |
500 |
10 |
32 |
12x |
1260 |
10 |
980.0 |
900 |
40 |
128 |
40x |
1233 |
11 |
Вертикальную проекцию таблицы РC можно получить, если перечислить только необходимые поля. Например, чтобы получить информацию только о скорости процессора и объеме оперативной памяти компьютеров, следует выполнить запрос:
SELECT speed, ram FROM PC;
который вернет следующие данные:
speed |
ram |
---|---|
500 |
64 |
750 |
128 |
500 |
64 |
600 |
128 |
600 |
128 |
750 |
128 |
500 |
32 |
450 |
64 |
450 |
32 |
500 |
32 |
900 |
128 |
Следует отметить, что вертикальная выборка может содержать дубликаты строк в том случае, если она не содержит потенциального ключа, однозначно определяющего запись. В таблице PC потенциальным ключом является поле code, которое выбрано в качестве первичного ключа таблицы. Поскольку это поле отсутствует в запросе, в приведенном выше результирующем наборе имеются дубликаты строк (например, строки 1 и 3). Если требуется получить уникальные строки (скажем, нас интересуют только различные комбинации скорости процессора и объема памяти, а не характеристики всех имеющихся компьютеров), то можно использовать ключевое слово DISTINCT:
SELECT DISTINCT speed, ram FROM Pc;
что даст такой результат:
speed |
ram |
---|---|
450 |
32 |
450 |
64 |
500 |
32 |
500 |
64 |
600 |
128 |
750 |
128 |
900 |
128 |
Помимо DISTINCT может применяться также ключевое слово ALL (все строки), которое принимается по умолчанию.
Чтобы упорядочить строки результирующего набора, можно выполнить сортировку по любому количеству полей, указанных в предложении SELECT. Для этого используется предложение ORDER BY , являющееся всегда последним предложением в операторе SELECT. При этом в списке полей могут указываться как имена полей, так и их порядковые позиции в списке предложения SELECT. Так если требуется упорядочить результирующий набор по объему оперативной памяти в порядке убывания, можно записать
SELECT DISTINCT speed, ram
FROM Pc
ORDER BY ram DESC
или
SELECT DISTINCT speed, ram
FROM Pc
ORDER BY 2 DESC
Результат, приведенный ниже, будет одним и тем же.
speed |
ram |
---|---|
600 |
128 |
750 |
128 |
900 |
128 |
450 |
64 |
500 |
64 |
450 |
32 |
500 |
32 |
Сортировку можно проводить по возрастанию (параметр ASC принимается по умолчанию) или по убыванию (параметр DESC). Сортировка по двум полям
SELECT DISTINCT speed, ram
FROM Pc
ORDER BY ram DESC, speed DESC
даст следующий результат:
speed |
ram |
---|---|
900 |
128 |
750 |
128 |
600 |
128 |
500 |
64 |
450 |
64 |
500 |
32 |
450 |
32 |
Горизонтальную выборку реализует предложение WHERE , которое записывается после предложения FROM. При этом в результирующий набор попадут только те строки из источника записей, для каждой из которых значение предиката равно TRUE. То есть предикат проверяется для каждой записи. Например, запрос "получить информацию о частоте процессора и объеме оперативной памяти для компьютеров с ценой ниже $500" можно сформулировать следующим образом:
SELECT DISTINCT speed, ram
FROM Pc
WHERE price<500
ORDER BY 2 DESC
speed |
ram |
---|---|
450 |
64 |
450 |
32 |
500 |
32 |
В последнем запросе использовался предикат сравнения с использованием операции сравнения "<" (меньше чем). Кроме этой операции сравнения могут использоваться: "=" (равно), "" (больше), "=" (больше или равно), "<=" (меньше или равно) и "<>" (не равно). Выражения в предикатах сравнения могут содержать любые поля из таблиц, указанных в предложении FROM. Символьные строки и константы типа дата/время записываются в апострофах.
Примеры простых предикатов сравнения:
price < 1000 |
Цена меньше $1000. |
type = 'laptop' |
Типом продукции является ПК-блокнот. |
cd = '24x' |
24-скоростной CD-ROM. |
color <>'y' |
Не цветной принтер. |
ram - 128 0 |
Объем оперативной памяти свыше 128 Mb. |
price <= speed*2 |
Цена не превышает удвоенной частоты процессора. |
Предикаты представляют собой выражения, принимающие истинностное значение. Они могут представлять собой как одно выражение, так и любую комбинацию из неограниченного количества выражений, построенную с помощью булевых операторов AND, OR или NOT. Кроме того, в этих комбинациях может использоваться SQL-оператор IS, а также круглые скобки для конкретизации порядка выполнения операций.
Предикат в языке SQL может принимать одно из трех значений TRUE (истина), FALSE (ложь) или UNKNOWN (неизвестно). Исключение составляют следующие предикаты: NULL (отсутствие значения), EXISTS (существование), UNIQUE (уникальность) и MATCH (совпадение), которые не могут принимать значение UNKNOWN.
Правила комбинирования всех трех истинностных значений легче запомнить, обозначив TRUE как 1, FALSE как 0 и UNKNOWN как 1/2 (где то между истинным и ложным).
* AND с двумя истинностными значениями дает минимум этих значений. Например, TRUE AND UNKNOWN будет равно UNKNOWN.
* OR с двумя истинностными значениями дает максимум этих значений. Например, FALSE OR UNKNOWN будет равно UNKNOWN.
* Отрицание истинностного значения равно 1 минус данное истинностное значение. Например, NOT UNKNOWN будет равно UNKNOWN.
Предикат сравнения представляет собой два выражения, соединяемых оператором сравнения. Имеется шесть традиционных операторов сравнения: =, >, <, >=, <=, <>.
Данные типа NUMERIC (числа) сравниваются в соответствии с их алгебраическим значением.
Данные типа CHARACTER STRING (символьные строки) сравниваются в соответствии с их алфавитной последовательностью. Если a1a2…an и b1b2…bn - две последовательности символов, то первая "меньше" второй, если а11, или а1=b1 и а22 и т.д. Считается также, что а1а2…аn1b2…bm, если n
Данные типа DATETIME (дата/время) сравниваются в хронологическом порядке.
Данные типа INTERVAL (временной интервал) преобразуются в соответствующие типы, а затем сравниваются как обычные числовые значения типа NUMERIC.
Пример. Получить информацию о компьютерах, имеющих частоту процессора не менее 500 Мгц и цену ниже $800:
SELECT * FROM Pc
WHERE speed = 500 AND price < 800;
Запрос возвращает следующие данные:
code |
model |
speed |
ram |
hd |
cd |
price |
---|---|---|---|---|---|---|
1 |
1232 |
500 |
64 |
5 |
12x |
600.0 |
3 |
1233 |
500 |
64 |
5 |
12x |
600.0 |
7 |
1232 |
500 |
32 |
10 |
12x |
400.0 |
10 |
1260 |
500 |
32 |
10 |
12x |
350.0 |
Пример. Получить информацию обо всех принтерах, которые не являются матричными и стоят меньше $300:
SELECT * FROM Printer
WHERE NOT (type = 'matrix') AND price < 300;
Результат выполнения запроса:
code |
model |
color |
type |
price |
---|---|---|---|---|
2 |
1433 |
y |
Jet |
270.0 |
3 |
1434 |
y |
Jet |
290.0 |
Предикат BETWEEN проверяет, попадают ли значения проверяемого выражения в диапазон, задаваемый пограничными выражениями, соединяемыми служебным словом AND. Естественно, как и для предиката сравнения, выражения в предикате BETWEEN должны быть совместимы по типам.
Синтаксис BETWEEN::=
[NOT] BETWEEN
AND
Предикат
exp1 BETWEEN exp2 AND exp3
равносилен предикату
exp1=exp2 AND exp1<=exp3
А предикат
exp1 NOT BETWEEN exp2 AND exp3
равносилен предикату
NOT (exp1 BETWEEN exp2 AND exp3)
Если значение предиката exp1 BETWEEN exp2 AND exp3 равно TRUE, в общем случае это отнюдь не означает, что значение предиката exp1 BETWEEN exp3 AND exp2 тоже будет TRUE, так как первый можно интерпретировать как предикат
exp1=exp2 AND exp1<=exp3
а второй как
exp1=exp3 AND exp1<=exp2
Пример. Найти модель и частоту процессора компьютеров стоимостью от $400 до $600:
SELECT model, speed FROM Pc
WHERE price BETWEEN 400 AND 600;
model |
speed |
---|---|
1232 |
500 |
1233 |
500 |
1232 |
500 |
Предикат IN определяет, будет ли значение проверяемого выражения обнаружено в наборе значений, который либо явно определен, либо получен с помощью табличного подзапроса. Табличный подзапрос это обычный оператор SELECT, который создает одну или несколько строк для одного столбца, совместимого по типу данных со значением проверяемого выражения. Если целевой объект эквивалентен хотя бы одному из указанных в предложении IN значений, истинностное значение предиката IN будет равно TRUE. Если для каждого значения Х в предложении IN целевой объект<>X, истинностное значение будет равно FALSE. Если подзапрос выполняется, и результат не содержит ни одной строки (пустая таблица), предикат принимает значение FALSE. Когда не соблюдается ни одно из упомянутых выше условий, значение предиката равно UNKNOWN.
Синтаксис IN::=
[NOT] IN ()
| (,...)
Пример. Найти модель, частоту процессора и объем жесткого диска тех компьютеров, которые комплектуются накопителями 10 или 20 Мб:
SELECT model, speed, hd FROM Pc
WHERE hd IN (10, 20);
model |
speed |
hd |
---|---|---|
1233 |
750 |
20 |
1232 |
500 |
10 |
1232 |
450 |
10 |
1260 |
500 |
10 |
Пример. Найти модель, частоту процессора и объем жесткого диска тех компьютеров, которые комплектуются накопителями 10 или 20 Мб и выпускаются производителем А:
SELECT model, speed, hd
FROM Pc
WHERE hd IN (10, 20) AND
model IN (SELECT model FROM product
WHERE maker = 'A');
model |
speed |
hd |
---|---|---|
1233 |
750 |
20 |
1232 |
500 |
10 |
1232 |
450 |
10 |
Имена столбцов, указанные в предложении SELECT, можно переименовать. Это делает результаты более читабельными, поскольку имена полей в таблицах часто сокращают с целью упрощения набора. Ключевое слово AS, используемое для переименования, согласно стандарту можно и опустить, т.к. оно неявно подразумевается.
Например, запрос
SELECT ram AS Mb, hd Gb
FROM Pc
WHERE cd = '24x';
переименует столбец ram в Mb (мегабайты), а столбец hd в Gb (гигабайты). Этот запрос возвратит объемы оперативной памяти и жесткого диска для тех компьютеров, которые имеют 24-скоростной CD-ROM:
Mb |
Gb |
---|---|
64 |
8 |
32 |
10 |
Переименование особенно желательно при использовании в предложении SELECT выражений для вычисления значения. Эти выражения позволяют получать данные, которые не находятся непосредственно в таблицах. Если выражение содержит имена столбцов таблицы, указанной в предложении FROM, то выражение подсчитывается для каждой строки выходных данных. Так, например, чтобы вывести объем оперативной памяти в килобайтах, можно написать:
SELECT ram * 1024 AS Kb, hd Gb
FROM Pc
WHERE cd = '24x';
Теперь будет получен следующий результат:
Kb |
Gb |
---|---|
65536 |
8 |
32768 |
10 |
Иногда бывает необходимо выводить поясняющую информацию рядом с соответствующим значением. Это можно сделать, добавив строковое выражение как дополнительный столбец. Например, запрос
SELECT ram, 'Mb' AS ram_units, hd, 'Gb' AS hd_units
FROM Pc
WHERE cd = '24x';
даст следующий результат:
ram |
ram_units |
hd |
hd_units |
---|---|---|---|
64 |
Mb |
8 |
Gb |
32 |
Mb |
10 |
Gb |
Если же явно не указать имя для выражения, то будет использован способ именования по умолчанию, который зависит от используемой СУБД. Так в MS Access будут использованы имена типа выражение1 и т.д., а выходной столбец в MS SQL Server вообще не будет иметь заголовка.
Синтаксис LIKE::=
[NOT] LIKE
[ESCAPE ]
Предикат LIKE сравнивает строку, указанную в первом выражении для вычисления значения строки, называемого проверяемым значением, с шаблоном, который определен во втором выражении для вычисления значения строки. В образце разрешается использовать два трафаретных символа:
* Символ подчеркивания (_), который можно использовать вместо любого единичного символа в проверяемом значении.
* Символ процента (%), который заменяет набор любых символов (число символов в наборе может быть от 0 и более) в проверяемом значении.
Если проверяемое значение соответствует образцу с учетом трафаретных символов, то значение предиката равно TRUE. Ниже приводится несколько примеров написания шаблонов.
Шаблон |
Описание |
---|---|
'abc%' |
Любые строки, которые начинаются с букв "abc". |
'abc_' |
Строки длиной строго 4 символа, причем первыми символами строки должны быть "abc". |
'%z' |
Любая последовательность символов, которая обязательно заканчивается символом "z". |
'%Rostov%' |
Любая последовательность символов, содержащая слово "Rostov" в любом месте строки. |
Пример. Найти все корабли, имена классов которых заканчиваются на букву 'о':
SELECT *
FROM Ships
WHERE class LIKE '%o';
Результатом выполнения запроса будет следующая таблица:
name |
class |
launched |
---|---|---|
Haruna |
Kongo |
1916 |
Hiei |
Kongo |
1914 |
Kirishima |
Kongo |
1915 |
Kongo |
Kongo |
1913 |
Musashi |
Yamato |
1942 |
Yamato |
Yamato |
1941 |
Пример. Найти все корабли, имена классов которых заканчиваются на букву 'о', но не на 'go':
SELECT *
FROM Ships
WHERE class NOT LIKE '%go' AND class LIKE '%o';
name |
class |
launched |
---|---|---|
Musashi |
Yamato |
1942 |
Yamato |
Yamato |
1941 |
Если искомая строка содержит трафаретный символ, то следует задать управляющий символ в предложении ESCAPE. Этот управляющий символ должен использоваться в шаблоне перед трафаретным символом, сообщая о том, что трафаретный символ следует трактовать как обычный символ. Например, если в некотором поле следует отыскать все значения, содержащие символ "_", то шаблон '%_%' приведет к тому, что будут возвращены все записи из таблицы. В данном случае шаблон следует записать следующим образом:
'%#_%' ESCAPE '#'
Для проверки значения на соответствие строке "25%" можно воспользоваться таким предикатом:
LIKE '25|%' ESCAPE '|'
Истинностное значение предиката LIKE присваивается в соответствии со следующими правилами:
* Если либо проверяемое значение, либо образец, либо управляющий символ равен NULL, истинностное значение равно UNKNOWN.
* В противном случае, если проверяемое значение и образец имеют нулевую длину, истинностное значение равно TRUE.
* В противном случае, если проверяемое значение соответствует шаблону, то предикат LIKE равен TRUE.
* Если не соблюдается ни одно из перечисленных выше условий, предикат LIKE равен FALSE.
Предикат
IS [NOT] NULL
позволяет проверить отсутствие (наличие) значения в полях таблицы. Использование в этих случаях обычных предикатов сравнения может привести к неверным результатам, т.к. сравнение со значением NULL дает результат UNKNOWN (неизвестно).
Так, если требуется найти записи в таблице Pc, для которых в поле price отсутствует значение (поиск ошибок ввода), можно воспользоваться следующим оператором:
SELECT *
FROM Pc
WHERE price IS NULL;
Как узнать количество моделей ПК, выпускаемых тем или иным поставщиком? Как определить среднее значение цены на компьютеры, имеющие одинаковые технические характеристики? На эти и многие другие вопросы, связанные с некоторой статистической информацией, можно получить ответы при помощи итоговых (агрегатных) функций. Стандартом предусмотрены следующие агрегатные функции:
Функция |
Описание |
---|---|
COUNT(*) |
Возвращает количество строк источника записей. |
COUNT() |
Возвращает количество значений в указанном столбце. |
SUM() |
Возвращает сумму значений в указанном столбце. |
AVG() |
Возвращает среднее значение в указанном столбце. |
MIN() |
Возвращает минимальное значение в указанном столбце. |
MAX() |
Возвращает максимальное значение в указанном столбце. |
Все эти функции возвращают единственное значение. При этом функции COUNT, MIN и MAX применимы к любым типам данных, в то время как SUM и AVG используются только для числовых полей. Разница между функцией COUNT(*) и COUNT() состоит в том, что вторая при подсчете не учитывает NULL-значения.
Пример. Найти минимальную и максимальную цену на персональные компьютеры:
SELECT MIN(price) AS Min_price, MAX(price) AS Max_price
FROM PC;
Результатом будет единственная строка, содержащая агрегатные значения:
Min_price |
Max_price |
---|---|
350.0 |
980.0 |
Пример. Найти имеющееся в наличии количество компьютеров, выпущенных производителем А:
SELECT COUNT(*) AS Qty
FROM PC
WHERE model IN
(SELECT model
FROM Product
WHERE maker = 'A');
В результате получим:
Qty |
---|
7 |
Пример. Если же нас интересует количество различных моделей, выпускаемых производителем А, то запрос можно сформулировать следующим образом (пользуясь тем фактом, что в таблице Product каждая модель записывается один раз):
SELECT COUNT(model) AS Qty_model
FROM Product
WHERE maker = 'A';
Совпадение результатов совершенно случайно, т.к. в базе данных количество компьютеров производителя А оказалось равным числу выпускаемых им моделей:
Qty_model |
---|
7 |
Пример. Найти количество имеющихся различных моделей, выпускаемых производителем А. Запрос похож на предыдущий, в котором требовалось определить общее число моделей, выпускаемых производителем А. Здесь же требуется найти число различных моделей в таблице PC (т.е. имеющихся в продаже).
Для того, чтобы при получении статистических показателей использовались только уникальные значения, при аргументе агрегатных функций можно использовать параметр DISTINCT. Другой параметр ALL используется по умолчанию и предполагает подсчет всех возвращаемых значений в столбце. Оператор,
SELECT COUNT(DISTINCT model) AS Qty
FROM PC
WHERE model IN
(SELECT model
FROM Product
WHERE maker = 'A');
даст следующий результат:
Qty |
---|
2 |
Если же нам требуется получить количество моделей ПК, производимых каждым производителем, то потребуется использовать предложение GROUP BY, синтаксически следующего после предложения WHERE.
Предложение GROUP BY используется для определения групп выходных строк, к которым могут применяться агрегатные функции (COUNT, MIN, MAX, AVG и SUM). Если это предложение отсутствует, и используются агрегатные функции, то все столбцы с именами, упомянутыми в SELECT, должны быть включены в агрегатные функции, и эти функции будут применяться ко всему набору строк, которые удовлетворяют предикату запроса. В противном случае все столбцы списка SELECT, не вошедшие в агрегатные функции, должны быть указаны в предложении GROUP BY. В результате чего все выходные строки запроса разбиваются на группы, характеризуемые одинаковыми комбинациями значений в этих столбцах. После этого к каждой группе будут применены агрегатные функции. Следует иметь в виду, что для GROUP BY все значения NULL трактуются как равные, т.е. при группировке по полю, содержащему NULL-значения, все такие строки попадут в одну группу.
Если при наличии предложения GROUP BY, в предложении SELECT отсутствуют агрегатные функции, то запрос просто вернет по одной строке из каждой группы. Эту возможность, наряду с ключевым словом DISTINCT, можно использовать для исключения дубликатов строк в результирующем наборе.
Рассмотрим простой пример:
SELECT model, COUNT(model) AS Qty_model, AVG(price) AS Avg_price
FROM PC
GROUP BY model;
В этом запросе для каждой модели ПК определяется их количество и средняя стоимость. Все строки с одинаковыми значениями model (номер модели) образуют группу, и на выходе SELECT вычисляются количество значений и средние значения цены для каждой группы. Результатом выполнения запроса будет следующая таблица:
model |
Qty_model |
Avg_price |
---|---|---|
1121 |
3 |
850.0 |
1232 |
4 |
425.0 |
1233 |
3 |
843.33333333333337 |
1260 |
1 |
350.0 |
Если бы в SELECT присутствовал столбец с датой, то можно было бы вычислять эти показатели для каждой конкретной даты. Для этого нужно добавить дату в качестве группирующего столбца, и тогда агрегатные функции вычислялись бы для каждой комбинации значений (модель−дата).
Существует несколько определенных правил выполнения агрегатных функций:
* Если в результате выполнения запроса не получено ни одной строки (или не одной строки для данной группы), то исходные данные для вычисления любой из агрегатных функций отсутствуют. В этом случае результатом выполнения функций COUNT будет нуль, а результатом всех других функций - NULL.
* Аргумент агрегатной функции не может сам содержать агрегатные функции (функция от функции). Т.е. в одном запросе нельзя, скажем, получить максимум средних значений.
* Результат выполнения функции COUNT есть целое число (INTEGER). Другие агрегатные функции наследуют типы данных обрабатываемых значений.
* Если при выполнении функции SUM был получен результат, превышающий максимальное значение используемого типа данных, возникает ошибка.
Итак, если запрос не содержит предложения GROUP BY, то агрегатные функции, включенные в предложение SELECT, исполняются над всеми результирующими строками запроса. Если запрос содержит предложение GROUP BY, каждый набор строк, который имеет одинаковые значения столбца или группы столбцов, заданных в предложении GROUP BY, составляет группу, и агрегатные функции выполняются для каждой группы отдельно.
Если предложение WHERE определяет предикат для фильтрации строк, то предложение HAVING применяется после группировки для определения аналогичного предиката, фильтрующего группы по значениям агрегатных функций. Это предложение необходимо для проверки значений, которые получены с помощью агрегатной функции не из отдельных строк источника записей, определенного в предложении FROM, а из групп таких строк. Поэтому такая проверка не может содержаться в предложении WHERE.
Пример. Получить количество ПК и среднюю цену для каждой модели при условии, что средняя цена менее $800:
SELECT model, COUNT(model) AS Qty_model, AVG(price) AS Avg_price
FROM PC
GROUP BY model
HAVING AVG(price) < 800;
В результате выполнения запроса получим:
model |
Qty_model |
Avg_price |
---|---|---|
1232 |
4 |
425.0 |
1260 |
1 |
350.0 |
Заметим, что в предложении HAVING нельзя использовать псевдоним (Avg_price), используемый для именования значений агрегатной функции. Дело в том, что предложение SELECT, формирующее выходной набор запроса, выполняется предпоследним перед предложением ORDER BY.
Ниже приведен порядок обработки предложений в операторе SELECT:
* FROM
* WHERE
* GROUP BY
* HAVING
* SELECT
* ORDER BY
Этот порядок не соответствует синтаксическому порядку общего формата оператора SELECT, представленному ниже:
SELECT [DISTINCT | ALL]{*
| [ [[AS] ]] [,…]}
FROM [[AS] ] [,…]
[WHERE ]
[[GROUP BY ]
[HAVING ] ]
[ORDER BY ]
Как видно из приведенного в конце предыдущего раздела синтаксиса оператора SELECT, в предложении FROM допускается указание нескольких таблиц. Простое перечисление таблиц практически не используется, поскольку оно соответствует реляционной операции декартова произведения. Т.е. в результирующем наборе каждая запись из одной таблицы будет сочетаться с каждой записью в другой. Например, для таблиц
A |
|
B |
||
---|---|---|---|---|
a |
b |
|
c |
d |
1 |
2 |
|
2 |
4 |
2 |
1 |
|
3 |
3 |
Результат запроса
SELECT * FROM A, B;
будет выглядеть следующим образом:
a |
b |
c |
d |
---|---|---|---|
1 |
2 |
2 |
4 |
1 |
2 |
3 |
3 |
2 |
1 |
2 |
4 |
2 |
1 |
3 |
3 |
Поэтому перечисление таблиц, как правило, используется совместно с условием соединения записей из разных таблиц, указываемым в предложении WHERE. Для приведенных выше таблиц таким условием может быть совпадение значений, скажем, в полях a и c:
SELECT * FROM A, B WHERE a=c;
Теперь результатом выполнения этого запроса будет следующая таблица:
a |
b |
c |
d |
---|---|---|---|
2 |
1 |
2 |
4 |
т.е. соединяются только те строки таблиц, у которых в указанных полях находятся равные значения (эквисоединение). Естественно, могут быть использованы любые условия, хотя эквисоединение используется чаще всего, поскольку эта операция воссоздает некую сущность, декомпозированную на две других в результате процедуры нормализации.
Если разные таблицы имеют столбцы с одинаковыми именами, то для однозначности требуется использовать точечную нотацию:
.
В тех случаях, когда это не вызывает неоднозначности, использование данной нотации не является обязательным.
Пример. Найти номер модели и производителя ПК, имеющих цену менее $600:
SELECT DISTINCT PC.model, maker
FROM PC, Product
WHERE PC.model = Product.model AND price < 600;
В результате каждая модель одного и того же производителя выводится только один раз:
model |
maker |
---|---|
1232 |
A |
1260 |
E |
Иногда в предложении FROM требуется указать одну и ту же таблицу несколько раз. В этом случае обязательным является переименование.
Пример. Вывести пары моделей, имеющих одинаковые цены:
SELECT DISTINCT A.model AS model_1, B.model AS model_2
FROM PC AS A, PC B
WHERE A.price = B.price AND A.model < B.model;
Здесь условие A.model < B.model используется для того, чтобы не выводились одинаковые пары, отличающиеся только перестановкой, например: 1232, 1233 и 1233, 1232. DISTINCT применяется для того, чтобы исключить одинаковые строки, поскольку в таблице PC имеются одинаковые модели по одной и той же цене. В результате получим следующую таблицу:
model_1 |
model_2 |
---|---|
1232 |
1233 |
1232 |
1260 |
Переименование также требуется, если в предложении FROM используется подзапрос. Так, первый пример можно переписать следующим образом:
SELECT DISTINCT PC.model, maker
FROM PC,
(SELECT maker, model
FROM Product) AS prod
WHERE PC.model = prod.model AND price < 600;
Обратите внимание, что в этом случае в других предложениях оператора SELECT уже нельзя использовать квалификатор Product, поскольку таблица Product уже не используется. Вместо него используется псевдоним prod. Кроме того, ссылаться теперь можно только на те поля таблицы Product, которые перечислены в подзапросе.
В предложении FROM может быть указана явная операция соединения двух и более таблиц. Среди ряда операций соединения, описанных в стандарте языка SQL, многими серверами баз данных поддерживается лишь операция соединения по предикату. Синтаксис соединения по предикату имеет вид:
FROM [INNER]
| {{LEFT | RIGHT | FULL } [OUTER]} JOIN
[ON ]
Соединение может быть либо внутренним (INNER), либо одним из внешних (OUTER). Служебные слова INNER и OUTER можно опускать, поскольку внешнее соединение однозначно определяется его типом - LEFT (левое), RIGHT (правое) или FULL (полное), а просто JOIN будет означать внутреннее соединение.
Предикат определяет условие соединения строк из разных таблиц. При этом INNER JOIN означает, что в результирующий набор попадут только те соединения строк двух таблиц, для которых значение предиката равно TRUE. Как правило, предикат определяет эквисоединение по внешнему и первичному ключам соединяемых таблиц, хотя это не обязательно.
Пример. Найти производителя, номер модели и цену каждого компьютера, имеющегося в базе данных:
SELECT maker, Product.model AS model_1, PC.model AS model_2, price
FROM Product INNER JOIN PC ON PC.model = Product.model
ORDER BY maker, PC.model;
В данном примере в результирующем наборе будут соединяться только те строки из таблиц PC и Product, у которых совпадают номера моделей.
Для контроля в результат включен как номер модели из таблицы PC, так и из таблицы Product:
maker |
model_1 |
model_2 |
price |
---|---|---|---|
A |
1232 |
1232 |
600.0 |
A |
1232 |
1232 |
400.0 |
A |
1232 |
1232 |
350.0 |
A |
1232 |
1232 |
350.0 |
A |
1233 |
1233 |
600.0 |
A |
1233 |
1233 |
950.0 |
A |
1233 |
1233 |
980.0 |
B |
1121 |
1121 |
850.0 |
B |
1121 |
1121 |
850.0 |
B |
1121 |
1121 |
850.0 |
E |
1260 |
1260 |
350.0 |
Внешнее соединение LEFT JOIN означает, что помимо строк, для которых выполняется условие предиката, в результирующий набор попадут все остальные строки из первой таблицы (левой). При этом отсутствующие значения полей из правой таблицы будут заполнены NULL-значениями.
Пример. Привести все модели ПК, их производителей и цену:
SELECT maker, Product.model AS model_1, PC.model AS model_2, price
FROM Product LEFT JOIN PC ON PC.model = Product.model
WHERE type = 'PC'
ORDER BY maker, PC.model;
Обратите внимание на то, что по сравнению с предыдущим примером, пришлось использовать предложение WHERE для отбора только производителей ПК. В противном случае в результирующий набор попали бы также и модели ПК-блокнотов и принтеров. В рассмотренном ранее примере это условие было бы излишним, т.к. соединялись только те строки, у которых совпадали номера моделей, и одной из таблиц была таблица PC, содержащая только ПК. В результате выполнения запроса получим:
maker |
model_1 |
model_2 |
price |
---|---|---|---|
A |
1232 |
1232 |
600.0 |
A |
1232 |
1232 |
400.0 |
A |
1232 |
1232 |
350.0 |
A |
1232 |
1232 |
350.0 |
A |
1233 |
1233 |
600.0 |
A |
1233 |
1233 |
950.0 |
A |
1233 |
1233 |
980.0 |
B |
1121 |
1121 |
850.0 |
B |
1121 |
1121 |
850.0 |
B |
1121 |
1121 |
850.0 |
E |
2111 |
NULL |
NULL |
E |
2112 |
NULL |
NULL |
E |
1260 |
1260 |
350.0 |
Поскольку моделей 2111 и 2112 из таблицы Product нет в таблице PC, в полях из таблицы PC содержится NULL.
Соединение RIGHT JOIN обратно соединению LEFT JOIN, т.е. в результирующий набор попадут все строки из второй таблицы, которые будут соединяться только с теми строками из первой таблицы, для которых выполняется условие соединения. В нашем случае левое соединение
Product LEFT JOIN PC ON PC.model = Product.model
будет эквивалентно правому соединению
PC RIGHT JOIN Product ON PC.model = Product.model
Запрос же
SELECT maker, Product.model AS model_1, PC.model AS model_2, price
FROM Product RIGHT JOIN PC ON PC.model = Product.model
ORDER BY maker, PC.model;
даст те же результаты, что и внутреннее соединение, поскольку в правой таблице (PC) нет таких моделей, которые отсутствовали бы в левой таблице (Product), что вполне естественно для типа связи "один-ко-многим", которая имеется между таблицами PC и Product. Наконец, при полном соединении (FULL JOIN) в результирующую таблицу попадут не только те строки, которые имеют одинаковые значения в сопоставляемых столбцах, но и все остальные строки исходных таблиц, не имеющие соответствующих значений в другой таблице. В этих строках все столбцы той таблицы, в которой не было найдено соответствия, заполняются NULL-значениями. Полное соединение представляет собой комбинацию левого и правого внешних соединений.
Так запрос для таблиц A и B, приведенных в начале главы,
SELECT A.*, B.*
FROM A FULL JOIN B
ON A.a = B.c;
даст следующий результат:
a |
b |
c |
d |
---|---|---|---|
1 |
2 |
NULL |
NULL |
2 |
1 |
2 |
4 |
NULL |
NULL |
3 |
3 |
Заметим, что это соединение симметрично, т.е. "A FULL JOIN B" эквивалентно "B FULL JOIN A". Обратите также внимание на обозначение A.*, что означает "все поля таблицы А".
Традиционные операции над множествами - это объединение, пересечение, разность и декартово произведение.
Ранее мы уже рассмотрели реализацию декартова произведения, перечисляя через запятую табличные выражения в предложении FROM (таблицы, представления, подзапросы). Кроме того, можно использовать еще одну явную операцию соединения:
SELECT Laptop.model, Product.model
FROM Laptop CROSS JOIN Product;
Напомним, что при декартовом произведении каждая строка из одной таблицы соединяется с каждой строкой второй таблицы. В результате количество строк результирующего набора равно произведению количества строк операндов декартова произведения. В нашем примере таблица Laptop содержит 5 строк, а таблица Product - 16. В результате получается 5*16 = 80 строк. Поэтому мы не приводим здесь результат выполнения этого запроса. Вы можете сами проверить это утверждение, выполнив приведенный выше запрос на учебной базе данных.
В чистом виде декартово произведение практически не используется. Оно, как правило, является промежуточным результатом выполнения операции горизонтальной проекции (выборки) при наличии в операторе SELECT предложения WHERE.
Для объединения запросов используется служебное слово UNION:
UNION [ALL]
Оператор UNION объединяет выходные строки каждого из запросов в один результирующий набор. Если определен параметр ALL, то сохраняются все дубликаты выходных строк, в противном случае в результирующем наборе остаются только уникальные строки. Заметим, что можно связывать вместе любое число запросов. Кроме того, с помощью скобок можно менять порядок объединения.
При этом должны выполняться следующие условия:
* Количество выходных столбцов каждого из запросов должно быть одинаковым.
* Выходные столбцы каждого из запросов должны быть сравнимыми между собой (в порядке их следования) по типам данных.
* В результирующем наборе используются имена столбцов, заданные в первом запросе.
* Предложение ORDER BY применяется к результату соединения, поэтому оно может быть указано только в конце составного запроса.
Пример. Найти номера моделей и цены ПК и ПК-блокнотов:
SELECT model, price
FROM PC
UNION
SELECT model, price
FROM Laptop
ORDER BY price DESC;
model |
price |
---|---|
1750 |
1200.0 |
1752 |
1150.0 |
1298 |
1050.0 |
1233 |
980.0 |
1321 |
970.0 |
1233 |
950.0 |
1121 |
850.0 |
1298 |
700.0 |
1232 |
600.0 |
1233 |
600.0 |
1232 |
400.0 |
1232 |
350.0 |
1260 |
350.0 |
Пример. Найти тип продукции, номер модели и цену ПК и ПК-блокнотов:
SELECT Product .type, PC.model, price
FROM PC INNER JOIN
Product ON PC.model = Product .model
UNION
SELECT Product .type, Laptop.model, price
FROM Laptop INNER JOIN
Product ON Laptop.model = Product .model
ORDER BY price DESC;
type |
model |
price |
---|---|---|
Laptop |
1750 |
1200.0 |
Laptop |
1752 |
1150.0 |
Laptop |
1298 |
1050.0 |
PC |
1233 |
980.0 |
Laptop |
1321 |
970.0 |
PC |
1233 |
950.0 |
PC |
1121 |
850.0 |
Laptop |
1298 |
700.0 |
PC |
1232 |
600.0 |
PC |
1233 |
600.0 |
PC |
1232 |
400.0 |
PC |
1232 |
350.0 |
PC |
1260 |
350.0 |
В стандарте языка SQL имеются предложения оператора SELECT для выполнения операций пересечения и разности запросов. Этими предложениями являются INTERSECT (пересечение) и EXCEPT (разность), которые работают аналогично предложению UNION. В результирующий набор попадают только те строки, которые присутствуют в обоих запросах (INTERSECT) или только те строки первого запроса, которые отсутствуют во втором (EXCEPT).
Однако многие СУБД не поддерживают эти предложения в операторе SELECT. Это справедливо и для MS SQL Server. Поэтому для выполнения операций пересечения и разности могут быть использованы другие средства. Здесь уместно заметить, что один и тот же результат можно получить с помощью различных формулировок оператора SELECT. В случае пересечения и разности можно воспользоваться предикатом существования EXISTS.
EXISTS::=
[NOT] EXISTS ()
Предикат EXISTS принимает значение TRUE, если подзапрос возвращает любое количество строк, иначе его значение равно FALSE. Для NOT EXISTS все наоборот. Этот предикат никогда не принимает значение UNKNOWN.
Обычно (как и в нашем случае) предикат EXISTS используется в зависимых подзапросах. Этот вид подзапроса имеет внешнюю ссылку, связанную со значением в основном запросе. Результат подзапроса может зависеть от этого значения и должен оцениваться отдельно для каждой строки запроса, в котором содержится данный подзапрос. Поэтому предикат EXISTS может иметь разные значения для каждой строки основного запроса.
Пример на пересечение. Найти тех производителей ПК-блокнотов, которые производят также и принтеры:
SELECT DISTINCT maker
FROM Product AS Lap_product
WHERE type = 'Laptop' AND EXISTS
(SELECT maker
FROM Product
WHERE type = 'Printer' AND maker = Lap_product.maker);
В подзапросе выбираются производители принтеров и сравниваются с производителем, значение которого передается из основного запроса. В основном же запросе отбираются производители ПК-блокнотов. Таким образом, для каждого производителя ПК-блокнотов проверяется, возвращает ли подзапрос строки (т.е. этот производитель также производит принтеры). Поскольку два условия в предложении WHERE должны выполняться одновременно (AND), то в результирующий набор попадут нужные строки. DISTINCT используется для того, чтобы каждый производитель присутствовал в выходных данных только один раз. В результате получим:
maker |
---|
A |
Пример на разность. Найти тех производителей ПК-блокнотов, которые не производят принтеров:
SELECT DISTINCT maker
FROM Product AS Lap_product
WHERE type = 'Laptop' AND NOT EXISTS
(SELECT maker
FROM Product
WHERE type = 'Printer' AND maker = Lap_product.maker);
В этом случае достаточно заменить в предыдущем примере EXIST на NOT EXIST. Т.е. выходные данные составят только те уникальные строки основного запроса, для которых подзапрос не возвращает ни одной строки. В итоге получим:
maker |
---|
B |
C |
<оператор сравнения SOME|ANY () SOME и ANY являются синонимами, т.е. может использоваться любое из них. Результатом подзапроса является один столбец величин. Если для какого-нибудь значения V, получаемого из подзапроса, результат операции " V " равняется TRUE, то предикат ANY также равняется TRUE.
ALL ()
исполняется так же, как и ANY, но для всех значений, получаемых из подзапроса, проверка должна удовлетворять результату TRUE для предиката " V ".
Пример. Найти поставщиков компьютеров, моделей которых нет в продаже (т.е. отсутствуют в таблице PC):
SELECT DISTINCT maker
FROM Product
WHERE type = 'PC' AND NOT model = ANY
(SELECT model
FROM PC);
Оказалось, что только у поставщика Е есть модели отсутствующие в продаже:
maker |
---|
E |
Рассмотрим подробно этот пример. Предикат
model = ANY (SELECT model FROM PC);
вернет значение TRUE, если модель, определяемая полем model основного запроса, найдется в списке моделей таблицы PC (возвращаемом подзапросом). Поскольку предикат используется в запросе с отрицанием NOT, то значение TRUE будет получено, если модели не окажется в списке. Этот предикат проверяется для каждой записи основного запроса, которыми являются все модели ПК (предикат type = 'PC') из таблицы Product. Результирующий набор состоит из одного столбца - имени производителя. Чтобы один производитель не выводился несколько раз (что может случиться, если он производит несколько моделей, отсутствующих в таблице PC), используется служебное слово DISTINCT.
Пример. Найти модели и цены ПК-блокнотов, стоимость которых превышает стоимость любого ПК:
SELECT DISTINCT model, price
FROM Laptop
WHERE price ALL
(SELECT price
FROM PC);
model |
price |
---|---|
1298 |
1050.0 |
1750 |
1200.0 |
1752 |
1150.0 |
Приведем формальные правила оценки предикатов, использующих параметры ANY|SOME и ALL:
* Если определен параметр ALL или SOME и все результаты сравнения значения выражения и каждого значения, полученного из подзапроса, являются TRUE, истинностное значение равно TRUE.
* Если результат выполнения подзапроса не содержит строк и определен параметр ALL, результат равен TRUE. Если же определен параметр SOME, результат равен FALSE.
* Если определен параметр ALL и результат сравнения значения выражения хотя бы с одним значением, полученным из подзапроса, является FALSE, истинностное значение равно FALSE.
* Если определен параметр SOME и хотя бы один результат сравнения значения выражения и значения, полученного из подзапроса, является TRUE, истинностное значение равно TRUE.
* Если определен параметр SOME и каждое сравнение значения выражения и значений, полученных из подзапроса, равно FALSE, истинностное значение тоже равно FALSE.
* В любом другом случае результат будет равен UNKNOWN.
Заметим, что в общем случае запрос возвращает множество значений. Поэтому использование подзапроса в предложении WHERE без операторов EXISTS, IN, ALL и ANY, которые дают булево значение, может привести к ошибке времени выполнения запроса.
Пример. Найти модели и цены ПК, стоимость которых превышает минимальную стоимость ПК-блокнотов:
SELECT DISTINCT model, price
FROM PC
WHERE price
(SELECT MIN(price)
FROM Laptop);
Этот запрос вполне корректен, т.к. скалярное значение price сравнивается с подзапросом, который возвращает единственное значение. В результате получим три модели ПК:
model |
price |
---|---|
1121 |
850.0 |
1233 |
950.0 |
1233 |
980.0 |
Однако, если в ответ на вопрос "найти модели и цены ПК, стоимость которых совпадает со стоимостью ПК-блокнота" написать следующий запрос:
SELECT DISTINCT model, price
FROM PC
WHERE price =
(SELECT price
FROM Laptop);
то при выполнении последнего мы можем получить такое сообщение об ошибке:
Эта ошибка будет возникать при сравнении скалярного значения с подзапросом, который либо возвращает более одного значения, либо ни одного.
Подзапросы, в свою очередь, также могут содержать вложенные запросы.
С другой стороны, подзапрос, возвращающий множество строк и содержащий несколько столбцов, вполне естественно может использоваться в предложении FROM. Это позволяет ограничить набор столбцов и/или строк при выполнении операции соединения таблиц.
Пример. Вывести производителя, тип, модель и частоту процессора для ПК-блокнотов, частота процессора которых превышает 600 МГц.
Этот запрос может быть сформулирован, например, следующим образом:
SELECT prod.maker, lap.*
FROM (SELECT 'Laptop' AS type, model, speed
FROM Laptop
WHERE speed 600) AS lap INNER JOIN
(SELECT maker, model
FROM Product) AS prod ON lap.model = prod.model;
В результате получим:
maker |
type |
model |
speed |
---|---|---|---|
B |
Laptop |
1750 |
750 |
A |
Laptop |
1752 |
750 |
Наконец, подзапросы могут присутствовать в предложении SELECT. Это иногда позволяет весьма компактно сформулировать запрос.
Пример. Найти разницу между средними значениями цены ПК-блокнотов и ПК, т.е. на сколько в среднем ПК-блокнот стоит дороже, чем ПК.
Здесь вообще можно обойтись одним предложением SELECT:
SELECT (SELECT AVG(price)
FROM Laptop) -
(SELECT AVG(price)
FROM PC) AS dif_price;
В результате получим:
dif_price |
---|
365.81818181818187 |
В реализациях языка SQL может быть выполнено неявное преобразование типов. Так, например, в T-SQL при сравнении или комбинировании значений типов smallint и int, данные типа smallint неявно преобразуются к типу int. Подробно о явном и неявном преобразовании типов в MS SQL Server можно прочитать в BOL.
Пример. Вывести среднюю цену ПК-блокнотов с предваряющим текстом "средняя цена = ".
Попытка выполнить запрос
SELECT 'Средняя цена = ' + AVG(price) FROM laptop;
приведет к сообщению об ошибке
Implicit conversion from data type varchar to money is not allowed. Use the CONVERT function to run this query.
Это сообщение означает, что система не может выполнить неявное преобразование типа varchar к типу money. В подобных ситуациях может помочь явное преобразование типов. При этом, как указано в сообщении об ошибке, можно воспользоваться функцией CONVERT. Однако эта функция не стандартизована, поэтому в целях переносимости рекомендуется использовать стандартное выражение CAST. С него и начнем.
Если переписать наш запрос в виде
SELECT 'Средняя цена = ' + CAST(AVG(price) AS CHAR(15)) FROM laptop;
в результате получим то, что требовалось:
Средняя цена = 1410.44 |
Мы использовали выражение явного преобразования типов CAST для приведения среднего значения цены к строковому представлению. Синтаксис выражения CAST очень простой:
CAST( AS )
При этом следует иметь в виду, во-первых, что не любые преобразования типов возможны (стандарт содержит таблицу допустимых преобразований типов данных). Во-вторых, результат функции CAST для значения выражения, равного NULL, тоже будет NULL.
Рассмотрим еще один пример: определить средний год спуска на воду кораблей из таблицы Ships. Запрос
SELECT AVG(launched) FROM ships;
даст результат 1926. В принципе все правильно, т.к. мы получили в результате то, что просили - ГОД. Однако среднее арифметическое будет составлять примерно 1926,2381. Тут следует отметить, что агрегатные функции (за исключением функции COUNT, которая всегда возвращает целое число) наследуют тип данных обрабатываемых значений. Поскольку поле launched - целочисленное, мы и получили среднее значение с отброшенной дробной частью (заметьте - не округленное).
А если нас интересует результат с заданной точностью, скажем, до двух десятичных знаков? Применение выражения CAST к среднему значению ничего не даст по указанной выше причине. Действительно,
SELECT CAST(AVG(launched) AS NUMERIC(6,2)) FROM ships;
вернет значение 1926.00. Следовательно, CAST нужно применить к аргументу агрегатной функции:
SELECT AVG(CAST(launched AS NUMERIC(6,2))) FROM ships;
Результат - 1926.238095. Опять не то. Причина состоит в том, что при вычислении среднего значения было выполнено неявное преобразование типа. Сделаем еще один шаг:
SELECT CAST(AVG(CAST(launched AS NUMERIC(6,2))) AS NUMERIC(6,2)) FROM ships;
В результате получим то, что нужно - 1926.24. Однако это решение выглядит очень громоздко. Заставим неявное преобразование типа поработать на нас:
SELECT CAST(AVG(launched*1.0) AS NUMERIC(6,2)) FROM ships;
Т.е. мы использовали неявное преобразование целочисленного аргумента к точному числовому типу (EXACT NUMERIC), умножив его на вещественную единицу, после чего применили явное приведения типа результата агрегатной функции.
Аналогичные преобразования типа можно выполнить с помощью функции CONVERT:
SELECT CONVERT(NUMERIC(6,2),AVG(launched*1.0)) FROM ships;
Функция CONVERT имеет следующий синтаксис:
CONVERT ([()], [, ])
Основное отличие функции CONVERT от функции CAST состоит в том, что первая позволяет форматировать данные (например, темпоральные данные типа datetime) при преобразовании их к символьному типу и указывать формат при обратном преобразовании. Разные целочисленные значения необязательного аргумента стиль соответствуют определенным форматам. Рассмотрим следующий пример
SELECT CONVERT(char(25),CONVERT(datetime,'20030722'));
Здесь мы преобразуем строковое представление даты к типу datetime, после чего выполняем обратное преобразование, чтобы продемонстрировать результат форматирования. Поскольку значение аргумента стиль не задано, используется значение по умолчанию (0 или 100). В результате получим
Jul 22 2003 12:00AM |
Ниже приведены некоторые другие значения аргумента стиль и результат, полученный на приведенном выше примере. Заметим, что значения стиль большие 100 приводят к четырехзначному отображению года.
стиль |
формат |
---|---|
1 |
07/22/03 |
11 |
03/07/22 |
3 |
22/07/03 |
121 |
2003-07-22 00:00:00.000 |
Перечень всех возможных значений аргумента стиль можно посмотреть в BOL.
Пусть требуется вывести список всех моделей ПК с указанием их цены. При этом если модель отсутствует в продаже (нет в таблице РС), то вместо цены вывести текст: "Нет в наличии".
Список всех моделей ПК с ценами можно получить с помощью запроса:
SELECT DISTINCT product.model, price FROM product LEFT JOIN pc c
ON product.model=c.model
WHERE product.type='pc';
В результирующем наборе отсутствующая цена будет заменена NULL-значением:
model |
price |
---|---|
1121 |
850 |
1232 |
350 |
1232 |
400 |
1232 |
600 |
1233 |
600 |
1233 |
950 |
1233 |
980 |
1260 |
350 |
2111 |
NULL |
2112 |
NULL |
Чтобы заменить NULL-значения нужным текстом, можно воспользоваться оператором CASE:
SELECT DISTINCT product.model,
CASE WHEN price IS NULL THEN 'Нет в наличии' ELSE CAST(price AS CHAR(20)) END price
FROM product LEFT JOIN pc c ON product.model=c.model
WHERE product.type='pc'
Оператор CASE в зависимости от указанных условий возвращает одно из множества возможных значений. В нашем примере условием является проверка на NULL. Если это условие выполняется, то возвращается текст "Нет в наличии", в противном случае (ELSE) возвращается значение цены. Здесь есть один принципиальный момент. Поскольку результатом оператора SELECT всегда является таблица, то все значения любого столбца должны иметь один и тот же тип данных (с учетом неявного приведения типов). Поэтому мы не можем наряду с ценой (числовой тип) выводить символьную константу. Вот почему к полю price применяется преобразование типов, чтобы привести его значения к символьному представлению. В результате получим
model |
price |
---|---|
1121 |
850 |
1232 |
350 |
1232 |
400 |
1232 |
600 |
1233 |
600 |
1233 |
950 |
1233 |
980 |
1260 |
350 |
2111 |
Нет в наличии |
2112 |
Нет в наличии |
Оператор CASE может быть использован в одной из двух синтаксических форм записи:
1-я форма
CASE
WHEN
THEN
…
WHEN
THEN
[ELSE ]
END
2-я форма
CASE
WHEN
THEN
…
WHEN
THEN
[ELSE ]
END
Все предложения WHEN должны иметь одинаковую синтаксическую форму, т.е. нельзя смешивать первую и вторую формы. При использовании первой синтаксической формы условие WHEN удовлетворяется, как только значение проверяемого выражения станет равным значению выражения, указанного в предложении WHEN. При использовании второй синтаксической формы условие WHEN удовлетворяется, как только предикат принимает значение TRUE. При удовлетворении условия оператор CASE возвращает значение, указанное в соответствующем предложении THEN. Если ни одно из условий WHEN не выполнилось, то будет использовано значение, указанное в предложении ELSE. При отсутствии ELSE, будет возвращено NULL-значение. Если удовлетворены несколько условий, то будет возвращено значение предложения THEN первого из них.
В приведенном выше примере была использована вторая форма оператора CASE.
Заметим, что для проверки на NULL стандарт предлагает более короткую форму оператора - COALESCE. Этот оператор имеет произвольное число параметров и возвращает значение первого, отличного от NULL. Для двух параметров оператор COALESCE(A, B) эквивалентен следующему оператору CASE:
CASE WHEN A IS NOT NULL THEN A ELSE B END
Решение рассмотренного выше примера при использовании оператора COALESCE можно переписать следующим образом:
SELECT DISTINCT product.model,
COALESCE(CAST(price as CHAR(20)),'Нет в наличии') price
FROM product LEFT JOIN pc c ON product.model=c.model
WHERE product.type='pc';
Использование первой синтаксической формы оператора CASE можно продемонстрировать на следующем примере: Вывести все имеющиеся модели ПК с указанием цены. Отметить самые дорогие и самые дешевые модели.
SELECT DISTINCT model, price,
CASE price WHEN (SELECT MAX(price) FROM pc) THEN 'Самый дорогой'
WHEN (SELECT MIN(price) FROM pc) THEN 'Самый дешевый'
ELSE 'Средняя цена' END comment
FROM pc ORDER BY price;
В результате выполнения запроса получим
model |
price |
comment |
---|---|---|
1232 |
350 |
Самый дешевый |
1260 |
350 |
Самый дешевый |
1232 |
400 |
Средняя цена |
1233 |
400 |
Средняя цена |
1233 |
600 |
Средняя цена |
1121 |
850 |
Средняя цена |
1233 |
950 |
Средняя цена |
1233 |
980 |
Самый дорогой |
Стандарт SQL-92 специфицирует только функции, возвращающие системную дату/время. Например, функция CURRENT_TIMESTAMP возвращает сразу и дату, и время. Плюс имеются функции возвращающие что-либо одно.
Естественно, в силу такой ограниченности, реализации языка расширяют стандарт за счет добавления функций, облегчающий работу пользователей с данными этого типа. Здесь мы рассмотрим функции обработки даты/времени в T-SQL.
Синтаксис
DATEADD ( datepart , number, date )
Эта функция возвращает значение типа datetime, которое получается добавлением к дате date количества интервалов типа datepart, равного number. Например, мы можем к заданной дате добавить любое число лет, дней, часов, минут и т.д. Допустимые значения аргумента datepart приведены ниже и взяты из BOL.
Datepart |
Допустимые сокращения |
---|---|
Year - год |
yy, yyyy |
Quarter - квартал |
qq, q |
Month - месяц |
mm, m |
Dayofyear - день года |
dy, y |
Day - день |
dd, d |
Week - неделя |
wk, ww |
Hour - час |
hh |
Minute - минута |
mi, n |
Second - секунда |
ss, s |
Millisecond - миллисекунда |
ms |
Пусть сегодня 23/01/2004, и мы хотим узнать, какой день будет через неделю. Мы можем написать
SELECT DATEADD(day, 7, current_timestamp)
а можем и так
SELECT DATEADD(ww, 1, current_timestamp)
В результате получим одно и то же; что-то типа 2004-01-30 19:40:58.923.
Однако мы не можем в этом случае написать
SELECT DATEADD(mm, 1/4, current_timestamp)
потому, что дробная часть значения аргумента datepart отбрасывается, и мы получим 0 вместо одной четвертой и, как следствие, текущий день.
Кроме того, мы можем использовать вместо CURRENT_TIMESTAMP функцию T-SQL GETDATE() с тем же самым эффектом. Наличие двух идентичных функций поддерживается, видимо, в ожидании последующего развития стандарта.
Пример (схема 4). Определить, какой будет день через неделю после последнего полета.
SELECT DATEADD(day, 7, (SELECT MAX(date) max_date FROM pass_in_trip))
Использование подзапроса в качестве аргумента допустимо, т.к. этот подзапрос возвращает ЕДИНСТВЕННОЕ значение типа datetime.
Синтаксис
DATEDIFF ( datepart , startdate , enddate )
Функция возвращает интервал времени, прошедшего между двумя временными отметками - startdate (начальная отметка) и enddate (конечная отметка). Этот интервал может быть измерен в разных единицах. Возможные варианты определяются аргументом datepart и перечислены выше применительно к функции DATEADD.
Пример (схема 4). Определить количество дней, прошедших между первым и последним совершенными рейсами.
SELECT DATEDIFF(dd, (SELECT MIN(date) FROM pass_in_trip), (SELECT MAX(date) FROM pass_in_trip))
Пример (схема 4). Определить продолжительность рейса 1123 в минутах.
Здесь следует принять во внимание, что время вылета (time_out) и время прилета (time_in) хранится в полях типа datetime таблицы Trip. Заметим, что SQL Server вплоть до версии 2000 не имеет отдельных темпоральных типов данных для даты и времени, появление которых ожидается в следующей версии (Yukon). Поэтому при вставке в поле datetime только времени (например, UPDATE trip SET time_out = '17:24:00' WHERE trip_no=1123), время будет дополнено значением даты по умолчанию ('1900-01-01').
Напрашивающееся решение
SELECT DATEDIFF(mi, time_out, time_in) dur FROM trip WHERE trip_no=1123,
(которое дает -760) будет неверным по двум причинам.
Во-первых, для рейсов, которые вылетают в один день, а прилетают на следующий, вычисленное таким способом значение будет неправильным. Во-вторых, ненадежно делать какие либо предположения относительно дня, который присутствует только в силу необходимости соответствовать типу datetime.
Но как определить, что самолет приземлился на следующий день? Тут помогает описание предметной области, где говорится, что полет не может продолжаться более суток. Итак, если время прилета не больше, чем время вылета, то этот факт имеет место. Теперь второй вопрос: как посчитать только время, с каким бы днем оно ни стояло?
Здесь может помочь функция T-SQL DATEPART.
Синтаксис
DATEPART ( datepart , date )
Эта функция возвращает целое число, представляющее собой указанную аргументом datepart часть заданной вторым аргументом даты (date).
Список допустимых значений аргумента datepart, описанный выше в данном разделе, дополняется еще одним значением
Datepart |
Допустимые сокращения |
---|---|
Weekday - день недели |
dw |
Заметим, что возвращаемое функцией DATEPART значение в этом случае (номер дня недели) зависит от настроек, которые можно изменить с помощью оператора SET DATEFIRST, устанавливающего первый день недели. Для кого-то понедельник - день тяжелый, а для кого-то - воскресенье. Кстати, последнее значение принимается по умолчанию.
Однако вернемся к нашему примеру. В предположении, что время вылета/прилета является кратным минуте, мы можем его определить как сумму часов и минут. Поскольку функции даты/времени работают с целочисленными значениями, приведем результат к наименьшему интервалу - минутам. Итак, время вылета рейса 1123 в минутах
SELECT DATEPART(hh, time_out)*60 + DATEPART(mi, time_out) FROM trip WHERE trip_no=1123
и время прилета
SELECT DATEPART(hh, time_in)*60 + DATEPART(mi, time_in) FROM trip WHERE trip_no=1123
Теперь мы должны сравнить, превышает ли время прилета время вылета. Если это так, вычесть из первого второе, чтобы получить продолжительность рейса. В противном случае к разности нужно добавить одни сутки (24*60 = 1440 минут).
SELECT CASE WHEN time_dep=time_arr THEN time_arr-time_dep+1440 ELSE time_arr-time_dep END dur FROM
( SELECT DATEPART(hh, time_out)*60 + DATEPART(mi, time_out) time_dep, DATEPART(hh, time_in)*60 + DATEPART(mi, time_in) time_arr FROM trip WHERE trip_no=1123) tm
Здесь, чтобы не повторять длинные конструкции в операторе CASE, использован подзапрос. Конечно, результат получился достаточно громоздким, зато абсолютно корректным в свете сделанных к этой задаче замечаний.
Пример (4 схема). Определить дату и время вылета рейса 1123.
В таблице совершенных рейсов Pass_in_trip содержится только дата рейса, но не время, т.к. в соответствии с предметной областью каждый рейс может выполняться только один раз в день. Для решения этой задачи нужно к дате, хранящейся в таблице Pass_in_trip, добавить время из таблицы Trip
SELECT pt.trip_no, DATEADD(mi, DATEPART(hh,time_out)*60 + DATEPART(mi,time_out), date) [time]
FROM pass_in_trip pt JOIN trip t ON pt.trip_no=t.trip_no WHERE t.trip_no=1123
Выполнив запрос, получим следующий результат
Trip_no |
Time |
---|---|
1123 |
2003-04-05 16:20:00.000 |
1123 |
2003-04-08 16:20:00.000 |
DISTINCT необходим здесь, чтобы исключить возможные дубликаты, поскольку номер и дата рейса дублируются в этой таблице для каждого пассажира данного рейса.
Синтаксис
DATENAME ( datepart , date )
Эта функция возвращает символьное представление составляющей (datepart ) указанной даты (date). Аргумент, определяющий составляющую даты, может принимать одно из значений, перечисленных в вышеприведенной таблице.
Это дает нам простую возможность конкатенировать компоненты даты, получая любой нужный формат представления. Например, конструкция
SELECT DATENAME ( weekday , '2003-12-31' )+', '+DATENAME ( day , '2003-12-31' )+' '+ DATENAME ( month , '2003-12-31' )+' '+DATENAME ( year , '2003-12-31' )
даст нам следующий результат
Wednesday, 31 December 2003 |
Следует отметить, что данная функция выявляет отличие значений day и dayofyear аргумента datepart. Первый дает символьное представление дня указанной даты, в то время как второй дает символьное представление этого дня от начала года. Т.е.
SELECT DATENAME ( day , '2003-12-31' )
даст нам 31, а
SELECT DATENAME ( dayofyear , '2003-12-31' )
- 365.
В ряде случаев функцию DATEPART можно заменить более простыми функциями. Вот они:
DAY ( date ) - целочисленное представление дня указанной даты. Эта функция эквивалентна функции DATEPART(dd, date).
MONTH ( date ) - целочисленное представление месяца указанной даты. Эта функция эквивалентна функции DATEPART(mm, date).
YEAR ( date ) - целочисленное представление года указанной даты. Эта функция эквивалентна функции DATEPART(yy, date).
Вот полный перечень функций работы со строками, взятый из BOL:
ASCII |
NCHAR |
SOUNDEX |
---|---|---|
CHAR |
PATINDEX |
SPACE |
CHARINDEX |
REPLACE |
STR |
DIFFERENCE |
QUOTENAME |
STUFF |
LEFT |
REPLICATE |
SUBSTRING |
LEN |
REVERSE |
UNICODE |
LOWER |
RIGHT |
UPPER |
LTRIM |
RTRIM |
|
Начнем с двух взаимно обратных функций - ASCII и CHAR.
Функция ASCII возвращает ASCII-код крайнего левого символа строкового выражения, являющегося аргументом функции.
Вот, например, как можно определить, сколько имеется разных букв, с которых начинаются названия кораблей в таблице Ships:
SELECT COUNT(DISTINCT ASCII(name)) FROM Ships
Результат - 11. Чтобы выяснить, какие это буквы, мы можем применить функцию CHAR, которая возвращает символ по известному ASCII-коду (от 0 до 255):
SELECT DISTINCT CHAR(ASCII(name)) FROM Ships ORDER BY 1
Следует отметить, что аналогичный результат можно получить проще с помощью еще одной функции - LEFT, которая имеет следующий синтаксис:
и вырезает заданное вторым аргументом число символов слева из строки, являющейся первым аргументом. Итак,
SELECT DISTINCT LEFT(name, 1) FROM Ships ORDER BY 1
А вот как, например, можно получить таблицу кодов всех алфавитных символов:
SELECT CHAR(ASCII('a')+ num-1) letter, ASCII('a')+ num - 1 [code]
FROM (SELECT 5*5*(a-1)+5*(b-1) + c AS num
FROM (SELECT 1 a UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) x
CROSS JOIN
(SELECT 1 b UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) y
CROSS JOIN
(SELECT 1 c UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) z
) x
WHERE ASCII('a')+ num -1 BETWEEN ASCII('a') AND ASCII('z')
Тех, кто еще не в курсе генерации числовой последовательности, отсылаю к соответствующей статье .
Как известно, коды строчных и прописных букв отличаются. Поэтому чтобы получить полный набор без переписывания запроса, достаточно просто дописать к вышеприведенному коду аналогичный:
UNION
SELECT CHAR(ASCII('A')+ num-1) letter, ASCII('A')+ num - 1 [code]
FROM (SELECT 5*5*(a-1)+5*(b-1) + c AS num
FROM (SELECT 1 a UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) x
CROSS JOIN
(SELECT 1 b UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) y
CROSS JOIN
(SELECT 1 c UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) z
) x
WHERE ASCII('A')+ num -1 BETWEEN ASCII('A') AND ASCII('Z')
Чтобы таблица выглядела более патриотично, достаточно заменить латинские буквы "a" и "A" на неотличимые на взгляд русские - "а" и "А", а "z" и "Z" на "я" и "Я". Вот только буквы "ё" вы не увидите в этой таблице, т.к. в кодовой таблице ASCII эти символы лежат отдельно, что легко проверить:
SELECT ASCII('ё') UNION ALL SELECT ASCII('Ё')
Я полагаю, что не будет сложным добавить эту букву в таблицу, если потребуется.
Рассмотрим теперь задачу определения нахождения искомой подстроки в строковом выражении. Для этого могут использоваться две функции - CHARINDEX и PATINDEX. Обе они возвращают начальную позицию (позицию первого символа подстроки) подстроки в строке. Функция CHARINDEX имеет синтаксис:
CHARINDEX (искомое_выражение, строковое_выражение[, стартовая_позиция])
Здесь необязательный целочисленный параметр стартовая_позиция определяет позицию в строковом выражении, начиная с которой выполняется поиск искомого_выражения . Если этот параметр опущен, поиск выполняется от начала строкового_выражения. Например, запрос
SELECT name FROM Ships WHERE CHARINDEX('sh', name) 0
будет выводить те корабли, в которых имеется сочетание символов "sh". Здесь используется тот факт, что если искомая строка не будет обнаружена, то функция CHARINDEX возвращает 0. Результат выполнения запроса будет содержать следующие корабли:
name |
---|
Kirishima |
Musashi |
Washington |
Следует отметить, что если искомая подстрока либо строковое выражение есть NULL, то результатом функции тоже будет NULL.
Следующий пример определяет позиции первого и второго вхождения символа "a" в имени корабля "California"
SELECT CHARINDEX('a',name) first_a,
CHARINDEX('a', name, CHARINDEX('a', name)+1) second_a
FROM Ships WHERE name='California'
Обратите внимание, что при определении второго символа в функции используется стартовая позиция, которой является позиция следующего за первой буквой "a" символа - CHARINDEX('a', name)+1. Правильность результата - 2 и 10 - легко проверить :-).
Функция PATINDEX имеет синтаксис:
Главное отличие этой функции от CHARINDEX заключается в том, что поисковая строка может содержать подстановочные знаки - % и _. При этом концевые знаки "%" являются обязательными. Например, использование этой функции в первом примере будет иметь вид
SELECT name FROM Ships WHERE PATINDEX('%sh%', name) 0
А вот, например, как можно найти имена кораблей, которые содержат последовательность из трех символов, первый и последний из которых есть "e":
SELECT name FROM Ships WHERE PATINDEX('%e_e%', name) 0
Результат выполнения этого запроса выглядит следующим образом:
name |
---|
Revenge |
Royal Sovereign |
Парная к LEFT функция RIGHT возвращает заданное число символов справа из строкового выражения:
Вот, например, как можно определить имена кораблей, которые начинаются и заканчиваются на одну и ту же букву:
SELECT name FROM Ships WHERE LEFT(name, 1) = RIGHT(name, 1)
То, что в результате мы получим пустой результирующий набор, означает, что таких кораблей в базе данных нет. Давайте возьмем комбинацию значений - класс и имя корабля.
Соединение двух строковых значений в одно называется конкатенацией, и в SQL Server для этой операции используется знак "+" (в стандарте "||"). Итак,
SELECT * FROM (
SELECT class +' '+ name AS cn FROM Ships
) x
WHERE LEFT(cn, 1)=RIGHT(cn, 1)
Здесь мы разделяем пробелом имя класса и имя корабля. Кроме того, чтобы не повторять всю конструкцию в качестве аргумента функции, используем подзапрос. Результат будет иметь вид:
cn |
---|
Iowa Missouri |
North Carolina Washington |
А если строковое выражение будет содержать лишь одну букву? Запрос выведет ее. В этом легко убедиться, написав
SELECT * FROM (
SELECT class +' '+ name AS cn FROM Ships
UNION ALL
SELECT 'a' as nc
) x
WHERE LEFT(cn, 1)=RIGHT(cn, 1)
, которая возвращает число символов в строке. Ограничимся случаем, когда число символов больше единицы:
SELECT * FROM (
SELECT class +' '+ name AS cn FROM Ships
UNION ALL
SELECT 'a' as nc
) x
WHERE LEFT(cn, 1)=RIGHT(cn, 1) AND LEN(cn)1
Замечание. Реализация этой функции в MS SQL Server имеет одну особенность, а именно, при подсчете длины не учитываются концевые пробелы.
Действительно, выполним следующий код:
DECLARE @chr AS CHAR(12), @vchr AS VARCHAR(12)
SELECT @chr = 'abcde' + REPLICATE(' ', 5), @vchr = 'abcde'+REPLICATE(' ', 5)
SELECT LEN(@chr), LEN(@vchr)
SELECT DATALENGTH(@chr), DATALENGTH(@vchr)
5 |
5 |
12 |
10 |
Функция REPLICATE дополняет константу 'abcde' пятью пробелами справа, которые не учитываются функцией LEN, - в обоих случаях получаем 5.
Функция DATALENGTH возвращает число байтов в представлении переменной и демонстрирует нам различие между типами CHAR и VARCHAR. DATALENGTH даст нам 12 для типа CHAR и 10 - для VARCHAR.
Как и следовало ожидать, DATALENGTH для переменной типа VARCHAR вернула фактическую длину переменной. Но почему для переменной типа CHAR результат оказался равным 12? Дело в том, что CHAR - это тип фиксированной длины. Если значение переменной оказывается меньше ее длины, а длину мы объявили как CHAR(12), то значение переменной будет "выровнено" до требуемой длины за счет добавления концевых пробелов.
На сайте имеются задачи, в которых требуется упорядочить (найти максимум и т.д.) в числовом порядке значения, представленные в текстовом формате. Например, номер места в самолете ("2d") или скорость CD ("24x"). Проблема заключается в том, что текст сортируется так (по возрастанию)
11a |
1a |
2a |
Действительно,
SELECT '1a' AS place
UNION ALL SELECT '2a'
UNION ALL SELECT '11a'
ORDER BY 1
Если же требуется упорядочить места в порядке возрастания рядов, то порядок должен быть такой
1a |
2a |
11a |
Чтобы добиться такого порядка, нужно выполнить сортировку по числовым значениям, присутствующим в тексте. Можно предложить такой алгоритм:
1. Извлечь число из строки.
2. Привести его к числовому формату.
3. Выполнить сортировку по приведенному значению.
Т.к. нам известно, что буква только одна, то для извлечения числа из строки можно воспользоваться следующей конструкцией, которая не зависит от числа цифр в номере места:
LEFT(place, LEN(place)-1)
Если только этим и ограничиться, то получим
place |
---|
1a |
11a |
2a |
Приведение к числовому формату может быть следующим:
CAST (LEFT(place, LEN(place)-1) AS INT)
Осталось выполнить сортировку
SELECT * FROM (
SELECT '1a' AS place
UNION ALL SELECT '2a'
UNION ALL SELECT '11a'
) x
ORDER BY CAST(LEFT(place, LEN(place)-1) AS INT)
Что и требовалось доказать.
Ранее мы для извлечения числа из текстовой строки пользовались функцией LEFT, т.к. нам было известно априори, какое число символов нужно убрать справа (один). А если же нужно извлечь строку из подстроки не по известной позиции символа, а по самому символу? Например: извлечь все символы до первой буквы "х" (значение скорости CD).
В этом случае мы можем использовать также уже рассмотренную ранее функцию CHARINDEX, которая позволит определить неизвестную позицию символа:
SELECT model, LEFT(cd, CHARINDEX('x', cd) -1) FROM PC
SUBSTRING (<выражение, <начальная позиция, <длина )
Эта функция позволяет извлечь из выражения его часть заданной длины, начиная от заданной начальной позиции. Выражение может быть символьной или бинарной строкой, а также иметь тип text или image. Например, если нам потребуется получить 3 символа в названии корабля, начиная со 2-го символа, то сделать без помощи функции SUBSTRING будет не так просто. А так мы пишем:
SELECT name, SUBSTRING(name, 2, 3) FROM Ships
В случае, когда нужно извлечь все символы, начиная с некоторого, мы также можем использовать эту функцию. Например,
SELECT name, SUBSTRING(name, 2, LEN(name)) FROM Ships
даст нам все символы в названиях кораблей от второй буквы в имени. Обратите внимание на то, что для указания числа извлекаемых символов я использовал функцию LEN(name), которая возвращает число символов в имени. Понятно, что поскольку мне нужны символы, начиная со второго, то их число будет меньше общего количества символов в имени. Однако это не вызывает ошибки, поскольку если указанное число символов превышает возможное число, то будут извлечены все символы до конца строки. Поэтому я и беру их с запасом, не утруждая себя вычислениями.
Эта функция переворачивает строку, как бы читая ее справа налево. Т.е. результатом запроса
SELECT REVERSE('abcdef')
будет 'fedcba'. Если бы в языке отсутствовала функция RIGHT, то запрос
SELECT RIGHT('abcdef',3)
можно было бы равносильно заменить запросом
SELECT REVERSE(LEFT(REVERSE('abcdef'),3))
Я вижу пользу этой функции в следующем. Пусть нам требуется определить позицию не первого, а последнего вхождения некоторого символа (или последовательности символов) в строке. Вспомним пример, в котором мы определяли позицию первого символа "а" в названии корабля "California":
SELECT CHARINDEX('a', name) first_a
FROM Ships WHERE name='California'
Определим теперь позицию последнего вхождения в это название символа "а". Функция
CHARINDEX('a', REVERSE(name))
позволит найти эту позицию, но справа. Для получения позиции этого же символа слева достаточно написать
SELECT LEN(name) + 1 - CHARINDEX('a', REVERSE(name)) first_a
FROM Ships WHERE name='California'
REPLACE ( <строка1 , <строка2 , <строка3 )
Заменяет в строке1 все вхождения строки2 на строку3. Эта функция, безусловно, полезна в операторах обновления (UPDATE), если нужно изменить (исправить) содержимое столбца. Пусть, например, нужно заменить все пробелы дефисом в названиях кораблей. Тогда можно написать
UPDATE Ships
SET name = REPLACE(name, ' ', '-')
(Этот пример можно выполнить на странице с упражнениями DML, где разрешаются запросы на изменение данных)
Однако эта функция может найти применение и в более нетривиальных случаях. Давайте определим, сколько раз в названии корабля используется буква "a". Идея проста: заменим каждую искомую букву двумя любыми символами, после чего посчитаем разность длин полученной и искомой строки. Итак,
SELECT name, LEN(REPLACE(name, 'a', 'aa')) - LEN(name) FROM Ships
А если нам нужно определить число вхождений произвольной последовательности символов, скажем, передаваемой в качестве параметра в хранимую процедуру? Использованный выше алгоритм в этом случае следует дополнить делением на число символов в искомой последовательности:
DECLARE @str AS VARCHAR(100)
SET @str='ma'
SELECT name, (LEN(REPLACE(name, @str, @str + @str)) - LEN(name))/LEN(@str) FROM Ships
Для удвоения числа искомых символов здесь применялась конкатенация - @str + @str . Однако для этой цели можно использовать еще одну функцию - REPLICATE, которая повторяет первый аргумент такое число раз, которое задается вторым аргументом.
SELECT name, (LEN(REPLACE(name, @str, REPLICATE(@str, 2))) - LEN(name))/LEN(@str) FROM Ships
Т.е. мы повторяем дважды подстроку, хранящуюся в переменной @str .
Если же нужно заменить в строке не определенную последовательность символов, а заданное число символов, начиная с некоторой позиции, то проще использовать функцию STUFF:
Эта функция заменяет подстроку длиной L, которая начинается со стартовой позиции в строке1, на строку2.
Пример. Изменить имя корабля: оставив в его имени 5 первых символов, дописать "_" (нижнее подчеркивание) и год спуска на воду. Если в имени менее 5 символов, дополнить его пробелами.
Можно решать эту задачу с помощью разных функций. Мы же попытаемся это сделать с помощью функции STUFF. В первом приближении напишем (ограничимся запросом на выборку):
SELECT name, STUFF(name, 6, LEN(name), '_'+launched) FROM Ships
Третьим аргументом (количество символов для замены) я использую LEN(name), т.к. мне нужно заменить все символы до конца строки, поэтому я беру с запасом - исходное число символов в имени. И все же этот запрос вернет ошибку. Причем дело не в третьем аргументе, а в четвертом, где выполняется конкатенация строковой константы и числового столбца. Ошибка приведения типа. Для преобразования числа к его строковому представлению можно воспользоваться еще одной встроенной функцией - STR:
STR ( <число с плавающей точкой [ , <длина [ , <число десятичных знаков ] ] )
При этом преобразовании выполняется округление, а длина задает длину результирующей строки. Например,
STR(3.3456, 5, 1) |
3.3 |
STR(3.3456, 5, 2) |
3.35 |
STR(3.3456, 5, 3) |
3.346 |
STR(3.3456, 5, 4) |
3.346 |
Обратите внимание, что если полученное строковое представление числа меньше заданной длины, то добавляются лидирующие пробелы. Если же результат больше заданной длины, то усекается дробная часть (с округлением); в случае же целого числа получаем соответствующее число звездочек "*":
STR(12345,4,0) |
**** |
Кстати, по умолчанию используется длина в 10 символов. Имея в виду, что год представлен четырьмя цифрами, напишем
SELECT name, STUFF(name, 6, LEN(name), '_'+STR(launched, 4)) FROM Ships
Уже почти все правильно. Осталось учесть случай, когда число символов в имени менее 6, т.к. в этом случае функция STUFF дает NULL. Ну что ж вытерпим до конца мучения, связанные с использованием этой функции в данном примере, попутно применив еще одну строковую функцию. Добавим конечные пробелы, чтобы длина имени была заведомо больше 6. Для этого имеется специальная функция SPACE :
SELECT name, STUFF(name + SPACE(6), 6, LEN(name), '_'+STR(launched,4)) FROM Ships
LTRIM (<строковое выражение)
RTRIM (<строковое выражение)
отсекают соответственно лидирующие и конечные пробелы строкового выражения, которое неявно приводится к типу VARCHAR.
Пусть требуется построить такую строку: имя пассажира_идентификатор пассажира для каждой записи из таблицы Passenger. Если мы напишем
SELECT name + '_' + CAST(id_psg AS VARCHAR) FROM Passenger,
то в результате получим что-то типа:
A _1
Это связано с тем, что столбец name имеет тип CHAR(30). Для этого типа короткая строка дополняется пробелами до заданного размера (у нас 30 символов). Здесь нам как раз и поможет функция RTRIM:
SELECT RTRIM(name) + '_' + CAST(id_psg AS VARCHAR) FROM Passenger
LOWER(<строковое выражение)
преобразуют все символы аргумента соответственно к нижнему и верхнему регистру. Эти функции оказываются полезными при сравнении регистрозависимых строк.
Пара интересных функций SOUNDEX и DIFFERENCE:
SOUNDEX(<строковое выражение)
DIFFERENCE (<строковое выражение_1, <строковое выражение_2)
Позволяют определить близость звучания слов. При этом SOUNDEX возвращает четырехсимвольный код, используемый для сравнения, а DIFFERENCE собственно и оценивает близость звучания двух сравниваемых строковых выражений. Поскольку эти функции не поддерживают кириллицы, отсылаю интересующихся к BOL за примерами их использования.
В заключение приведем функции и несколько примеров использования юникода.
UNICODE (<строковое выражение)
возвращает юникод первого символа строкового выражения.
NCHAR (<целое)
возвращает символ по его юникоду. Несколько примеров.
SELECT ASCII('а'), UNICODE('а')
возвращает код ASCII и юникод русской буквы "а": 224 и 1072.
SELECT CHAR(ASCII('а')), CHAR(UNICODE('а'))
Пытаемся восстановить символ по его коду. Получаем "а" и NULL. NULL-значение возвращается потому, что кода 1072 нет в обычной кодовой таблице.
SELECT CHAR(ASCII('а')), NCHAR(UNICODE('а'))
Теперь все нормально, в обоих случаях "а". Наконец,
SELECT NCHAR(ASCII('а'))
даст "a", т.к. юникод 224 соответствует именно этой букве.
Приведенные здесь примеры можно выполнить непосредственно на сайте, установив флажок "Без проверки" на странице с упражнениями на SELECT.
Язык манипуляции данными (DML - Data Manipulation Language) помимо оператора SELECT, осуществляющего извлечение информации из базы данных, включает операторы, изменяющие состояние данных. Этими операторами являются:
INSERT Добавление записей (строк) в таблицу БД
UPDATE Обновление данных в столбце таблицы БД
DELETE Удаление записей из таблицы БД
Оператор INSERT вставляет новые строки в таблицу. При этом значения столбцов могут представлять собой литеральные константы либо являться результатом выполнения подзапроса. В первом случае для вставки каждой строки используется отдельный оператор INSERT; во втором случае будет вставлено столько строк, сколько возвращается подзапросом.
Синтаксис оператора
INSERT INTO <имя таблицы[(<имя столбца,...)]
{VALUES (< значение столбца,…)}
| <выражение запроса
| {DEFAULT VALUES};
Как видно из представленного синтаксиса, список столбцов не является обязательным. В том случае, если он отсутствует, список вставляемых значений должен быть полный, т.е. обеспечивать значения для всех столбцов таблицы. При этом порядок значений должен соответствовать порядку столбцов, заданному оператором CREATE TABLE для таблицы, в которую вставляются строки. Кроме того, каждое из этих значений должно быть того же типа (или приводиться к нему), что и тип, определенный для соответствующего столбца в операторе CREATE TABLE. В качестве примера рассмотрим вставку строки в таблицу Product, созданную следующим оператором CREATE TABLE:
CREATE TABLE [dbo].[product] (
[maker] [char] (1) NOT NULL ,
[model] [varchar] (4) NOT NULL ,
[type] [varchar] (7) NOT NULL )
Пусть требуется добавить в эту таблицу модель ПК 1157 производителя B. Это можно сделать следующим оператором:
INSERT INTO Product VALUES ('B', 1157, 'PC');
Если задать список столбцов, то можно изменить "естественный" порядок их следования:
INSERT INTO Product (type, model, maker) VALUES ('PC', 1157, 'B');
Казалось бы, это совершенно излишняя возможность, которая делает конструкцию только более громоздкой. Однако она становится выигрышной, если столбцы имеют значения по умолчанию. Рассмотрим следующую структуру таблицы:
CREATE TABLE [product_D] (
[maker] [char] (1) NULL ,
[model] [varchar] (4) NULL ,
[type] [varchar] (7) NOT NULL DEFAULT 'PC' )
Отметим, что здесь значения всех столбцов имеют значения по умолчанию (первые два - NULL, а последний столбец - type - 'PC'). Теперь мы могли бы написать:
INSERT INTO Product_D (model, maker) VALUES (1157, 'B');
В этом случае отсутствующее значение при вставке строки будет заменено значением по умолчанию - 'PC'. Заметим, что если для столбца в операторе CREATE TABLE не указано значение по умолчанию и не указано ограничение NOT NULL, запрещающее использование NULL в данном столбце таблицы, то подразумевается значение по умолчанию NULL.
Возникает вопрос: а можно ли не указывать список столбцов и, тем не менее, воспользоваться значениями по умолчанию? Ответ положительный. Для этого нужно вместо явного указания значения использовать зарезервированное слово DEFAULT:
INSERT INTO Product_D VALUES ('B', 1158, DEFAULT);
Поскольку все столбцы имеют значения по умолчанию, для вставки строки со значениями по умолчанию можно было бы написать:
INSERT INTO Product_D VALUES (DEFAULT, DEFAULT, DEFAULT);
Однако для этого случая предназначена специальная конструкция DEFAULT VALUES (смотри синтаксис оператора), с помощью которой вышеприведенный оператор можно переписать в виде
INSERT INTO Product_D DEFAULT VALUES;
Заметим, что при вставке строки в таблицу проверяются все ограничения, наложенные на данную таблицу. Это могут быть ограничения первичного ключа или уникального индекса, проверочные ограничения типа CHECK, ограничения ссылочной целостности. В случае нарушения какого-либо ограничения вставка строки будет отвергнута.
Рассмотрим теперь случай использования подзапроса. Пусть нам требуется вставить в таблицу Product_D все строки из таблицы Product, относящиеся к моделям персональных компьютеров (type = 'PC'). Поскольку необходимые нам значения уже имеются в некоторой таблице, то формирование вставляемых строк вручную, во-первых, является неэффективным, а, во-вторых, может допускать ошибки ввода. Использование подзапроса решает эти проблемы:
INSERT INTO Product_D SELECT * FROM Product WHERE type = 'PC';
Использование в подзапросе символа "*" является в данном случае оправданным, т.к. порядок следования столбцов является одинаковым для обеих таблиц. Если бы это было не так, следовало бы использовать список столбцов либо в операторе INSERT, либо в подзапросе, либо в обоих местах, который приводил бы в соответствие порядок следования столбцов:
INSERT INTO Product_D(maker, model, type)
SELECT * FROM Product WHERE type = 'PC';
или
INSERT INTO Product_D
SELECT maker, model, type FROM Product WHERE type = 'PC';
или
INSERT INTO Product_D(maker, model, type)
SELECT maker, model, type FROM Product WHERE type = 'PC';
Здесь, также как и ранее, можно указывать не все столбцы, если требуется использовать имеющиеся значения по умолчанию, например:
INSERT INTO Product_D (maker, model)
SELECT maker, model FROM Product WHERE type = 'PC';
В данном случае в столбец type таблицы Product_D будет подставлено значение по умолчанию 'PC' для всех вставляемых строк.
Отметим, что при использовании подзапроса, содержащего предикат, будут вставлены только те строки, для которых значение предиката равно TRUE (не UNKNOWN!). Другими словами, если бы столбец type в таблице Product допускал бы NULL-значение, и это значение присутствовало бы в ряде строк, то эти строки не были бы вставлены в таблицу Product_D.
Преодолеть ограничение на вставку одной строки в операторе INSERT при использовании VALUES позволяет искусственный прием использования подзапроса, формирующего строку с предложением UNION ALL. Так если нам требуется вставить несколько строк при помощи одного оператора INSERT, можно написать:
INSERT INTO Product_D
SELECT 'B' AS maker, 1158 AS model, 'PC' AS type
UNION ALL
SELECT 'C', 2190, 'Laptop'
UNION ALL
SELECT 'D', 3219, 'Printer';
Использование UNION ALL предпочтительней UNION даже, если гарантировано отсутствие строк-дубликатов, т.к. в этом случае не будет выполняться проверка для исключения дубликатов.
Многие коммерческие продукты допускают использование автоинкрементируемых столбцов в таблицах, т.е. полей, значение которых формируется автоматически при добавлении новых записей. Такие столбцы широко используются в качестве первичных ключей таблицы, т.к. они автоматически обеспечивают уникальность. Типичным примером столбца такого типа является последовательный счетчик, который при вставке строки генерирует значение на единицу большее предыдущего значения (значения, полученного при вставке предыдущей строки).
Ниже приводится пример создания таблицы с автоинкрементируемым столбцом (code) в MS SQL Server.
CREATE TABLE [Printer_Inc] (
[code] [int] IDENTITY(1,1) PRIMARY KEY ,
[model] [varchar] (4) NOT NULL ,
[color] [char] (1) NOT NULL ,
[type] [varchar] (6) NOT NULL ,
[price] [float] NOT NULL )
Автоинкрементируемое поле определяется посредством конструкции IDENTITY (1, 1). При этом первый параметр свойства IDENTITY (1) определяет, с какого значения начнется отсчет, а второй - какой шаг будет использоваться для приращения значения. Таким образом, в нашем примере первая вставленная запись будет иметь в столбце code значение 1, вторая - 2 и т.д.
Поскольку в поле code значение формируется автоматически, оператор
INSERT INTO Printer_Inc VALUES (15, 3111, 'y', 'laser', 2599);
приведет к ошибке, даже если в таблице нет строки со значением в поле code, равным 15. Поэтому для вставки строки в таблицу просто не будем указывать это поле точно так же, как и в случае использования значения по умолчанию, т.е.
INSERT INTO Printer_Inc (model, color, type, price)
VALUES (3111, 'y', 'laser', 2599);
В результате выполнения этого оператора в таблицу Printer_Inc будет вставлена информация о модели 3111 цветного лазерного принтера, стоимость которого равна $2599. В поле code окажется значение, которое только случайно может оказаться равным 15. В большинстве случаев этого оказывается достаточно, т.к. значение автоинкрементируемого поля, как правило, не несет никакой информации; главное, чтобы оно было уникальным.
Однако бывают случаи, когда требуется подставить вполне конкретное значение в автоинкрементируемое поле. Например, нужно перенести уже имеющиеся данные во вновь создаваемую структуру; при этом эти данные участвуют в связи "один-ко-многим" со стороны "один". Таким образом, мы не можем допустить тут произвола. С другой стороны, нам не хочется отказываться от автоинкрементируемого поля, т.к. оно упростит обработку данных при последующей эксплуатации базы данных.
Поскольку стандарт языка SQL не предполагает наличия автоинкрементируемых полей, то соответственно не существует и единого подхода. Здесь мы покажем, как это реализуется в MS SQL Server. Оператор
отключает (значение ON) или включает (OFF) использование автоинкремента. Поэтому чтобы вставить строку со значением 15 в поле code, нужно написать
SET IDENTITY_INSERT Printer_Inc ON;
INSERT INTO Printer_Inc(code, model, color, type, price)
VALUES (15, 3111, 'y', 'laser', 2599);
Обратите внимание, что список столбцов в этом случае является обязательным, т.е. мы не можем написать так:
SET IDENTITY_INSERT Printer_Inc ON;
INSERT INTO Printer_Inc
VALUES (15, 3111, 'y', 'laser', 2599);
ни, тем более, так
SET IDENTITY_INSERT Printer_Inc ON;
INSERT INTO Printer_Inc(model, color, type, price)
VALUES (3111, 'y', 'laser', 2599);
В последнем случае в пропущенный столбец code значение не может быть подставлено автоматически, т.к. автоинкрементирование отключено.
Важно отметить, что если значение 15 окажется максимальным в столбце code,то далее нумерация продолжится со значения 16. Естественно, если включить автоинкрементирование: SET IDENTITY_INSERT Printer_Inc OFF.
Наконец, рассмотрим пример вставки данных из таблицы Product в таблицу Product_Inc, сохранив значения в поле code:
SET IDENTITY_INSERT Printer_Inc ON;
INSERT INTO Printer_Inc(code, model,color,type,price)
SELECT * FROM Printer;
По поводу автоинкрементируемых столбцов следует еще сказать следующее. Пусть последнее значение в поле code было равно 16, после чего строка с этим значением была удалена. Какое значение будет в этом столбце после вставки новой строки? Правильно, 17, т.к. последнее значение счетчика сохраняется, несмотря на удаление строки, его содержащей. Поэтому нумерация значений в результате удаления и добавления строк не будет последовательной. Это является еще одной причиной для вставки строки с заданным (пропущенным) значением в автоинкрементируемом столбце.
Оператор UPDATE изменяет имеющиеся данные в таблице. Команда имеет следующий синтаксис
UPDATE
SET {имя столбца = {выражение для вычисления значения столбца
| NULL
| DEFAULT},...}
[ {WHERE }];
С помощью одного оператора могут быть заданы значения для любого количества столбцов. Однако в одном и том же операторе UPDATE можно вносить изменения в каждый столбец указанной таблицы только один раз. При отсутствии предложения WHERE будут обновлены все строки таблицы.
Если столбец допускает NULL-значение, то его можно указать в явном виде. Кроме того, можно заменить имеющееся значение на значение по умолчанию (DEFAULT) для данного столбца.
Ссылка на "выражение" может относиться к текущим значениям в изменяемой таблице. Например, мы можем уменьшить все цены ПК-блокнотов на 10 процентов с помощью следующего оператора:
UPDATE Laptop SET price=price*0.9
Разрешается также значения одних столбцов присваивать другим столбцам. Пусть, например, требуется заменить жесткие диски менее 10 Гб в ПК-блокнотах. При этом емкость новых дисков должна составлять половину объема RAM, имеющейся в данных устройствах. Эту задачу можно решить следующим образом:
UPDATE Laptop SET hd=ram/2 WHERE hd<10
Естественно, типы данных столбцов hd и ram должны быть совместимы. Для приведения типов может использоваться выражение CAST.
Если требуется изменять данные в зависимости от содержимого некоторого столбца, можно воспользоваться выражением CASE. Если, скажем, нужно поставить жесткие диски объемом 20 Гб на ПК-блокноты с памятью менее 128 Мб и 40 гигабайтные - на остальные ПК-блокноты, то можно написать такой запрос:
UPDATE Laptop
SET hd = CASE WHEN ram<128 THEN 20 ELSE 40 END
Для вычисления значений столбцов допускается также использование подзапросов. Например, требуется укомплектовать все ПК-блокноты самыми быстрыми процессорами из имеющихся. Тогда можно написать:
UPDATE Laptop
SET speed = (SELECT MAX(speed) FROM Laptop)
Необходимо сказать несколько слов об автоинкрементируемых столбцах. Если столбец code в таблице Laptop определен как IDENTITY(1,1), то следующий оператор
UPDATE Laptop SET code=5 WHERE code=4
не будет выполнен, т.к. автоикрементируемое поле не допускает обновления, и мы получим соответствующее сообщение об ошибке. Чтобы выполнить все же эту задачу, можно поступить следующим образом. Сначала вставить нужную строку, используя SET IDENTITY_INSERT, после чего удалить старую строку:
SET IDENTITY_INSERT Laptop ON
INSERT INTO Laptop_ID(code, model, speed, ram, hd, price, screen)
SELECT 5, model, speed, ram, hd, price, screen
FROM Laptop_ID WHERE code=4
DELETE FROM Laptop_ID WHERE code=4
Разумеется, другой строки со значением code=5 в таблице быть не должно.
В Transact-SQL оператор UPDATE расширяет стандарт за счет использования необязательного предложения FROM. В этом предложении специфицируется таблица, обеспечивающая критерий для операции обновления. Дополнительную гибкость здесь дает использование операций соединения таблиц.
Пример. Пусть требуется указать "No PC" (нет ПК) в столбце type для тех моделей ПК из таблицы Product, для которых нет соответствующих строк в таблице PC. Решение посредством соединения таблиц можно записать так:
UPDATE Product
SET type='No PC'
FROM Product pr LEFT JOIN PC ON pr.model=pc.model
WHERE type='pc' AND pc.model IS NULL
Здесь используется внешнее соединение, в результате чего столбец pc.model для моделей ПК, отсутствующих в таблице PC, будет содержать NULL-значение, что и используется для идентификации подлежащих обновлению строк. Естественно, эта задача имеет решение и в "стандартном" исполнении:
UPDATE Product
SET type='No PC'
WHERE type='pc' and model NOT IN (SELECT model FROM PC)
Оператор DELETE удаляет строки из временных или постоянных базовых таблиц, представлений или курсоров, причем в двух последних случаях действие оператора распространяется на те базовые таблицы, из которых извлекались данные в эти представления или курсоры. Оператор удаления имеет простой синтаксис:
DELETE FROM [WHERE ];
Если предложение WHERE отсутствует, удаляются все строки из таблицы или представления (представление должно быть обновляемым). Более быстро эту операцию (удаление всех строк из таблицы) в Transact-SQL можно также выполнить с помощью команды
TRUNCATE TABLE
Однако есть ряд отличий в реализации команды TRUNCATE TABLE по сравнению с использованием оператора DELETE, которые следует иметь в виду:
1. Не журнализируется удаление отдельных строк таблицы. В журнал записывается только освобождение страниц, которые были заняты данными таблицы.
2. Не отрабатывают триггеры. Как следствие, эта команда неприменима, если на данную таблицу имеется ссылка по внешнему ключу.
3. Значение счетчика (IDENTITY) сбрасывается в начальное значение.
Пример. Требуется удалить из таблицы Laptop все ПК-блокноты с размером экрана менее 12 дюймов.
DELETE FROM Laptop
WHERE screen<12
Все блокноты можно удалить с помощью оператора
DELETE FROM Laptop
или
TRUNCATE TABLE Laptop
Transact-SQL расширяет синтаксис оператора DELETE, вводя дополнительное предложение FROM
FROM
При помощи источника табличного типа можно конкретизировать данные, удаляемые из таблицы в первом предложении FROM.
При помощи этого предложения можно выполнять соединения таблиц, что логически заменяет использование подзапросов в предложении WHERE для идентификации удаляемых строк.
Поясним сказанное на примере. Пусть требуется удалить те модели ПК из таблицы Product, для которых нет соответствующих строк в таблице PC.
Используя стандартный синтаксис, эту задачу можно решить следующим запросом:
DELETE FROM Product
WHERE type='pc' AND model NOT IN (SELECT model FROM PC)
Заметим, что предикат type='pc' необходим здесь, чтобы не были удалены также модели принтеров и ПК-блокнотов.
Эту же задачу можно решить с помощью дополнительного предложения FROM следующим образом:
DELETE FROM Product
FROM Product pr LEFT JOIN PC ON pr.model=pc.model
WHERE type='pc' AND pc.model IS NULL
Здесь используется внешнее соединение, в результате чего столбец pc.model для моделей ПК, отсутствующих в таблице PC, будет содержать NULL-значение, что и используется для идентификации подлежащих удалению строк.
Моисеенко С.И. (22-08-2008)
Такие вопросы с завидной регулярностью появляются на страницах различных форумов. К слову сказать, для меня до сих пор остается загадкой, почему при этом ставится дополнительное условие не использовать UNION и/или JOIN. Могу лишь предположить, что это вопросы, которые задают на собеседовании при приеме на работу.
Лирическое отступление. Догадываюсь, как ответил бы на этот вопрос Джо Селко: налицо ошибка проектирования, состоящая в том, что один атрибут расщеплен на два. Однако оставим в стороне вопросы проектирования и перейдем к решению этой задачи.
Создадим тестовую таблицу и добавим в нее немного данных:
CREATE TABLE T (
col1 INT
, col2 INT
)
GO
INSERT INTO T
SELECT 1, 1
UNION ALL SELECT 1, 3
UNION ALL SELECT NULL, NULL
UNION ALL SELECT NULL, 2
GO
Итак, имеется таблица T, которая содержит два столбца с данными одного типа:
SELECT col1, col2
FROM T
col1 col2
1 1
1 3
NULL NULL
NULL 2
Требуется получить следующий результат:
col
1
1
NULL
NULL
1
3
NULL 2
Мне известны три способа, реализуемых стандартными средствами интерактивного языка SQL.
Очевидное решение, не требующее комментариев. Заметим лишь, что UNION не подходит для решения этой задачи, т.к. устраняет дубликаты.
SELECT col1 col FROM T
UNION ALL
SELECT col2 FROM T
Чтобы не потерять дубликаты, находящиеся в разных столбцах, выполним полное соединение (FULL JOIN) по заведомо ложному предикату, скажем, 1 = 2:
SELECT T.col1,T1.col2
FROM T FULL JOIN T AS T1 ON 1=2
Результат:
col1 col2
1 NULL
1 NULL
NULL NULL
NULL NULL
NULL 1
NULL 3
NULL NULL
NULL 2
Далее используем функцию COALESCE, которая даст нам все, что нужно:
SELECT COALESCE(T.col1,T1.col2) col
FROM T FULL JOIN T AS T1 ON 1=2
Конструкции PIVOT и UNPIVOT появились в последних версиях стандарта SQL и были реализованы SQL Server, начиная с версии 2005. Первая из них позволяет значения в столбце вытянуть в строку, а вторая поможет нам выполнить обратную операцию:
SELECT col
FROM
(SELECT col1, col2
FROM T) p
UNPIVOT
(col FOR xxx IN
(col1, col2)
)AS unpvt
Значения из столбцов col1 и col2 собираются в одном столбце col вспомогательной таблицы unpvt. Однако есть одна особенность в использовании операторов PIVOT и UNPIVOT - они не учитывают NULL-значения. Результат последнего запроса будет таким:
col
1
1
1
3
2
Это препятствие на пути к решению нашей задачи можно преодолеть, если заменить NULL-значение на входе оператора UNPIVOT псевдозначением, т.е. значением, которого заведомо не может быть в исходных данных, а потом выполнить обратное преобразование:
SELECT NULLIF(col,777)
FROM
(SELECT COALESCE(col1,777) col1, COALESCE(col2,777) col2
FROM T) p
UNPIVOT
(col FOR xxx IN
(col1, col2)
)AS unpvt
Здесь COALESCE(colx,777) заменяет NULL-значения в столбце colx на 777, а функция NULLIF(col,777) выполняет обратное преобразование.
Последнее решение дает нам требуемый результат, однако содержит один изъян - значение 777 может рано или поздно появиться в данных, что будет приводить к неверным результатам. Чтобы устранить этот огрех, можно использовать значение другого типа, которого заведомо не может присутствовать в целочисленном столбце, например, символ 'x'. Естественно, чтобы применить этот подход, для совместимости типов целочисленный тип столбцов следует конвертировать к символьному типу, выполнив при необходимости обратное преобразование конечного результата:
SELECT CAST(NULLIF(col,'x') AS INT)
FROM
(SELECT COALESCE(CAST(col1 AS VARCHAR),'x') col1,
COALESCE(CAST(col2 AS VARCHAR),'x') col2
FROM T) p
UNPIVOT
(col FOR xxx IN
(col1, col2)
)AS unpvt
Несколько слов об эффективности представленных решений. Согласно плану выполнения запроса, основные затраты обусловлены чтением данных (операция сканирования таблицы - Table scan). Для двух первых решений сканирование выполняется дважды, в то время как для последнего (UNPIVOT) - один раз, чем и обусловлено его двойное преимущество в производительности.
DROP TABLE T
Комментарии
Н.Петров (aka sql chuvak) 28-08-2008
Есть еще один вариант, который я использую:
SELECT
CASE a WHEN 1 THEN col1 ELSE col2 END col
FROM T, (SELECT 1 a UNION ALL SELECT 2) B
Декартово произведение таблицы T с выборкой из 2-х строк дает "удвоение" (каждая строка таблицы повторяется 2 раза - для а=1 и а=2). Для первого случая берем значение из col1, а для второго - из col2.
Тут, конечно, есть и union, и join, но, по-моему, в данном вопросе интересует именно единственное сканирование таблицы.