Glorf.it

Glorf IT

Bedenkliches aus dem IT-Alltag

17. September 2007 um 18:40

Identity-Werte und Trigger

Wenn man mit Identity-Werten arbeitet, dann kann man ganz bequem über die Funktionen @@IDENTITY und SCOPE_IDENTITY() den zuletzt vergebenen Wert erfragen. Wenn man mit Triggern arbeitet, dann kann man auf dabei auf unerwartete Schwierigkeiten stoßen. Wird in dem Trigger beispielsweise in einer Protokoll-Tabelle ein Eintrag gemacht, dann liefern die beiden genannten Funktionen unterschiedliche Werte zurück:

  • @@IDENTITY liefert den zuletzt vergebenen Identity-Wert zurück. In diesem Fall die ID der Protokoll-Tabelle.
  • SCOPE_IDENTITY() liefert hingegen die ID aus der „richtigen“ Tabelle.

Das ist ziemlich einleuchtend, kann aber noch komplizierter werden, wenn man mit Instead-Of-Triggern arbeitet. Für unser Beispiel stelle ich mit eine View vor, die in deren Instead-Of-Trigger die Aktion auf die Basistabelle weiterleitet, also INSERT, UPDATE oder DELETE und dann einen Eintrag in eine Protokoll-Tabelle macht.
In dieser fall sind die Ergebnisse der Funktionen anders:

  • @@IDENTITY liefert den zuletzt vergebenen Identity-Wert zurück. In diesem Fall die ID der Protokoll-Tabelle. Falls auf der Tabelle ein Trigger liegt, dann diese, sonst der aus dem Trigger der View.
  • SCOPE_IDENTITY() liefet hingegen NULL, weil in dem Kontext ja gar kein INSERT durchgeführt wurde, sondern nur im Instead-Of-Trigger.
  • Will man sich mit der OUTPUT-Klausel behelfen, dann wird für inserted.ID immer der Wert 0 ausgegeben.

Die Schlüsse daraus mag nun jeder selber ziehen. Ich für meinen Teil lasse die Finger lieber von Triggern und setze statt dessen Stored-Procedures ein.

Anbei ein paar Samples zum selber ausprobieren:

if object_id ('protokoll') IS NOT NULL
drop table protokoll
go
if object_id ('protokoll') IS NULL
create table protokoll (
id integer identity(1,1) NOT null primary key nonclustered,
ts datetime NOT NULL default getdate(),
spid integer NOT NULL default @@SPID,
[user] sysname NOT NULL default user,
pid integer NULL,
uid integer NULL,
stmt varchar(100) NOT NULL,
comment varchar(300) NULL)
go

if object_id ('MyTriggTab') IS NOT NULL
DROP TABLE MyTriggTab
go
CREATE TABLE MyTriggTab (
id integer identity(1,1) NOT null primary key nonclustered,
comment varchar(300) NULL)
go
CREATE TRIGGER T1 ON MyTriggTab
AFTER INSERT
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'AFTER INSERT', @@SPID, user_id()
go
CREATE TRIGGER T2 ON MyTriggTab
AFTER UPDATE
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'AFTER UPDATE', @@SPID, user_id()
go
CREATE TRIGGER T3 ON MyTriggTab
AFTER DELETE
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'AFTER DELETE', @@SPID, user_id()
go
INSERT INTO MyTriggTab(comment) VALUES ('Test');
SELECT 'INSERT INTO MyTriggTab' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle

UPDATE MyTriggTab set comment = 'Hello';
SELECT 'UPDATE MyTriggTab' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle

DELETE MyTriggTab WHERE ID = SCOPE_IDENTITY();
SELECT 'DELETE FROM MyTriggTab' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle
go
IF object_id ('MyTriggView') IS NOT NULL
DROP VIEW MyTriggView
go
CREATE VIEW MyTriggView (ID, info)
AS
SELECT TOP(3) ID, comment
FROM MyTriggTab
ORDER BY ID DESC
go
CREATE TRIGGER T4 ON MyTriggView
INSTEAD OF INSERT
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'INSTEAD OF INSERT', @@SPID, user_id()
INSERT INTO MyTriggTab(comment)
SELECT inserted.info
FROM inserted
go
CREATE TRIGGER T5 ON MyTriggView
INSTEAD OF UPDATE
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'INSTEAD OF UPDATE', @@SPID, user_id()
UPDATE MyTriggTab
set comment = inserted.info
FROM MyTriggTab JOIN inserted ON MyTriggTab.ID=inserted.ID;
go
CREATE TRIGGER T6 ON MyTriggView
INSTEAD OF DELETE
AS
INSERT INTO protokoll (stmt, pid, uid)
SELECT 'INSTEAD OF DELETE', @@SPID, user_id()
DELETE FROM MyTriggTab
FROM MyTriggTab JOIN deleted ON MyTriggTab.ID=deleted.ID;
go
INSERT INTO MyTriggView(info) VALUES ('View-Test');
SELECT 'INSERT INTO MyTriggView' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle

