Microsoft Business Intelligence


Аналитика по остаткам в SSAS: MDX, Snapshot и Many-To-Many. Часть третья.

Часть третья. Реализация через Many-To-Many
(Начало - здесь:http://www.sql.ru/blogs/dklmnmsbi/1450) и здесь: http://www.sql.ru/blogs/dklmnmsbi/1454)
  Немного порассуждаем.
  Изначальная таблица фактов с проводками содержала у нас 1 454 673 записей.
Таблица фактов с остатками, "расширенная" до каждого дня в измерении,- 2 146 853 записей.
Разница не слишком большая, но дает повод задуматься над вопросом: "От чего может зависеть размер таблицы фактов с остатками"? Каждый факт в этой таблице фиксирует наличие ненулевого остатка в определенный день для определенного сочетания "Товар- Склад..". Стало быть, количество строк в этой таблице фактов равно сумме ряда, где каждый член ряда соответствует одному сочетанию "Товар-Склад" и равен числу дней, в течение которых для данного сочетания был ненулевой остаток.
  Рассмотрим вырожденный случай. В некий час X три года назад в учетной системе разом появилось 10 тысяч товаров, распределенные по ста складам. Итого - миллион комбинаций товар склад. (Напоминаю, что мы рассматриваем упрощенный случай, в реально жизни гранулярность учета обычно включает дополнительные измерения, например номер приходной накладной, и тогда это число еще увеличивается).
И дальше все три года товар двигался туда-суда, но ни на одном из складов не было такого, что какой то товар обнулялся. Стало быть, число строк таблиц фактов за трехлетний период в этом случае равно 10000*100 *(365*3)=.. чуть больше миллиарда. Многовато даже для SSAS.
  Можно ли как то избежать этого? Попробуем.
Для этого нужно подойти к задаче несколько с другой стороны. До сих пор мы рассматривали остаток как некую цифирь, которую нужно запихнуть в запрос для таблицы фактов и построить на нем меру. Но его можно рассмотреть и как некое состояние, характеризующее "Товар-Склад", и это состояние характеризуется неким периодом времени, в течении которого оно действительно.
  Из этого следует логика дальнейших действий:
  а)Мы выделяем в предметной области - "Период" как отдельную аналитическую сущность и соответственно отдельное измерение.
Ключом этого измерения логично сделать два поля - Дату начала периода и Дату конца периода.
Приятным бонусом к этому измерению будет приложен атрибут "Длительность периода", который логично и удобно использовать для аналитики по длительности любых бизнес процессов.
  б)Таблицу фактов мы строим только для изменений остатка. С учетом того, что для некоторых сочетаний "Товар-Склад" бывает несколько движений в течении дня, а нас интересует остаток только на конец дня (ну или начало), эта таблица фактов будет содержать не больше строк, чем исходная таблица с проводками.
Для связи с измерением "Период" у нас в этой таблице фактов будут два поля. Их физический смысл - "Дата появления остатка с таким значением на складе" и "Дата изменения этого значения". Например, если 1 января 2011 г на склад X попал товар Y в количестве 10 штук, а 10 января этот товара со склада был израсходован в кол-ве 5 штук, то для связи с измерение "Период" мы используем две даты - 1 января и 9 января. Если же больше движений по этому товару на этом складе не было, второй датой мы берем дату - сегодня (GETDATE()).
  в)но нам нужна связь с нашим обычным измерение "Дата". Здесь вполне очевидно напрашивается Many-To-Many, так как одному элементу измерения "Периоды" может соотв. более одного элемента измерения "Даты". Для этой связи нам нужна Bridge группа мер, в которой каждой дате мы соотносим те периоды, в которые эта дата попадает.
  г)Увязав все это в кучу, мы получаем снова полуаддитивную меру с остатками, но на этот раз без катастрофического разбухания таблицы фактов.
  д)Так как красоту подхода в бухгалтерии не оценят, а Альфред Нобель жил в докомпьютерную эпоху.. придется довольствоваться моральным удовлетворением... :-)
--
  Итак, поехали.
  а)Строим измерение "Период".
