Archiv der Kategorie: MS Integration Services

Microsoft Integration Services 2005, 2008, 2008 R2, 2012, 2014, 2016

NULL-Werte bei Lookups

Lookups kommen bei SSIS-Data Flows ziemlich häufig vor. Zum Beispiel müssen bei der Verarbeitung von Fakten-Sätzen enthaltene Dimensions-Referenzen umgesetzt werden. Zum Beispiel könnte es sein, dass Deutschland im Quellsystem ‘D’ ist, im DWH aber mit 49 abgebildet wird.

Das ist mit Lookups natürlich ganz einfach.

Nun kommt es aber vor, dass nicht alle Fakten eine Referenz auf ein Land haben müssen, also NULL sind. Diese sollen dann auch NULL bleiben.

Wie kann eine solche Aufgabe gelöst werden?

Die Standard-Lösung wird so aussehen:

Vor dem Lookup wird über einen Conditional Split überprüft, ob das Land leer ist:

  • Wenn ja, dann wird der Zielwert auch auf NULL gesetzt
    (Dafür muss nichts gemacht werden, da dies beim folgenden UNION ALL automatisch erfolgt)
  • Wenn nein, dann wird der Lookup durchgeführt

Danach werden beide Pfade über einen UNION wieder zusammengeführt:

image

In meinem Beispiel sieht die Lookup-Quelle so aus:

SELECT N’D‘ as Country, 49 as nummer
UNION ALL
SELECT N’A‘ as Country, 43 as nummer
UNION ALL
SELECT N’CH‘ as Country, 41 as nummer

Diese Vorgehensweise hat mehrere Nachteile:

  • Über das UNION ALL wird immer ein neuer Buffer angelegt, was insbesondere bei großen Data Flows unnötig Speicher belegt (UNION ALL ist semi-blocking)
  • Die Lösung ist einigermaßen komplex
  • Falls eine Sortierung vorliegt, geht die Sortierung durch das UNION ALL verloren. Dies sollte nicht durch den Einsatz eines MERGE verhindert werden, da dies die Performance sehr negativ beeinflussen kann (vielleicht schreibe ich dazu mal einen eigenen Blog-Eintrag)

Deswegen hatten wir nach einer besseren Lösung gesucht:

Der erste Ansatz war folgender: Wir können beim Lookup Nicht-Treffer ignorieren und danach überprüfen, ob wir ein Mapping übersehen haben und dann selbst einen Fehler schmeißen:

imageDer Conditional Split enthält folgende Bedingung:

!(ISNULL(Country)) && ISNULL(nummer)

In der Skriptkompopnente wird ein Fehler mit folgendem Code erzeugt:

public override void Input0_ProcessInputRow(Input0Buffer Row)
{
    bool p=false;
    ComponentMetaData.FireError(10, „Testkomponente“, „Country was not null, but could not be mapped to a country number“, „“, 0, out p);
}

Dieser Ansatz hat folgenden Vorteil:

  • Alle Komponenten sind synchron, benötigen also keine zusätzlichen Buffer.

– aber auch folgende Nachteile:

  • Die Lösung ist noch komplexer
  • Die Lösung ist unübersichtlich und schlechter wartbar, da ohne die Skriptkomponente der Lookup falsch programmiert wäre. Das muss aber der wartende Mitarbeiter wissen. Außerdem ist ein Error Handling “Fehler ignorieren” irreführend.
  • Skript-Komponenten haben selbst keinen Fehler-Output. Damit kann eine Skript-Komponente leider nicht mit der vorhin vorgestellten Quarantäne-Idee automatisch korrigiert werden.

Die beste Lösung ist somit eine ganz einfache:

Wir  stellen den Lookup wieder auf Fehler bei einem Nicht-Treffer. Wir ergänzen die Quelle des Lookups um NULL-Werte, also in meinem Beispiel

SELECT N’D‘ as Country, 49 as nummer
UNION ALL
SELECT N’A‘ as Country, 43 as nummer
UNION ALL
SELECT N’CH‘ as Country, 41 as nummer
UNION ALL
SELECT NULL, NULL

bzw. allgemein

SELECT * FROM <lookup-tabelle>
UNION ALL
SELECT NULL, NULL

Dann sieht der Data Flow ganz einfach aus:

image

Dies hat natürlich etliche Vorteile:

  • Die Lösung ist super-einfach.
  • Die Lösung ist natürlich synchron und somit sehr schnell.

Allerdings funktioniert diese Lösung nur, wenn Full Cache eingestellt ist:

image

Dies ist aber eh die meistens verwendete (da performanteste) Variante.

Die Ursache, warum die anderen Cache-Einstellungen nicht funktionieren, liegt darin, dass im SQL NULLs anders verwendet werden (sie können nicht mit = abgeprüft werden). In den anderen Cache-Einstellungen würden SQLs wie SELECT * FROM <lookuptabelle> where Country = NULL ausgeführt.