UPDATE MyTriggView set info = 'Hallo';
SELECT 'UPDATE MyTriggView' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle

DELETE MyTriggView WHERE ID = (SELECT MIN(ID) FROM MyTriggView);
SELECT 'DELETE FROM MyTriggView' as "Statement",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY"; –> liefert ID aus Protokoll-Tabelle
go
DECLARE @outtable TABLE(ID integer);

INSERT INTO MyTriggView(info)
OUTPUT inserted.ID INTO @outtable
VALUES ('View-Test');
SELECT 'INSERT INTO MyTriggView' as "Statement",
ID as "OUTPUT",
SCOPE_IDENTITY() as "SCOPE_IDENTITY()", –> liefert null
@@IDENTITY as "@@IDENTITY" –> liefert ID aus Protokoll-Tabelle
FROM @outtable
go
SELECT * FROM MyTriggView;
SELECT * FROM MyTriggTab
SELECT * FROM protokoll

14. September 2007 um 19:27

SQL-Server: select * in Views

Wenn man "select *" verwendet, dann erwartet man immer alle Attribute einer Tabelle in der Ergebnismenge. Das ist für AdHoc-Abbfragen sehr praktisch, aber für den Einsatz in Programmen ungeeignet.

  • Wenn man später die Tabelle um eine weitere Spalte ergänzt, dann leifert der Befehl plötzlich eine um das Feld erweiterte Ergebnismenge, mit der die Anwendung nicht rechnet.
  • Außerdem erwartet meine Anwendung die Spalten in einer bestimmten Reihenfolge. Nach einer Datenbankänderung kann sich die Reihenfolge aber ändern.

In beiden Fällen muss ich meine Anwendung ändern und das ist sicher mehr Aufwand als die Spalten auszuformulieren, zumal das Visual-Studio for DB-Pros jetzt auch im Refactoring das "*" durch die aktuellen Spalten ersetzen kann.

In Views führt ein "select *" zu einem "eigenwilligen" Verhalten. Es werden hier nämlich nicht immer alle Attribute einer Tabelle in der Ergebnismenge angezeigt!

  • Wenn eine neue Spalte in einer Basistabelle hinzukommt, dann ändert sich die View-Definition bzw. die Struktur seiner Ergebnismenge dadurch nicht. Es werden danach weiterhin nur die "alten" Attribute angezeigt.
  • Wenn eine vorhandene Spalte geändert wird, dann ändert sich die View nicht, es werden die gleichen Spalten (inkl. Typen) zurückgeliefert, wie vor der Änderung. Die Werte werden jedoch aus dem neuen in den alten Datentyp konvertiert.
  • Wird eine vorhandene Spalte entfernt und die View enthält nun mehr Spalten als die Tabelle, dann kommt eine Fehlermeldung.
  • Wird hingegen eine neue Spalte angefügt und eine vorhandene Spalte entfernt, dann wird die View nicht compiliert und die Werte werden aus den falschen Spalten angezeigt. Die Neue wird hinten angefügt und die Fehlende einfach weggelassen.

Blöderweise wird darauf in den Books-Online nicht explizit hingewiesen. Stattdessen wird in allen Beispielen bei "CREATE VIEW" das "*" verwendet! Bei MS liest sich das so:

If a view is not created with the SCHEMABINDING clause, sp_refreshview should be run when changes are made to the objects underlying the view that affect the definition of the view. Otherwise, the view might produce unexpected results when it is queried.

Hier ein paar Samples, zum selber ausprobieren:

use tempdb
go

– Aufräumen, wenn nötig
if object_id('blabla') IS NOT NULL drop table blabla
if object_id('vlavla') IS NOT NULL drop view vlavla
go
– Testtabelle anlegen
create table blabla (id integer identity(1,1), bla varchar(200), z integer)
go
– und füllen
insert into blabla (bla, z) values ('2007-31-12 12:12', 1)
insert into blabla (bla, z) values ('2007-31-12 12:12', 2)
insert into blabla (bla, z) values ('2007-31-12 12:12', 3)
insert into blabla (bla, z) values ('2007-31-12 12:12', 4)
insert into blabla (bla, z) values ('2007-31-12 12:12', 5)
go
– View anlegen
create view vlavla
as select * from blabla
go
– So sieht das Ergebnis im Original aus
select * from vlavla
go

