Metrika

21 декабря 2011 г.

C#: Конкатенация строк, StringBuilder vs +

В Сети много пишут о работе со строками в .Net. Что касательно конкатенации, я давно уже знал, что при небольших конкатенациях (несколько строк) лучше использовать +, а при конкатенации в циклах лучше использовать StringBuilder. Но ни когда не подозревал на сколько лучше тот или иной метод.
При написании прототипа нового проекта вообще не обратил на это внимания и для вывода массива объектов в JSON написал вот так:
string s_json = "[";

foreach (Building b in arr) {
     s_json += b.ToJSON() + ",";
}

На маленьких массивах проблем с производительностью не наблюдалось, но при тестах на более менее вменяемых данных (около 15000 записей) этот код на ноуте выполнялся 2 мин!!! Epic Fail!!

Тот же код с использованием StringBuilder на тех же данных выполняется секунду!! Прирост производительност более чем в сто раз на не очень больших объемах данных:

StringBuilder sb_json = new StringBuilder("[");

foreach (Building b in arr) {
            sb_json.AppendFormat("{0},", b.ToJSON());
}
Интересные ссылки:
1. Работа со строками. Строковые функции
2.  String concatenation the fast way...maybe not

28 ноября 2011 г.

Сегментирование данных в SSAS

Статья о сегментировании (кластеризации) данных с помощью SQL Server Analytsys Services на примере сегментирования  пользователей он-лайн шутера PointBlank.

Что такое сегментирование (кластеризация). 
Сегментирование - это группировка данных по некоторым признакам. Например есть пользователи, они играют часто или редко, платят мало или много. Т.е. есть два измерения, по которым бьются пользователи, в каждом измерении два значения, т.е. возможно сделать 4 сегмента. В принципе при маленьком количестве измерений специальных средств для построения сегментов можно и не использовать, определить сегменты можно с помощью OLAP-кубов или с помощью SQL-запросов к базе. Но, если измерений много, то разбить данные на сегменты становиться довольно сложно и необходимо применять специальные алгоритмы сегментирования.

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

Этапы большого пути.
Для начала надо создать таблицу с данными, на основе которых будут строиться сегменты. В принципе, если данных не много, можно использовать вьюху. В таблице должен быть primary key, по которому можно идентифицировать пользователя и поля, которые содержат необходимые параметры пользователей.
Для примера я создам сегменты на основе таблицы с небольшим количеством параметров. На самом деле выбор правильных параметров для анализа само по себе является не простой задачей, и нужно быть готовым, что таблица с исходными данными в процессе построения сегментов может часто менять свою структуру и часто пересчитываться. Например с одной стороны можно решить, что параметр "количество убийств" хорошо характеризует успешность игрока, но на самом деле один игрок может набить 100 убийств за 3 матча, а другой за 10, более подходящий аргумент - количество убийств за матч и т.д.
Создаю таблицу:
create table _dm_test(
    user_id int not null primary key,
    perc_win_per_match int null,            -- Процент побед к матчам
    perc_kill_per_death int null,            -- Процент фрагов к смертям
    kill_per_match int null,                -- Фраги за матч
    perc_headshot_per_kill int null,        -- Процент хедшотов к фрагам
    connecttime_min int null,                -- Продолжительность сессий
    match_per_play_day int null,            -- Количество матчей за игровой день
    days_of_play int not null,                -- Количество игровых дней за месяц
    days_created int null,                    -- Возраст персонажа в днях
    rank_current int not null,                -- Текущее звание игрока
    rank_range varchar(100) null            -- Диапазоны званий
)
Агрегирую в нее данные за последние 7 игровых дней за 30 календарных дней.

Затем в BI Studio создаю проект Analysis Services Project. В проекте создаю DataSource и DataView, который содержит таблицу _dm_test.
После этого в проекте тыкаю правой кнопкой на Mining Structures и выбираю New Mining Structure:
  




Таблица _dm_test выбирается как Case для модели. Nested - используется в более сложных случаях анализа, например для учета покупок пользователей, более подробно можно почитать в книге
Далее проставляем для колонок, как они будут использоваться в модели:

Далее для колонок проставляем Content Type - это то как данные из колонки будут распределяться в модели по кластерам, например Continuous будут показаны в виде распределения вокруг среднего значения (имеет смысл, когда данные в колонке изменяются в широком диапазоне величин и довольно равномерно), Discrete - дискретизация будет взята из таблицы (имеет смысл, когда необходимо разбить данные самому, без участия алгоритма разбиения на сегменты), Discretized - данные будут разбиты на диапазоны алгоритмом построения сегментов. В данном примере Rank Range я предопределил в таблице, поэтому ставлю ему Discrete, остальным данным, кроме User Id, ставлю Discretized:

 Выбираем процент данных, которые будут использоваться для проверки построенной модели:

 Последняя страница создания модели - ввод имен структуры и модели:

 Окно редактирования Mining Structure будет иметь такой вид:

Окно редактирования модели:
 Здесь можно более подробно выставить параметры для обработки модели. Например на изображении сверху я для аргумента Connecttime Min выставляю количество диапазонов (DiscretizationBucketCount =10), на которые его будет разбивать модель (по умолчанию 5). Так же можно поменять Content Type.
