суббота, 31 октября 2009 г.

Dev magic и прочий ooops

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

DevMagic

Как может заключить "простой пользователь" взглянув на это окно, магии сделано уже 4655 из потенциальных 31820. Типа "побольше магии хорошей и разной". Что за магия и к чему она применяется - непонятно. Впрочем, может оно и к лучшему - меньше поводов волноваться.

Конечно же это не более чем баловство (от которого именно в этом конкретном случае можно, и даже нужно, было бы отказаться), но оно поднимает более важный вопрос об отношении к тому, что ты делаешь и как. На эту тему интересно высказался Jeff Atwood в своем посте о сообщениях об ошибках в Google Chrome, и я его поддерживаю - серьезное и вдумчивое отношение к продукту своей работы совершенно не обязательно должно выражаться в стандартизированной скуке всего и вся.

 

HTH,

AlexS

воскресенье, 18 октября 2009 г.

Объединение истории сообщений Skype

В последние полтора года я стал достаточно активно использовать Skype в качестве IM-клиента. Причем корпоративная политика отразилась и на персональных предпочтениях, ведь привычка - вторая натура. Тем более, что начиная с версии 4 Skype стал достаточно удобным в повседневном использовании (до этого я очень плохо с ним уживался).

В связи со сменой работы возник вопрос синхронизации истории сообщений между профилем, который я забрал с работы и личным профилем, который хранился дома. Ну или экспорт/импорт (в крайнем случае). Поясню: я использую одину и ту же учетную запись дома и на работе. Частенько получается, что начинаешь разговор с кем-то с работы, а заканчиваешь его дома. В результате история сообщений разорвана между двумя (а то и больше) компьютерами. Гугл мало чем смог помочь: есть плагин к Skype, который автоматом сохраняет все звонки и исообщения в гуглопочте - неплохо, конечно, но во-первых избыточно, во-вторых за деньги ($25 может и немного, но нет, спасибо). Еще на форуме Skype можно найти пару ссылок на программы, которые могут вытащить историю и выгрузить ее в txt или html. Тоже не совсем то. Пришлось разбираться самому.

На проверку все оказалось не так плохо (хотя и несколько запутано):

  • для хранения профиля (в том числе и истории звонков и сообщений) Skype использует базу данных SQLite - что хорошо (ибо эта СУБД открытая и для нее имеется .NET Provider)
  • схема не то чтобы сложная (16 таблиц), но о связях приходится догадываться (ибо внешние ключи не определены) и таблицы довольно "широкие" (до 40 полей)

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

image

Скачать его можно отсюда (выложил на SkyDrive на случае если кому еще пригодится).

Что следует иметь в виду, в ходе синхронизации:

  • профили должны быть от одной версии Skype (строго, ибо даже между v.4.0 и v.4.1 схема имеет некоторые различия)
  • профиль хранится в каталоге %user dir%\AppData\Roaming\Skype\<your skype name>\main.db
  • перед внесением изменений в целевой профиль, делается его резервная копия (так что в случае чего его можно вернуть)
  • свойства чата не обновляются - так что в списке "последних разговоров" можно не увидеть действительно последние разговоры, которые были добавлены в ходе синхронизации (но в базе они будут)
  • иногда происходит дублирование сообщений (не всегда) - я так и не смог надежно определить условия, при которых это происходит
  • в некоторых случаях в истории появляется сообщение "это сообщение было удалено из чата" - тоже не смог понять из-за чего (подозреваю что где-то не хватате связанной записи ... но вот где?)

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

HTH,

AlexS

понедельник, 28 сентября 2009 г.

Неочевидный redirect

На днях коллега ((C) Серега К.) натолкнулся на такую любопытную особенность HttpResponse.Redirect. Предположим, что у нас где-то в коде страницы или компонента ASP.NET есть такой-вот код, вполне на первый взгляд логичный, и не вызывающий бурю протеста:

try

{

     // что-то делаем

     Response.Redirect("url1");

}

catch (Exception ex)

{

     // Обрабатываем все на свете

     Response.Redirect("url2");

}