– Tabelle ändern: neue Spalte hinzu
ALTER TABLE blabla ADD muh datetime null
go
UPDATE blabla SET muh = getdate()
go
select * from vlavla
go

– Tabelle ändern: Datentyp ändern
ALTER TABLE blabla ALTER COLUMN bla datetime
go
select * from vlavla
go

– Tabelle ändern: Spalte entfernen (Aber es sind noch so viele Spalten in der Tabelle, wie in der View-Definition)
ALTER TABLE blabla DROP COLUMN bla
go
– So sieht in der View aus
select * from vlavla
– Und so die echte Tabelle
select * from blabla
go

– Tabelle ändern: noch eine Spalte entfernen (Nun sind weniger Spalten in der Tabelle als in der bisherigen View-Definition)
ALTER TABLE blabla DROP COLUMN muh
go
select * from vlavla
–> Jetzt kommt ein Fehler!

12. September 2007 um 23:24

Paul Randal ist nicht mehr bei Microsoft

OK, wahrscheinlich wissen es sowieso schon alle, aber ich habe mich immer noch nicht durch alle Blogs-Postings meiner 4 Urlaubswochen durchgeackert: Paul Randal ("Mr DBCC") ist nicht mehr bei Microsoft. Wie schade. Er sucht sein Auskommen jetzt bei SQLskills mit Schulungen, in denen er sein bei MS erworbenes Wissen weiter gibt. Naja, wenigstens erhöht das die Chancen ihn auch mal auf ein paar europäischen Konferenzen zu sehen…

In dem Zuge hat er auch ein neues Weblog begonnen: "In Recovery…". Sein erster Artikel ist eine aktualisierte Zusammenfassung von Artikeln seines alten Weblogs über defekte Datenbanken. Lesenswert.

12. September 2007 um 22:51

PASS-Franken: Reginaltreffen im September

Für den Fall, dass der ein oder andere Franke es noch nicht weiß, weise ich daraufhin, dass wir am Dienstag, den 18.9.2007 um 18 Uhr in Nürnberg unser nächstes Treffen haben.

Ort: IT innovationsGmbH
Thomas-Mann-Str. 59
90471 Nürnberg
Anfahrt

Der Einstieg ist ein technisches Thema, danach geht es mit dem persönlichen Erfahrungsaustausch weiter:
"Untersuchen und Lösen/Vermeiden von Sperren, Blockaden, Deadlocks etc.."
Schweregrad: 200-400 (je nach Fragen / Diskussionen der Teilnehmer)
Sprecher ist Jörg A. Stryk [MS MVP]

Abstract:
Ein Thema, das mir sehr am Herzen liegt, ist das Untersuchen und Lösen/Vermeiden von Sperren, Blockaden, Deadlocks etc.. Hier habe ich aus der Not heraus schon ein wenig Erfahrung, und möchte deshalb gerne meine Methoden aufzeigen und zur Diskussion stellen um – hoffentlich – von den Kollegen noch weitere hilfreiche Hinweise zu erhalten.

Das wird sicher wieder richtig gut.

10. September 2007 um 19:39

benötigte Rechte für Datenbank-Projekte

Bei meiner ersten Spielerei mit Visual Studio 2008 Beta 2 stieß ich sogleich auf eine erste Hürde: Rechte. Normalerweise sollte man mit der Version als Entwickler keine Windows-Admin-Rechte mehr benötigen. Das mag ja stimmen, wenn man Datenbank-Projekte nutzen will, dann benötigt man aber wenigstens am SQL-Server weitgehende Rechte. Als ich erst mal SysAdmin war, ging es dann.

Ohne kam die Meldung:
"You have insufficient permissions to create the database project. For more information, see product documentation."

Wenn man die Rechte nicht pauschal vergeben will, sondern dediziert, dann kann man sich im Artikel "Required Permissions in Team Edition for Database Professionals" informieren.

10. September 2007 um 18:22

DB-Pro: Unit-Tests zu anderer DB

Notiz für mich (vielleicht hilft es auch jemand anderem):

