вторник, 24 июля 2012 г.

Построение всех возможных комбинаций с помощью T-SQL

Забава или рутина?

Построение всех возможных комбинаций(удовлетворяющих набору условий) это задача высосана из пальца или рутина? Давайте представим, что вы работаете в компании, которая предоставляет какие-то услуги своим клиентам, как правило компании продает не одну услугу, а целый набор. Например, покупая летом путевку в жаркую страну, вы наверняка приобретете следующее

  • Авиабилет в страну отдых, то есть билет эконом-класса на %airlines%.
  • Трансфер до отеля, то есть поездка на большом автобусе с еще 50 туристами.
  • Проживание в отеле, то есть 14 ночей в %hotel% в номере с видом на сад.
  • Трансфер до аэропорта, то есть еще одна поездка в автобусе.
  • Авиабилет домой, возвращение обратным рейсом той же %airlines%.
Турбизнес, как и любой другой, готов удовлетворить любую потребность туриста, за небольшую доплату. То есть заменить эконом на бизнес, групповой трансфер на индивидуальный, а отель на другой - поближе к морю. Таким образом перед нами встает задача - показать клиенту наш прайс лист. То есть получить все возможные комбинации услуг и показать цену на каждый набор услуг.

Опишем эту же задачу с помощью кода.

--Классы услуг
CREATE TABLE [Classes] (
  [id] [int] NOT NULL PRIMARY KEY
 ,[name] varchar(50) NOT NULL
);
GO
INSERT INTO [Classes] VALUES (1,'Авиаперелет');
INSERT INTO [Classes] VALUES (2,'Трансфер');
INSERT INTO [Classes] VALUES (3,'Проживание');

--Список туров
CREATE TABLE [Tours](
  [id] [int] NOT NULL PRIMARY KEY
 ,[name] varchar(50) NOT NULL
);
GO
INSERT INTO [Tours] VALUES (1, 'Тур');

--Тур состоит набора услуг
--В данном случае из тех же 5, что приведены в примере
CREATE  TABLE [TourDetails] (
  [id] [int] NOT NULL PRIMARY KEY
 ,[tour] [int] NOT NULL
 ,[class] [int] NOT NULL
);
GO
INSERT INTO [TourDetails] VALUES (1,1,1)
INSERT INTO [TourDetails] VALUES (2,1,2)
INSERT INTO [TourDetails] VALUES (3,1,3)
INSERT INTO [TourDetails] VALUES (4,1,2)
INSERT INTO [TourDetails] VALUES (5,1,1)

-- Справочник услуг.
--Рассмотрим простую систему, где все услуги хранятся в одной табличке
CREATE TABLE [Services](
  [id] [int] NOT NULL PRIMARY KEY
 ,[name] varchar(50)
);
GO
INSERT INTO [Services] VALUES (1, 'Эконом класс');
INSERT INTO [Services] VALUES (2, 'Бизнес класс');

INSERT INTO [Services] VALUES (3, 'Групповой трансфер');
INSERT INTO [Services] VALUES (4, 'Номер с видом на бассейн');

--И последнее
--Привязка услуги к классу услуг.
CREATE TABLE [ServiceClasses](
  [id] [int] NOT NULL PRIMARY KEY
 ,[class] [int] NOT NULL
 ,[service] [int] NOT NULL
);
GO
INSERT INTO [ServiceClasses] VALUES (1, 1, 1);
INSERT INTO [ServiceClasses] VALUES (2, 1, 2);
INSERT INTO [ServiceClasses] VALUES (3, 2, 3);
INSERT INTO [ServiceClasses] VALUES (4, 3, 4);

А получить мы хотим все перестановки для данного тура(туров), то есть

id тура     id комбинации id деталей тура id услуги
----------- ------------- --------------- -----------
1           1             1               1
1           1             2               3
1           1             3               4
1           1             4               3
1           1             5               1
-----------------------------------------------------
1           2             1               2
1           2             2               3
1           2             3               4
1           2             4               3
1           2             5               1
-----------------------------------------------------
1           3             1               1
1           3             2               3
1           3             3               4
1           3             4               3
1           3             5               2
-----------------------------------------------------
1           4             1               2
1           4             2               3
1           4             3               4
1           4             4               3
1           4             5               2