Так же для полей можно выбрать то, как они будут участвовать в построении сегментов. Все поля со значением Input будут участвовать в алгоритме, Ignore - не будут участвовать вообще, Predict - используется в алгоритмах предсказания для того, что бы использовать аргумент и на входе алгоритма и на выходе, в построении кластеров его роль не ясна, PredictOnly - для алгоритма кластеризации данные будут просто распределены по кластерам, но в самом разбиении участвовать не будут. В моем случае, я не хочу чтобы поле Rank Current участвовало в разбиении, а поле Rank Range должно быть просто распределено по кластерам:

В этом же окне можно уточнить параметры алгоритма, для этого надо выделить заголовок модели и в свойствах перейти в AlgorithmParameters:
 У меня параметров модели не много, поэтому ограничу количество кластеров (сегментов) пятью - значение CLUSTER_COUNT = 5 (по умолчанию 10):

Далее процессим модель, тойже кнопкой, что и процессим кубы:

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

Так же тут видны связи между сегментами (черные линии). Слева бегунком можно отфильтровать более сильные или слабые связи. На подсветку кластеров влияют выпадающие списки Shading Variable и State. На изображении сверху видно, что 32% пользователей попали в Кластер 1 (проценты отображаются в контроле Density), а на изображении снизу, видно, что большинство тех, у кого процент побед за матч больше 70 попали в Кластер 3:

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

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



После всех настроек кластеры стали выглядеть вот так:

Что дальше?
Просто разбить данные на кластеры не достаточно, их надо как то использовать. Как минимум надо понять, кто в какой кластер попал. Для этого нужно создать SSIS пакет, который пропустит данные из исходной таблицы через модель и выдаст их в результирующую таблицу.
Создаю в BI Studio проект Integration Services Project, перетаскиваю в Control Flow элемент Data Mining Query Task:

Двойным кликом перехожу в его Editor:
 На вкладке Mining model нажимаю кнопку New, что бы создать новое подключение к SSAS. В списке провайдеров надо выбирать Microsoft OLE DB Provider for Analysis Services, провайдер ... for Data Mining Services у меня не заработал:

В выпадающем списке Initial catalog выбираю модель:
В итоге модель появляется в списке Mining models:
Далее переходим во вкладку Query, где пишеться DMX-запрос к модели или строиться через мастер (кнопка Build New Query):
Тут нужно определить Mining Model, через которую пропускаются данные; Input Table, которая содержит данные, в моем случае я использую ту же таблицу; и внизу формы нужно выбрать поля и/или Prediction Functions, которые будут выведены в результирующую таблицу, мне нужно выбрать кластер и ID пользователя.
После этого во вкладке Query появиться DMX-запрос:
Далее переходим во вкладку Output и выбираем там результирующую таблицу:

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

Полезные ссылки
2. Так же советую книгу "Программируем коллективный разум" Тоби Сегаран - просто великолепная книга про глубокий анализ данных;
3. Статья о датамайнинге в он-лайн игре Айон:  http://habrahabr.ru/blogs/data_mining/134163/

10 октября 2011 г.

SSRS: Как объединить данные из двух датасетов в одну таблицу

Если есть два и более датасета и их результаты надо объединить в одну таблицу, то можно воспользоваться функцией Lookup. Для этого нужно, что бы в датасетах были общие поля, по которым можно объединить результаты запросов.

В моем случае было два датасета (ds_regs и ds_sales), обращение шло к двум разным OLAP-кубам. В каждом было поле даты. В одном кубе это была дата регистрации пользователей, во втором - дата платежей.

Далее необходимо для таблицы выбрать в качестве источника данных один из датасетов, я выбрал ds_regs.  Поля из этого датасета будут добавляться в таблицу как обычно. Для добавления полей из второго датасета нужно добавить пустые ячейки и в Expression добавить функцию Lookup:
=Lookup(Fields!Day.Value, Fields!Day.Value, Fields!Sales.Value, "ds_sales")
Аргументы следующие: 1 - Ключевое поле из первого датасета (из ds_regs); 2 - ключевое поле из второго датасета (из ds_sales); 3 - поле из второго датасета, которое надо отобразить; 4 - название второго датасета.

Ключевые атрибуты можно варьировать, например, у меня из одно куба день выводился просто числом, а из второго куба с добавлением слова "Day" перед самим числом, поэтому функцию Lookup пришлось записать вот так:
=Lookup("Day " + Fields!Day.Value, Fields!Day.Value, Fields!Sales.Value, "ds_sales")

30 сентября 2011 г.

MDX: Запрос для вывода статистики с нарастающим итогом

Допустим есть куб "Регистрации", в котором лежат реги на неком сервисе по дням. Т.е. в кубе есть мера [Measures].[Accounts] и измерение [DimRegDate] с иерархией [DimRegDate].[Year-Month-Day]. Необходимо вывести за некий промежуток дат общее количество рег на этот день и количество новых рег вэтот день.
Для этого нужно создать два мембера - один с общим количеством рег за все время и второй с суммой рег за дни, которые идут после текущего дня. Потом из первого вычесть второй.
Запрос будет выглядеть вот так:

WITH MEMBER [Measures].[TotalAccounts_ALL] AS SUM([Measures].[Accounts], [DimRegDate].[Year-Month-Day].[All])
MEMBER [Measures].[TotalAccounts_After] AS SUM([DimRegDate].[Year-Month-Day].nextmember:null, [Measures].[Accounts])
MEMBER [Measures].[TotalAccounts] AS [Measures].[TotalAccounts_ALL] - [Measures].[TotalAccounts_After]