Wenn man mittels "Visual Studio Team Edition for Database Professionals" Unit-Tests von Datenbank-Objekten (z.B. Views oder Prozeduren) durchführt und man dann man die Verbindungseigenschaften des Projektes ändern will (z.B. andere Datenbank oder anderer SQL-Server), dann kann man das in der Datei "app.config" des Projektes tun.

5. September 2007 um 18:01

Zugang zum SQL-Server erzwingen

Ende Juli veröffentlichte Microsoft eine Beschreibung, wie man sich mit ansonsten abgeschotteten SQL-Servern verbinden kann. Voraussetzung dazu ist, dass man den Dienst stoppen/starten kann. Dazu muss man lediglich den Startparameter "-m" anfügen.

Das kann man natürlich wie beschrieben über die Registry machen, aber das ist ja nicht wirklich spaßig. Ich würde das Werkzeug SQL Server Configuration Manager"" (Aufruf über "Startmenü\Alle Programme\Microsoft SQL Server 2005\Configuration Tools\SQL Server Configuration Manager" oder mittels Start\Ausführen "SQLServerManager.msc"): Dort unter Eigenschaften des SQL-Server-Dienstes im Reiter "Advanced" im Eintrag "Startup Parameters" unten noch "-m" hinzufügen. Dann den Dienst einfach neu starten.

Man kann das auch gerne direkt unter "Start\Ausführen" machen:
"c:\Programme\Microsoft SQL Server\MSSQL.1\MSSQL\Binn\sqlservr.exe" -sMyYukon -m -c

Jetzt kann man sich ganz einfach mittels der Windows-Authentifizierung zu dem SQL-Server verbinden und das vergessene SA-Passwort zurücksetzen oder andere schöne Dinge tun. Denn man hat nun Adminrechte. Am Ende darf an nicht vergessen den Parameter "-m" wieder zu entfernen, sonst hat man den ansonsten lästigen Single-User-Mode immer…

Wenigstens gibt es eine Einschränkung: Für diese Methode benötigt man Admin-Rechte an den betreffenden Rechner. Offensichtlich geht Microsoft davon aus, dass der Windows-Admin auch Administrator des SQL-Servers ist.

Weitere Details stehen im KB-Artikel 937682 "How to add a Windows user to the sysadmin fixed server role in SQL Server 2005 as a failure recovery mechanism"

Vielen Dank an Spencer Tracy, der das sogar schon am Tag vor meinem Urlaub als Kommentar postete.

4. September 2007 um 18:19

Power Tools für DB-Pro

Aufmerksame Leser haben sicher schon den Kommentar von Spencer (13.8.) entdeckt und wissen schon, dass die Power-Tools für das "Visual Studio for Database Professionals" verfügbar sind. Urlaubsbedingt kam ich erst jetzt dazu mir die Tools anzusehen und bin voll begeistert. Die Features können sich wirklich sehen lassen:

  • Data/Schema Compare als Batch mittels MS-Build ausführen
  • Dependency Tool Window
  • Mehr Möglichkeiten im Refactoring
29. August 2007 um 12:35

SQL-Server-Hotfixes per WSUS installieren

Weil ich in einem anderen Blog gerade einen Beitrag zum Windows Server Update Services (WSUS) sah, fiel mir ein neues Feature ein, dass ich mit sehr gemischten Gefühlen sehe:

Man kann jetzt sicherheitskritische Hotfixes zum SQL-Server-2005 automatisch per WSUS installieren lassen. Dazu muss man dem WSUS lediglich sagen, dass er auch die SQL-Server-Hotfixes runterladen soll und auf welchen PCs er sie installieren soll.

Das ist von der Idee her ganz praktisch, aber hat doch ein paar "Herausforderungen":

  • Man kann nicht einzelne Instanzen angeben oder ausschließen. Das kleinste Korn ist der Rechner.
  • Der Update-Client muss in einem Benutzerkontext laufen, der Adminrechte hat. Andernfalls wird der Hotfix-Update gestartet, kopiert schon mal fleißig los, kann sich nicht zum SQL-Server verbinden und beendet daraufhin den SQL-Server-Dienst. Der Stand ist danach völlig inkonsistent. MS hat es abgelehnt das als Bug anzuerkennen.
  • Zukünftig (zuerst nur für SQl-Server-2008) werde ein Feature genutzt mit dem sie auch dann Hotfixes installieren können, wenn der Dienste-Benutzer keinen Zugriff zum SQL-Server hat.
  • Man weiß nicht, welche Hotfixes installiert werden sollen. Neulich wurde ein Hotfix ausgeliefert, der nicht sicherheitsrelevant war. In dem Security-Bulletin war er auch nciht erwähnt. Trotzdem wurde er installiert. Die Antwort von MS darauf war, dass es ein kritischer Hotfix sei und er deswegen ausgeliefert wurde. Woran man das erkennen könne? Na daran, dass er vom WSUS ausgeliefert wurde. Nein, das wird nicht dokumentiert.

