Database
 sql >> Datenbank >  >> RDS >> Database

Übersehene T-SQL-Edelsteine

Mein guter Freund Aaron Bertrand hat mich zu diesem Artikel inspiriert. Er erinnerte mich daran, wie wir manchmal Dinge für selbstverständlich halten, wenn sie uns offensichtlich erscheinen, und uns nicht immer die Mühe machen, die ganze Geschichte dahinter zu überprüfen. Die Relevanz für T-SQL besteht darin, dass wir manchmal davon ausgehen, dass wir alles wissen, was es über bestimmte T-SQL-Funktionen zu wissen gibt, und uns nicht immer die Mühe machen, die Dokumentation zu überprüfen, um zu sehen, ob mehr dahinter steckt. In diesem Artikel behandle ich eine Reihe von T-SQL-Features, die entweder oft völlig übersehen werden oder Parameter oder Fähigkeiten unterstützen, die oft übersehen werden. Wenn Sie eigene Beispiele für T-SQL-Juwelen haben, die oft übersehen werden, teilen Sie diese bitte im Kommentarbereich dieses Artikels mit.

Bevor Sie mit dem Lesen dieses Artikels beginnen, fragen Sie sich, was Sie über die folgenden T-SQL-Funktionen wissen:EOMONTH, TRANSLATE, TRIM, CONCAT und CONCAT_WS, LOG, Cursor-Variablen und MERGE with OUTPUT.

In meinen Beispielen verwende ich eine Beispieldatenbank namens TSQLV5. Das Skript, das diese Datenbank erstellt und füllt, finden Sie hier und ihr ER-Diagramm hier.

EOMONTH hat einen zweiten Parameter

Die EOMONTH-Funktion wurde in SQL Server 2012 eingeführt. Viele Leute denken, dass sie nur einen Parameter unterstützt, der ein Eingabedatum enthält, und dass sie einfach das Monatsendedatum zurückgibt, das dem Eingabedatum entspricht.

Betrachten Sie eine etwas anspruchsvollere Notwendigkeit, das Ende des Vormonats zu berechnen. Angenommen, Sie müssen die Tabelle „Sales.Orders“ abfragen und Bestellungen zurücksenden, die am Ende des Vormonats aufgegeben wurden.

Eine Möglichkeit, dies zu erreichen, besteht darin, die EOMONTH-Funktion auf SYSDATETIME anzuwenden, um das Monatsendedatum des aktuellen Monats zu erhalten, und dann die DATEADD-Funktion anzuwenden, um einen Monat vom Ergebnis zu subtrahieren, wie folgt:

USE TSQLV5; 
 
SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

Beachten Sie, dass Sie, wenn Sie diese Abfrage tatsächlich in der TSQLV5-Beispieldatenbank ausführen, ein leeres Ergebnis erhalten, da das letzte in der Tabelle aufgezeichnete Bestelldatum der 6. Mai 2019 ist. Wenn die Tabelle jedoch Bestellungen mit einem Bestelldatum enthält, das auf das Letzte fällt Tag des Vormonats hätte die Abfrage diese zurückgegeben.

Was viele Leute nicht wissen, ist, dass EOMONTH einen zweiten Parameter unterstützt, mit dem Sie angeben, wie viele Monate addiert oder subtrahiert werden sollen. Hier ist die [vollständig dokumentierte] Syntax der Funktion:

EOMONTH ( start_date [, month_to_add ] )

Unsere Aufgabe kann einfacher und natürlicher gelöst werden, indem einfach -1 als zweiter Parameter der Funktion angegeben wird, etwa so:

SELECT orderid, orderdate
FROM Sales.Orders
WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

TRANSLATE ist manchmal einfacher als REPLACE

Viele Leute sind mit der REPLACE-Funktion und ihrer Funktionsweise vertraut. Sie verwenden es, wenn Sie alle Vorkommen einer Teilzeichenfolge durch eine andere in einer Eingabezeichenfolge ersetzen möchten. Manchmal jedoch, wenn Sie mehrere Ersetzungen anwenden müssen, ist die Verwendung von REPLACE etwas knifflig und führt zu verworrenen Ausdrücken.

Angenommen, Sie erhalten eine Eingabezeichenfolge @s, die eine Zahl mit spanischem Format enthält. In Spanien verwendet man einen Punkt als Trennzeichen für Tausendergruppen und ein Komma als Dezimaltrennzeichen. Sie müssen die Eingabe in das US-Format konvertieren, wobei ein Komma als Trennzeichen für Tausendergruppen und ein Punkt als Dezimaltrennzeichen verwendet wird.