SELECT { [Measures].[TotalAccounts], [Measures].[Accounts]}  ON 0,
{ [DimRegDate].[Year-Month-Day].[CreatedYear].&[2011].&[9].&[19]:[DimRegDate].[Year-Month-Day].[CreatedYear].&[2011].&[9].&[25] } ON 1
FROM [Регистрации]

29 сентября 2011 г.

ASP.NET, логин через Facebook и ошибка e.root is undefined

Сегодня начал делать логин через Facebook на ASP.NET. Ну в общем тут и не важна серверная технология, т.к. все можно сделать на стороне клиента. Описание, как все делать, есть тут: http://developers.facebook.com/docs/guides/web/#login
Вроде бы все просто. Добавляем скрипт:
<script src="http://connect.facebook.net/en_US/all.js"></script>
Добавляем инициализацию:
       <script>
         FB.init({
            appId:'YOUR_APP_ID', cookie:true,
            status:true, xfbml:true
         });
      </script>
Добавляем специальный тег, который потом чудесным образом превратиться в красивую фейсбуковскую кнопку:
<fb:login-button>Login with Facebook</fb:login-button>
Запускаем и .. нифига не работает. Кнопка не появляется, а в яваскрипте возникает ошибка, какой то там root is undefined.
Погуглив, нашел решение, оказывается надо в код страницы еще добавить вот такой тег:
<div id="fb-root"></div>
Во всех примерах он есть, но вот в тексте он не упоминается и я его не заметил.
После его добавления все работает.
Собственно что делать дальше, читайте фейсбуковскую доку.


15 августа 2011 г.

T-SQL: Поиск по джобам

Для поиска джоба можно применять следующий скрипт:

select j.job_id, j.name, s.step_name, j.description
from msdb.dbo.sysjobs j
inner join msdb.dbo.sysjobsteps s on j.job_id = s.job_id
where s.command like '%some_text%'
s.command - команда, которую выполняет шаг джоба;
j.name - название джоба;
s.step_name - название шага джоба;
j.description - описание джоба.

nolock при обращении к талице на линкованом сервере

Если обратиться к таблице на линкованом SQL-сервере с директивой nolock:
SELECT * FROM [192.168.0.1].Users.dbo.users (nolock)
то будет выдаваться ошибка:
Msg 4122, Level 16, State 1, Line 1
Remote table-valued function calls are not allowed.
Решается вот так:
SELECT * FROM [192.168.0.1].Users.dbo.users WITH (nolock)
Ответ нашел тут.

11 августа 2011 г.

SSRS: Как рассылать отчеты по подписке НЕ по расписанию

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

Все относиться к SQL Server 2008 R2 Enterprise Edition, т.к. от версии к версии база SSRS может меняться. Я делал подписку управляемую данными и не уверен, что все нижеследующее подойдет к обычной подписке, не проверял.

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

Так вот, при создании расписания рассылки отчета, SSRS создает на сервере, где лежит его база, джоб. Называться он будет как-нибудь вот так: 2D45C6DB-559E-43CB-A019-F8F1484E17FF и он будет запускаться по расписанию, которое было указано в SSRS.

В джобе вызывается процедура ReportServer.dbo.AddEvent, которая кладет ID подписки в таблицу ReportServer.dbo.Event, из этой таблицы SSRS понимает, что пора отчет отсылать, после отсылки запись из таблицы ReportServer.dbo.Event удаляется. 

Исходя из вышесказанного, задача сводиться к тому, что надо добавить запись с ID подписки в таблицу ReportServer.dbo.Event в тот момент, когда пора отсылать отчет.

Шаг 1 - узнаем ID отчета (все селекты идут в БД ReportServer):
select ItemID from catalog where name = @name
Шаг 2 - узнаем ID подписки:

select SubscriptionID from Subscriptions where report_OID =@ItemID
 Шаг 3 - записываем данные в таблицу Event:
exec ReportServer.dbo.AddEvent @EventType='TimedSubscription', @EventData=@SubscriptionID
В моем случае, в джобе, после шага, который процессит куб, я добавил шаг, который выполняет:
exec ReportServer.dbo.AddEvent @EventType='TimedSubscription', @EventData='fa502959-73cf-59a1-b018-8ddcdae2e01'

Надо иметь в виду, что если отчет удалить, а потом выложить заново, все ID поменяются . Если отчет выложить,  перезаписав его, то все ID останутся прежними.

И еще есть важный момент - надо сделать так, что бы расписание самого отчета не срабатывало, для чего в интерфейсе SSRS на странице создания расписания надо поставить Однократно.

T-SQL: Как проверить выполнение джоба

Бывают случаи, когда необходимо сделать так, что в джобе некоторые шаги при ошибочном выполнении не приводят к завершению джоба с ошибкой, а переходят на другой шаг джоба. У меня таким образом работает куб, который процессит OLAP-кубы. Что бы не следить каждый день за выполнением джоба, мы настроили SMS-рассылку. Для этого потребовалось разработать механизм проверки работы джоба. Т.е. задача - найти шаги джоба, которые завершились ошибкой, но при этом джоб продолжил работу.
Для начала нужно получить ID джоба:
select job_id from msdb..sysjobs where name = @job_name
Лог работы джобов лежит в таблице msdb..sysjobhistory. Нужно вытащить записи, которые относятся к последнему выполнению джоба. У меня джоб выполняется каждый день, поэтому просто беру сегодняшнее число. Но в таблице msdb..sysjobhistory дата начала выполнения шага храниться не в datetime, а в двух полях типа int: run_date и run_time, т.е. дата 2011-08-11 13:54:33 будет лежать в виде run_date = 20110811 и run_time = 135433.
Наверное с такими датами можно было бы работать и в интовых значениях, но мне как то привычнее в datatime. Для преобразования такого формата в datetime я написал две функции:
 -- Конвертация времени из формата HHMMSS типа int в секунды
