Archiv der Kategorie: MS SQL Server

relationale Datenbank SQL Server 2005, 2008, 2008 R2, 2012, 2014

Sichere Logging-Tabellen trotz @@identity

In meinem letzten Blog-Eintrag hatte ich geschrieben, man solle möglichst scope_identity() statt @@identity verwenden.

Ich hatte bei einem Kunden genau den Fall, dass wir nachträglich in einer bestehenden Datenbank-Applikation über Trigger Protokollierungen erstellen wollten, um genauere Informationen über ein Fehlverhalten zu bekommen.

Deshalb legten wir eine Logging-Tabelle an, die die entsprechenden Informationen aufnehmen sollte und einen Trigger auf bestimmte Tabellen, um diese Informationen zu sammeln und wegzuschreiben.

Leider hatte der damalige Entwickler @@identity und nicht scope_identity() verwendet, so dass eine Identity-Column in unserer Logging-Tabelle zu Fehlern führte: Beim INSERT erhält das aufrufende Programm eine falsche ID. Hoffentlich ist das Programm sonst sauber programmiert, dann wird man dies wahrscheinlich über krachende Foreign Key-Beziehungen erfahren.

Wir kann man so ein Problem aber lösen, wenn man nichts von dem zugrunde liegenden Programm weiß?

Man  könnte die Identity-Column in der Logging-Tabelle entfernen und stattdessen Unique Identifiers verwenden. Ich hatte ja schon einiges über die Nachteile von Unique Identifiers geschrieben.

In diesem Fall scheint es mir aber gerechtfertigt. Natürlich muss man die Spalte mit newSequentialId() initialisieren, also etwa so:

CREATE TABLE [dbo].[Logging](
    [LogID] [uniqueidentifier]  NOT NULL default(newSequentialId()),
    [Timestamp] [datetime] NOT NULL default(getdate()),
    [Logtext] [nvarchar](max) NULL,
CONSTRAINT [PK_Logging] PRIMARY KEY CLUSTERED
(
    [LogID] ASC
)
)

Dann ist die Änderung unschädlich für den uns unbekannten Code, der möglicherweise @@identity verwendet.

Scope_Identity statt @@Identity

Beides sind Funktionen, um den Wert der Identity-Column nach dem Insert zu erhalten. Als ich SQL Server gelernt hatte (1997), hatte ich nur @@identity gekannt.

@@Identity hat aber einen entscheidenden Nachteil, aber dazu später mehr.

Nehmen wir an, wir haben eine Kunden-Tabelle mit Identity-Column KundeID:

CREATE TABLE [dbo].[Kunden](
    [KundeID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Kunden] PRIMARY KEY CLUSTERED
(
    [KundeID] ASC
)
)

Wenn wir in diese Tabelle einen Kunden eintragen und dann das Ergebnis mit @@Identity oder SCOPE_Identity() abfragen, gibt es keinen Unterschied:

INSERT INTO Kunden SELECT ‚Martin‘
SELECT @@IDENTITY
SELECT SCOPE_Identity()

Beides liefert den Wert 1.

Wenn wir aber nun eine Logging-Tabelle hinzufügen, die ebenfalls eine Identity-Column hat:

CREATE TABLE [dbo].[Logging](
    [LogID] [int] IDENTITY(1,1) NOT NULL,
    [Timestamp] [datetime] NOT NULL default(getdate()),
    [Logtext] [nvarchar](max) NULL,
CONSTRAINT [PK_Logging] PRIMARY KEY CLUSTERED
(
    [LogID] ASC
)
)

Und der Tabelle Kunden einen Insert-Trigger geben, der in diese Tabelle schreibt:

CREATE TRIGGER dbo.tr_i_Kunden
   ON  dbo.Kunden
   AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;
   
    INSERT INTO Logging (LogText) VALUES (‚Kunde angelegt‘)
    INSERT INTO Logging (LogText) VALUES (‚und dann habe ich noch überprüft, ob er schon da ist‘)
    INSERT INTO Logging (LogText) VALUES (‚und noch irgendwas anderes auch gemacht‘)