А теперь вопрос: куда будет перенаправлен запрос в результате нормального (т.е. без исключений) выполнеия кода? url1? А вот и нет - на самом деле url2. Все дело в том, что внутри HttpResponse.Redirect для прекращения обработки запроса вызывается Thread.CurrentThread.Abort. Таким образом наш обработчик "всего на свете", отловит это исключение и перенаправим запрос на url2.

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

Redirect(string url, bool endResponse)

второй аргумент следует установить в false.

 

HTH

AlexS

суббота, 8 августа 2009 г.

За что я не люблю визуальные дизайнеры и case-средства для разработки БД

  1. Не дают полного контроля над тем, что происходит с БД.
    Особенно это неприятно/опасно в случае, когда с помощью визуального дизайнера редактируется таблица, в которой есть данные - во многих случаях изменения потребуют пересоздания таблицы. Да, SQL Server Management Studio (в частности) во-первых будет сохранять данные, а во-вторых по-умолчанию запрещает вносить подобные изменения, но все-равно ... я предпочитаю вносить подобные изменения сознательно и сохранить соответствующий скрипт "для будущих поколений" (ибо он наверняка понадобится).
  2. Задают (слишком) много параметров "по-умолчанию".
    Чем больше таких параметров - тем больше вероятность забыть о них, что чревато проблемами. А чем позднее мы обнаруживаем эти проблемы - тем дороже стоит их решение.
  3. Не слишком-то экономят время.
    Нечасто приходится разрабатывать схему большой БД "с нуля". И тем более, это никогда не пироисходит "все и сразу" - схема появляется постепенно. А коли так, то я быстрее напишу SQL код "вручную", чем "дизайнер + генерация + доработка напильником". Если мне нужна картинка, то reverse engineering по уже созданной (из скриптов) базе никто не отменял.
  4. Не способствуют изучению SQL.
    Это, конечно, так себе аргумент, но тем не менее - знание SQL еще никому не мешало, а использование дизайнеров развращает.

HTH,

AlexS

понедельник, 6 июля 2009 г.

Журнал транзакций и запросы на выборку данных

Решил разобраться в том, что же на самом деле представляет из себя журнал транзакций (transaction log) в SQL Server-е.

Весь Books Online/MSDN пронизан мыслью о том, что журнал транзакций - это жизненно важный компонент базы данны и содержит данные о всех транзакциях, которые в ней выполняются. Эта мысль понятна, но меня долгое время занимал вопрос: как же быть с запросами на выборку данных? Они ведь тоже имеют свой уровень изоляции и выполняются в транзакции (пусть и неявной). Записываются ли они в журнал? И если да, то в каком виде и зачем?

Провел пару экспериментов, посмотрел сам журнал и выяснилось: в журнал транзакций не записываются "нерезультативные" тразакции (те, тразакции, которые не приводят к изменению данных). Перефразирую: журнал транзакций содержит "изменения, вносимые в БД", а не "запросы, выполняемые к БД".

Ниже приведу факты, которые (на мой взгляд) существенны для понимания того, чем является и чем не является журнал тразакций в SQL Server:

  1. Запись в журнал производится ДО того, как происходит изменение данных.
  2. Каждая запись содержит идентификатор транзакции, в рамках которой производится данное изменение - это позволяет откатить или заново выполнить любую запротоколированную транзакцию.
  3. Все записи в журнале (и, соответственно, все действия, производимые на физическом увроне с БД) делятся на две категории: те, для отката/повторения которых записывается логическая операция и те, для отката/повторения которых записывается образ данных до и после выполенния операции.
  4. Протоколируются только действия, приводящие к изменению данных - т.е. фактически журнал транзакций содержит результат обработки входящего потока транзакций, а не сами транзакции.

HTH,

AlexS

четверг, 2 июля 2009 г.

Забавные картинки

Недавно разжился лицензией на Red-Gate SQL Toolbelt. Среди прочего, в него входит инструмент с незатейливым названием SQL Dependency Tracker. Название меня особенно не впечатлило, казалось: ну что нового можно сказать о взаимозависимостях объектов в базе данных (хоть sp_MSdependencies и является недокументированной, но используется довольно широко)? Но из любопытства решил посмотреть.