create function [dbo].[fn_convert_intdt_to_sec] ( @ts int )
    returns int
as
begin
    if (@ts < 60) return @ts
     
    declare @sec int = @ts - (@ts/100)*100
    declare @min int = (@ts - (@ts/10000)*10000)/100
    declare @hr int = @ts/10000
     
    return @hr*3600 + @min*60 + @sec
end

-- Конвертация времени из формата даты в джобе в нормальный DateTime
create function [dbo].[fn_convert_job_date_to_datetime]( @date int, @time int )
returns DateTime
as
begin
    return dateadd(second, dbo.fn_convert_intdt_to_sec(@time), convert(datetime, cast(@date as varchar), 112))
end
Далее выводим список шагов:
select instance_id, step_id, step_name, run_status, dbo.fn_convert_job_date_to_datetime(run_date, run_time) run_dt
from msdb..sysjobhistory
where job_id = @job_id
and step_id <> 0 and dbo.fn_convert_job_date_to_datetime(run_date, run_time) >= @dt
order by step_id
step_id = 0 - это строка для всего джоба, поэтомы ее исключаем.

Тут (http://msdn.microsoft.com/en-us/library/ms174997.aspx) лежит описание таблицы  msdb..sysjobhistory.Статусы у шага могут быть такие:
0 = Failed
1 = Succeeded
2 = Retry
3 = Canceled
 В проге, которая дергает данные, я проверяю, что у всех шагов статус = 1 , если нет, то шлю СМС с ошибкой.

20 июля 2011 г.

C#: System.Diagnostics.Process - не срабатывает событие Exited

Для того, что бы срабатывало событие Exited экземпляра класса System.Diagnostics.Process нужно установить свойство EnableRaisingEvents в true. Т.е.:

process.EnableRaisingEvents = true;
process.Exited += new EventHandler(process_Exited);

Распространяется свойство только на событие Exited, т.е. если его не установить в true, то другие события, например OutputDataReceived, будут срабатывать. Описание тут: http://msdn.microsoft.com/en-us/library/system.diagnostics.process.enableraisingevents.aspx

C#: как запустить приложение из кода

Для того, что бы запустить приложение из кода, существует класс System.Diagnostics.Process.
Я запускал консольное приложение через WEB-страницу. То, что консольное приложение выводит в стандартный вывод надо было собирать в лог-файл. Весь код страницы приводить не буду, приведу только ту часть, которая запускает приложение и пишет лог:

private string path_log = "C:\\parser\\"; // Папка, где лежат логи парсера
private string path_ex = "C:\\parser\\log\\"; // Папка, где лежит экзешник парсера
private string file_name = "log.txt"; // Лог-файл
private System.IO.StreamWriter sw_log = null;
private System.Diagnostics.Process parser = null;

private void StartParser()
{
        parser = new Process();
        parser.StartInfo.FileName = path_ex + "parser.exe";
        parser.StartInfo.UseShellExecute = false;
        // Указываем, что будем хватать стандартный вывод
        parser.StartInfo.RedirectStandardOutput = true;
        // А так же кодировку, в которой данные выходят в стандартный вывод
        parser.StartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8;
        parser.StartInfo.CreateNoWindow = true;
        parser.StartInfo.WorkingDirectory = path_ex;
        // ВАЖНОЕ СВОЙСТВО!! Без него не будет срабатывать событие parser.Exited
        parser.EnableRaisingEvents = true;
        // Событие, которое срабатывает, когда в стандартный вывод поступает инфа
        parser.OutputDataReceived += new DataReceivedEventHandler(parser_OutputDataReceived);
        parser.Exited += new EventHandler(parser_Exited);
        // ПУСК!
        parser.Start();

        sw_log = new System.IO.StreamWriter(path_log + "\\" + file_name + ".txt");
        // Говорим, что пора ловить данные из стандартного вывода
        parser.BeginOutputReadLine();
        // Возвращаю ID процесса      
        log_content.InnerHtml = string.Format("Parser process ID: {0}", parser.Id);
}

// Пишем стандартный вывод процесса в лог-файл. 
// Я сделал, что бы файл после каждой строчки флашился, мне просто так надо было, не принципиально
private void parser_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
        if (!String.IsNullOrEmpty(e.Data))
        {
            sw_log.WriteLine(e.Data);
            sw_log.Flush();
        }
}

// Закрыаем все и освобождаем
private void parser_Exited(object sender, EventArgs e)
{
        parser.Close();
        parser.Dispose();

        sw_log.Close();
        sw_log.Dispose();
}

В общем все. Из возможно полезного в классе Process есть метод WaitForExit() - который будет ждать, когда запускаемый процесс завершит работу. Так же как стандартный вывод можно ловить инфу из вывода ошибки, для этого есть событие ErrorDataReceived и метод BeginErrorReadLine. Так же там много всякой отладочной инфы по процессу. Подробности тут: http://msdn.microsoft.com/en-us/library/system.diagnostics.process.aspx.

7 июля 2011 г.

ASP.NET: Как файл PrecompiledApp.config влияет на Global.asax