END

Wenn wir jetzt einen Kunden anlegen, werden automatisch noch 3 Zeilen in die Logging-Tabelle geschrieben.

Da @@identity die letzte Identity-Column der aktuellen Connection zurückliefert, liefert dies den Identity-Wert aus der Logging-Tabelle.

Da SCOPE_Identity() die letzte Identity-Column des aktuellen Scopes (also der betrachteten Tabelle) in der aktuellen Connection zurückliefert, gibt das den Identity-Wert der Kunden-Tabelle.

Wir sehen das hier:

INSERT INTO Kunden SELECT ‚mein Schatz‘
SELECT @@IDENTITY
SELECT SCOPE_Identity()

liefert die Werte 3 für @@identity (da in Logging als letztes die Zeile 3 eingefügt wurde) bzw. 2 für Scope_identity().

Man sollte deshalb grundsätzlich SCOPE_IDENTITY() verwenden, da dies das in der Regel gewünschte Verhalten ist.

Es gibt noch eine Funktion IDENT_CURRENT(‘Tabellenname’). Diese liefert den letzten Identity-Wert für diese Tabelle – egal ob aus meiner Connection oder nicht. Damit sollte man also nicht versuchen, den gerade eingefügten Identity-Wert zu ermitteln. Wenn nämlich parallel eine andere Connection schreibt, hat man Pech gehabt.

Dynamisches Top n in SQL 2000

Ab SQL Server 2005 kann man TOP n über SELECT TOP (@n) … erledigen:

declare @n integer
set @n = 2

select top (@n) a
from
(select 1 as a
union all
select 2
union all
select 3
union all
select 4
union all
select 5
union all
select 6
) as t
order by a desc

Das liefert als Ergebnis

a
———–
6
5

(2 row(s) affected)

In früheren Versionen geht das leider nicht. Da hilft die Verwendung von set rowcount, das die Anzahl der zurückgegebenen Zeilen definiert:

declare @n integer
set @n = 2

set rowcount @n

select distinct  a
from
(select 1 as a
union all
select 2
union all
select 3
union all
select 4
union all
select 5
union all
select 6
) as t
order by a desc

Das liefert das gleiche Ergebnis wie oben.

Allerdings muss man danach wieder

set rowcount 0

absetzen, damit alle nachfolgenden SQL-Statements in dieser Session wieder alle Datensätze zurückliefern.

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

Verhalten von Views bei Strukturänderungen der zugrundeliegenden Tabellen

Ein View, der mittels * alle Spalten einer verwendeten Tabelle zurückgibt, wie z.B.

CREATE VIEW dbo.testView
AS
SELECT u.*, k.name as Kunden_Name, k.AnzahlKinder
FROM umsatz u
INNER JOIN kunde k
ON k.kunde_id = u.kunde_id

verhält sich überaschend, wenn sich die Tabellenstruktur der zugrunde liegenden Tabellen ändert. Die Metainformationen aktualisieren sich nämlich nicht automatisch, was sogar zu Datentypsverletzungen führen kann.

Nehmen wir an, die Tabelle umsatz aus obigen Beispiel habe folgenden Inhalt:

Inhalt der Tabelle Umsatz

Damit liefert die Abfrage des Views

SELECT * FROM testView

folgende Daten:

Inhalt des TestView original

Wird nun in der Tabelle umsatz eine Spalte Rabatt hinzugefügt (und mit Werten gefüllt), liefert das SELECT überaschender Weise:

nach Hinzunahme der Spalte Inhalt des Views

Dabei fällt auf, dass die Spaltentitel die ursprünglichen Inhalte haben, d.h. Rabatt ist nirgends zu sehen, die Werte der Spalten sind aber falsch: In der Spalte Kundenname steht nun der Rabatt und im Feld AnzahlKinder (Datentyp ist eigentlich int!) steht nun der Kundenname.