И вот оно: схема зависимостей между объектами в базе данных нашего проекта.

ClientDatabaseDiagram

Вроде бы банальная штука, зато как смотрится! Ну просто форменный computer art! И главное: одного взгляда достаточно для того, чтобы понять вокруг чего крутятся колеса системы.

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

HTH

AlexS

среда, 24 июня 2009 г.

T-SQL Simple Facts

В последние пару недель читал блоги/Books Online и (заново) открыл для себя некоторые "простые" факты, некоторые из которых здорово меня удивили (признаюсь). Некоторые другие всплыли "по ассоциации", в итоге решил поделиться всеми сразу:

  1. Всем хорошо известен оператор GO, означающий окончание пакета. Так вот, полный синтаксит этого оператора: GO [count] - где count - положительное целое число, указывающее, что предшествующий пакет необходимо выполнить count раз (квадратные скобки указывают, что этот параметр может отсутствовать (что и происходит в ошеломляющем большинстве случаев)).
  2. Для типа данных datetime определена операция сложения, причем вторым аргументом может быть целое число, которое интерпретируется как количество дней. Так:
    DECLARE @D datetime
    SET @D = '2009/06/24 15:00'
    SET @D = @D + 2
    PRINT @D
    выдаст 26 июня 2009, 15:00 ... когда я увидел эту запись, то признаться сильно удивился.
  3. Для добавления данных в таблицу можно использовать следующий синтаксис:
    INSERT INTO <Имя таблицы> DEFAULT VALUES;
    Сработает, конечно, только в том случае, когда для всех полей таблицы, не считая автоинкрементных и timestamp, определены значения по-умолчанию, но все-равно любопытно.
  4. Тип данных timestamp не имеет ничего общего с *nix timestamp (отсчитывающего количество миллисекунд с начала Unix-эры). В SQL Server колонки этого типа данных обновляются автоматически при любых операциях добавления/изменения записи - т.е. это некий аналог "версии строки", за который отвечает сам сервер. Гарантируется уникальность значений полей timestamp в рамках каждой базы данных.
  5. Переменные не покрываются транзакцией:
    DECLARE @i
    SET @i = 10
    BEGIN TRANSACTION
    SET @i = 20
    INSERT INTO SomeTable VALUES (@i)
    ROLLBACK TRANSACTION
    PRINT @i
    напечатает 20 (а не 10, как можно было бы ожидать).
  6. Синтаксис ALTER TABLE ... ALTER позволяет изменить определение поля таблицы но не его имя (что можно было бы ожидать). Для переименования необходимо воспользоваться хранимой процедурой sp_rename (не слишком элегантно, но действенно).

HTH,

AlexS

вторник, 12 мая 2009 г.

Что такое OPENQUERY и чем оно может нам помочь

На днях мне задали задачку (перефразируя):

Есть сервера A, B и С. Сервер A связан с сервером B (т.е. A доступен с B). Сервер B связан с сервером C (т.е. сервер B доступен с C). При этом в соответствии с политикой безопасности сервера A и C "не видят друг-друга" (т.е. связи между ними нет). При этом на сервере A есть некоторые данные, которые нужно получить/обработать в ходе выполнения задания на сервере C. Можно ли такое организовать?

Перефразирую: можно ли каким-либо образом из T-SQL кода открыть соединение к связанному серверу и выполнить какой-то SQL код удаленно? Т.е. в нашем случае: сделать на сервере C что-нибудь такое, чтобы некий SQL код на самом деле выполнился на сервере B (откуда есть доступ к серверу A), а результат вернул нам?

Ответ: можно (только осторожно). Ключом в данном случае является та самая функция OPENQUERY, которая вынесена в заголовок.

Эта функция позволяет выполнить некоторый запрос на связанном сервере (linked server) и использовать его результаты как обычную таблицу:

OPENQUERY (linked_server, 'query')

Например:

SELECT *

FROM

  Table1 t1

  LEFT JOIN OPENQUERY([ServerB], 'SELECT * FROM [ServerA].dbname.dbo.SomeTable') q1 ON q1.ID = t1.ID

