This article was
originally written for Analysis Services 2005, but everything still
applies today even for Analysis Services 2012 in Multidimensional mode.
First thing to do: figure out what type of currency conversion you need. There are 3 types.
-
MANY-TO-MANY. This is where the facts are stored in multiple currencies – i.e. amounts for the same measure are in various currencies in the same fact table. Also, the users might want to report the total amount in different currencies.
-
MANY-TO-ONE. The facts are stored in multiple currencies, but this time it is only necessary to ever report the total amount in a single currency – e.g. a corporate currency.
-
ONE-TO-MANY. The facts are always stored in one currency only. However, the users might want to report the total amount in different currencies.
This post focuses on the different options available for One-to-Many. With One-to-Many, there are 2 options: measure expressions or the currency conversion wizard.
If
you create a DSV on top of AdventureWorksDW and include DimTime,
FactInternetSales, FactCurrencyRate and DimCurrency, it should look
something like this.
The
relationship between FactInternetSales and DimCurrency is for the case
where your facts are stored in multiple currencies (Many-to-Many and
Many-to-One). To look at One-to-Many, we will delete this relationship. Also,
we will delete the ShipDate and DueDate relationships between
FactInternetSales and DimTime, but leave the OrderDate relationship
(this post is about currency conversion, not role-playing dimensions!).
We now have the following DSV diagram.
ONE-TO-MANY USING MEASURE EXPRESSIONS
I created a cube on top of this DSV. Here is a backup of it http://blogs.conchango.com/christianwade/CurrencyConversionBackup.zip You
can download, restore and open it using the VisualStudio using the File
> Open > Analysis Services Database menu option.
The only measures in it are Sales Amount (from FactInternetSales) and End Of Day Rate (from FactCurrencyRate). Sales Amount has a FormatString of “Currency”. End Of Day Rate has an AggregateFunction property value of “LastNonEmpty”. LastNonEmpty is a semi-additive measure. We want it to sum for all dimensions except Date. For the Date dimension, it will take the last non-empty child. For
example, the exchange rate we would want to see for a week member in a
report would not be the sum of the exchange rate on Monday + exchange
rate on Tuesday + exchange rate on Wednesday, … Instead, we just want the most recent exchange rate (maybe exchange rate on Saturday).
The Date dimension is very simple. The
Date attribute, which is the key attribute, has a KeyColumns property
of TimeKey (which is the surrogate key of the DimTime table) and a
NameColumn of FullDateAlternateKey (what the users see at Date level).
I used the Dimension Wizard to create the Date dimension so that it would be flagged with Type = Time, etc.
This is one of the few cases where having these properties set
correctly actually affects cube behaviour. For example, semi-additive
measures and some MDX functions like YTD won't work without Type = Time.
Here are the mappings in the Dimension Usage tab. They are pretty straightforward. There is a many-to-many relationship between Currency and Fact Internet Sales.
Here is a screenshot of the Currency dimension. The main points about the Currency dimension are as follows.
-
The KeyColumns property of the Currency attribute is set to CurrencyKey (which is the surrogate key in for the DimCurrency table in AdventureWorksDW).
-
The NameColumn property of the Currency attribute is set to CurrencyName (which is what we want the users to see).
-
The Type property of the dimension is set to Currency. The only purpose of this (unlike Type=Time that does affect cube behaviour) is to inform client tools in case they want to display currency dimensions differently to regular dimensions.
-
The Type property of the Currency attribute is set to CurrencyName. Again, this is just to inform client tools.
-
The IsAggregatable property of the Currency attribute is set to False. This removes the All level for the attribute. We would after all not want to sum the values of different currencies. 500 pounds sterling plus 100,000 cruzeiros equals 100,500 of what currency? Monopoly money?
-
Set the DefaultMember property of the Currency attribute to whatever the Sales Amount values are stored in. In the case of AdventureWorksDW, it is US Dollars.
-
The attribute relationship between Currency and Currency Alternate Key has its Cardinality property set to One. This is because, for a particular Currency, there can be only one Currency Alternate Key – i.e. they have a one-to-one relationship. This improves efficiency in aggregation because the numbers will always be the same (e.g. £500 for “Great Britain Pounds” will always result in £500 for “GBP”). Analysis Services will therefore not bother figuring out the aggregate values for Currency Alternate Key. It will simply re-use those of Currency.
-
Set the AttributeHierarchyEnabled property of the Currency Alternate Key attribute to False. If it is not set, Analysis Services will not allow deployment of the project because of the attribute relationship with Currency and that Currency has IsAggregatable = False.
Here is the measure expressions bit. The Sales Amount measure has the following measure expression: “[Sales Amount] / [End Of Day Rate]”. The facts are stored in US Dollars and the “pivot currency” is US Dollars. The pivot currency is the currency the exchange rate values convert from. The measure expression is a division rather than a multiplication because this is the way the exchange rates are held. For example, the exchange rate for GBP is roughly 1.5 in the FactCurrencyRate table. Therefore, to convert $15 from the pivot currency to GBP, 15 / 1.5 gives us 10. Doing a multiplication would result in £22.50 (obviously wrong). Note: measure expressions are done at leaf level.
It is worth going back into the Dimension Usage tab and setting the DirectSlice property of the many-to-many relationship. Setting
it to “([Currency].[Currency].&[100])” means that, when querying
Sales Amounts by the base/pivot currency (US Dollars), Analysis Services
will just return the value as it appears in the fact table – i.e.
without applying the measure expression. After all, there is no need to
convert US Dollars into US Dollars! If we did not set
DirectSlice, we would have to ensure that the FactCurrencyRate table has
an exchange rate of 1 for converting US Dollars into US Dollars (and
for every single day for which we have data). Otherwise, Analysis Services would be looking for an End Of Day Rate to divide by, find null and return null.
It should also perform better when querying by USD with DirectSlice
populated correctly because it doesn't have to bother with
the conversion calculation at all. So what we want is just a tuple with
the default member for every enabled attribute in the outer
many-to-many dimension. Incidentally, this is what the Root(<dimension_expression>)
function would return, but we can't use any MDX functions in the
DirectSlice property (hence the Functions pane is disabled), so we have
to explicitly list each default member in a tuple.
Lastly, we will insert some MDX in the Calculations tab that sets the Locale ID for currencies. This will avoid displaying 500 pounds sterling as “$500” (goodness gracious!). For a complete list of the Locale IDs, see http://www.microsoft.com/globaldev/reference/lcid-all.mspx
Language([Currency].[Currency].[United Kingdom Pound]) = 2057;
Language([Currency].[Currency].[Brazilian Real]) = 1046;
Language([Currency].[Currency].[US Dollar]) = 1033;
These assignments are for illustration purposes only. The main problem with this approach is maintainability. If we bring in new currencies in the future, we need to modify the MDX script. The ideal would be to store them in the DimCurrency table, expose them using the ValueColumn property of the Currency attribute, and use that for the assignment. Some may prefer to go the member property route, but I think this is a good use case for the ValueColumn property.
Language(([Currency].[Currency].Members, [Measures].[Sales Amount])) =
[Dim Currency].[Currency].CurrentMember.MemberValue;
Let’s build and deploy! Now let’s browse!
On the face of it, these numbers seem OK. 29,358,677 is roughly 1.5 times 19,685,311. But let’s double check. Copying and pasting into Excel reveals that this is not the case. There is actually a discrepancy of £784,195! Why is that? Well, this is actually the behaviour that we want …
If you remember, measure expressions are done at leaf level. This calculation is, as George Spofford would say, “non-commutative”. Addition/subtraction
combined with multiplication/division will result in different numbers
depending on whether the multiplication/division is done at leaf level
or the aggregated level. Analysis Services has done the currency conversion at leaf level – i.e. at the day that each transaction actually took place. This is of course much more accurate.
So let’s test this as well. Drilling into Date reveals the following.
Copying
and pasting the Date-level data into Excel and applying the
multiplication reveals that the numbers are indeed calculated correctly
at leaf level.
Locale Ids: To Get Complete List Of Locale Ids.
No comments:
Post a Comment