Когда-то давно, на университетском курсе по “Базам данных”, на котором нам немного рассказывали про FoxPro (я же говорил, что давно :)), преподаватель рассказывал про способы получения данных и их обработки. Как сейчас помню, он сказал фразу “а еще есть мощный язык для обработки данных, он называется SQL” и, после эффектной паузы, добавил – “там есть четыре инструкции – SELECT, INSERT, UPDATE и DELETE, но рассказать что-то еще у меня не хватит времени”.
Честно говоря, только через пару лет, на первой работе, я понял две вещи:
- На этом курсе я практически ничего полезного так и не узнал.
- Программирование БД может быть не менее увлекательным, чем программирование на высокоуровневых языках.
Как правило, начиная разговор о DML, в первую очередь рассказывают про SELECT… Но когда я делал как все? :) Если серьезно, мне кажется более логичным создать сначала структуру таблиц, потом наполнить ее данными, после этого что-нибудь обновить/удалить, а уже потом получать данные различными способами. В реальных приложениях обычно так и происходит, не так ли? Есть, правда, одна оговорка – довольно часто используется конструкция “INSERT … SELECT…”, но об этом позже.
Поскольку ранее я говорил о важности повторно запускаемых скриптов, то и сейчас буду приводить примеры DML, который можно будет запускать повторно. Для этого, потребуется использовать конструкции EXISTS(SELECT…) и LEFT JOIN, на случай, если они кому-то незнакомы, объясню их позднее.
Импорт данных
Речь в этой статье пойдет, прежде всего, про оператор INSERT, который позволяет добавлять либо одну строку к таблице, либо несколько строк (результат запроса). Однако перед этим имеет смысл упомянуть альтернативные варианты, которые обычно используются для импорта данных:- BULK INSERT – вставка записей из внешнего файла (с разделителями, например - CSV).
- BCP – аналог BULK INSERT в виде утилиты командной строки.
- SqlBulkCopy – класс Microsoft .NET Framework, позволяющий импортировать данные (по тому же протоколу, что и BULK INSERT) не из файла, а, в частности, из IDataReader.
- Table-Valued Parameters – начиная с MS SQL Server 2008 появилась возможность использовать передачу таблиц в качестве параметров. Работает достаточно быстро, некоторые рекомендации по использованию есть по ссылке выше.
- Есть еще несколько вариантов для импорта данных (XQuery, OPENXML, строки с разделителями), но на этих решениях я останавливаться не буду, потому что они имеют слабое отношение к SQL.
Добавление одной строки
А теперь, собственно об INSERT. Базовый синтаксис довольно прост, так выглядит вставка строк по одной (для создания соответствующей таблицы можно использовать скрипт из предыдущей статьи):IF NOT EXISTS(SELECT * FROM dbo.Persons WHERE LastName = 'Иванов') BEGIN INSERT dbo.Persons(FirstName, MiddleName, LastName) VALUES('Иван', 'Иванович', 'Иванов') INSERT dbo.Persons(FirstName, MiddleName, LastName) VALUES('Петр', 'Петрович', 'Иванов') END
В простейших случаях список столбцов можно не указывать, но это, как показывает практика, не стоит делать по ряду причин:
- Увеличивается вероятность ошибок. Во-первых, потому что порядок следования столбцов может измениться. Во-вторых, потому что можно банально забыть порядок и поставить, скажем, в нашем случае на первое место значение для фамилии, которое запишется в имя.
- Чаще всего, в таблице рано или поздно появляются новые столбцы, которые не будут учитываться в написанном ранее INSERT (получим ошибку, что количество столбцов не совпадает с количеством значений). Помимо этого, часто есть столбцы, которые должны заполняться не в этом INSERT.
- Код без списка столбцов менее понятен тому, кто еще не очень близко знаком со структурой подопытной таблицы.
Добавление нескольких строк
Добавить несколько строк можно с помощью использования “SELECT” вместо “VALUES”. Поскольку запросы на чтение я оставил на потом, здесь рассмотрим только простейшие варианты.Давайте добавим каждому человеку в нашей БД по одному тестовому телефонному номеру:
INSERT dbo.Phones (PersonID, Phone) SELECT p.PersonID, '123-'+CAST(p.PersonID AS VARCHAR(10)) FROM dbo.Persons p LEFT JOIN dbo.Phones f ON f.PersonID = p.PersonID AND f.Phone = '123-'+CAST(p.PersonID AS VARCHAR(10)) WHERE f.PersonID IS NULL
Делать проверки на существование можно по-разному, навскидку, перечислю еще 3 способа, помимо LEFT JOIN:
- IF NOT EXISTS – сначала проверяем, если записей нет – добавляем. Минус в том, что этот метод вида “все или ничего”, тогда как остальные позволяют вставить только часть новых записей, когда некоторые уже есть в таблице.
- WHERE NOT EXISTS – похож на предыдущий способ, но проверка идет в WHERE от INSERT, поэтому нет вышеупомянутого минуса.
- WHERE Идентификатор NOT IN – почти аналогичен предыдущему, но не очень приспособлен к ситуации, когда первичный ключ составной.
И, конечно, вы уже наверняка догадались, что “CAST” в Transact-SQL является аналогом приведения типа в других языках программирования.
Добавление тестовых данных
Вставкой тестовых данных мы занялись еще в прошлом разделе, теперь хочу остановиться на этом подробнее.Довольно часто требуется добавить некоторый набор данных, который будет использоваться для тестирования (сейчас не буду останавливаться на том, почему, чаще всего, ручного ввода для этого недостаточно). Разумеется, есть достаточно инструментов для генерации тестовых данных. Однако вы ведь лучше знаете, какими должны быть тестовые данные, чем внешние инструменты? Поэтому, вот небольшой набор идей, которого обычно хватает для генерации тестовых данных:
- Можно активно использовать числовые первичные ключи для заполнения тестовых полей (например, для названий вида Name1,.. NameN, или, как в нашем случае, для генерации разных телефонных номеров).
- Там, где нужны случайные значения для многострочных запросов нельзя использовать функцию RAND – она вернет одно и то же значение. Если корректность “случайности” не очень важна, можно использовать код на базе функции NEWID (генерирует GUID).
- Если хочется сделать различные по длине списки для разных родительских записей, удобно пользоваться теми же первичными ключами, но, например, с остатком от деления на некоторое число или с другими арифметическими операциями. Например, можно присоединить таблицу натуральных чисел (см. далее) по условию, что число меньше или равно остатку от деления на 5 плюс 1.
- Весьма полезная вещь (и не только для тестовых данных) – таблица натуральных чисел (например, от 1 до 100 или от 1 до 1000 – когда нужно будет больше чисел, с ней можно соединиться дважды). С ее помощью очень просто получать некоторые списки значений, что вы увидите на примере далее.
IF OBJECT_ID('dbo._Numbers') IS NULL CREATE TABLE dbo._Numbers (Number INT NOT NULL PRIMARY KEY) GO SET NOCOUNT ON IF NOT EXISTS (SELECT * FROM _Numbers) BEGIN DECLARE @i INT SET @i = 0 WHILE @i <= 1000 BEGIN INSERT _Numbers VALUES(@i) SET @i = @i + 1 END END
А теперь, добавим избыточных телефонных номеров (немного модифицировав пример из предыдущего раздела):
INSERT dbo.Phones (PersonID, Phone) SELECT p.PersonID, CAST(n.Number AS VARCHAR(3))+'-'+CAST(p.PersonID AS VARCHAR(10)) FROM dbo.Persons p JOIN dbo._Numbers n ON n.Number BETWEEN 100 AND 100 + 50 * p.PersonID LEFT JOIN dbo.Phones f ON f.PersonID = p.PersonID AND f.Phone = CAST(n.Number AS VARCHAR(3)) + '-' + CAST(p.PersonID AS VARCHAR(10)) WHERE f.PersonID IS NULL
Добавление результата хранимой процедуры
Поскольку такой вариант вставки данных немного выходит за рамки базовых функций, я не буду останавливаться на нем подробно.Когда хранимая процедура возвращает результаты запроса, можно вставить эти результаты в таблицу (разумеется, при условии совпадения количества и совместимости типов полей). Синтаксис выглядит как “INSERT Название_таблицы EXEC Название_процедуры Параметры”. Вот простой пример, без особой смысловой нагрузки:
IF OBJECT_ID('dbo.TestSelect') IS NOT NULL DROP PROC dbo.TestSelect GO CREATE PROC dbo.TestSelect AS SELECT FirstName, MiddleName, LastName FROM dbo.Persons GO DECLARE @p TABLE(FirstName VARCHAR(100), MiddleName VARCHAR(100), LastName VARCHAR(100)) INSERT @p EXEC dbo.TestSelect SELECT * FROM @p
За кадром
В этой статье не рассматривались такие расширения оператора INSERT как WITH, OUTPUT и TOP (присутствуют в сравнительно свежих версиях MS SQL Server). За подробностями можно обратиться к статье про INSERT в MSDN.Также за кадром осталась такая важная тема, как блокировки. Но это относится уже не к основам SQL, а к более глубокому его изучению. Надеюсь, после этой серии смогу перейти к следующей, в которой и расскажу как про блокировки, так и про “взаимоблокировки” (deadlocks).
Анонс
В следующих сериях:- Продолжение разговора о DML - UPDATE, DELETE, SELECT
- Рефакторинг с учетом данных
- Подробнее о запросах
- Управляем доступом и транзакциями
>Добавление тестовых данных
ОтветитьУдалитьИМХО, лучше уж без курсоров. Есть же немало изощрений как это можно сделать с SELECT. Например, вот http://www.sql-tutorial.ru/ru/book_number_sequence_generation.html
Не нужно будет заводить таблицу для чисел, можно прямо поджойнить.
Ну, я думаю, ты Олег про это знаешь, просто нехорошо, что не написал это в статье ;)
> ИМХО, лучше уж без курсоров
ОтветитьУдалитьДэн, на всякий случай поискал слова "курсор" и "CURSOR" в тексте статьи - нашел только в твоем комментарии - объясни плиз, что ты имел в виду :)
P.S. Способ даже не упоминал, потому что один раз завести таблицу не проблема, а в указанном примере скорее "криво" поджойнить :) Тем кто думает, что *один раз заполненная* таблица _Numbers проигрывает в производительности cross join, рекомендую сравнить планы выполнения аналогичных запросов ;)
ОтветитьУдалитьОлег, ссори, у тебя там не курсор, а просто цикл :)
ОтветитьУдалить1. Спасибо!
ОтветитьУдалить2. По теме впечатлила эта статья: http://experience.openquality.ru/elegant-coding-habits-2/
3. Было бы удобнее читать код с включенным подсвечиванием синтаксиса.
To @aeea:
ОтветитьУдалить1. Всегда пожалуйста :)
2. У меня сейчас немного другая направленность - хочу рассказать *базовые* вещи и дать понимание того, что есть *рефакторинг БД*. Оригинал статьи читал - много правильных вещей, однако со многим не согласен. *Возможно* объяснятся тем, что автор не работал с большими объемами данных и с эволюционирующей структурой БД.
Тема, в принципе, интересная, если будет время, попробую и про нее рассказать.
3. А что не в порядке - в каком браузере? Вроде пробовал в разных - подсвечивает (пользуюсь SyntaxHighlighter).
- а здесь наверняка очепечатка :-)
ОтветитьУдалить_DLL_ и нескольких вариантах рефакторинга, с ним связанных.
Fixed. И еще раз спасибо :)
ОтветитьУдалитьСпасибо, доходчиво.
ОтветитьУдалить