Вот еще некоторые факты об OPENQUERY:

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

 

HTH
AlexS

понедельник, 20 апреля 2009 г.

Полнотекстовый поиск: MS Full-Text Search

Вторая часть серии о полнотекстовом поиске несколько задержалась - работа не давала продохнуть. Впрочем, это лирика, приступим к делу.

Итак, я не буду повторять массу открыто доступной информации о Microsoft Full-Text Search (общее описание от Microsoft (eng/рус) и Wikipedia, архитектура), не буду и переписывать простые и не очень примеры. Сосредоточимся на следующем сценарии: у нас имеется некоторое не слишком сложное приложение (веб-приложение, два-три слоя, до несколько Гб данных (до десятка-другого), пара-тройка вспомогательных сервисов) и нам необходимо обеспечить "интеллектуальный поиск" для некоторых сущностей этого приложения. "Интеллектуальность" поиска - маркетинговый прием, которым sales/accounts привлекают пользователей. С чисто технической точки зрения за ним может скрываться следующее:

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

Из описания можно заключить что скорее всего этот функционал все-таки в большей степени ориентирован на конечного пользователя (и скорее всего "продвинутого пользователя") и врядли будет использоваться в каких-то вспомогательных сервисах (хотя как знать ...). Теперь рассмотрим насколько сложно/просто будет все это сделать и отгрузить заказчику. Обратимся к следующим основным вопросам: изменения в архитектуре приложения, поддержка/сопровождение и альтернативы.

Архитектура

Здесь Microsoft потрудилась на славу: технология достается нам бесплатно не только в смысле денег (идет в комплекте с SQL Server и есть даже у SQL Server Express (with Advanced Services)) но и в смысле интеграции. Вся сложность ложится на плечи SQL Server-а:

  1. Создаем полнотекстовый каталог
  2. Создаем полнотекстовый индекс
  3. Используем специальные функции (CONTAINS, CONTAINSTABLE, FREETEXT, FREETEXTTABLE) для обращения к индексам из T-SQL кода

Таким образом, код приложения может быть полностью абстрагирован от того, каким образом получены данные: с применением полнотекстового поиска или без него. В обмен на эту простоту на стороне СУБД нас поджидает:

  1. Необходимость проектирования физического уровня: сколько каталогов нам нужно и где они будут храниться
  2. Логический уровень: какие индексы нам нужны, какие колонки индексировать
  3. Подробности алгоритмов обработки данных: стоп-список (noise words (в SQL Server 2005 - фиксированный, один на сервер, в SQL Server 2008 можно создавать пользовательские)), алгоритм разбиения на слова (word breaker), выделение словоформ (stammer), фильтрация содержимого (content filters)

Первые два уровня для начала/в несложных случаях, пожалуй, можно отдать на откуп SQL Server-у, а вот третий пункт таит в себе массу подводных камней, которые могут значительно усложнить жизнь "среднестатистическому разработчику":

  • стоп-список: слова, входящие в него, попросту игнорируются и не включаются в индекс (поэтому неудивительно, что найти их тоже не получится)
  • разбиение на слова: зависит от выбранного языка (локали), может быть нейтральным (по пробелам/знакам препинания)
  • выделение словоформ: зависит от языка, список доступных языков можно узнать из системного представления sys.fulltext_languages (зависит от версии SQL Server, в 2005м нужно было некоторые языки (в том числе и русский) регистрировать вручную). Если выберем "неправильный" язык (например в поле, индексируемом согласно правилам английского языка, будем хранить русский текст), то словоформы работать не будут - все слова будут искаться "как есть" (буквально). Проблемы также возникнут с синонимами и "похожестью" слов.
  • фильтрация содержимого: фактически дает возможность проиндексировать документы, хранящиеся в БД (в поле типа image/varbinary(max)). Хорошая новость заключается в том, что поддерживаются все форматы, которые "понимает" Windows, на которой выполняется SQL Server - нужно лишь задать специальное поле, в котором для данной строки будет храниться формат потока (фактически расширение файла).

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

  1. Хранить данные для всех языков в одних и тех же полях вперемешку - проще всего, но теряется значительная часть функциональности
  2. Для каждого языка создавать отдельное поле/поля (например Title_EN, Title_RU и т.д.) - функциональность остается при нас, но придется довольно много "плясать" вокруг этих данных (в том числе и в приложении - чтобы правильно сохранить/загрузить)