Wenn man bedenkt, dass Gordon Mangione damals noch davon abriet einfach SP1 für den SQL-Server-2000 zu installieren. Man solle das erst mal auf einem Testrechner installieren, gründlich die Nebenwirkungen auschecken und dann geplant umsteigen. Dabei werden SPs sehr gründlich getestet, jedenfalls im Vergleich zu den Hotfixes. Und jetzt soll das ratzfatz am Server installiert werden? Wen fragt man zu Risiken und Nebenwirkungen der Hotfixes?

26. August 2007 um 11:11

SQL-Server: Parameter

Auf Jeff's Weblog fand ich den sehr guten Artikel "Always Use Parameters. Even if you don't use Stored Procedures., der mehrere gute Gründe beschreibt, warum es wichtig (nicht nur sinnvoll) ist, generell Parameter zu verwenden.

Neben der Performance spielt auch die Sicherheit – der Schutz vor SQL-Injektion – eine große Rolle. Seine Beispiele beziehe sich auf ADO.Net, aber das gesagte gilt meiner Ansicht nach uneingeschränkt für jedes API.

21. August 2007 um 12:10

fieser Fehler im Tablediff

Neulich stellte ich das Tool TableDiff des SQL-Servers-2005 vor. Leider hat es einen ganz fiesen Fehler:

Lässt man das Werkzeug TableDiff ein Fix-File ("-f") erzeugen, also ein Skript mit den INSERT/UPDATE/DELETE-Befehlen, die Datenbank 2 auf den Stand der ersten heben, dann können dabei fehlerhafte SQL-Befehle erzeugt werden. Die Werte vom Typ NUMERIC liefern falsche Ergebnisse, wenn das Tool von einem Benutzer mit deutschen Ländereinstellungen ausgeführt wird. Als Dezimalpunkt wird dann "," anstelle von "." verwendet, was in SQL natürlich syntaktisch falsch ist.

Anstelle von (numerischer Wert: 12.34)
INSERT INTO [dbo].[difftest] ([id],[num]) VALUES (1,12.34)
kommt leider (numerischer Wert: 12,34)
INSERT INTO [dbo].[difftest] ([id],[num]) VALUES (1, 12,34)

Microsoft erkannte den Bug recht schnell an. Der Entwickler hatte bei der Ausgabe einfach nur ToString() angegeben ohne daran zu denken, dass die Konvertierungsfunktion von den Ländereinstellungen abhängt. Er hätte einfach eine feste Ländereinstellung vorsehen sollen.

Microsoft hat einen Bugfix dazu abgelehnt. Jeder der das Tool in Europa einsetzen will, der muss sich etwas überlegen. Wer einfach seine Ländereinstellungen ändern kann, ist fein raus. Bei mir geht das nicht.
Ich werde mir so behelfen:

  • Ich legte einen lokalen Benutzer "englisch" an. Einmal musste ich mich anmelden und die Ländereinstellungen auf us-amerikanisch setzen.
  • Jetzt starte ich meine Dos-Box immer unter dem Benutzer. Wenn ich dann das Tablediff aufrufe, sind die Befehle richtig.
  • Dazu nutze ich eine kleine selbstgeschriebene RunAs-Variante, der man gleich ein Passwort mitgeben kann. So kann ich eine Verknüpfung hinterlegen, die die Dos-Box sofort (ohne lästigen Dialog) startet.

Das ist natürlich nur eine Notlösung, aber was soll man machen…

18. August 2007 um 10:25

SQL-Server-Konfigurationsparameter

Mit der Abfrage

select name, is_dynamic, is_advanced, minimum, maximum, value, value_in_use, description
from sys.configurations

bekommt man übrigens die Liste der Konfigurationseinstellungen des SQL-Servers-2005. Interessant ist, dass man hier genau sehen kann, ob ein Parameter dynamisch ist oder nach der Änderung ein Neustart des Dienstes erforderlich ist.

"Value_in_use" ist der aktuell gültige Wert, "Value" ist der Wert, der zuletzt mit sp_configure eingestellt wurde.