Dagens lärdom om DistinctCount i SSAS

by Martin 30. June 2010 23:32
Det är ju lätt att tänka sig att ett mätetal som har aggregeringsformen _DistinctCount_ fungerar precis som följande sql-kod hade gjort: {code:sql} select count(distinct Namn) from MinTabell {code:sql} Om vi tänker oss att *MinTabell* haft följande innehåll: || Id || Namn || Antal || | 1 | Sven | 4 | | 2 | (NULL) | 3 | | 3 | David | 8 | | 4 | Rikard | 2 | | 5 | Rikard | 4 | | 6 | David | 7 | | 7 | Sven | 7 | Så hade ju sql-koden ovan returnerat 3 (tre). Hade man gjort samma sak med ett mätetal som haft aggregeringstypen _DistinctCount_ hade man istället fått svaret 4 (fyra). Anledningen till detta är att SQL räknar inte med null-värden medan SSAS anser att null är ett värde som alla andra. Lätt att glömma men helt klart värt att tänka på.

Hur man använder "in" i filter för t ex ett dataset

by Martin 29. June 2010 11:02
Om man använt sig av filter på sina dataset så har man förmodligen även sett att man under _operator_ kan välja _in_. Jag har aldrig lyckats få till hur de funkar men nu tog jag tag i saken och kom fram till att det kan vara ganska användbart. Hemligheten är att under _Value_ skriva ett uttryck som returnerar en array. Smidigt nog sker detta automatiskt om man har en parameter där man tillåter flera värden (_Allow multi values_). Man kan då ange värden enligt följande: [image:http://www.martinfranson.se/image.axd?picture=2010%2f6%2fFilterDataset.png] I _Expression_ har jag alltså hänvisat till ett fält i mitt dataset och i _Value_ till parametern _ObjectFilter_. Vill man skriva dessa "normalt" så blir då det under _Expression_: {{ =Fields!Kurs.Value }} och under _Value_: {{ =Parameters!ObjectFilter.Value }} Om man inte har en parameter utan vill använda sig av en kommaseparerad sträng bör det gå lika bra genom att skriva något i stil med följande under _Value_: {{ ="Kurs1,Kurs2,Kurs3".Split(,) }} Detta eftersom funktionen *Split* returnerar en array.

Hämta klientdatorns namn eller ip-adress

by Martin 21. June 2010 10:01
Ibland vore det smidigt att kunna skriva ut på en rapport från vilken dator en användare skapade den, t ex om användaren Lisa exekverade rapporten från hennes arbetsstation kallad CorpComp1. Det finns ingen inbyggd funktion för detta i Reporting Services. Däremot finns ju alltid möjligheten att nyttja Custom Code. Följande kod skulle kunna användas: {code:vb.net} Public Function GetClientIP() As String Dim result as String result = System.Web.HttpContext.Current.Request.ServerVariables("REMOTE_ADDR") return result End Function Public Function GetClientHostName() As String Dim result as String result = System.Net.Dns.GetHostEntry(System.Web.HttpContext.Current.Request.ServerVariables("remote_addr")).HostName return result End Function {code:vb.net} Där du då vill skriva ut användarens datornamn skriver du följande uttryck (expression) {{ =Code.GetClientHostName() }} Vill du istället skriva ut IP-adressen så görs det med följande uttryck (expression) {{ =Code.GetClientIP() }}

Subrapporter och snabshots

by Martin 15. June 2010 13:05
Vill man ha en sammanställning av rapporter i en huvudrapport och sedan få bra prestanda känns det ju lockande att använda sig av snapshots. Vid försök så går rapporten utmärkt att köra i Report Designern (Visual Studio) men vid deploy till Report Server får man bara felet *Error: Subreport could not be shown* Anledningen till detta är helt enkelt att subrapporter inte kan visa rapporter som exekveras som snapshots. Lösningen är att göra så rapporten kör live hela tiden. _Källa: [url:http://connect.microsoft.com/SQLServer/feedb...|http://connect.microsoft.com/SQLServer/feedback/details/196067/reporting-services-subreports-and-snapshots]_

Bygg om alla index i en databas

by Martin 2. June 2010 11:54
Nedanstående skript städar index på en 2005 databas (OBS måste vara i 90-mode på kompabilitet, (dvs SQL2005) Se till att rätt databas är vald innan man startar skriptet, det är generellt och kan köras på alla 2005 databaser. Innan man kör detta kan det vara en poäng att kolla artikeln [kontrollera fragmenteringsgrad på index] {code:sql} SET NOCOUNT ON; DECLARE @objectid int; DECLARE @indexid int; DECLARE @partitioncount bigint; DECLARE @schemaname sysname; DECLARE @objectname sysname; DECLARE @indexname sysname; DECLARE @partitionnum bigint; DECLARE @partitions bigint; DECLARE @frag float; DECLARE @command varchar(8000); -- ensure the temporary table does not exist IF EXISTS (SELECT name FROM sys.objects WHERE name = 'work_to_do') DROP TABLE work_to_do; -- conditionally select from the function, converting object and index IDs to names. SELECT object_id AS objectid, index_id AS indexid, partition_number AS partitionnum, avg_fragmentation_in_percent AS frag INTO work_to_do FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'LIMITED') WHERE avg_fragmentation_in_percent > 10.0 AND index_id > 0; -- Declare the cursor for the list of partitions to be processed. DECLARE partitions CURSOR FOR SELECT * FROM work_to_do; -- Open the cursor. OPEN partitions; -- Loop through the partitions. FETCH NEXT FROM partitions INTO @objectid, @indexid, @partitionnum, @frag; WHILE @@FETCH_STATUS = 0 BEGIN; SELECT @objectname = o.name, @schemaname = s.name FROM sys.objects AS o JOIN sys.schemas as s ON s.schema_id = o.schema_id WHERE o.object_id = @objectid; SELECT @indexname = name FROM sys.indexes WHERE object_id = @objectid AND index_id = @indexid; SELECT @partitioncount = count (*) FROM sys.partitions WHERE object_id = @objectid AND index_id = @indexid; -- 30 is an arbitrary decision point at which to switch between reorganizing and rebuilding IF @frag < 30.0 BEGIN; SELECT @command = 'ALTER INDEX ' + @indexname + ' ON ' + @schemaname + '.' + @objectname + ' REORGANIZE'; IF @partitioncount > 1 SELECT @command = @command + ' PARTITION=' + CONVERT (CHAR, @partitionnum); EXEC (@command); END; IF @frag >= 30.0 BEGIN; SELECT @command = 'ALTER INDEX ' + @indexname +' ON ' + @schemaname + '.' + @objectname + ' REBUILD'; IF @partitioncount > 1 SELECT @command = @command + ' PARTITION=' + CONVERT (CHAR, @partitionnum); EXEC (@command); END; PRINT 'Executed ' + @command; FETCH NEXT FROM partitions INTO @objectid, @indexid, @partitionnum, @frag; END; -- Close and deallocate the cursor. CLOSE partitions; DEALLOCATE partitions; -- drop the temporary table IF EXISTS (SELECT name FROM sys.objects WHERE name = 'work_to_do') DROP TABLE work_to_do; GO {code:sql}

Hindra Reporting Services från att automatiskt ändra MDX-frågor

by Martin 2. June 2010 11:38
Om man i Reporting Services har ett MDX-dataset med en parameter så skapar Reporting Services automatiskt upp ett dataset för den parametern. Om det inte syns från början så kan man alltid visa det genom att höger-klicka på datakällan och välja _Show hidden datasets_. Det i sig är en ganska trevlig funktion. Vad som INTE är så roligt är att om jag har ändrat mitt dataset som genererades, t ex filtrerat bort vissa parametervärden, och sedan ändrar den första MDX-en, då skapas parameterdatasetet om och tar bort min filtrering. Som tur är går det hindra Reporting Services från detta. Hemligheten är att ändra i RDL-filen. Högerklicka på rapporten och välj _View code_. Sök upp slutet av Query-taggen och ändra enligt nedanstående: Ändra {code:xml} {code:xml} till {code:xml} true {code:xml}

If 1 = 2 then null

by Martin 1. June 2010 11:36
Ibland vill man ju ändra så om en sträng är tom, dvs längden noll, så vill man ha null istället. Antingen kan man då skriva det som detta: {code:sql} set @var = case when @text = '' then null else @text end {code:sql} Ett snyggare sätt att skriva det på är att använda funktionen nullif(expr1, expr2) istället : {code:sql} set @var = nullif(@text, '') {code:sql} Funktionen nullif returnerar helt enkelt null om de två uttrycken har samma värde, och annars returnerar den det första uttrycket.