Что бы мы ни выбрали в конечном итоге, за языковыми настройками индексации каждого поля (как и за collation ;-) ) надо следить.

Поддержка/сопровождение

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

  • автоматическое наполнение/обновление (CHANGE_TRACKING AUTO): сервер сам будет следить за актуальностью индексов - хорошее решение со многих точек зрения, но могут возникнуть некоторые неожиданные побочные эффекты, связанные с тем, что к нашей базе данных будет обращаться еще один (системный) процесс, накладывающий некоторое количество блокировок
  • наполнение/синхронизация индексов по запросу (CHANGE_TRACKING MANUAL): больше ответственности, но и больше уверенности в том что и когда мы делаем - может потребоваться в основном при большой загрузке, когда мы хотим максимально разгрузить сервер БД в течении рабочего времени и готовы смириться с некоторой степенью неактуальности информации в индексах

Довольно сложно дела обстоят и с мониторингом/оптимизацией производительности. Microsoft не разглашает внутреннюю структуру хранения полнотекстовых индексов поэтому на них не распространяется опыт оптимизации "обычных" индексов. Единственное, что можно сказать наверняка: чем больше индексов (в том числе и полнотекстовых) "навернуто" на таблицу, тем больше времени будут занимать операции добавления/обновления/удаления данных.

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

image

Выводы делаем сами (впрочем, я думаю и так понятно, что бесплатного в этом мире ничего не бывает).

Альтернативы

Если не рассматривать в качестве альтернативы переход на другую СУБД (в PostgreSQL, Oracle и MySql есть аналогичные технологии), то остается применение неких внешних по отношению к нашему приложению сервисов, производящих индексацию содержимого. Тут можно упомянуть:

  • Windows Search - встроен в Windows (следовательно "бесплатен"), позволяет индексировать файлы, хранящиеся на компьютере
  • Google Desktop - аналогичный продукт от Google
  • Apache Lucene / Solr - индексатор / поисковый сервер от Apache Foundation

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

 

HTH

AlexS

четверг, 12 марта 2009 г.

Полнотекстовый поиск: введение

Данным постом я начинаю серию статей о полнотекстовом поиске. Для начала определимся с тем, что это такое и зачем это нужно.

Итак, можно выделить несколько основных способов поиска информации.

Поиск подстроки в строке - самый простой вариант, грубо говоря сканируем все строки/документы и проверяем в каких из них содержится заданная последовательность символов.

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

"Полнотекстовый поиск" - технология, призванная увеличить объемы обрабатываемой информации, скорость поиска и функциональность. Так, например, используя первые два способа, достаточно сложно найти все строки/документы, содеражащие некоторое слово (с учетом весех возможных словофор) - требуется либо несколько запросов, либо достаточно сложное регулярное выражение (да и то не всегда это помогает). В ситуации когда мы хотим найти несколько слов (которые могут идти не подряд), ситуация еще больше усложняется.

Итак, основная особенность полнотекстового поиска заключается в том, что мы ищем не "строгую последовательность символов", а слова (со всеми словоформами). Большинство продуктов, реализующих эту технологию, также позволяют искать "слова, похожие на заданное", использовать булевые операторы и т.п. Кроме того, все коммерческие реализации ищут не по самим строкам/документам, а по индексу, построенному для них.

Таким образом, каждая строка/документ подвеграется предварительному анализу (индексируется). В ходе этого анализа в общем случае нужно выполнить следующие действия:

  1. Разбить текст на слова (более точно: лексемы) - за это отвечает лексический аналазитор (tokenizer).
  2. Произвести фильтрацию полученных слов (пропустить некоторые из них (стоп-список), обработать одинаковые и т.п.) - за это отвечает (необязательный) фильтр лексем (token filter).
  3. Найти основу каждого слова (стем) - за это отвечает стеммер (stemmer).

