46378 (665526), страница 3
Текст из файла (страница 3)
Большинству разработчиков для построения своих приложений вполне хватает стандартного набора агрегирующих функций. Однако теперь настал праздник и для редких любителей сделать что-то необычное – в новом MS SQL Server можно реализовать свой способ выйти за пределы SUM, AVG и СOUNT.
Создаются они при помощи оператора CREATE AGGREGATE:
CREATE AGGREGATE [ schema_name. ] aggregate_name ( @param_name ) RETURNS EXTERNAL NAME assembly_name [ :class_name ] ::= system_scalar_type | { [ udt_schema_name. ] udt_type_name } ::= system_scalar_type | { [ udt_schema_name. ] udt_type_name } |
На этот раз написания одного метода недостаточно. Вместо этого для подсчета агрегатов используются объекты. Идея проста – по мере просмотра исходных данных мы накапливаем то, что нужно накапливать, а зетем выводим накопленное в выходной набор. Соответственно для каждого из этих действий нужно реализовать по методу:
Имя метода | Описание |
public void Init()public void Init(input_type value) | Инициализирует объект. Вызывается один раз на группу агрегируемых значений. Если реализована версия метода с одним параметром, то SQL Server может использовать ее для передачи первого значения в группе. Тип параметра value (input_type) должен быть совместимым с тем типом, который указан как input_sqltype в операторе CREATE AGGREGATE. |
public void Accumulate(input_type value) | После инициализации объекта, сервер вызывает этот метод по одному разу для каждого агрегируемого значения. (На список подаваемых на вход значений, помимо состава полей в операторе GROUP BY, оказывает влияние также и наличие ключевого слова distinct перед агрегируемым выражением. Как и для встроенных функций, это ключевое слово приведет к тому, что в список для каждой группы попадут только различные значения агрегируемого выражения). Тип параметра value должен быть совместимым с тем типом, который указан как input_sqltype в операторе CREATE AGGREGATE. |
public return_type Terminate() | Несмотря на страшное название, этот метод всего лишь должен вернуть то самое агрегированное значение, которое было вычислено для группы входных значений. Тип результата должен быть совместимым с тем типом, который указан как return_sqltype в операторе CREATE AGGREGATE. |
public void Merge(udagg_type group) | Этот метод предназначен для случаев, когда SQL Server создает больше одного агрегирующего объекта на одну группу входных значений. Например, при выполнении запроса на многопроцессорной машине, входные данные могут быть разделены на несколько потоков для одновременной обработки. Перед выводом данных необходимо выполнить слияние рассчитанных агрегатных значений. Именно это и делает этот метод. Он принимает единственный параметр того же класса, в котором объявлен. |
Таблица 4.
Помимо этих методов, у класса должен быть определен конструктор без аргументов (иначе SQL Server не сможет создавать объекты этого класса). Кроме того, должна быть обеспечена возможность сериализации объектов – для случаев, когда серверу нужно сохранить промежуточный результат на диске. Мы отложим описание подробностей сериализации до следующего раздела, а пока что попробуем сделать свою функцию для вычисления среднего геометрического.
Тем, кто плохо помнит школьный курс, напомню, что среднее геометрическое из N чисел – это корень N-ной степени из их произведения. (А среднее арифметическое N чисел, которое обычно и подразумевается под термином «среднее значение» - это сумма этих чисел, деленная на N).
ПРИМЕЧАНИЕ К сожалению, расчет среднего геометрического по определению очень быстро приводит к переполнению даже на очень небольших наборах входных данных – произведение растет слишком быстро. Поэтому мы схитрим и воспользуемся тем математическим фактом, что произведение N чисел равно экспоненте от суммы их логарифмов. Вместо извлечения корня степени N (а это то же самое, что и возведение в степень 1/N) мы поделим на N сумму логарифмов перед применением функции Exp(). |
[Serializable] [SqlUserDefinedAggregate(Format.Native, IsInvariantToDuplicates = false, IsInvariantToNulls = true, IsInvariantToOrder = true, IsNullIfEmpty = true)] [StructLayout(LayoutKind.Sequential)] public class AvgGeom: INullable { private double _agg; private int _count; private bool _isNull = true; #region User-Defined Attribute Required Methods public void Init() { _agg = 0; _count = 0; _isNull = true; } public void Accumulate(SqlDouble Value) { if (!Value.IsNull) { _agg += System.Math.Log(Value.Value); _count++; _isNull = false; } } public void Merge(AvgGeom Group) { if (!Group.IsNull) { _agg += Group._agg; _count += Group._count; _isNull = false; } } public SqlDouble Terminate() { if (IsNull) return SqlDouble.Null; else return new SqlDouble(System.Math.Exp(_agg / _count)); } #endregion #region INullable Members public bool IsNull { get { return _isNull; } } #endregion } |
В первую очередь обратим внимание на атрибут SqlUserDefinedAggregate, который предваряет описание нашего класса. В нем определено несколько параметров (таблица 5).
Имя параметра | Описание |
Format | Формат сериализации объектов этого класса. Подробности – в следующем разделе. |
MaxByteSize | Максимальный размер сериализованного объекта. Подробности – в следующем разделе. |
IsInvariantToDuplicates | Зависит ли агрегированное значение от наличия дубликатов во входных данных (по умолчанию – да). Например, для функции MIN() совершенно неважно, сколько раз повторяются входные значения, а для функции SUM() – важно. Оптимизатор запросов SQL Server может использовать эту информацию для минимизации количества вызовов метода Accumulate. |
IsInvariantToNulls | Влияет ли наличие NULL-значений во входных данных на агрегированное значение. Для большинства встроенных агрегирующих функций (кроме COUNT()) это так. |
IsNullIfEmpty | Означает, что агрегирующая функция возвращает NULL для пустых входных наборов. Например, функция MIN при выполнении на пустом наборе возвращает как раз NULL , а функция COUNT() – 0. |
IsInvariantToOrder | Данный параметр пока не документирован; судя по названию, он должен определять, влияет ли на результат порядок подачи значений в метод Accumulate(). См. примечание после таблицы |
Таблица 5.
ПРЕДУПРЕЖДЕНИЕ Все встроенные агрегирующие функции (а также наш пример) являются коммутативными, что позволяет серверу выбирать порядок сканирования входных данных по своему усмотрению. Однако, например, результат функций типа First() или Last(), (которые должны возвращать соответственно первое или последнее значение в наборе), очевидным образом зависит от порядка входных значений. Тем не менее, пока непонятно, как можно использовать подобные функции – дело в том, что синтаксис SQL не позволяет определять порядок агрегирования записей. Оператор ORDER BY применим только к выходному набору записей, и использовать в нем можно только те поля, по которым выполняется группировка. В обычных вложенных запросах (по результатам которых можно строить запросы с группировкой) применение ORDER BY запрещено. Скорее всего (это только мое предположение!) разработчики MS SQL Server Yukon предполагают использовать свойство SqlUserDefinedAggregateAttribute.IsInvariantToOrder для тех случаев, когда программист каким-либо способом все же может гарантировать определенное упорядочивание входных данных – это свойство должно убедить сервер воздержаться от переупорядочивания записей перед агрегированием. Пока что мне не удалось обнаружить какого-либо влияния этого свойства на поведение сервера. |
Для того, чтобы наш объект мог принимать значение NULL, необходимо реализовать интерфейс INullable. Этот интерфейс определяет единственное read-only свойство bool IsNull. Все классы из System.Data.SqlTypes реализуют этот интерфейс. В нашем примере объект принимает значение NULL при инициализации, и перестает быть Null сразу, как только ему будет передано не-NULL значение в метод Accumulate или Merge.
Пользовательские типы данных
Систему типов SQL Server можно расширить с помощью пользовательских типов данных (User-defined Types, UDT). Пользовательские типы реализуются как управляемый класс на любом из CLR-языков и регистрируются в SQL Server. Такой тип можно использовать для определения типа колонки в таблице, или как переменную (параметр процедуры) в выражении Т-SQL. При этом методы объектов можно вызывать прямо из T-SQL.
Создание пользовательского типа данных
В T-SQL пользовательский тип данных регистрируется при помощи оператора CREATE TYPE:
CREATE TYPE [ type_schema_name. ] type_name { [ FROM base_type [ ( precision [ , scale ] ) | ( 'urn:schema-namespace' ) ] [ NULL | NOT NULL ] ] | [ EXTERNAL NAME [ assembly_schema_name. ] assembly_name [ :class_name ] ] } |
В операторе указывается имя класса из предварительно загруженной в базу сборки.
Альтернативой прямому использованию T-SQL, как и в других случаях, служит автоматическое развертывание проектов MS Visual Studio .Net Whidbey. Классы, помеченные атрибутом SqlUserDefinedType (мы подробно рассмотрим его чуть позже – при обсуждении сериализации) автоматически регистрируются в качестве пользовательских типов при развертывании проектов типа SQL Server Project.
Для того, чтобы класс .NET можно было использовать в качестве пользовательского типа данных SQL Server, он должен выполнять некоторые обязанности:
Иметь конструктор без параметров. Как правило, он возвращает экземпляр, соответствующий значению NULL (об этом далее).
Поддерживать NULL-значения. Класс должен реализовывать интерфейс INullable, который описан в предыдущем разделе. Также необходима реализация в классе статического свойства Null, которое возвращает NULL-объект этого класса, т.е. должно быть MyClass.Null.IsNull == true. Все методы должны корректно обрабатывать передачу в параметрах как экземпляра Null, так и значения null.
Поддерживать конверсию в строку и обратно: в классе должен быть определен метод Parse(SqlString s) и должным образом перекрыт метод ToString().
Поддерживать сериализацию. Поскольку объекты этого класса будут храниться на диске, необходимо обеспечить преобразование в «плоский» формат (а также восстановление).
Сериализация является настолько существенной частью поведения пользовательских типов, что ей посвящен отдельный подраздел этой статьи. А пока что мы попробуем создать свой несложный тип данных.
В качестве упражнения я реализовал тип «точка на плоскости». Он умеет представлять свои координаты как в декартовых, так и в полярных координатах.
using System; using System.Data.Sql; using System.Data.SqlTypes; using System.Text.RegularExpressions; using System.Runtime.InteropServices; [Serializable] [SqlUserDefinedType(Format.Native)] [StructLayout(LayoutKind.Sequential)] public class SqlPoint: INullable { #region NULLability private bool _isNull = true; public bool IsNull { get { return _isNull; } } public static SqlPoint Null { get { return new SqlPoint(); } } #endregion #region Конверсия в строку и обратно public override string ToString() { return IsNull? "null" : String.Format("X: {0}; Y: {1}", x, y); } public static SqlPoint Parse(SqlString s) { // Я не очень хорошо владею регулярными выражениями, // тем не менее, этот метод в состоянии преобразовать // результат вызова метода ToString обратно в SqlPoint. if (s.IsNull || s.Value.ToLower() == "null") return Null; SqlPoint p = new SqlPoint(); Regex t = new Regex(@"x:(?\d*(\.\d+)?)\s*\W*y:(?\d*(\.\d+)?)", RegexOptions.IgnoreCase); Match match = t.Match(s.Value); p.x = SqlDouble.Parse(match.Groups["x"].Value); p.y = SqlDouble.Parse(match.Groups["y"].Value); return p; } #endregion #region Наши данные private double _x=0; private double _y=0; public SqlDouble x { get { return IsNull ? SqlDouble.Null : new SqlDouble(_x); } set { if (!value.IsNull) { _x = value.Value; _isNull = false; } } } public SqlDouble y { get { return IsNull? SqlDouble.Null: new SqlDouble(_y); } set { if (!value.IsNull) { _y = value.Value; _isNull = false; } } } public SqlDouble R // А эти свойства мы будем вычислять. { get { return IsNull ? SqlDouble.Null : new SqlDouble(System.Math.Sqrt(_y*_y +_x*_x)); } set { if (value.IsNull) { _isNull = true; return; } double alpha = Alpha.IsNull? 0 : Alpha.Value; _x = System.Math.Cos(alpha) * value.Value; _y = System.Math.Sin(alpha) * value.Value; } } public SqlDouble Alpha { get { return (IsNull) ? SqlDouble.Null : new SqlDouble(System.Math.Atan2(_y, _x)); } set { if (value.IsNull) { _isNull = true; return; } double r = R.IsNull ? 0 : R.Value; _x = System.Math.Cos(value.Value) * r; _y = System.Math.Sin(value.Value) * r; } } #endregion } |
После регистрации данного класса как пользовательского типа выполнение вот таких запросов:
declare @p SqlPoint set @p::x = 3 set @p::y = 4 select @p::x, @p::y, @p::R, @p::ToString() set @p::R = 10 select @p::x, @p::y, @p::R, @p::ToString() set @p::Alpha = 0 select @p::x, @p::y, @p::R, @p::ToString() set @p = convert(SqlPoint, ' x:6; y:5.00') select @p::x, @p::y, @p::R, @p::ToString() |
Приводит к результатам, приведенным в таблице 6:
@p::x | @p::y | @p::R | @p::ToString() |
3.0 | 4.0 | 5.0 | X: 3; Y: 4 |
6.0000000000000009 | 7.9999999999999991 | 10.0 | X: 6; Y: 8 |
10.0 | 0.0 | 10.0 | X: 10; Y: 0 |
6.0 | 5.0 | 7.810249675906654 | X: 6; Y: 5 |
Таблица 6
В приведенном выше примере продемонстрирована работа с членами объектов пользовательских типов. Синтаксис достаточно прост – оператором выбора члена объекта выступает двойное двоеточие.