четверг, 30 августа 2012 г.

iTextSharp. Как же с ним работать?

Недавно перед мной встала задача - генерировать на лету pdf документы. При использовании ключевых слов C# и PDF поисковые системы, как правило подсказывают, что нужно использовать библиотеку iTextSharp. У меня нет оснований не верить им, поэтому так и поступим.

После первого знакомства с возможностями библиотеки и примерами из сети объявились 4 неприятные новости.

  • iTextSharp это порт библиотеки iText, и никакие комментарии не приведены в соответствующий вид. Поэтому действовать приходится наощупь или заглядывать в код методов.
  • Раньше можно было генерировать файл из xml шаблона, который должен был соответствовать itext.dtd. Больше эта возможность не поддерживается.
  • В интернете много примеров кода с вызовом ITextHandler, в новых версиях такого класса в принципе нет. Поэтому половина сообщений на форумах по этой теме уже бесполезна.
  • Видимо сайт разработчика был переделан, поэтому многие ссылки, которые обещали нам решение приводят нас на главную страницу.
Так как же работать с этой библиотекой? Об этом, а так же о самых часто встречаемых трудностях и их решениях можно узнать под катом.

понедельник, 27 августа 2012 г.

Место есть?

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

select db_name(sa.dbid) as DBname 
 , sa.name as LogicalName
 , case sa.groupid
  WHEN 0 then 'LOG'
  ELSE sfg.groupname
   end as Filegroup
 , sa.filename as Filename
 , cast(sf.size*8/1024. as numeric(19,3))as sizeMB
 , cast(sf.spaceused*8/1024. as numeric(19,3)) as spaceusedMB
 , cast((sf.size-sf.spaceused)*8/1024. as numeric(19,3)) as freespaceMB
 , case sf.maxsize
  when -1 then 'Unlimited'
  else cast(cast(sf.maxsize*8/1024.  as numeric(19,3))as varchar(22))
   end as maxsizeMB
 , cast(sf.growth*8/1024. as numeric(19,3)) as nextgrowthMB
from master..sysaltfiles sa 
left join (
 select   cast(size as bigint) as size
   ,fileid
   ,groupid
   ,cast(fileproperty(name,'SpaceUsed')as bigint) as spaceused 
   ,cast(maxsize as bigint) as maxsize
   ,cast(case 
    when status & 0x100000 = 0 then growth
    else size*growth/100
    end as bigint) as growth
 from sysfiles
 ) sf on sf.fileid=sa.fileid 
  and sf.groupid = sa.groupid
left outer join sysfilegroups sfg on sfg.groupid = sf.groupid
where sa.dbid = db_id()
order by case when sa.groupid = 0 then 1 else 0 end, sa.groupid, sa.fileid

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

четверг, 9 августа 2012 г.

Поиск неиспользуемых индексов

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

SELECT   
         OBJECT_SCHEMA_NAME(i.object_id)AS [Schema Name],
  OBJECT_NAME(i.object_id) AS [Table Name],
         i.name AS [Not Used Index Name],
         s.last_user_update AS [Last Update Time],
         s.user_updates AS [Updates]
FROM     sys.dm_db_index_usage_stats AS s
JOIN     sys.indexes AS i
ON       i.object_id = s.object_id
AND      i.index_id = s.index_id
JOIN     sys.objects AS o
ON       o.object_id = s.object_id
WHERE    s.database_id = DB_ID()
AND      (    user_scans   = 0
          AND user_seeks   = 0
          AND user_lookups = 0
          AND last_user_scan   IS NULL
          AND last_user_seek   IS NULL
          AND last_user_lookup IS NULL 
         )
AND      OBJECTPROPERTY(i.[object_id],         'IsSystemTable'   ) = 0
AND      INDEXPROPERTY (i.[object_id], i.name, 'IsAutoStatistics') = 0
AND      INDEXPROPERTY (i.[object_id], i.name, 'IsHypothetical'  ) = 0
AND      INDEXPROPERTY (i.[object_id], i.name, 'IsStatistics'    ) = 0
AND      INDEXPROPERTY (i.[object_id], i.name, 'IsFulltextKey'   ) = 0
AND      (i.index_id between 2 AND 250 OR (i.index_id=1 AND OBJECTPROPERTY(i.[object_id],'IsView')=1))
AND      o.type != 'IT'

--and OBJECT_SCHEMA_NAME(i.object_id) = 'Price'
ORDER BY 1,2,3

P.S. Прежде чем ринуться удалять индексы, ответьте себе на вопрос о том, как давно вы "чистили" статистику или перезагружали сервер? А все ли операции, запускаемые на вашем инстансе, были запущены после этого?

вторник, 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]