Далее в индексе сохраняется информация о том, какие стемы были найдены в конкретной строке/документе и, возможно, еще какая-то информация (взаимное расположение стемов, их количество и т.п.).

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

Далее я планирую остановиться на двух реализциях данной технологии: MS SQL Server Full Text Search и Apache Solr.

HTH

AlexS

понедельник, 2 марта 2009 г.

Для быстрой страницы шепчите на ушко богам SQL-я

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

Итак, речь пойдет о списочных представлениях (aka list view). Я уже как-то ранее упоминал о них здесь. Вкратце напомню особенности этого способа отображения данных:

  • отображаются данные сразу из нескольких таблиц (от двух до десятка)
  • предполагается сортировка по любому полю
  • предполагается разбиение данных на страницы (для удобства просмотра)
  • чаще всего пользователь имеет возможность фильтровать данных по различным (порой довольно сложным) условиям
  • в большенстве случаев НЕ предполагается inline-редактирование данных (в силу пункта 1)

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

Итак, чтобы быть более конкретным, возьмем две таблицы (простейший случай):

CREATE TABLE [dbo].[CityData](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [CityName] [varchar](50) NULL,
    [Province] [varchar](50) NULL,
    [ZipCode] [varchar](10) NULL);

CREATE TABLE [dbo].[PersonData](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [CityId] [int] NULL,
    [FullName] [varchar](255) NULL,
    [ZipCode] [varchar](10) NULL,
    [Email] [varchar](255) NULL,
    [ContactPhone] [varchar](100) NULL);

Отображать будем данные о людях (PersonData) вместе с информацией о городе:

select *
from
(
    select
        p.Id, p.FullName, c.CityName,
        ROW_NUMBER() OVER (ORDER BY p.FullName) as RowNumber
    from
        PersonData p
        left join CityData c on c.id = p.CityId
) q
where q.RowNumber between @StartRow and @EndRow

Все хорошо, НО: зачем присоединять данные о городах ко всем записям из таблицы PersonData в то время, как нам нужно получить всего лишь (@EndRow - @StartRow) записей? Правильно, севершенно незачем. Поэтому можем сделать так:

select
    q.*,
    c.CityName
from
(
    select
        p.Id, p.FullName, p.CityId,
        ROW_NUMBER() OVER (ORDER BY p.FullName) as RowNumber
    from
        PersonData p
) q
left join CityData c on c.id = q.CityId
where q.RowNumber between @StartRow and @EndRow

Что изменилось? Пришлось явно выбирать p.CityId (чтобы потом можно было присоединить данные о городах) и ... в общем-то все. Каков результат? Вот что говорит нам план выполнения запроса:

image

  • таблица CityData в первом случае присоединяется ДО того, как происходит фильтрация данных для страницы, а во-втором ПОСЛЕ
  • стоимость выполнения второго запроса в два раза меньше (это на нашем простом примере, где в PersonData 100 записей, а в CityData - 50)
  • статистика ввода/вывода тоже вдохновляет: удалось почти вдвое сократить количество логических чтений на обработке таблицы CityData

Примерно такая же картина наблюдается в MySQL и PostgreSQL (только план запроса не так наглядно выглядит).

Несколько замечание касательно этой техники:

  1. Чем больше таблиц нужно присоединять и чем больше у нас данных - тем значительнее выигрыш (хотя зависимость нелинейная)
  2. Те таблицы, которые связаны с главной таблицей через inner join, должны всегда быть во внутреннем запросе, а вот те, которые через left join можно смело выносить наружу
  3. Та таблица, по полю которой нужно сортировать результат, должна быть во внутреннем запросе

HTH

вторник, 24 февраля 2009 г.

Использование и настройка псевдонимов (server alias)

Сегодня почти весь день (с перерывами на работу) пришлось заниматься "разруливанием проблем" с приложением на стороне заказчика. Один из сервисов все никак не хотел подключаться к SQL Server-у. Проблема усугублялась тем, что у нашей команды нет доступа к целевому окружению (вообще никакого доступа). Есть доступ только к индусам, которые в свою очередь имеют доступ к этому самому окружению. Причем "интерфейс" к индусам - электронная почта и Skype. После многочисленных советов "разобраться в конфигурации" и столь же многочисленных ответов "все точно так же, как и на стэйджинге", причина таки нашлась: на "проблемном" сервере не был сконфигурирован псевдоним (alias).