Использую ASP.NET 2.0. При развертывании веб-приложения на сервере (IIS) удалил файл PrecompiledApp.config, т.к. посчитал его не важным. После чего долго удивлялся, почему в файле Global.asax.cs не выполняется метод Application_Start

Здесь обсуждается эта проблема: http://forums.asp.net/t/1694548.aspx/1/10

После того, как положил на сервер файл PrecompiledApp.config все заработало. Не знаю баг это или фича. Вообще, судя по описаниям, файл используется для отслеживания надо ли какие то файлы компилировать при очередном деплое сайта.

6 июля 2011 г.

Ошибка MySQL "data too long" при вставке

При вставке данных в таблицу в поле с типом  blob произошла ошибка "data too long". У меня стоит MySQL 5.5.13 на Windows Server 2008 R2 Standard, винда английская. Погуглив, нашел массу сообщений об этой ошибке и несколько вариантов решения. Мне помогло вот это: http://forums.mysql.com/read.php?103,51906,103853#msg-103853 Надо в my.ini из строки
sql-mode=STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
убрать STRICT_TRANS_TABLES и перезапустить мускул.

23 июня 2011 г.

C#: как получить символ по коду UTF-8

Мне нужно было из строки удалить символ, который имел код UTF-8 8232. Думал получить по коду переменную типа string и с помощью метода Replace заменить на пустую строку.
Что бы получить строку по коду, можно воспользоваться методом:
System.Text.Encoding.UTF8.GetString
Метод принимает массив byte[], соответственно код 8232 нельзя туда подсунуть, т.к. он больше максимального байта. Я подумал, может, если я разобью этот код на два байта, то все получиться, т.е. сделал так:
string str = System.Text.Encoding.UTF8.GetString(new byte[] { 0x20, 0x28 }, 0, 2);
Но в итоге, в переменной str оказывалось значение " (". Совсем не то, что нужно.
Погуглив нашел решение, надо взять не код в таблице символов, а строку, в которую кодируется этот символ в URL. В моем случае - это строка %E2%80%A8 и выполнить следующий метод:
string str = Uri.UnescapeDataString("%E2%80%A8");
Тогда в str окажется необходимый символ. И далее можно с ним работать строковыми методами.

8 июня 2011 г.

T-SQL: Функция ROW_NUMBER

Недавно открыл для себя очень удобную в некоторых случаях функцию ROW_NUMBER. Появилась она в SQL Server 2005.
Ее можно использовать в селектах для нумирации строк, можно с помощью нее селектить только строки с такого-то по такой-то номер, а так же можно выдавать номер строки в некоторй группе строк. Примеры можно посмотреть в описании.
Мне больше всего нравиться последний случай ее применения. Т.к. раньше такого рода задачи приходилось решать через курсоры, что вело к большим блокам кода и тормозам при исполнении.
Т.е. допустим есть таблица:
CREATE TABLE user_items
(
    id int not null primary key,
    user_id int not null,
    item_id int not null,
    added_at datetime not null
)
И хочется, что бы для каждого пользователя в таблице осталось не более 5 записей, которые были добавлены последними. С использованием ROW_NUMBER это можно сдедать так:

delete u from user_items u
inner join
(
    select ROW_NUMBER() OVER (partition by user_id order by added_at desc) rn, user_id, item_id, id
    from user_items
) a on u.id = a.id
where a.rn > 5
 Т.е. во вложенном запросе выбираются все записи и проставляются номера записей для каждого пользователя в обратном порядке добавления. Соответственно записи, у которых номер больше 5, надо удалить.

2 июня 2011 г.

Автоматическое партицирование OLAP-кубов в SSAS 2008


В статье Партицирование OLAP-кубов в SSAS 2008 я рассказывал зачем и как партицировать кубы в SSAS. Бывают случаи, когда партицировать надо часто и руками это делать скучно. Поэтому хочется этот процесс автоматизировать.
В моем случае мне нужно партицировать в трех кубах в общей сложности 10 групп мер. Если этого не делать каждую неделю, то время процессинга за две недели возрастает с 3,5 часов до 5 с лишним.
В статье Взаимодействие с OLAP кубами (SSAS 2008) на C# я рассказывал, как работать с кубами из кода. Сначала я начал решать задачу автоматического партицирования через разработку проги, но что то сильно углубился в программирование и мне эта идея перестала нравиться, поэтому я выбрал другой путь - создание пакета SQL Server Integration Services (SSIS). Здесь я и опишу этот способ.
Идея следующая - группы мер партицируются по годам плюс есть текущая партиция, которая процесситься каждый день. Т.е., например, для продаж есть партиции за 2010 год, 2011 и current. Раз в неделю для каждой группы мер необходимо изменять партицию за текущий год (2011) и партицию current, так что бы текущая партиция содержала данные только за несколько дней. Потом процессить партицию за текущий год, что бы не было пробелов в данных. После нового года надо будет руками создавать новую годовую партицию и менять процедуру изменения партиций.
Начальное партицирование куба надо провести руками, автоматически все с нуля не создается.
Для примера возьмем, что партиции группы мер продаж формируются следующими SQL-запросами:
2010: SELECT id, sale_date, user_id, money_sum, product_id FROM sales WHERE sale_date >= '2010-01-01' AND sale_date < '2011-01-01'
2011: SELECT id, sale_date, user_id, money_sum, product_id FROM sales WHERE sale_date >= '2011-01-01' AND sale_date < '2011-05-31'
current: SELECT id, sale_date, user_id, money_sum, product_id FROM sales WHERE sale_date >= '2010-05-31'
Эти запросы и надо будет менять в партициях за 2011 год и в текущей партиции.
Для начала необходимо сгенерить скрипты XMLA для изменения партиций. Я буду делать все на примере только одной группы мер. Пакет SSIS будет сделан так, что количество не будет иметь значения.
Cоздаем папку: C:\XMLAScripts. Далее, для генерации скрипта в Management Studio открываем список партиций нужной группы мер, на текущей партиции тыкаем правой кнопкой, а далее как на картинке:

В последнем меню выбираем File... и сохраняем в созданной папке под названием sales_current.xmla. Далее тоже самое повторяем для партиции текущего года и сохраняем в другой файл sales_2011.xmla.
Далее открываем файлы по-очереди и видим что-то похожее на это:
<Alter ObjectExpansion="ExpandFull" xmlns="http://schemas.microsoft.com/analysisservices/2003/engine">
    <Object>
        <DatabaseID>cubeDbSales</DatabaseID>
        <CubeID>cubeSales</CubeID>
        <MeasureGroupID>Vw Sales 1</MeasureGroupID>
        <PartitionID>Vw Sales 1</PartitionID>
    </Object>
    <ObjectDefinition>
        <Partition ...>
            <ID>Vw Sales 1</ID>
            <Name>Vw Sales</Name>
            <Source xsi:type="QueryBinding">
                <DataSourceID>dbOLAP</DataSourceID>
                <QueryDefinition>SELECT id, sale_date, user_id, money_sum, product_id FROM sales WHERE sale_date &gt;= '2010-05-31'
</QueryDefinition>
            </Source>
            <StorageMode>Molap</StorageMode>
            <ProcessingMode>Regular</ProcessingMode>
            <ProactiveCaching>
                <SilenceInterval>-PT1S</SilenceInterval>
                <Latency>-PT1S</Latency>
                <SilenceOverrideInterval>-PT1S</SilenceOverrideInterval>
                <ForceRebuildInterval>-PT1S</ForceRebuildInterval>
                <Source xsi:type="ProactiveCachingInheritedBinding" />
            </ProactiveCaching>
        </Partition>
    </ObjectDefinition>
</Alter>

В каждом файле заменяем дату, которая разделяет партицию текущего года и текущую партицию, в данном примере это 2011-05-31, на какую-нибудь последовательность символов. Я поменял на "###dt###", т.е. запрос в скрипте будет выглядеть вот так:
 <QueryDefinition>SELECT id, sale_date, user_id, money_sum, product_id FROM sales WHERE sale_date &gt;= '###dt###'</QueryDefinition>
Далее эта последовательность символов будет меняться на нужную дату.

В BIDS создаем Integration Services Project. Из Toolbox'а перетаскиваем Foreach Loop Container, а в него Script Task и Analysis Services Execute DDL Task. В итоге должно получиться вот так:
Далее создаем переменные:
Розовые уголки в иконках - это у меня плагин стоит, который я описываю в статье Как поменять область видимости переменной в BIDS (SSIS).
Для переменных dt и dt_str устанавливаем свойство EvaluateAsExpression = True. Для dt свойство Expression: GETDATE(). Для dt_str свойство Expression:
(DT_WSTR, 4)(YEAR(@[User::dt]))+"-"+
(LEN((DT_WSTR, 2)(MONTH(@[User::dt])))==1 ? "0"+(DT_WSTR, 2)(MONTH(@[User::dt])) : (DT_WSTR, 2)(MONTH(@[User::dt])))+"-"+
(LEN((DT_WSTR, 2)(DAY(@[User::dt])))==1 ? "0"+(DT_WSTR, 2)(DAY(@[User::dt])) : (DT_WSTR, 2)(DAY(@[User::dt])))
Такое выражение формирует из даты строку формата yyyy-MM-dd, другого пути я не нашел.
В совойствах Foreach контенера устанавливаем:
  1. В разделе Collection:
    1. Enumerator = Foreach File Enumerator
    2. Folder = C:\XMLAScripts
    3. Files = *.xmla 
  2.  В разделе Variable Mappings: Variable=User:file_name, Index = 0
 В свойствах блока Script в разделе Script:
  1. ReadOnlyVariables = User::dt_str,User::file_name
  2. ReadWriteVariables = User::alter_code