Error Handling im Data Flow eines SSIS-Pakets

In den meisten Komponenten innerhalb eines Data Flows in einem SSIS-Paket können als Error Handling folgende Einstellungen getroffen werden:

Englisch Deutsch Bemerkung
Fail Component Fehler bei Komponente Dies ist die Standard-Einstellung. Sie führt dazu, dass bei einem – wie auch immer gearteten Fehler – der gesamte Data Flow fehlschlägt. Noch nicht verarbeitete Daten werden nicht weiter verarbeitet.
Ignore Failure Fehler ignorieren Diese Einstellung würde bedeuten, dass trotz des Fehlers normal weitergearbeitet wird. Dies ist in der Regel nicht sinnvoll.
Redirect Row Zeile umleiten Die fehlerhafte Zeile wird umgeleitet und der aufgetretene Fehler kann durch einen speziellen Ablauf behandelt werden.

In der Regel ist es sinnvoll, während der Entwicklung “Fail Component” einzusetzen. Während der Entwicklung und der Tests möchte man ja schließlich wissen, ob und wenn ja, welche Fehler auftreten, damit man sie programmatisch beheben kann.

Auch für den produktiven Einsatz kann diese Einstellung sinnvoll sein, da sie dafür sorgt, dass keine fehlerhaften Daten ins DWH übernommen werden.

Auf der anderen Seite ist die Einstellung problematisch, da dann ein fehlerhafter Satz die ETL-Strecke zum Erliegen bringt. In einem meiner Projekte laden wir alle 10 Minuten aus einem Quellsystem, das sehr großzügig mit Eingaben vorgeht.  Für dieses Projekt haben wir eine Quarantäne definiert:

Alle Komponenten sollen auf “Redirect Row” gestellt werden und diese fehlerhaften Sätze werden in eine Quarantäne-Tabelle geschrieben (unter Angabe des Primärschlüssels und des aufgetretenen Fehlers).

Danach müssen noch “halbe” Datensätze entfernt werden – es kann ja sein, dass in einem Vater-Satz ein Fehler auftritt und dieser in die Quarantäne läuft, aber alle Kinder-Sätze (der 1:n-Beziehung) richtig durchlaufen würden. Diese sollen dann aber natürlich nicht im DWH landen.

Die Umstellung der Komponenten auf “Redirect Row” wollten wir dabei nicht manuell durchführen, da wir Data Flows mit ca. 150 Komponenten haben. Deshalb haben wir ein (VB).NET-Programm erstellt, das via SSIS API diese Komponenten umstellt. Dieses Programm werde ich in den nächsten Blog-Einträgen vorstellen.

SSIS: Tabellen-Variablen als Datenquelle

In SSIS kann man als Datenquelle ein SQL-Statement eintragen:

Eingabefeld für SQL als Datenquelle

Manchmal möchte man in dieser Maske kompliziertere Berechnungen verwenden. Dafür kann man T-SQL (wie bei der Programmierung von Stored Procedures) verwenden. Allerdings funktioniert es nicht ganz wie gedacht:

Als erstes kann man keine temporären Tabellen (beginnend mit #) verwenden. Als Beispiel betrachten wir das SQL-Statement:

select 1 as zahl ,2 as doppelte into #t
select * from #t

Dieses einfache Statement liefert im SQL Server Management Studio – wie erwartet:

(1 Zeile(n) betroffen)
zahl        doppelte
———– ———–
1           2

(1 Zeile(n) betroffen)

Fügt man dieses Statement aber in der SSIS-Datenquelle hinzu, erhält man nach OK folgenden Fehler:

Fehler beim Vorbereiten: #t unbekannt

bzw. als Text:

Fehler bei Datenflusstask [T-SQL Programm [1]]: SSIS-Fehlercode ‚DTS_E_OLEDBERROR‘. OLE DB-Fehler. Fehlercode: 0x80040E14.
Ein OLE DB-Datensatz ist verfügbar. Quelle: ‚Microsoft SQL Server Native Client 10.0‘ HRESULT: 0x80040E14 Beschreibung: ‚Anweisung(en) konnte(n) nicht vorbereitet werden.‘.
Ein OLE DB-Datensatz ist verfügbar. Quelle: ‚Microsoft SQL Server Native Client 10.0‘ HRESULT: 0x80040E14 Beschreibung: ‚Ungültiger Objektname ‚#t‘.‘.

Auch eine verspätete Überprüfung des SQLs bringt keine Besserung.

Allerdings funktioniert es, Tabellen-Variablen zu verwenden. Dort heißt obiger Code entsprechend:

Declare @t as TABLE (zahl integer, doppelte integer)
insert into @t select 1 ,2
select * from @t

Dieser Code lässt sich als Datenquelle eintragen und SSIS erkennt korrekt die beiden Spalten:

Spalten korrekt erkannt

Startet man das SSIS-Paket (damit man etwas sieht, habe ich ein UNION ALL dahinter gehängt), scheint es zu funktionieren:

Datenquelle zu UNION ALL

Alles ist grün – wunderbar.

Aber nein! Es wurde keine Zeile übertragen.

Das ist ein bekanntes Problem, dass in dem INSERT-Statement bereits ein Dataset erzeugt wird – dem entspricht in obiger Ausführung der erste Text “(1 Zeile(n) betroffen)”. Die Daten des zweiten Befehls werden dann gar nicht mehr im SSIS weiterverwendet!

Also muss man das SQL so abwandeln:

set nocount on
Declare @t as TABLE (zahl integer, doppelte integer)
insert into @t select 1 ,2
select * from @t

Durch das “set nocount on” wird das erste Dataset unterdrückt, so dass es jetzt funktioniert:

image

Wir sehen, es wurde tatsächlich eine Zeile übertragen – und die Daten stimmen auch, wie der Data Viewer zeigt.

SSIS: Laufzeitprotokollierung je Task

Wenn man größere SSIS-Jobs schreibt, sollten diese Jobs später auf ihre Laufzeiten kontrolliert werden. Läuft die Laufzeit eines SSIS-Jobs aus dem Ruder, ist es sinnvoll, protokolliert zu haben, wie lange welcher Task des SSIS-Jobs läuft, um schnell den Schuldigen ausfindig zu machen. Alternative Techniken wie Analyse des Log-Files sind sehr mühselig.

In dem Code-Beispiel gehen wir davon aus, dass es eine SQL-Tabelle gibt, in der für jeden SSIS-Job ein Eintrag erzeugt wird, so dass es eine eindeutige Lauf-ID gibt. Diese Lauf-ID wird im ersten Schritt des SSIS-Jobs erzeugt. Dies ist bei uns “Best Practice”

Zur Protokollierung legen wir eine Tabelle wie folgt an:

CREATE TABLE [dbo].[META_ImportLaufTaskDauer](
    [ImportLauf_ID] [int] NOT NULL,
    [TaskName] [nvarchar](200) NOT NULL,
    [Startzeitpunkt] [datetime] NOT NULL,
    [Endezeitpunkt] [datetime] NULL,
CONSTRAINT [PK_META_ImportLaufTaskDauer] PRIMARY KEY CLUSTERED
(
    [ImportLauf_ID] ASC,
    [TaskName] ASC
)
)

Die Spaltennamen sollten selbst erklärend sein.

In diese Tabelle wird nun über Events im SSIS-Paket geschrieben. Dazu werden auf der obersten Ebene des Pakets (und nur dort – also nicht etwa für jede Task) die Events für OnPreExecute und OnPostExecute angelegt:

In dem OnPreExecute wird nun der Startzeitpunkt der Task in die Tabelle geschrieben… :

OnPreExecute

Dazu wird ein Execute SQL-Task angelegt. Dieser beinhaltet folgendes SQL:

declare @Laufid int
set @Laufid = ?

if @LaufId > 0
  INSERT INTO META_ImportLaufTaskDauer (ImportLauf_ID, TaskName, Startzeitpunkt)
VALUES
  (@laufid, left(?, 200), getdate())

Das Mapping der Variablen sieht so aus:

Parameter bei OnPreExecute

Die ID des Importlaufs findet sich hier in der Benutzer-Variablen LaufID. Die andere Variable ist eine System-Variable, die uns den verantwortlichen Task nennt.

… und im OnPostExecute der Endzeitpunkt der Task mit folgendem SQL:

declare @Laufid int
set @Laufid = ?

if @LaufId > 0
UPDATE META_ImportLaufTaskDauer
Set Endezeitpunkt = getdate()
where ImportLauf_ID = @Laufid and TaskName= left(?, 200)

Das ParameterMapping ist wie im OnPreExecute.

Performanceprobleme bei SSAS-Dimensionsabfragen aus SSRS

Nachdem wir in einem Projekt unseren Test-Datenbestand (12 Mio Fakten) auf den Produktiv-Bestand (73 Mio Fakten) erweitert hatten, zeigten die Berichte (SSRS 2008 R2) massiv schlechte Antwortszeiten in bestimmten Berichten auf unseren Cube (SSAS 2008 R2) – jeweils nach der Cubeaufbereitung. Somit war klar, dass Ursache war, dass einige der im Bericht verwendeten Abfragen nicht im Cache waren, da dieser ja durch die inkrementelle Dimensionsaufbereitung und Aufbereitung einiger Cubepartitionen gelöscht wird.

Naheliegende Strategien waren:

  • Aufteilung in mehr Partitionen unter Angabe der Slice-Property
  • Verbesserte Aggregationen über Usage based Aggregation Design
  • Cache Warming – Strategien

All das brachte uns aber nicht wirklich weiter.

Letztendlich stellte sich aber heraus, dass die Ursache gar nicht in den (tlw. komplexen) Abfragen zur Ermittlung der Fakten zu suchen war, sondern in „Dimensions-Abfragen“. Damit meine ich folgendes: Unsere Berichte werden (aus einer eigenen Web-Applikation heraus) mit Parametern aufgerufen, die IDs sind (eigentlich die Member Unique Names, also z.B. Datum.Jahr.&[2010]). Dabei haben die Parameter (aus Performancegründen) keine Datasets, die alle verfügbaren Werte enthalten. Nun wollen wir aber in dem Bericht natürlich auch den übergebenen Wert im Klartext anzeigen, also in obigem Beispiel das Jahr 2010. Deswegen mussten also einfache Abfragen her, die aus dem Member Unique Name den Klartext (Member Caption) ermitteln.

In unserem Projekt hatten wir uns darauf geeinigt, wenn möglich den SSRS-Designer für SSAS-Abfragen zu verwenden, um eine gute Wartbarkeit (ohne tiefere MDX-Kenntnisse) zu erreichen.

Damit gibt es zwei einfache Arten, dieses Problem zu lösen:

Die erste Art, hat eine Spalte – das Jahr – und zusätzlich den (MultiSelect) Report Parameter als Parameter für die Abfrage:

Wizard für die langsame Abfrage
(Bild zum Vergößern anklicken!)

Als MDX ergibt sich:

SELECT { } ON COLUMNS, { ([Datum].[Jahr].[Jahr].ALLMEMBERS ) } DIMENSION PROPERTIES MEMBER_CAPTION, MEMBER_UNIQUE_NAME ON ROWS FROM ( SELECT ( STRTOSET(@DatumJahr, CONSTRAINED) ) ON COLUMNS FROM [Verkaeufe]) CELL PROPERTIES VALUE

Man kann auch die Abfrage etwas erweitern:

Man fügt ein berechnets Measure (in diesem Fall namens „UniqueName“) mit dem MDX

[Datum].[Jahr].CurrentMember.UniqueName

hinzu:

Wizard der schnellen Abfrage
(Bild zum Vergößern anklicken!)

Damit ergibt sich folgendes MDX:

WITH MEMBER [Measures].[UniqueName] AS [Datum].[Jahr].CurrentMember.UniqueName SELECT NON EMPTY { [Measures].[UniqueName] } ON COLUMNS, NON EMPTY { ([Datum].[Jahr].[Jahr].ALLMEMBERS ) } DIMENSION PROPERTIES MEMBER_CAPTION, MEMBER_UNIQUE_NAME ON ROWS FROM ( SELECT ( STRTOSET(@DatumJahr, CONSTRAINED) ) ON COLUMNS FROM [Verkaeufe]) CELL PROPERTIES VALUE, BACK_COLOR, FORE_COLOR, FORMATTED_VALUE, FORMAT_STRING, FONT_NAME, FONT_SIZE, FONT_FLAGS

Anmerkung: Das dabei enstehende versteckte Dataset zum Befüllen der „verfügbaren Werte“ des Parameters löschen wir, da wir ja keine „verfügbaren Werte“ anzeigen wollen (unsere Dimension hatte zu viele Einträge).

Beide Abfragen scheinen dasselbe zu tun und mit der selben Performance. Dies ist aber ein Trugschluss!

Leeren wir zunächst (und vor Ausführung eines neuen Test-Statements) den SSAS-Cache mit

<ClearCache xmlns=“http://schemas.microsoft.com/analysisservices/2003/engine“>
<Object>
<DatabaseID>SimpleCube</DatabaseID>
<CubeID>Verkaeufe</CubeID>
</Object>
</ClearCache>

(siehe dazu auch: http://www.ssas-info.com/analysis-services-faq/27-mdx/133-mdx-how-do-i-clear-analysis-services-ssas-database-cache)

Dann betrachten wir die Darstellung im SQL Server Profiler (mit den Standard-Einstellungen für den SSAS):

Die erste Abfrage ergibt folgendes Bild:

SQL Profiler Ergebnis der langsamen Abfrage
(Bild zum Vergößern anklicken!)

Schauen wir uns die einzelnen Zeilen an:

  • Session Initialize – spricht für sich
  • Query Begin: Hier fängt die Abfrage an – im unteren Bereich sieht man das MDX und die Parameterwerte für die @Parameter im MDX
  • Progress Report Begin: Daten aus einer Partition werden gelesen (!), d.h. von der Festplatte in den Speicher übernommen.
  • Progress Report End: das dazugehörige Ende
  • Query SubCube: die gelesenen Daten werden verwendet, um die Abfrage zu beantworten (in diesem Fall Non-Cache)
  • Query End: Das Ende der Abfrage

Die zweite Abfrage ergibt ein anderes Bild:

SQL Profiler Ergebnis der schnellen Abfrage
(Bild zum Vergößern anklicken!)

Hier sehen wir, dass kein Zugriff auf eine Partition erfolgt!

Der Zugriff auf die Partition ist natürlich schädlich, da er bei großen Datenmengen lang dauern kann, zumal er gar nicht benötigt wird, da wir ja nur Dimensions-Element-Bezeichnungen abfragen wollen.
Es ist nicht erklärbar, warum SSAS hier dennoch auf die Fakten zugreift.
Deswegen ist die zweite Abfrage auf jeden Fall vorzuziehen!

Noch ein interessante Anmerkung zum Schluss: Wenn man die erste Abfrage im SQL Server Management Studio ausführt, ist sie auch nicht langsam. Da im Management Studio keine parametrisierten Abfragen möglich sind, muss man dazu die Parameter durch die entsprechenden Strings ersetzen, und erhält somit folgendes MDX:

SELECT { } ON COLUMNS, { ([Datum].[Jahr].[Jahr].ALLMEMBERS ) } DIMENSION PROPERTIES MEMBER_CAPTION, MEMBER_UNIQUE_NAME ON ROWS FROM ( SELECT ( STRTOSET(„{ [Datum].[Jahr].&[2001],[Datum].[Jahr].&[2008] }“, CONSTRAINED) ) ON COLUMNS FROM [Verkaeufe]) CELL PROPERTIES VALUE

Führt man dieses MDX aus (nachdem man den Cache geleert hat), so erhält man folgendes Bild:

Ergebnis des Profilers der langsamen Abfrage ausgeführt im Management Studio
(Bild zum Vergößern anklicken!)

Man sieht hier also auch nur ein Query Begin und ein Query End, also auch keinen Zugriff auf die Cube-(bzw. Partitions-)Daten.

Dies hatte uns die Fehlersuche erschwert, da wir natürlich zunächst auf der Suche nach dem verantwortlichen Statement alle Statements im SQL Server Profiler mitgeschnitten und dann einzeln im Management Studio ausgeführt hatten, was – wie eben gesehen – das problematische Statement leider nicht offenbart.

begin try in SQL-Batch-Statements

Es kommt öfter vor, dass man im Batch mehrere SQL-Statements ausführen möchte, sei es in einer Stored Procedure oder im Execute SQL-Task von SSIS.

Meistens hat man folgende Anforderung:

Läuft ein Statement auf einen Fehler, soll ein Rollback der Statements gemacht werden. Außerdem soll natürlich dem aufrufenden System der Fehler gemeldet werden.

Lässt man einen Batch einfach so laufen, wird dieses Ziel nicht erreicht, da im Fehlerfall auch die Statements nach dem Statement, das den Fehler verursacht, ausgeführt werden.

Beispiel:

set nocount on
select 1
select 1/0
select 2

liefert:

———–
1

———–
Msg 8134, Level 16, State 1, Line 3
Divide by zero error encountered.

———–
2

In Versionen vor SQL Server 2005 musste man die Error-Variable auslesen, etwa so:

set nocount on
declare @fehler as int
set @fehler = 0
select 1
set @fehler = @fehler + @@error
select 1/0
set @fehler = @fehler + @@error
select 2
set @fehler = @fehler + @@error
if @fehler>0 begin
print ‚Ein Fehler ist aufgetreten‘
end

was folgendes Ergebnis liefert:

———–
1

———–
Msg 8134, Level 16, State 1, Line 6
Divide by zero error encountered.

———–
2

Ein Fehler ist aufgetreten

Das Problem ist, man muss die Zeile „set @fehler = @fehler + @@error“ nach jedem Statement schreiben, da sie nach jedem (!) Statement zurückgesetzt wird.

Leichter geht das in SQL 2005 mit begin try … end try – angelehnt an Konstrukte aus Programmiersprachen wie C#:

set nocount on
begin try

select 1
select 1/0
select 2

end try
begin catch
print ‚Ein Fehler aufgetreten‘
end catch

Am Ergebnis

———–
1

———–

Ein Fehler aufgetreten

sieht man, dass nach dem fehlerhaften Statement die Bearbeitung beendet wird.

Nun fehlen nur noch 2 Anforderungen:

Dass keine Datenmanipulation statt findet, erreicht man über eine Transaktion, die im catch-Block zurückgerollt (rollback) wird.

Dass der Aufruf dennoch den Fehler mitbekommt, erreicht man über einen raiserror.

Das fertige Skript sieht dann so aus:

set nocount on

begin tran
begin try

/* hier die eigentlichen SQL-Statements schreiben */
select 1
select 1/0
select 2

end try
begin catch
if @@trancount > 0 begin
rollback tran
end
declare @fehler_text nvarchar(4000)
set @fehler_text = ERROR_MESSAGE()
declare @fehler_severity int
set @fehler_severity = ERROR_SEVERITY()
declare @fehler_state int
set @fehler_state = ERROR_STATE()
RAISERROR (@fehler_text, — Message text.
@fehler_severity, — Severity.
@fehler_state — State.
)

end catch
if @@trancount > 0 begin
commit tran
end

SELECTS beim Lookup dynamisch zusammenstellen

Manchmal reicht es nicht aus, feste SQL-Statements als Quelle oder bei Lookups zu hinterlegen.

Bei einer OLE-DB-Quelle kann man ganz einfach das SQL-Statement in einer Variable ablegen.

Wie man das auch bei Lookups machen kann, zeigt dieser Eintrag.

Zunächst zur Motivation:

Man könnte sich vorstellen, dass innerhalb eines Datenflusstasks immer nur Daten eines Tages in eine Tabelle geschrieben werden. Der Tag steht dabei in einer Variablen. Nun soll bei jedem Lookup überprüft werden, ob der Primary Key der Daten an diesem Tag schon in einer Tabelle steht. Da der Lookup alle Daten cacht, macht es Sinn nur die Daten des Datums aus der Variablen zu lesen.

In unserem Beispiel heißt die Variable „Tag_deutsch“ und speichert das Datum als String mit deutschem Datumsformat.

Das Lookup-SQL müsste also so aussehen:

select * from ZahlenProTag
Where Tag = CONVERT(date, '7.5.2010', 104)

oder allgemein

select * from ZahlenProTag
Where Tag = CONVERT(date, '<Inhalt der Variablen Tag_deutsch>', 104)

Deswegen legen wir eine berechnete (EvaluateAsExpression:=true) Variable „Lookup-SQL“ an, mit der Expression:

"select * from ZahlenProTag
Where Tag = CONVERT(date, '" + @[User::Tag_deutsch] + "', 104)"

Jetzt bauen wir erst mal unseren Data Flow Task, noch mit dem kompletten Lookup auf ZahlenProTag:

BeispielDataFlow

Zu den einzelnen Komponenten:

  • alle Zahlen von 1 bis 10 habe ich bereits in meinem letzten Blogeintrag verwendet (s. https://www.csopro.de/biblog/2010/04/temporaere-tabellen-in-ssis-verwenden/). Es liefert alle Zahlen von 1 bis 10
  • Eine abgeleitete Spalte mit dem Namen Datum wird hinzugefügt, mit folgender Formel:
    (DT_DBDATE)@[User::Tag_deutsch]
    ACHTUNG. Dabei muss als LocaleID deutsch eingestellt sein, da der String ja in deutschem Datumsformat steht
  • Der Lookup sieht auf dem ersten Tab so aus:
    LookupAllgemein
    und so auf dem zweiten:
    LookupVerbindung
    und so auf dem dritten:
    LookupSpalten
    Man beachte, dass zum Lookup nur die Zahl und nicht das Datum verwendet wird.
    Letzteres könnte man natürlich machen. Dann würde aber die gesamte Tabelle in den Cache geladen werden, obwohl wir wissen, dass nur ein kleiner Bruchteil benötigt wird.
  • Das OLE-DB-Ziel ist wieder naheliegend:
    Tab 1:
    ZielVerbindung
    Tab 2:
    ZielZuordnungen

Soweit so gut.

Nun wollen wir das Lookup-SQL auf die oben angelegte Variable umstellen. Dazu gehen wir in die Ablaufsteuerung / Workflow und selektieren den Datenfluss-Task, so dass wir im Eigenschaftsfenster dessen Eigenschaften sehen.

Dort fällt auf, dass es unter Sonstiges eine Eigenschaft mit dem Titel [Zahl an diesem Tag schon da?].[SqlCommand] gibt:

SqlCommand

[Zahl an diesem Tag schon da?] ist ja der Name der Lookup-Komponente.

SqlCommand gibt das SQL an, das zum Befüllen des Caches verwendet wird (wir hatten ja den Standard Vollcache belassen)

SqlCommandParam würde man nur bei den anderen Cache-Typen benötigen.

Nun können wir über Expressions des Data Flow Tasks diese Eigenschaft überschreiben:

ExpressionsÜberschreiben

So jetzt geht’s!

Im Status bzw. Protokoll kann man kontrollieren, wieviele Zeilen der Lookup gecacht hat:

[Zahl an diesem Tag schon da? [13]] Informationen: ‚Komponente ‚Zahl an diesem Tag schon da?‘ (13)‘ hat insgesamt 0 Zeilen zwischengespeichert.

Diese Zeile kommt, wenn man die Variable auf ein neues Datum stellt.

Wenn die Variable auf einem bereits verwendeten Datum steht, kommt:

[Zahl an diesem Tag schon da? [13]] Informationen: ‚Komponente ‚Zahl an diesem Tag schon da?‘ (13)‘ hat insgesamt 10 Zeilen zwischengespeichert.

Also ist alles OK!

temporäre Tabellen in SSIS verwenden

Für manche Aufgaben erscheint es sinnvoll, Daten zunächst in temporäre Tabellen zu übertragen, um sie dann zum Beispiel per Insert oder Update in die Zieltabelle zu überführen (damit kann man ein „Bulk Update“ durchführen).

Dazu kann man natürlich im SQL-Server beliebige Tabellen anlegen, die dann nur „logisch“ temporär sind, da sie ja ständig in der Datenbank sind.

Der SQL Server bietet aber auch temporäre Tabellen an, die sessionweit (# – sobald die Connection geschlossen wird, verschwindet die Tabelle) oder global (## – sobald die letzte Connection geschlossen wird, die diese Tabelle verwendet, verschwindet sie) gelten.

Diese in SSIS zu verwenden, mag z.B. Sinn machen, wenn man keine Tabellen-Erstellungs-Rechte auf dem Zielserver hat.

Sie zu verwenden, ist nicht ganz einfach:

Nehmen wir an, wir wollen eine temporäre Tabelle mit einer einzigen numerischen Spalte anlegen, so geht das so:

CREATE TABLE [dbo].[#ZielTabelle]
(
[zahl] [int] NOT NULL
)

Randbemerkung:
Man kann auch Primary Keys vergeben, z.B. mit folgendem Statement:

CREATE TABLE [dbo].[#ZielTabelle]
(
[zahl] [int] NOT NULL,
CONSTRAINT ZielTabelle_PK PRIMARY KEY CLUSTERED
(
[zahl] ASC
)
)

Das wäre aber nicht so gut, da dann ein Vorteil verloren geht: Eine solche temporäre Tabelle (#) kann mehrfach existieren (je Connection einmal). Verwendet man aber einen festen Primary-Key-Namen, so geht das nicht mehr. Man könnte in den Namen des Primary Key eine GUID einbauen und hätte wieder Eindeutigkeit.

Dieses Create-Table-Statement baut man in eine Execute-SQL-Task und kann dann folgenden Workflow abbilden:

Workflow

Den DROP TABLE braucht man nicht unbedingt, da nach Schließen der Connection die Tabelle eh verschwindet. Aber aus Gründen der Hygiene empfehle ich es.

Unser Datenflusstask soll im Beispiel einfach alle Zahlen von 1 bis 10 in die temporäre Tabelle schreiben:

DataFlow

Die Skript-Komponente hat folgenden Code:

public override void CreateNewOutputRows()
{
for (int i = 1; i <= 10; i++)
{
AusgabeBuffer.AddRow();
AusgabeBuffer.Zahl = i;
}
}

Nun haben wir aber ein Problem: Die temp-Tabelle taucht nicht in der Liste der Tabellen auf. Deswegen empfiehlt es sich, auf dem Entwicklungsrechner eine Tabelle mit der selben Struktur anzulegen, und diese dann auszuwählen (hier „ZielTab_gibtsNetProduktiv“):

Ziel

Nun kann man auf dem nächsten Reiter die Zuordnungen – wie gewohnt – einstellen (was in diesem Beispiel sehr einfach ist 🙂 ):

Zuordnungen

Noch haben wir aber nichts gewonnen, da die temporäre Tabelle nicht verwendet wird.

Es müssen deshalb noch ein paar Einstellungen vorgenommen werden:

  • Unter Eigenschaften des OLEDB-Ziels muss man in OpenRowset die temporäre Tabelle eintragen:
    Vorher:
    nachher
    Nachher:
    nachher
  • Die weitere Eigenschaft „ValidateExternalMetaData“ des OLEDB-Ziels muss auf „false“ gestellt, da zum Zeitpunkt der Entwicklung bzw. des Starten des Pakets die temporäre Tabelle noch nicht existiert.

Jetzt funktioniert das Paket immernoch nicht. Es tritt folgender Fehler auf:

[Speichern in temp Tabelle [16]] Fehler: Fehler beim Öffnen eines FastLoad-Rowsets für ‚#ZielTabelle‘. Überprüfen Sie, ob das Objekt in der Datenbank vorhanden ist.

Offensichtlich kennt der DataFlow Task die Tabelle nicht. Dies liegt daran, dass der CREATE TABLE-Befehl und der Datenfluss-Task nicht in der selben Connection ausgeführt werden. Deswegen müssen wir noch die Eigenschaft der Connection „RetainSameConnection“ auf true setzen:

RetainSameConnection

Jetzt funktioniert es.

Eine Anmerkung:
Man kann die Zuordnungen des OLEDB-Ziels nicht mehr mit dem normalen Editor bearbeiten, da zum Design-Zeitpunkt die temporäre Tabelle nicht existiert. Man kann das durch das oben gezeigte Umstellen auf eine nur auf dem Entwicklungsserver existierende Tabelle umgehen.

Unerwartetes Nicht-Logging SSIS

Dieser Artikel widmet sich nicht dem Logging, das man selbst in einem Paket aktivieren kann und über Log-Provider detailliert steuern kann.

Gemeint ist mehr das Logging, das SSIS automatisch ohne Zutun des Entwicklers oder Administrators durchführt, s. dazu auch „Understanding Events Logged by an Integration Services Package„.

Standardmäßig loggt jedes SSIS-Paket den Start, das erfolgreiche Ende oder das Fehlschlagen oder Abbrechen eines SSIS-Pakets:

EventLog Start und Failure eines SSIS Pakets

(Zum Vergößern auf das Bild klicken)

Details des Eventlogs beim Scheitern eines Pakets

(Randbemerkung: Das Logging von Start und erfolgreichem Ende lässt sich abschalten – in der Registry HKEY_LOCAL_MACHINESOFTWAREMicrosoftMicrosoft SQL Server100SSIS, Wert von „LogPackageExecutionToEventLog“ auf 1 setzen – die anderen Einträge lassen sich nicht verhindern)

In dem Beispiel hatte ich ein SSIS-Paket erstellt, das nur aus einer Script-Task bestand, die immer einen Fehler schmeißt:

Script-Task zum Fehler schmeißen

Dort ist folgender Code enthalten:

public void Main()
{
Dts.Events.FireError(999, „SubKomponente A“, „Fehler – Test fehler“, „“, 0);
Dts.TaskResult = (int)ScriptResults.Success;
}

Das Ergebnis ist oben zu bewundern.

Nun hatten wir die Idee, dieses Eintrag im Event Log zu überwachen (mit NetIQ oder einem anderen Werkzeug zur Überwachung von Servern, Diensten, etc.) und so zu erfahren, wenn ein Paket gescheitert ist.

Das funktioniert aber leider nicht.

Der Grund ist, dass ein solcher Eintrag im Eventlog nur erzeugt wird, wenn das Paket bereits angelaufen ist. Wenn aber schon bei der Validierung ein Fehler auftritt, wird kein Eintrag im Eventlog erzeugt.

Dazu habe ich das Paket erweitert um einen Data Flow Task, bei dem ich in der Quelle „SELECT 1 as Zahl“ eingetragen habe. SSIS Paket mit Fehler brerits beim Validieren

Dieses Paket habe ich dann im SQL Server Agent als Job eingerichtet. Dabei habe ich eingestellt, dass im Fehlerfall der SQL Server Agent das Ergebnis im Eventlog festhalten soll:

Properties des SQL Server Agent Jobs zum Protokollieren im Event Log

(Zum Vergößern auf das Bild klicken)

Solange die Quelle des SSIS-Pakets bekannt ist, ist alles wie erwartet. Im Eventlog werden folgende 3 Events festgehalten (da ja mein Script-Task dazu führt, dass das Paket auf einen Fehler läuft):

Die Events, die durch ein Paket mit vorhandener Connection erzeugt werden

Die Meldungen sind:

  1. Starten des SSIS Pakets (Information)
  2. Fehler des SSIS Pakets (Fehler)
  3. Fehler des SQL Server Agent Jobs (Warnung)

Letzte Meldung sieht im Detail so aus:

Ereignis-Details der SQL Server Agent-Meldung, dass der Job nicht erfolgreich war

Wenn ich jetzt aber die Connection des Statements „SELECT 1“ ändere, so dass der dort angegebene Computer nicht existiert, passiert folgendes, wenn das Paket im SQL Server Agent gestartet wird:

Ereignisanzeige, wenn die Connection des SSIS Pakets nicht validiert werden kann

Man sieht, dass die SSIS-Eventlog-Einträge nicht vorhanden sind. Dies liegt daran, dass der Fehler beim Validieren auftritt und deshalb das Paket noch gar nicht angelaufen ist.

Deswegen macht es keinen Sinn, die SSIS-Eventlog-Einträge für eine Überwachung der Jobs zu verwenden.

Stattdessen können die SQL Server Agent-Einträge überwacht werden. Dabei ist aber zu beachten, dass dies bei jedem SQL Server Agent Job – wie oben gezeigt – eingestellt werden muss.