Мне сразу по приходу в этот проект не понравилось использование этой "фичи" - и вот опасения материализовались (правда это первый раз когда все было так серьезно). Не могу не признать, что в определенной степени alias - это удобно. Он, например, дает свободу в выборе сервера: можно переключиться с "общественного" на локальный меняя не строку подключения, а настройки псевдонима - т.е. действуя снаружи приложения. Причем псевдоним действует в рамках комьютера, т.е. (потенциально) можно влиять сразу на целый пакет приложений и сервисов. Побочный эффект: пока не посмотришь в настройки - не узнаешь куда же приложение "ходит" за данными. В целом я бы, наверное, избегал использования этой возможности.

Возник, однако, другой вопрос. В случае, когда на компьютере установлен SQL Server Client Tools (на машине разработчика есть с вероятностью 99%) все просто: запускаем SQL Server Configuration Manager, идем в раздел SQL Native Client Configuration, далее Aliases. А если этого пакета нет (на сервере - не редкость)? Как быть? Вариантов несколько:

  • "для слабаков": установить SQL Server Client Tools и получить SQL Server Configuration Manager
  • "для простых админов": в комплекте с клиентскими библиотеками SQL Server идет утилита cliconfg.exe (находится в папке Windows\systems32 так что можно вызывать из командной строки), в которой можно сконфигурировать не только псевдоним, но и много чего еще
  • "для хардкорных админов": воспользоваться Power Shell и SMO (используя объект Microsoft.SqlServer.Management.Smo.Wmi.ServerAlias) - правда для этого придется установить на компьютер PowerShell (в Server 2008 и новее есть всегда) и собственно SMO

Рекомендуемый вариант - "для админов". Или "для хардкорных админов" (возможно с вариациями) - если нужно чтобы настройка происходила автоматически и/или была повторяемой.

За деталями отсылаю к MSDN и документации по PowerShell. А еще рекомендую блог SQL Protocols - такой подробной информации о протоколах SQL Server и решении проблем с соединением вы больше нигде не найдете.

HTH

понедельник, 9 февраля 2009 г.

Об опциях соединения вообще и об IDENTITY_INSERT в частности

Сегодня пришлось "играться" с IDENTITY_INSERT. С удивлением обнаружил прочувствовал что это таки "установка уровня соединения". Не скажу, что это прямо так удивительно, в MSDN об этом явно сказано:

At any time, only one table in a session can have the IDENTITY_INSERT property set to ON. If a table already has this property set to ON, and a SET IDENTITY_INSERT ON statement is issued for another table, SQL Server 2000/2005/2008 returns an error message that states SET IDENTITY_INSERT is already ON and reports the table it is set ON for.

но как-то до сих пор я не особенно обращал внимание на это примечание.

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

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

using(SqlConnection = new SqlConnection())

{

con.Open();

SqlCOmmand com = new SqlCommand("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- очень важный SQL код", con);

com.ExecuteNonQuery();

}

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

Граждане! Будьте бдительны применяя опции соединения (особенно уровень изоляции).

HTH

среда, 28 января 2009 г.

Мой первый тренинг в качестве MCT

Легкий оффтоп.

Сегодня закончился мой первый тренинг в качестве сертифицированного тренера Microsoft.

IMG_4536 IMG_4537

Группа была не слишком многочисленная, но бодрая - держали меня в тонусе и постоянно задавали вопросы "вширь и вглубь". Отчасти именно из-за моего неумения вовремя остановить свои пояснения приходилось жертвовать практикой, отдавая ее на откуп самостоятельной (считай домашней) работе.

На собственном опыте убедился, что подобный тренинг и преподавание в ВУЗе - это далеко не одно и тоже. Во-первых трениг проходит интенсивнее, чем обычные занятия, во-вторых очень здорово помогает наличие учебных материалов. Ну и кроме того, методика подачи материала совсем другая, нет разделения на лекции/практику, тут "все в одном": рассказал - показал. В общем - новый опыт.