В коде скрипта пишем (у меня SQL Server 2008, поэтому пишу на C#):
        public void Main()
        {
            string file_name = Dts.Variables["file_name"].Value.ToString();
            StreamReader sr = new StreamReader(file_name);

            string content = sr.ReadToEnd();

            sr.Close();
            sr.Dispose();

            Dts.Variables["alter_code"].Value = content.Replace("###dt###",  Dts.Variables["dt_str"].Value.ToString());

            Dts.TaskResult = (int)ScriptResults.Success;
        }
 В свойствах блока Alter Cube в разделе DDL:
  1. Connection -> создаем новое соединение с кубом
  2. SourceType =Variable
  3. Source = User::alter_code
На этом создание SSIS пакета закончено. Foreach контейнер выбирает из папки C:\XMLAScripts все файлы с расширением xmla, в блоке Script текущий файл читается, в нем производиться замена строки "###dt###" на сегодняшнюю дату в формате yyyy-MM-dd, а блок Alter Cube передает полученную таким образом команду OLAP-серверу. Для того, что бы добавить другие группы мер для партицирования, надо в папку C:\XMLAScripts добавить XMLA-файлы с альтерами для этих групп, в которых дата в SQL-запросе будет заменена на "###dt###".
Далее нужно создать джоб, в котором первым шагом сделать бэкап куба на всякий случай, вторым шагом запустить этот SSIS пакет, а далее отпроцессить все партиции за текущий год.


1 июня 2011 г.

Партицирование OLAP-кубов в SSAS 2008

В целях повышения производительности работы кубов в SSAS предусмотрена возможность разбиения групп мер на партиции. Вот так это выглядит в Management Studio:
При процессинге куба процессится только партиция Vw Sales By Products, две другие уже не процессятся, т.к. данные за эти периоды не меняются. При больших объемах данных партицирование может значительно уменьшить время процессинга, а так же уменьшит время обращения к кубу, особенно если партиции разнести по разным физическим носителям.
Создать партиции можно как в Management Studio, так и в BIDS. Рекомендую делать партиции в BIDS, т.к. если их сделать в Management Studio, т.е. на рабочем кубе, а не в проекте, то при следующем деплое куба все партиции удаляться.
Для создания партиций в BIDS необходимо в дизайнере куба открыть вкладку Partitions:












Здесь видно две группы мер Sales и Sales 1 и список их партиций. Для того, что бы разбить на партиции группу мер Sales, необходимо сначала изменить текущую партицию. Разбивать на партиции будем по дате продажи. Для этого в колонке Source для партиции жмем кнопку, которая появляется при получении этим полем фокуса ввода. Появиться окно:

 По умолчанию Binding type = Table Binding. Его надо изменить на Query Binding,  в окне появиться поле с SQL-запросом, который используется для обращения к базе при процессинге куба, с пустым условием WHERE. В условие надо добавить выражение для партицирования. Я хочу разбивать на партиции по дате продажи, текущая партиция будет процесситься каждый день, в условие я добавляю:
WHERE sale_date >= '2011-05-31'
Жмем OK. Текущая партиция создана. Далее необходимо создать партицию (или несколько партиций) для исторических данных. Предположим продажи есть только за 2011 год. Для создания партиции жмем ссылку New Partition. Появиться окно:


 Выбираем таблицу в поле Available tables. Появиться окно, где вводиться запрос, в нем чекаем галку Specify a query to restrict rows и в запросе пишем условие ограничения:

WHERE sale_date >= '2011-01-01' AND sale_date < '2011-05-31'
 Далее нажимаем Next, появиться окно где можно выбрать опцию Processing loaction (не знаю, что это, не разбирался) и Storage location (т.е. где будут физически лежать файлы для этой партиции). Далее нажимеам Next, появиться окно, где в поле Name вбиваем название партиции, например Vw Sales 2011, выбираем Design aggregations later и жмем Finish.
Все, после этого окно партиций примет такой вид:
 Далее деплоим и процессим куб. Далее, в повседневной работе куба,    процесим только партицию Vw Sales, т.к. данные будут обновляться только в ней.

18 мая 2011 г.

Перевод строки в выражениях SSRS

Для добавления новых строк в выражениях SSRS используется константа vbCrLf. Например, вот так я добавлял тултип для графика:
="MIN: " + Format(Min(Fields!Сумма_покупок.Value), "#,##0") + vbCrLf + "MAX: " +   Format(Max(Fields!Сумма_покупок.Value), "#,##0")

16 мая 2011 г.

Конвертация даты в строку в SSRS 2008

Для того, что бы преобразовать дату в строку в произвольном формате в SQL Server Reporting Services, надо воспользоваться функцией Format:
=Format(Parameters!par_date.Value, "yyyy-MM-dd")
Так же можно написать так:
=Parameters!par_date.Value.ToString()
или
=FormatDateTime(Parameters!par_date.Value, DateFormat.ShortDate)
но в обоих последних примерах нельзя выбрать произвольный формат даты, в ToString вообще нельзя указать формат, а в FormatDateTime можно выбрать из нескольких вариантов.

3 мая 2011 г.

SSAS 2008 R2: ошибка при включении сервиса


Стоит SQL Server 2008 R2 Enterprise Edition на Windows Server 2008 R2 Standard.
При включении сервиса SQL Server Analysis Services возникает ошибка. В логах винды пишет такое:
The file '\\?\C:\DATA\CryptKey.bin' could not be opened. Please check the file for permissions or see if other applications locked it.
Сервис запускался из под пользователя NETWORK SERVICE. По какой-то причине на папку C:\Data у этого пользоватля не было. Для решения проблемы надо дать пользователю, из под которого запускается сервис, полный доступ на папку C:\Data и на все файлы и вложенные папки в этой папке.

28 апреля 2011 г.

Разбиение отчета в SSRS на страницы через параметр отчета

При выводе отчета в Web SSRS автоматически разбивает его на страницы, но есть возможность управлять этим процессом через параметр отчета. Все нижеследующее относиться к SQL Server 2008 R2. Я раньше делал это на SQL Server 2005 таким же способом, но какие то детали могут отличаться.

Для того, что бы разбить отчет на страницы через параметр, надо создать параметр типа Int, в моем случае он будет означать количество строк на станице. Далее создаем группу и в значение группировки вставляем выражение:
=Ceiling(RowNumber(Nothing)/Parameters!rows_per_page.Value)
В таблицу добавиться поле, что бы оно не мешалось, я делаю его ширину нулевой.

Далее, в свойствах группы, в разделе Page Breaks ставим галку Between each instance of a group. После этого я запустил отчет и он мне выдал ошибку, суть которой в том, что у таблицы выражение сортировки использует функцию RowNumber, а такого не может быть. После некоторых попыток избавиться от этого пришел к такому решению - для таблицы и для группы (на всякий случай) установил значения сортировки на первом попавшемся поле, а потом сбросил эти значения. Видимо при создании группы, выражение группировки автоматом попало в выражение сортировки (Мелкомягким привет!). 

Далее на кнопке строки отчета, которая появляется слева от самой таблицы при попадании таблицы в фокус ввода, тыкаем правой кнопкой и выбираем пункт Tablix Properties. На вкладке General нужно поставить галку Keep together on one page if possible, иначе могут появиться странности, например у меня при значении 50 строк на страницу выдавало каждые несколько страниц по 3-4 записи. Видимо автоматическое разбиение на страницы создается  без учета других разбиений.

Единственное чего я не смог сделать, так это оставить названия колонок на каждой странице. В окне Tablix Properties есть галка Repeat header rows on each page, но что-то ее установка ни к чему не привела.

К стати, здесь про разбиение на странице кратко написано: http://msdn.microsoft.com/en-us/library/ms157328.aspx , раздел Page Breaks.

Значок "замка" на папке или файле в Windows Server 2008 R2

Иногда в Windows Server 2008 R2, а так же в Windows 7, можно увидеть в эксплорере значок "замка" на папке или файле. Это знак того, что к папке (файлу) ограничен доступ пользователей, т.е. можо разрешить доступ, например, только себе. С одной стороны штука полезная, но с другой, может и напрячь, особенно если о ней не знать.
Я хотел к SQL Server приаттачить базу, которая лежала в папке с таким ограничением и SQL Server просто не видел файл внутри папки. Решение оказалось простым, надо было дать доступ к папке и самому файлу пользователю, из под которого был запущен SQL Server. В моем случае NETWORK SERVICE.
Как так получилось, что на папке оказался "замок", точно не знаю, я деаттачил базу, потом хотел расшарить папку для того, что бы перелить файл на другой сервер. Потом шару снял и появился замок и обратно приаттачить базу не вышло :))