Посмотрим на результаты чуть внимательней. В первую колонку попадает id Тура([Tours].[id]), так как он у нас всего один, то значение всегда равно 1. Во второй колонке id комбинации, для каждой группы данных/набора оно свое. В третей колонке хранятся идентификаторы деталей тура, то есть [TourDetails].[id]. И в последней идентификаторы услуг. Таким образом, в первой комбинации оба перелета эконом класса, во второй и третей по одному перелету эконом класса, а в последней и туда и обратно турист полетит бизнес классом.

А теперь самое интересное, как сделать это с помощью T-SQL? Ответ и немного кода можно найти под катом.

суббота, 21 июля 2012 г.

Сравнение временных таблиц и таблиц переменных.

Существует много путаницы о том, что такое таблицы переменные(Table Variables) и чем они не являются. А так же как они соотносятся с временными таблицами(Temporary Tables). Я хочу внести свою лепту в объяснения различий между двумя этими типами таблиц. И в развенчивании нескольких мифах об этих объектах.

вторник, 17 июля 2012 г.

SQL Azure vs Amazon RDS SQL Server

Ниже приведен перевод статьи Is it time to move from SQL Azure to Amazon RDS SQL Server?

Мне известно, что Microsoft собирается выбросить слово Azure из названия технологии SQL Azure и переименовать ее в SQl Database, но в этой статье я буду использовать старое название.

В середине 2011 года мы выпустили новую систему для нашего клиента. Эта система требовала пару баз данных, web сервер и некоторый хостинг служб Windows. Нагрузка связанная с обработкой запросов была не очень существенная, так что мы легко смогли разместиться на небольшом выделенном сервере.

Наш клиент не имел собственной службы тех. поддержки, да и мы сами не хотели обслуживать их инфраструктуру. Бюджеты были жесткие, но некоторые простои могли допускаться при условии, что вмешательство человека не требовалось. Так как мы уже использовали Amazon SQS (Simple Queue Service), мы сразу же подумали об использовании Amazon EC2 с экземпляром SQL Express, виртуальную машину можно было бы перезагрузить с помощью AWS CloudFormation, если бы она зависла. Очень быстро стало понятно, что писать сценарии для того, чтобы вернуть БД в согласованное состояние будет задачей не простой и потребует немало времени, но сделать это надо было в самый сжатые сроки. Суммы за выделенные сервер доходили до нескольких тысяч долларов в год плюс плата за соединение, и подписать соглашение мы должны были на период от 1 до 3 лет. Все это заставило нас посмотреть в сторону Windows Azure и SQL Azure. SQL Azure предлагал нам вариант при котором поддержка была не нужна, по значительно более низкой цене, чем выделенный сервер. Кроме того SQL Azure не привязывает клиента к себе, то есть мы в любой момент могли отказаться от этой услуги. А так же данные копируются в реплику, то есть мы получили избыточную защищенность, которую не ожидали за те деньги, которые был готов платить клиент.

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

Соревнование, как правило, это хорошо, поэтому я был очень рад услышать, что Amazon расширила свою RDS (Relational Database Service) что бы включить SQL Server в дополнение к MySQL и Oracle. Мы уже использовали Amazon RDS для работы с MySQL, так что я очень хотел посмотреть, что RDS cможет предложить с SQL Server. Естественно, я хотел понять, поступаем ли мы правильно, все еще используя SQL Azure.

понедельник, 16 июля 2012 г.

Цены и дни недели.

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

CREATE TABLE [Costs](
  [id] int NOT NULL PRIMARY KEY
 ,[cost] money NOT NULL
 ,[begin] [smalldatetime] NOT NULL
 ,[end]  [smalldatetime] NOT NULL
);

GO

insert into [Costs] ([id],[cost],[begin],[end])
select 1, 500, convert(smalldatetime,'01.06.2012',104), convert(smalldatetime,'31.08.2012',104)

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

Теперь два раза в неделю цена на услугу будет изменять. Если мы будем действовать по старой схеме, вместо 1 записи указывающей цену на 3 месяца, придется заводить 2 записи на каждую неделю, которых в летние месяцы более 13(итого более 26 записей). Что довольно неприятно.

Во многих системах работающих в туристическом сегменте является практически стандартом хранения данных в следующем виде.