Mit einem Aufruf der REPLACE-Funktion können Sie nur alle Vorkommen eines Zeichens oder einer Teilzeichenfolge durch ein anderes ersetzen. Um zwei Ersetzungen anzuwenden (Punkte auf Kommas und Kommas auf Punkte), müssen Sie Funktionsaufrufe verschachteln. Der knifflige Teil ist, dass Sie, wenn Sie REPLACE einmal verwenden, um Punkte in Kommas zu ändern, und dann ein zweites Mal gegen das Ergebnis, um Kommas in Punkte zu ändern, am Ende nur Punkte erhalten. Probieren Sie es aus:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
 
SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

Sie erhalten die folgende Ausgabe:

123.456.789.00

Wenn Sie bei der Verwendung der REPLACE-Funktion bleiben wollen, benötigen Sie drei Funktionsaufrufe. Einer zum Ersetzen von Punkten durch ein neutrales Zeichen, von dem Sie wissen, dass es normalerweise nicht in den Daten erscheinen kann (z. B. ~). Ein weiterer gegen das Ergebnis, alle Kommas durch Punkte zu ersetzen. Ein weiteres gegen das Ergebnis, alle Vorkommen des temporären Zeichens (in unserem Beispiel ~) durch Kommas zu ersetzen. Hier ist der vollständige Ausdruck:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

Diesmal erhalten Sie die richtige Ausgabe:

123,456,789.00

Es ist irgendwie machbar, aber es führt zu einem langen und verworrenen Ausdruck. Was wäre, wenn Sie mehr Ersatz beantragen müssten?

Viele Menschen wissen nicht, dass SQL Server 2017 eine neue Funktion namens TRANSLATE eingeführt hat, die solche Ersetzungen erheblich vereinfacht. Hier ist die Syntax der Funktion:

TRANSLATE ( inputString, characters, translations )

Die zweite Eingabe (Zeichen) ist eine Zeichenfolge mit der Liste der einzelnen Zeichen, die Sie ersetzen möchten, und die dritte Eingabe (Übersetzungen) ist eine Zeichenfolge mit der Liste der entsprechenden Zeichen, durch die Sie die Quellzeichen ersetzen möchten. Das bedeutet natürlich, dass der zweite und der dritte Parameter die gleiche Zeichenanzahl haben müssen. Wichtig an der Funktion ist, dass sie nicht für jeden der Ersetzungen separate Durchgänge durchführt. Wenn dies der Fall wäre, hätte dies möglicherweise zu demselben Fehler geführt wie im ersten Beispiel, das ich mit den beiden Aufrufen der REPLACE-Funktion gezeigt habe. Folglich wird die Bewältigung unserer Aufgabe zum Kinderspiel:

DECLARE @s AS VARCHAR(20) = '123.456.789,00';
SELECT TRANSLATE(@s, '.,', ',.');

Dieser Code erzeugt die gewünschte Ausgabe:

123,456,789.00

Das ist ziemlich ordentlich!

TRIM ist mehr als LTRIM(RTRIM())

SQL Server 2017 hat die Unterstützung für die Funktion TRIM eingeführt. Viele Leute, mich eingeschlossen, gehen zunächst einfach davon aus, dass es sich nicht um mehr als eine einfache Abkürzung zu LTRIM(RTRIM(input)) handelt. Wenn Sie jedoch die Dokumentation überprüfen, stellen Sie fest, dass es tatsächlich leistungsfähiger ist.

Bevor ich ins Detail gehe, betrachten Sie die folgende Aufgabe:Entfernen Sie bei einer Eingabezeichenfolge @s führende und nachgestellte Schrägstriche (rückwärts und vorwärts). Nehmen Sie als Beispiel an, dass @s die folgende Zeichenfolge enthält:

//\\ remove leading and trailing backward (\) and forward (/) slashes \\//

Die gewünschte Ausgabe ist:

 remove leading and trailing backward (\) and forward (/) slashes 

Beachten Sie, dass die Ausgabe die führenden und abschließenden Leerzeichen beibehalten sollte.

Wenn Sie die vollen Möglichkeiten von TRIM nicht kannten, hier ist eine Möglichkeit, wie Sie die Aufgabe möglicherweise gelöst haben:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT
  TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
    AS outputstring;

Die Lösung beginnt mit der Verwendung von TRANSLATE, um alle Leerzeichen durch ein neutrales Zeichen (~) und Schrägstriche durch Leerzeichen zu ersetzen, und verwendet dann TRIM, um führende und nachfolgende Leerzeichen aus dem Ergebnis zu entfernen. Dieser Schritt schneidet im Wesentlichen führende und nachgestellte Schrägstriche ab, wobei vorübergehend ~ anstelle von ursprünglichen Leerzeichen verwendet wird. Hier ist das Ergebnis dieses Schritts:

\\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

Der zweite Schritt verwendet dann TRANSLATE, um alle Leerzeichen durch ein anderes neutrales Zeichen (^) und Backslashes durch Leerzeichen zu ersetzen, und verwendet dann TRIM, um führende und abschließende Leerzeichen aus dem Ergebnis zu entfernen. Dieser Schritt schneidet im Wesentlichen führende und nachgestellte Backslashes ab, wobei vorübergehend ^ anstelle von Zwischenräumen verwendet wird. Hier ist das Ergebnis dieses Schritts:

~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

Der letzte Schritt verwendet TRANSLATE, um Leerzeichen durch Backslashes, ^ durch Forwardslashes und ~ durch Leerzeichen zu ersetzen, wodurch die gewünschte Ausgabe generiert wird:

 remove leading and trailing backward (\) and forward (/) slashes 

Versuchen Sie als Übung, diese Aufgabe mit einer vor SQL Server 2017 kompatiblen Lösung zu lösen, bei der Sie TRIM und TRANSLATE nicht verwenden können.

Zurück zu SQL Server 2017 und höher, wenn Sie sich die Mühe gemacht hätten, die Dokumentation zu überprüfen, hätten Sie festgestellt, dass TRIM anspruchsvoller ist, als Sie ursprünglich dachten. Hier ist die Syntax der Funktion:

TRIM ( [ characters FROM ] string )

Die optionalen Zeichen VON Mit part können Sie ein oder mehrere Zeichen angeben, die am Anfang und am Ende der Eingabezeichenfolge abgeschnitten werden sollen. In unserem Fall müssen Sie lediglich '/\' als diesen Teil angeben, etwa so:

DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
 
SELECT TRIM( '/\' FROM @s) AS outputstring;

Das ist eine ziemlich deutliche Verbesserung im Vergleich zur vorherigen Lösung!

CONCAT und CONCAT_WS

Wenn Sie schon eine Weile mit T-SQL arbeiten, wissen Sie, wie umständlich es ist, mit NULL-Werten umzugehen, wenn Sie Zeichenfolgen verketten müssen. Betrachten Sie als Beispiel die Standortdaten, die für Mitarbeiter in der Tabelle „HR.Employees“ aufgezeichnet wurden:

SELECT empid, country, region, city
FROM HR.Employees;

Diese Abfrage generiert die folgende Ausgabe:

empid       country         region          city
----------- --------------- --------------- ---------------
1           USA             WA              Seattle
2           USA             WA              Tacoma
3           USA             WA              Kirkland
4           USA             WA              Redmond
5           UK              NULL            London
6           UK              NULL            London
7           UK              NULL            London
8           USA             WA              Seattle
9           UK              NULL            London

Beachten Sie, dass für einige Mitarbeiter der Regionsteil irrelevant ist und eine irrelevante Region durch eine NULL dargestellt wird. Angenommen, Sie müssen die Standortteile (Land, Region und Stadt) verketten, indem Sie ein Komma als Trennzeichen verwenden, aber NULL-Regionen ignorieren. Wenn die Region relevant ist, soll das Ergebnis das Format <coutry>,<region>,<city> haben und wenn die Region irrelevant ist, soll das Ergebnis die Form <country>,<city> haben . Normalerweise führt das Verketten von etwas mit NULL zu einem NULL-Ergebnis. Sie können dieses Verhalten ändern, indem Sie die Sitzungsoption CONCAT_NULL_YIELDS_NULL deaktivieren, aber ich würde nicht empfehlen, nicht standardmäßiges Verhalten zu aktivieren.

Wenn Sie nicht von der Existenz der CONCAT- und CONCAT_WS-Funktionen gewusst hätten, hätten Sie wahrscheinlich ISNULL oder COALESCE verwendet, um eine NULL durch eine leere Zeichenfolge zu ersetzen, etwa so:

SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
FROM HR.Employees;

Hier ist die Ausgabe dieser Abfrage:

empid       location
----------- -----------------------------------------------
1           USA,WA,Seattle
2           USA,WA,Tacoma
3           USA,WA,Kirkland
4           USA,WA,Redmond
5           UK,London
6           UK,London
7           UK,London
8           USA,WA,Seattle
9           UK,London

SQL Server 2012 hat die Funktion CONCAT eingeführt. Diese Funktion akzeptiert eine Liste von Zeichenfolgeneingaben und verkettet sie, wobei sie NULLen ignoriert. Mit CONCAT können Sie die Lösung also wie folgt vereinfachen:

SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
FROM HR.Employees;

Dennoch müssen Sie die Trennzeichen explizit als Teil der Eingaben der Funktion angeben. Um uns das Leben noch einfacher zu machen, hat SQL Server 2017 eine ähnliche Funktion namens CONCAT_WS eingeführt, bei der Sie beginnen, indem Sie das Trennzeichen angeben, gefolgt von den Elementen, die Sie verketten möchten. Mit dieser Funktion wird die Lösung weiter vereinfacht wie folgt:

SELECT empid, CONCAT_WS(',', country, region, city) AS location
FROM HR.Employees;

Der nächste Schritt ist natürlich das Gedankenlesen. Am 1. April 2020 plant Microsoft die Veröffentlichung von CONCAT_MR. Die Funktion akzeptiert eine leere Eingabe und findet automatisch heraus, welche Elemente sie verketten soll, indem sie Ihre Gedanken liest. Die Abfrage sieht dann so aus:

SELECT empid, CONCAT_MR() AS location
FROM HR.Employees;

LOG hat einen zweiten Parameter

Ähnlich wie bei der EOMONTH-Funktion wissen viele Leute nicht, dass die LOG-Funktion bereits ab SQL Server 2012 einen zweiten Parameter unterstützt, mit dem Sie die Basis des Logarithmus angeben können. Davor unterstützte T-SQL die Funktion LOG(input), die den natürlichen Logarithmus der Eingabe zurückgibt (unter Verwendung der Konstanten e als Basis), und LOG10(input), die 10 als Basis verwendet.

Sich der Existenz des zweiten Parameters der LOG-Funktion nicht bewusst zu sein, wenn Leute Logb berechnen wollten (x), wo b eine andere Basis als e und 10 ist, haben sie es oft auf die lange Reise gemacht. Sie könnten sich auf die folgende Gleichung verlassen:

Protokollb (x) =Protokolla (x)/Loga (b)

Als Beispiel, um Log2 zu berechnen (8) verlassen Sie sich auf die folgende Gleichung:

Protokoll2 (8) =Loge (8)/Loge (2)

In T-SQL übersetzt wenden Sie die folgende Berechnung an:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x) / LOG(@b);