Именованный запрос в .dsv для этого измерения выглядит так:
/*Попарная комбинации всех дат из диапазона от начала аналитики до текущей даты*/
SELECT dbo.udfDateToInteger(d1.DATE_CURRENT) AS DATE_BEGIN_PERIOD_KEY/*Дата начала периода*/,
dbo.udfDateToInteger(d2.DATE_CURRENT) AS DATE_END_PERIOD_KEY /*дата конца периода*/,
 DATEDIFF([day], d1.DATE_CURRENT, d2.DATE_CURRENT) AS INTERVAL, 
 CONVERT(char(10), d1.DATE_CURRENT, 103) + '-' + CONVERT(char(10), d2.DATE_CURRENT, 103) AS N_PERIOD
FROM   dbo.udf_GetDateRange('20110801', GETDATE()) d1 
JOIN dbo.udf_GetDateRange('20110801', GETDATE()) AS d2 ON d1.DATE_CURRENT <= d2.DATE_CURRENT

а само измерение - так:
Картинка с другого сайта.

  б)Таблица фактов для группы мер с остатками.
select mr.C_MOVE, mr.C_PRODUCT, mr.C_WARE_HOUSE,  dbo.udfDateToInteger(mr.DATE_BEGIN) as DATE_BEGIN_KEY,  dbo.udfDateToInteger(mr.DATE_END) as DATE_END_KEY , mr.MOVE_REM from vOlapMoveRemByPeriods mr


  в)Таблица фактов F_BRIDGE_DATE_VALUES_IN_DATE_RANGES для промежуточной - Bridge группы мер , необходимой для Many-To-Many связи с измерением Даты.
SELECT         dbo.udfDateToInteger(d1.DATE_CURRENT) AS DATE_BEGIN_PERIOD_KEY,  dbo.udfDateToInteger(d2.DATE_CURRENT) AS DATE_END_PERIOD_KEY,  dbo.udfDateToInteger(dates_in_period.DATE_CURRENT) AS DATE_IN_PERIOD_KEY
FROM dbo.udf_GetDateRange('20110801', GETDATE()) AS d1 
JOIN  dbo.udf_GetDateRange('20110801', GETDATE()) AS d2 ON d1.DATE_CURRENT <= d2.DATE_CURRENT 
CROSS APPLY[dbo].[udf_GetDateRange](d1.DATE_CURRENT, d2.DATE_CURRENT) dates_in_period


  г)Связываем все это в структуре куба в dsv, строим нужны меры:
Картинка с другого сайта.

и прописываем Many-to-many связи
Картинка с другого сайта.

Деплоим, процессим, тестим. Теперь у нас есть три варианта реализации мер с остатками.
Идентичность значений можно проверить например таким запросом:
select 
{[cmRemByINCOME_OUTCOME],[REM_FOR_EVERY_DAY],[REM_BY_PERIODS]} on 0,
non empty [DimDate].[Hierarchy].[Month].&[201211].Children on 1 
from [Inventory-Cube]
--where{[DimWareHouses].[WareHouseParent].&[110119643]}


Картинка с другого сайта.

   д)Таким образом, мы реализовали одну задачу тремя способами.
Вопрос, в каких случаях от этого может быть практическая польза (ну кроме уважительного "Месье знает толк в извращениях" от специалистов MS BI :-) ), я постараюсь обсудить в следующей статье..
  Замечу, что этот подход хорошо работает если у нас есть дофига чего дофига где, но двигается оно не слишком часто.
Если у нас каждый день по каждому сочетанию "Товар-Склад" происходят изменения, то уменьшения таблицы фактов - не будет. А уменьшение производительности из за M-2-M обязательно произойдет.
Продолжение - следует.
(SSAS проект и скрипт для создания метаданных в реляционной базе можно взять здесь:https://www.dropbox.com/s/e42ueesskoexk83/Inventory-SSAS-Project.zip )
----------------
Кальманович Дмитрий.
добавлено: 04 фев 13 просмотры: 2572, комментарии: 0



Аналитика по остаткам в SSAS: MDX, Snapshot и Many-To-Many. Часть вторая.

Часть Вторая. Реализация через semiadditive measures
(Начало - здесь:http://www.sql.ru/blogs/dklmnmsbi/1450)
Итак, наша новая задача - кардинально увеличить производительность запросов, в которых выводятся наши остатки (не поймите выражение "наши остатки" неправильно :-) ).
План - такой:
а)Делаем расчет в реляционной базе остатков, которые образуются после каждого "Движения".
б)Делаем в SSAS таблицу фактов, в которой для каждого сочетания "Товар - Склад" (при добавлении других измерении естественно гранулярность подсчета меняется), и для каждого дня, когда есть ненулевой остаток, подсчитаны текущие значения.
в)Делаем в SSAS на основе этой таблицы полуаддитивные меры с остатками. Привязка к измерениям остается тривиальной.
г)Тестируем отчетность. Убеждаемся что значения новой меры совпадает со старой, меняем используемую меру в существующих отчетах.
д)Наблюдаем удовлетворенные лица пользователей. Идем в кассу за премией.