CREATE TABLE [Costs](
  [id] int NOT NULL PRIMARY KEY
 ,[cost] money NOT NULL
 ,[begin] [smalldatetime] NOT NULL
 ,[end]  [smalldatetime] NOT NULL
 ,[week] char(7) NOT NULL
);

GO

insert into [Costs] ([id],[cost],[begin],[end],[week])
select 1, 500, convert(smalldatetime,'01.06.2012',104), convert(smalldatetime,'31.08.2012',104), '....567'

union all 
select 2, 400, convert(smalldatetime,'01.06.2012',104), convert(smalldatetime,'31.08.2012',104), '1234...'

Поле week хранит дни недели, в которые действует эта цена. Каждый день недели от понедельника до воскресенья пронумерован от единицы до семи соответственно. Для i от 1 до 7, если на i-ой позиции стоит i, то цена действует в этот день недели, если ".", то нет.

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

суббота, 14 июля 2012 г.

CSV с помощью T-SQL

Знакомы следующие вопросы
  • Как вывести значения в строку через запятую?
  • Как получить CSV (Comma Separated Values) из значений столбца?
Сформулируем проблему с помощью кода. Как имея следующие
CREATE TABLE [Data](
  [objectId] [int] NOT NULL
 ,[value] varchar(10) NOT NULL

);
GO

insert into [Data] ([objectId], [value])
          select 1, 'A'
union all select 1, 'B'
union all select 1, 'C'

union all select 2, '11'
union all select 2, '22'
union all select 2, '33'

union all select 3, 'Tom'
union all select 3, 'and'
union all select 3, 'Jerry'
получить такой результат?
--objectId    csv
------------- --------------
--1           A,B,C
--2           11,22,33
--3           Tom,and,Jerry
Порой мне кажется, что это одна из самых простых и часто встречаемых задач, которая заставляет разработчик лезть в google. Под катом решение этой задачи.

пятница, 13 июля 2012 г.

Произведение

Вот и конец недели, а значит сейчас самое время для небольшого пятничного поста! Вставал ли перед вами один из этих вопросов
  • Как найти произведение значений в столбце?
  • Агрегирующая функция произведение?
  • Произведение без использование CLR?
Перефразируем вопрос с помощью кода. Как имея такие данные
CREATE TABLE [Data](
 [objectId] [int] NOT NULL
   ,[value] [int] NOT NULL
);
GO

insert into [Data] ([objectId],[value])
          select 1, 2
union all select 1, 2
union all select 1, 2
union all select 1, 2
union all select 1, 2

union all select 2, 7
union all select 2, 2
union all select 2, 3
union all select 2, 2

union all select 3, 1
union all select 3, 3
union all select 3, 5
 
Получить следующий результат.
objectId    multiplication
----------- --------------
1           32            --2*2*2*2*2
2           84            --7*2*3*2
3           15            --1*3*5
Если проблема вам знакома, то прошу под кат

четверг, 12 июля 2012 г.

Поиск подряд идущих дат (Островов)

Задача которая в том или ином виде встает перед многими. Как набор дат разбить на группы подряд идущих дней? Нередко для обозначения таких групп используется слово острова (Islands).
Итак, имеем таблицу с исходными данными.
CREATE TABLE [Data] (
[Date] [datetime] NOT NULL
);
GO
insert into [Data] ([Date])
      select convert(datetime,'01.04.2012',104)
union select convert(datetime,'02.04.2012',104)
union select convert(datetime,'31.03.2012',104)

union select convert(datetime,'09.05.2012',104)
union select convert(datetime,'10.05.2012',104)

union select convert(datetime,'31.12.2012',104)
union select convert(datetime,'01.01.2013',104)
И хотим в итоге получить следующие результат

среда, 11 июля 2012 г.

Унарный минус

Вопрос

Начнем с небольшого вопроса. Что вернет такой запрос?
select count(distinct [i]) [count]
from (
        select -30.0 / 5.0 / 3.0 / 2.0 / 1.0 [i]
  union select  30.0 /-5.0 / 3.0 / 2.0 / 1.0
  union select  30.0 / 5.0 /-3.0 / 2.0 / 1.0
  union select  30.0 / 5.0 / 3.0 /-2.0 / 1.0
  union select  30.0 / 5.0 / 3.0 / 2.0 /-1.0
) [Data]