Das Aktualisieren der Views geht einfach: Nach

exec sp_refreshview 'testview'

sieht wieder alles richtig aus.

Hat man mehrere Views, in einer Datenbank, kann man über

select 'exec sp_refreshview ''' + name + '''' from sysobjects
where xtype = 'V'

sich die notwendigen SQL-Statements automatisch generieren lassen, die man dann einfach ausführt.

Einige Hinweise:

  • Views können natürlich verschachtelt sein, d.h. ein View greift auf einen anderen View zu. Dann müssen die Refresh-View-Statements in der richtigen Reihenfolge oder einfach mehrfach ausgeführt werden.
  • Clustered / Indexed Views können nicht refresht werden – das Statement liefert einen Fehler und muss aus der Liste der auszuführenden Statements gelöscht werden. Das Aktualisieren eines indizierten Views ist aber auch unnötig, da die zugrunde liegenden Tabellen nicht geändert werden können (da der View mit Schema Binding angelegt wurde)

SQL: Dateiname extrahieren

Hier ein schönes Beispiel, in der man die SQL-Server-String-Funktion reverse zum Umdrehen eines Strings (Hallo –> ollaH) sinnvoll einsetzen kann.

Wir nehmen an, dass in einer Tabelle Dateinamen voll qualifiziert stehen, also z.B. c:tempblogtest.txt.

Nun sei die Aufgabe, den Dateinamen (hier test.txt) zu ermitteln. Dazu muss das letzte Auftreten von gefunden werden und der String rechts davon ermittelt werden.

Dies geschieht so:

case when charindex(“, Dateiname) > 0 then
substring(Dateiname, len(dateiname)-charindex(“, reverse(Dateiname))+2, len(dateiname)) else Dateiname end

Performance und GUIDs

In einem Kunden-Projekt sollte ich die Performance der SQL-Zugriffe einer .NET-Applikation verbessern.

Hier beschreibe ich die Ergebnisse, da sie sich auch auf andere Szenarien verallgemeinern lassen.

Die erste Veränderung war, keine GUIDs als clustered primary keys zu verwenden. Stattdessen setzen wir nun ints (mit Identity) ein. Dies führt zu einer immensen Beschleunigung bei den INSERTs in die Datenbank. Dies ist sehr gut nachvollziehbar, da nun die INSERTs immer am Ende der Tabelle statttfinden, so dass kein zeitaufwändiges Umorganisieren der Seiten innerhalb der Tabelle notwendig wird.

Leider ging es aber in meiner Aufgabe um die SELECT-Performance und nicht um die INSERT-Performance. Aber auch diese verbesserte sich durch die Verwendung der integer-Werte deutlich (Abfragezeit ungefähr halbiert). Als Test verwendete ich die Abfrage von 500 Datensätzen, die ich zu Beginn zufällig ausgewählt hatte. Vor jeder Abfrage wurde natürlich der Cache geleert 🙂 Die Steigerung lässt sich dadurch erklären, dass alle Indizes nun vom Platzbedarf viel kleiner wurden (1 int = 4 byte, 1 guid = 16 byte –> ca. 4x so viele Daten gehen auf eine Index-Page [natürlich abhängig von den weiteren Feldern des Index]). Außerdem ist ein Zähler besser verteilt als eine Guid.

Als nächste Verbesserung verwendete ich in m:n-Tabellen als clustered primary key nicht einen Zähler, sondern einen zusammengesetzten Schlüssel aus den beiden referenzierten Tabellen (+ ein weiteres Feld, um die Eindeutigkeit sicherzustellen). Dabei verwendete ich als erstes Feld das Feld der beiden, das in den meisten Abfragen bekannt ist. (Auf der umgekehrten Reihenfolge lag natürlich auch ein Index). Dadurch muss beim Standard-Zugriff nicht mehr über einen non-clustered Index zugegriffen werden, wodurch ein Zugriff eingespart wird. Dies brachte eine weitere Halbierung der Zugriffszeit.

ALs letztes gab es spezielle Szenarien, in denen nach Texten gesucht werden musste – ein Beispiel: Man möchte alle Aufträge ermitteln, die in einer Position einen bestimmten Positionsfreitext enthalten. Dann wird folgendes SQL-Statement abgesetzt:

SELECT * FROM Auftrag a INNER JOIN Auftragsposition pos on pos.AuftragID = a.AuftragID WHERE pos.Positionsfreitext like ‚Test%‘

Natürlich war auf der Auftragspositions-Tabelle ein Index auf Positionsfreitext. Ich erweiterte diesen Index um die AuftragID. Dadurch kann der Join direkt über den Index abgewickelt werden und ein Zugriff auf die Tabelle wird eingespart, was für diesen Spezialfall ebenfalls eine deutliche Performance-Steigerung einbrachte.

SQL Server: zufällige Auswahl von n Zeilen einer Tabelle

Bei einer Aufgabe zur Performance-Steigerung einer SQL Server 2008-Applikation (über die ich später berichten werde), war die erste Aufgabe, ein Test-Szenario aufzubauen, anhand dessen die Abfragezeiten verglichen werden konnten.

Der erste Schritt war dazu die Auswahl 100 beliebiger Sätze aus einer Tabelle. Dabei zeigte mir ein Kollege folgende super einfache Möglichkeit:

select top 100 * from sysobjects order by newid()

[Natürlich muss sysobjects durch die entsprechende Applikations-Tabelle ersetzt werden 🙂 ]

Berechnung der deutschen Kalenderwoche in SQL

Die datepart-Funktion des SQL-Servers liefert eine Woche zurück, wenn man wk als Parameter wählt. Dies ist allerdings nicht die Kalenderwoche. Die deutsche Kalenderwoche funktioniert nämlich so, dass die erste Woche des Jahres mit mindestens 4 Tagen in diesem Jahr die KW 1 dieses Jahres ist. Somit kann der 1.1. eines Jahres noch in der letzten KW des letzten Jahres liegen (nicht so im neuen Jahr 2009 – aber bis 2010 wollte ich nicht mit diesem Blog-Eintrag warten 🙂 )

Offensichtlich hängt die Berechnung davon ab, mit welchem Wochentag die Woche beginnt – bei uns in Deutschland mit dem Montag. Der SQL-Server verfügt über eine Einstellung @@DateFirst, die genau das angibt. Mit SET DATEFIRST 1 kann angegeben werden, dass der Monatg der 1. Tag der Woche ist. Diese Anweisung ist gültig für die Session.

In meinen hier beigefügten Skripten habe ich mich unabhängig von den Einstellungen mit DATEFIRST gemacht, so dass sie unabhängig von allen Einstellungen funktionieren und immer die deutsche Kalenderwoche zurückgeben:

select convert(nvarchar(10), getdate(), 104) Tag, dbo.getKW_Woche(getdate()) KW_Woche, dbo.getKW_Jahr(getdate()) KW_Jahr

liefert als Ergebnis

Tag KW_Woche KW_Jahr
01.01.2009 1 2009

Das bedeutet, dass der heutige Tag in der KW 1/2009 liegt, wie übrigens der 31.12.2008 auch.

In diesem Sinne, ein gutes, gesundes und erfolgreiches Neues Jahr

SQL Server: Versteckte Einstellung für Copy & Paste mit Spaltenüberschriften

Es kommt ziemlich oft vor, dass man das Ergebnis einer SQL-Abfrage (aus dem SQL Server Management Studio) in Excel weiterbearbeiten will (z.B. Filtern, Sortieren etc.). Dabei ist extrem störend, dass die Spaltenüberschriften nicht mit kopiert werden. Glücklicherweise kann man das unter Tools > Options einstellen:

Einstellungen, um die Spaltenüberschriften bei Copy&Paste mitzunehmen

PS: Danke, Stefan, für den Tip.