Sobald Sie feststellen, dass LOG einen zweiten Parameter unterstützt, bei dem Sie die Basis angeben, wird die Berechnung einfach zu:

DECLARE @x AS FLOAT = 8, @b AS INT = 2;
SELECT LOG(@x, @b);

Cursor-Variable

Wenn Sie schon eine Weile mit T-SQL arbeiten, hatten Sie wahrscheinlich viele Gelegenheiten, mit Cursorn zu arbeiten. Wie Sie wissen, verwenden Sie bei der Arbeit mit einem Cursor normalerweise die folgenden Schritte:

  • Deklarieren Sie den Cursor
  • Öffnen Sie den Cursor
  • Durch die Cursor-Datensätze iterieren
  • Cursor schließen
  • Cursor freigeben

Angenommen, Sie müssen in Ihrer Instanz eine Aufgabe pro Datenbank ausführen. Bei Verwendung eines Cursors würden Sie normalerweise Code verwenden, der dem folgenden ähnelt:

DECLARE @dbname AS sysname;
 
DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN C;
 
FETCH NEXT FROM C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM C INTO @dbname;
END;
 
CLOSE C;
DEALLOCATE C;

Der CLOSE-Befehl gibt die aktuelle Ergebnismenge frei und gibt Sperren frei. Der DEALLOCATE-Befehl entfernt eine Cursor-Referenz und gibt, wenn die Zuordnung der letzten Referenz aufgehoben wird, die Datenstrukturen frei, aus denen der Cursor besteht. Wenn Sie versuchen, den obigen Code zweimal ohne die Befehle CLOSE und DEALLOCATE auszuführen, erhalten Sie die folgende Fehlermeldung:

Msg 16915, Level 16, State 1, Line 4
A cursor with the name 'C' already exists.
Msg 16905, Level 16, State 1, Line 6
The cursor is already open.

Stellen Sie sicher, dass Sie die Befehle CLOSE und DEALLOCATE ausführen, bevor Sie fortfahren.

Viele Leute wissen nicht, dass, wenn sie mit einem Cursor in nur einem Stapel arbeiten müssen, was der häufigste Fall ist, Sie anstelle eines normalen Cursors mit einer Cursor-Variablen arbeiten können. Wie bei jeder Variablen ist der Gültigkeitsbereich einer Cursorvariablen nur der Stapel, in dem sie deklariert wurde. Das bedeutet, dass alle Variablen verfallen, sobald ein Batch beendet ist. Wenn Sie eine Cursor-Variable verwenden, schließt SQL Server nach Abschluss eines Stapels diesen automatisch und hebt die Zuordnung auf, sodass Sie die Befehle CLOSE und DEALLOCATE nicht explizit ausführen müssen.