1 апреля 2011 г.

Проверка правильности JSON

Вот здесь: http://json.org/ внизу страницы перечислены различные утилиты для работы с JSON. Мне понравилась утилита проверки правильности JSON-кода написанная на C#: JSON_checker.

Кстати, о том, зачем я начал проверять JSON. Сначала я использовал в проекте www.asvix.ru jQuery 1.3.2 и когда я делал вызов метода:
$.getJSON(url, callback);
То callback-функция нормально вызывалась после получения ответа с сервера. У меня с сервера возвращался не очень корректный ответ в виде JSON, примерно такой: {x:"50",y:"100"}.
После перехода на jQuery 1.5 колбэки перестали работать, т.е. сама callback-функция работала, если ее вызвать на прямую, но из getJSON она не вызывалась. Проблема оказалась в том, что JSON был некорректен. Исправил на сервере, что бы возвращало правильно: {"x":"50","y":"100"} и все заработало.

31 марта 2011 г.

Редирект 301 (Moved Permanently) в IIS 7

Поисковые машины считают, что сайты http://site.ru и http://www.site.ru являются разными сайтами. Поэтому, если оба этих URL ведут на один и тот же контент, поисковая машина может понижать ваш сайт в результатах выдачи за дублирущийся контент. Чтобы такого не произошло, нужно в файле robots.txt указать строку:
Host: www.site.ru
В которой указывается основное доменное имя (все равно какое выбрать с www или без него, дело вкуса). А со второго имени нужно сделать редирект с кодом 301 на основное доменное имя.
В IIS 7 редирект можно сделать несколькими способами, правда не все подходят для этого случая.
1. Самый простой способ сделать редирект - это выделить в IIS в папке Sites необходимый домен и в основной панели два раза тыкнуть HTTP Redirect. В открывшемся окне установить галку Redirect requests to this destination и указать необходимый URL, ниже в выпадающем списке выбрать код редиректа 301 и в меню справа тыкнуть Apply. Подробнее и с картинками можно почитать тут: http://www.trainsignaltraining.com/windows-server-2008-http-redirection .
Этот способ для рассматриваемого случая не подходит, т.к. он предполагает наличие двух разных сайтов на одном сервере или редирект с одного сервера на другой.

2. Второй способ: установить дополнительный модуль для ASP.NET URL Rewrite Module 2.0, а далее прописать правила редиректа в Web.Config:
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Redirect to WWW" stopProcessing="true">
          <match url=".*" />
          <conditions>
            <add input="{HTTP_HOST}" pattern="^site.ru$" />
          </conditions>
          <action type="Redirect" url="http://www.site.ru/{R:0}" redirectType="Permanent" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

3. Третий способ в Global.asax добавить метод  Application_BeginRequest со следующим содержанием:
    void Application_BeginRequest(object sender, EventArgs e)
    {
        string req_domain = HttpContext.Current.Request.Url.ToString().ToLower();

        if (req_domain.Contains("http://site.ru"))
        {
            req_domain = req_domain.Replace("http://site.ru", "http://www.site.ru");
           
            Response.Clear();
            Response.StatusCode = 301;
            Response.AddHeader("Location", req_domain);
            Response.End();
        }
    }