Надеюсь первый блин все-таки не вышел комом.

понедельник, 5 января 2009 г.

Организация доступа к данным: как сделать шоколадно-конфетное приложение

Дано: база данных, разработчик. Разработчик разрабатывает приложение. Приложение работает с данными, хранящимися в ... базе данных - отображает, редактирует, сохраняет.

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

Сразу ограничу область "приложения" - не слишком большое (не больше 2х - 3х слоев, ибо бизнес-логика присутствует), но и не слишком маленькое (т.е. о масштабировании думать уже нужно, и вариант "меня спасут помощники в Visual Studio" не пройдет). Также договоримся, что большая часть тех данных, с которыми работает приложение хранится именно в реляционной СУБД - т.е. для доступа к ним используется SQL (в той или иной форме).

Несколько лет назад (5-6) в подобных случаях советовалось не хранить SQL в коде приложения, а выносить его в хранимые процедуры. Отлично. Результатом применения такого подхода является умопомрачительное количество этих самых процедур - по одной накаждую из CRUD -операций плюс еще что-то на бизнес-логику и логику отображения данных (как раз сейчас работаю с одним legacy приложением, в котором на 130 таблиц более 550 хранмых процедур). Такое положение дел не очень хорошо по целому ряду причин:

  • сложно поддерживать
  • сложно расширять
  • сложно обновлять
  • по производительности тривиальные хранимые процедуры, реализующие CRUD-операции, хуже динамически сгенерированного SQL-кода (особенно это касается операций обновления данных)

Новое время дает нам новые инструменты. На замену хранимым процедурам пришли ORM-библиотеки. Вроде бы все стало хорошо, мило и приятно - теперь уже можно вообще не писать SQL код. Но не все так просто. Да, с CRUD-операциями ORM справляется хорошо, но вот бизнес-логика зачастую вполне ощутимо "подтормаживает", т.к. требует извлечения довольно большого количества сущностей из БД. Если мы хотим создать приложение, которое будет "быдро пошевеливаться", то полностью отказываться от хранимых процедур нам нельзя - проверку сложной бизнес-логики, работающей на множестве объектов, из которых состоит/которыми оперирует наша система, лучше держать поближе к хранилищу этих объектов, т.е. в БД.

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

  1. необходимо обеспечить сортировку по широкому спектру атрибутов (что делает невозможным применение хранимых процедур для получения данных)
  2. отображаемые данных "собираются" из нескольких сущностей (что ставит под сомнение эффективность применения ORM)
  3. необходимо обеспечить разбивку на страницы (что вкупе с первыми двумя пунктами "напрягает" и хранимые процедуры и ORM-средства)
  4. наличие функциональности по фильтрации данных (так что либо мы изощренно издеваемся над ORM, генерирую хитрые условия, либо динамически генерируем SQL-код в хранимых процедурах)
  5. в большинстве случаев данные, отображаемые в списке, не нужно редактировать (для этого есть отдельная страница)

По моему опыту, именно такие запросы сильнее всего загружают сервер баз данных. Можно, конечно, решить все перечисленные выше проблемы "методом лома" - грубой силой заставив выбранную технологию (хранимые процедуры или ORM) делать то, что требуется. Но тогда наверняка пострадает гибкость и производительность решения. А мы хотим "конфетку". В итоге мы приходим к необходимости еще одного нишевого решения - динамической генерации SQL для отображения списков (list view). Положительный эффект на уровне БД при этом может достигать 50 - 60% (по сравнению с вариантом "в лоб").

Итак, с моей точки зрения, сбаллансированный подход к организации доступа к данных (СУБД) заключается в следующем:

  • слой бизнес-логики (aka domain model) с применением ORM
  • бизнес-логика проверяется на сервере БД и (по крайнем мере часть ее) материализуется в виде хранимых процедур
  • для отображения списков используется динамическая генерация SQL-кода, которая позволяет получить эффективный (SQL-)код и уменьшить нагрузку на сервер БД