Детали реализации
а)Для хранения подсчитанного остатка мы создадим отдельную таблицу.
CREATE TABLE [dbo].[L_MOVE_REM](
	[C_MOVE] [int] PRIMARY KEY REFERENCES [dbo].[L_MOVE] ([C_MOVE]),
/*ссылка на проводку, которая сформировала остаток*/
	[C_MOVE_NEXT] [int] NULL REFERENCES [dbo].[L_MOVE] ([C_MOVE]),
/*ссылка на следующую проводку по этому сочитанию Товар- Склад - (и т.д. в зависимости от гранулярности)
-нужно для оптимизации запроса*/
	[MOVE_REM] [decimal](18, 3) NOT NULL,
/*остаток который образовался после "движения товара" в жизни, ну или "проводки" в учетной системе*/
)


Сам расчет производим следующим скриптом:
+ Скрипт для расчета нарастающего итога и остатков

USE Inventory;
truncate table L_MOVE_REM
DECLARE @RunningTotal decimal(18,3)
declare @tmp_moves table  (C_MOVE int, MOVE_DATE DATE, QTY DECIMAL(18,3), QTY_ROLLING decimal(18,3));
DECLARE @C_PRODUCT int, @C_WARE_HOUSE INT;
declare @cnt int; 
set @cnt=0;
DECLARE @mess varchar(100);
DECLARE CRS CURSOR FOR
select   m.C_PRODUCT,m.C_WARE_HOUSE
from L_MOVE m group by  m.C_PRODUCT,m.C_WARE_HOUSE --having COUNT(*)  between 5 and 10;

OPEN CRS; --цикл по всем сочетаниям "Товар-Склад"
FETCH NEXT FROM CRS
INTO @C_PRODUCT, @C_WARE_HOUSE
WHILE @@FETCH_STATUS = 0 
BEGIN
set @cnt=@cnt+1;
set @mess=CAST(@cnt as varchar(10));raiserror(@mess,5,10) with nowait; 
delete from @tmp_moves; 

insert @tmp_moves(C_MOVE, MOVE_DATE, QTY)--
SELECT
max(m.c_move) as MIN_C_MOVE /*НА конец  ДНЯ СЧИТАЕМ, поэтому соотносим остаток с последней проводокой задень если их несколько*/,  
M.MOVE_DATE,sum(m.MOVE_QTY  *case when m.DTCR=0 then 1 else -1 end) 
 from L_MOVE m 
 where  m.C_PRODUCT=@C_PRODUCT  and m.C_WARE_HOUSE=@C_WARE_HOUSE
 group by m.MOVE_DATE order by  m.MOVE_DATE asc;
 ----
SET @RunningTotal = 0 
UPDATE @tmp_moves SET @RunningTotal = QTY_ROLLING = @RunningTotal + QTY FROM @tmp_moves;
--считаем остаток через "нарастающий итог"
with cte_rn as (/*джойним по порядковому номеру чтобы наити следующее значение остатка*/
select m.*, ROW_NUMBER() over (order by move_date asc) as RN from  @tmp_moves m)
, cte_joined_to_next as (
select  m.*, mnext.C_MOVE as C_MOVE_NEXT from cte_rn m left join cte_rn mnext on m.RN=mnext.RN-1)
insert L_MOVE_REM(C_MOVE, C_MOVE_NEXT,MOVE_REM)
select mm.C_MOVE, mm.C_MOVE_NEXT, mm.QTY_ROLLING from cte_joined_to_next mm;
--
 FETCH NEXT FROM CRS
INTO @C_PRODUCT, @C_WARE_HOUSE
   END
CLOSE CRS;
DEALLOCATE CRS;
--------