Hier ist der überarbeitete Code, der diesmal eine Cursor-Variable verwendet:

DECLARE @dbname AS sysname, @C AS CURSOR;
 
SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
  SELECT name FROM sys.databases;
 
OPEN @C;
 
FETCH NEXT FROM @C INTO @dbname;
 
WHILE @@FETCH_STATUS = 0
BEGIN
  PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
  /* ... do your thing here ... */
  FETCH NEXT FROM @C INTO @dbname;
END;

Fühlen Sie sich frei, es mehrmals auszuführen und beachten Sie, dass Sie dieses Mal keine Fehler erhalten. Es ist einfach sauberer und Sie müssen sich keine Gedanken über das Aufbewahren von Cursorressourcen machen, wenn Sie vergessen haben, den Cursor zu schließen und die Zuordnung aufzuheben.

MERGE mit OUTPUT

Seit der Einführung der OUTPUT-Klausel für Änderungsanweisungen in SQL Server 2005 hat sie sich als sehr praktisches Werkzeug erwiesen, wenn Sie Daten aus geänderten Zeilen zurückgeben wollten. Benutzer verwenden diese Funktion regelmäßig für Zwecke wie Archivierung, Prüfung und viele andere Anwendungsfälle. Eines der ärgerlichen Dinge an dieser Funktion ist jedoch, dass Sie, wenn Sie sie mit INSERT-Anweisungen verwenden, nur Daten aus den eingefügten Zeilen zurückgeben dürfen, wobei den Ausgabespalten das Präfix inserted vorangestellt wird . Sie haben keinen Zugriff auf die Spalten der Quelltabelle, obwohl Sie manchmal Spalten aus der Quelle neben Spalten aus dem Ziel zurückgeben müssen.

Betrachten Sie als Beispiel die Tabellen T1 und T2, die Sie erstellen und füllen, indem Sie den folgenden Code ausführen:

DROP TABLE IF EXISTS dbo.T1, dbo.T2;
GO
 
CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
 
INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

Beachten Sie, dass eine Identitätseigenschaft verwendet wird, um die Schlüssel in beiden Tabellen zu generieren.

Angenommen, Sie müssen einige Zeilen von T1 nach T2 kopieren; sagen wir diejenigen, bei denen keycol % 2 =1 ist. Sie möchten die OUTPUT-Klausel verwenden, um die neu generierten Schlüssel in T2 zurückzugeben, aber Sie möchten neben diesen Schlüsseln auch die entsprechenden Quellschlüssel von T1 zurückgeben. Die intuitive Erwartung besteht darin, die folgende INSERT-Anweisung zu verwenden:

INSERT INTO dbo.T2(datacol)
    OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
  SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

Leider erlaubt Ihnen die OUTPUT-Klausel, wie bereits erwähnt, nicht, auf Spalten aus der Quelltabelle zu verweisen, sodass Sie die folgende Fehlermeldung erhalten:

Msg 4104, Level 16, State 1, Line 2
Die mehrteilige Kennung "T1.keycol" konnte nicht gebunden werden.

Viele Leute wissen nicht, dass diese Einschränkung seltsamerweise nicht für die MERGE-Anweisung gilt. Auch wenn es etwas umständlich ist, können Sie Ihre INSERT-Anweisung in eine MERGE-Anweisung umwandeln, aber dazu muss das MERGE-Prädikat immer falsch sein. Dadurch wird die WHEN NOT MATCHED-Klausel aktiviert und die einzige unterstützte INSERT-Aktion dort angewendet. Sie können eine Dummy-False-Bedingung wie 1 =2 verwenden. Hier ist der vollständig konvertierte Code:

MERGE INTO dbo.T2 AS TGT
USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
  ON 1 = 2
WHEN NOT MATCHED THEN
  INSERT(datacol) VALUES(SRC.datacol)
OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

Diesmal wird der Code erfolgreich ausgeführt und erzeugt die folgende Ausgabe:

T1_keycol   T2_keycol
----------- -----------
1           1
3           2
5           3

Hoffentlich wird Microsoft die Unterstützung für die OUTPUT-Klausel in den anderen Änderungsanweisungen verbessern, um auch die Rückgabe von Spalten aus der Quelltabelle zu ermöglichen.

Schlussfolgerung

Gehen Sie nicht davon aus, und RTFM! :-)