Подозреваю, что такой способ подсчета не является оптимальным по производительности, к тому же здесь используется не вполне задокументированный способ подсчета нарастающего итога
(UPDATE @tmp_moves SET @RunningTotal = QTY_ROLLING = @RunningTotal + QTY FROM @tmp_moves;).
С благодарностью приму варианты реализации подсчета без таких "кул-хаков".

б) Таблица фактов для SSAS
У нас есть подсчитанные значения всех остатков на те дни, когда этот остаток был изменен.
Но нам для таблицы фактов нужны все дни, в том числе те, когда движения не происходили.
Чтобы облегчить себе жизнь при дальнейшем развитии системы,мы делаем некоторые вспомогательные функции и вьюхи
+ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ и вьюхи

CREATE  FUNCTION [dbo].[udfDateToInteger](@Date DATE)
returns int --для превращения дат в интовые ключи -удобные для использования в качестве ключа измерения
as
begin
    return Year(@Date)*10000+Month(@Date)*100+Day(@Date);
end
GO

GO
CREATE FUNCTION [dbo].[udf_GetDateRange] 
(	--просто выводим перечень последовательных дат из заданного диапазона
	@DATE_BEGIN DATE,
	@DATE_END DATE
)
RETURNS  @RETTABLE TABLE(DATE_CURRENT DATE)
AS 
BEGIN
declare @i int
declare @dat datetime
set @i=0
set @dat=@DATE_BEGIN;
while @dat<=@DATE_END
begin
INSERT INTO @RETTABLE VALUES (@dat);
set @i=@i+1;
set @dat=DATEADD("d",@i,@DATE_BEGIN)
end
return;
END
GO
CREATE  VIEW [dbo].[vOlapMoveRemByPeriods] as 
select 
 m.C_MOVE,
m.MOVE_DATE as DATE_BEGIN, coalesce(DATEADD(DAY, -1,mnext.move_date), cast(getdate() as date)) as DATE_END,
--dbo.udfDateToInteger(m.MOVE_DATE) as DATE_BEGIN_KEY,  dbo.udfDateToInteger(coalesce(DATEADD(DAY, -1,mnext.move_date), cast(getdate() as date))) as DATE_END_KEY,
  m.C_PRODUCT, m.C_WARE_HOUSE,mr.MOVE_REM 
  from L_MOVE_REM  mr join L_MOVE m on m.C_MOVE=mr.C_MOVE left join L_MOVE mnext on mnext.C_MOVE=mr.C_MOVE_NEXT
GO


И с помощью этих функций формируем запрос для таблицы фактов F_REM_FOR_EVERY_DAY,которая по сути представляет собой некий "Вычисляемый snapshop на конец дня".
SELECT m.C_MOVE, dbo.udfDateToInteger(daterange.DATE_CURRENT) as DATE_REM_KEY,
m.C_PRODUCT, m.C_WARE_HOUSE,m.MOVE_REM 
FROM vOlapMoveRemByPeriods m
CROSS APPLY dbo.udf_GetDateRange(m.DATE_BEGIN,m.DATE_END) daterange

в)Строим в SSAS на этот раз уже физическую меру "Остаток" (REM_FOR_EVERY_DAY) с AggregateFunction=LastChild
Получается такая картина:
Картинка с другого сайта.

г)Тестируем через сравнение с значениями остатка по старой, вычисляемой мере, например таким запросом
select 
{[DimWareHouses].[WareHouseParent].&[110119643]}
*{[cmRemByINCOME_OUTCOME],[REM_FOR_EVERY_DAY]} on 0,
non empty [DimDate].[Hierarchy].[Month].&[201211].Children on 1 
from [Inventory-Cube]

Убедившись что значения новой (физической) и старой (вычисляемой) мерой совпадают, выдумываем запрос посложнее и смотрим в профайлере производительность.
Изменяем название меры в существующих отчетах
д)Забираем в кассе премию и .. задумываемся еще немного..
Продолжение - следует.
(SSAS проект и скрипт для создания метаданных в реляционной базе можно взять здесь: https://www.dropbox.com/s/e42ueesskoexk83/Inventory-SSAS-Project.zip )
----------------
Кальманович Дмитрий.
добавлено: 02 фев 13 просмотры: 1740, комментарии: 0



Аналитика по остаткам в SSAS: MDX, Snapshot и Many-To-Many. Часть первая.

Аналитика по складским остаткам, - классическая задача, с которой сталкиваются, наверное, почти все специалисты Business Intelligence. В рунете на эту тему можно почитать например, на сайте Южакова: SSAS - анализ остатков.
Здесь я хочу показать на примере два стандартных подхода к решению этой задачи, и один, как мне кажется, новый.
  Обычный бизнес- сценарий:
-на входе имеем такую схему первичного учета, - есть специальная таблица «проводок», в которой каждое движение товара на складе (поступление, перемещение, списание и т.д.) фиксируется отдельной записью.
-на выходе нужно получать отчетность, позволяющую увидеть остаток на произвольную дату в разрезе товара, склада и т.д.    Если мы заменим слово «Склад» на слово «Счет», а слово «Количество товара» на «Сумма», то задача складского учета превращается в задачу финансового учета, соответственно на выходе бухгалтерия хочет увидеть «Оборотно-Сальдовую ведомость».
  Рассмотрим на примере.
Изначально у нас есть: два справочника (с возможность построить "деревянные иерархии" – товары и склады,
и таблица "Проводок" (Движения товара по складам, приход и расход - отдельными записями).
Метаданные в хранилище данных выглядят так:
+ DDL

CREATE TABLE [dbo].[S_WARE_HOUSES](/*Склады*/
	[C_WARE_HOUSE] [int] PRIMARY KEY,
	[C_WARE_HOUSE_PARENT] [int] NOT NULL REFERENCES      [S_WARE_HOUSES]([C_WARE_HOUSE]), --для "Деревянной иерархии"
	[N_WARE_HOUSE] [nvarchar](240) NOT NULL)
GO
CREATE TABLE [dbo].[S_PRODUCTS](/*Товары*/
	[C_PRODUCT] [int] PRIMARY KEY,
	[C_PRODUCT_PARENT] [int] NOT NULL REFERENCES [S_PRODUCTS]([C_PRODUCT]), --для "Деревянной иерархии"
	[N_PRODUCT] [nvarchar](240) NOT NULL,
) 
GO
CREATE TABLE [dbo].[L_MOVE]( /*Проводки*/
	[C_MOVE] [int] PRIMARY KEY,
	[C_PRODUCT] [int] NOT NULL  REFERENCES [S_PRODUCTS]([C_PRODUCT]),  --ссылка на товар
	[C_WARE_HOUSE] [int] NOT NULL REFERENCES [S_WARE_HOUSES]([C_WARE_HOUSE]), --ссылка на склад 
	[MOVE_DATE] [date] NOT NULL, --дата проводки
	[DTCR] [int] NOT NULL, /*0- приход, 1 расход*/
	[MOVE_QTY] [decimal](18, 3) NOT NULL, /*количество товара которое пришло (0)   или ушло (1)*/
	[MOVE_REM] [decimal](18, 3) NULL,
	[MOVE_DATE_KEY]  AS ((datepart(year,[MOVE_DATE])*(10000)+datepart(month,[MOVE_DATE])*(100))+datepart(day,[MOVE_DATE])) PERSISTED
	/*вычисляемое поле нужное для связи с олапным измерением «Дата»*/,
) 
GO

CREATE  INDEX [IX_L_MOVE_C_PRODUCT] ON [dbo].[L_MOVE] ([C_PRODUCT] )
CREATE  INDEX [IX_L_MOVE_C_WARE_HOUSE] ON [dbo].[L_MOVE]([C_WARE_HOUSE] )
CREATE  INDEX [IX_L_MOVE_MOVE_DATE] ON [dbo].[L_MOVE] ([MOVE_DATE] )


В эти таблицы я загрузил некую тестовую выборку данных из реальной базы. Получилось 12 000 товаров, 3 400 складов, и 1 500 000 проводок за период в три года.
   Для начала попробуем решить задачу через MDX Calculations. Нам нужно:
а)Построить таблицу фактов, в которой будут храниться движения товара на складе.
б)На основе этой таблицы фактов строим группу мер с мерами «Приход», «Расход» и «Оборот».
в)Построить вычисляемые меры "Приход", "Расход", "Оборот" и "Остаток на дату".
Причем "Остаток на дату" мы вычисляем суммируя меру "Оборот" "с начала времен".
г)Обеспечить заказчика нужной отчетностью и повысить тем самым производительность труда своей фирмы
д)Идти в бухгалтерию получить премию. :-)

  Детали реализации:
   а)В dsv рисуем следующие таблицы:
Картинка с другого сайта.

  Б)Измерения и меры в кубе
На основе этих таблиц строим:
-Измерения:
--Склады (DimWareHouses)
--Товары (DimProducts)
--Тип Движения (приход или расход) (DimTransactionTypes)
--Дата (DimDate).
-Группа мер:
--Проводки ([F_MOVE]) - обычная мера с суммированием.
Картинка с другого сайта.

  в)Вычисляемые меры:
+ MDX CubeCalculatedMembers

CREATE MEMBER CURRENTCUBE.[Measures].[cmMOVE_QTY_INCOME] --ПРИХОД
as ([DimTransactionTypes].[TransactionTypes].&[0], [MOVE_QTY])
,NON_EMPTY_BEHAVIOR={[MOVE_QTY]} 
,FORMAT_STRING="#,#"
,Caption='Приход'
,ASSOCIATED_MEASURE_GROUP = 'F_MOVE';
 CREATE MEMBER CURRENTCUBE.[Measures].[cmMOVE_QTY_OUTCOME] --РАСХД
as ([DimTransactionTypes].[TransactionTypes].&[1], [MOVE_QTY])
,FORMAT_STRING="#,#"
,NON_EMPTY_BEHAVIOR={[MOVE_QTY]} 
,Caption='Расход'
,ASSOCIATED_MEASURE_GROUP = 'F_MOVE';
CREATE MEMBER CURRENTCUBE.[Measures].[cmMOVE_QTY_INCOME_OUTCOME] --ОБОРОТ
as [cmMOVE_QTY_INCOME] - [cmMOVE_QTY_OUTCOME]
,FORMAT_STRING="#,#"
,NON_EMPTY_BEHAVIOR={[MOVE_QTY]} 
,Caption='Оборот'
,ASSOCIATED_MEASURE_GROUP = 'F_MOVE'; 
CREATE MEMBER CURRENTCUBE.[Measures].[cmRemByINCOME_OUTCOME] 
/*остаток на конец периода расчитываемый по 
сумме оборота с начала времен*/
as sum({null:tail(exists([DimDate].[Day].[Day], [DimDate].[Hierarchy].CurrentMember),1)(0)},[cmMOVE_QTY_INCOME_OUTCOME] ) 
,Caption='Ост. на конец пер.'
,FORMAT_STRING="#,#"
,ASSOCIATED_MEASURE_GROUP = 'F_REM_FOR_EVERY_DAY'; 



  г)Отчетность.
Все. У нас есть меры "Приход", "Расход", "Оборот" и "Остаток на дату".
Мы можем строить отчет, например такой -"Динамика остатка на складе за один месяц":
select 
{[cmMOVE_QTY_INCOME],[cmMOVE_QTY_OUTCOME] ,[cmMOVE_QTY_INCOME_OUTCOME], [cmRemByINCOME_OUTCOME]} on 0,
non empty [DimDate].[Hierarchy].[Month].&[201211].Children on 1 
from [Inventory-Cube]
where 
({[DimWareHouses].[WareHouseParent].&[110119643]})


Результатом этого запроса будет такая таблица.
ДатаПриходРасходОборотОст. на конец пер.
01.11.20121776634082-163161538312
02.11.20121538312
03.11.20121668624898-82121530100
04.11.20125931453-139371516163
05.11.2012218327043-24861491303
06.11.20125100614568364381527741
07.11.20121527741
08.11.20121527741
09.11.2012129436162851131511640892
10.11.2012


  д)В теории.. все шоколадно. А в жизни - по разному.
Гранулярность мер в кубе обычно включает большее количество измерений, чем в тестовом примере.
Пользователь, привыкнув к "кубо-верчению", выдумывает свои отчетные показатели с своей логикой агрегирования вычисляемых мер, вроде "Динамики среднего остатка по товарным группам, в которых этот средний остаток превышает определенный порог", и тому подобное.
Рано или поздно при использовании вычисляемых мер мы сталкиваемся с проблемой производительности.
И выясняется что.. пункт "д", где про премию, откладывается. :-(

Продолжение - следует.
(SSAS проект и скрипт для создания метаданных в реляционной базе можно взять здесь:
https://www.dropbox.com/s/e42ueesskoexk83/Inventory-SSAS-Project.zip )
----------------
  Кальманович Дмитрий.
добавлено: 31 янв 13 просмотры: 2268, комментарии: 0