Quantcast
Channel: Uwe Ricken, Autor bei db-Berater GmbH
Viewing all 109 articles
Browse latest View live

Temporal Tables – Verwendung von Triggern

$
0
0

Trigger sind eine beliebte Technologie, um Geschäftsregeln auf Ebene von Tabellen zu implementieren. Durch die Verwendung von Triggern kann z. B. für die bearbeiteten Datensätze immer der Name und das Datum des letzten Anwenders gespeichert werden, der den Datensatz manipuliert hat. Von relativ einfachen bis zu komplexen Regelwerken sind Trigger in Datenbanken von vielen Entwicklern eine gerne adaptierte Technologie. So “elegant” die Verwendung von Triggern für viele Entwickler sein mag  –  im Zusammenhang mit “System Versioned Temporal Tables” sollten sie auf keinen Fall verwendet werden. Der folgende Artikel zeigt einen klassischen Anwendungsfall, der bei Implementierung von “System Versioned Temporal Tables” eklatante Nachteile in sich birgt.

Testumgebung

Für die nächsten Beispiele wird erneut die Tabelle [dbo].[Customer] in leicht abgewandelter Form verwendet. Die Tabelle besitzt ein Attribut mit dem Namen [UpdateUser]. Dieses Attribut soll bei jeder Aktualisierung automatisch mit dem Namen des Bearbeiters aktualisiert werden.

-- Create a dedicated schema for the history data
IF SCHEMA_ID(N'history') IS NULL
    EXEC sp_executesql N'CREATE SCHEMA [history] AUTHORIZATION dbo;';
    GO
 
IF OBJECT_ID(N'dbo.Customers', N'U') IS NOT NULL
    DROP TABLE dbo.Customers;
    GO
 
CREATE TABLE dbo.Customers
(
    Id         INT          NOT NULL    IDENTITY (1, 1),
    Name       VARCHAR(100) NOT NULL,
    Street     VARCHAR(100) NOT NULL,
    ZIP        CHAR(5)      NOT NULL,
    City       VARCHAR(100) NOT NULL,
    UpdateUser SYSNAME      NOT NULL    DEFAULT (ORIGINAL_LOGIN()),
    ValidFrom  DATETIME2(0) GENERATED ALWAYS AS ROW START NOT NULL DEFAULT ('2016-01-01T00:00:00'),
    ValidTo    DATETIME2(0) GENERATED ALWAYS AS ROW END   NOT NULL DEFAULT ('9999-12-31T23:59:59'),
 
    CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id),
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
);
GO
 
CREATE TABLE history.Customers
(
    Id         INT          NOT NULL,
    Name       VARCHAR(100) NOT NULL,
    Street     VARCHAR(100) NOT NULL,
    ZIP        CHAR(5)      NOT NULL,
    City       VARCHAR(100) NOT NULL,
    UpdateUser SYSNAME      NOT NULL,
    ValidFrom  DATETIME2(0) NOT NULL,
    ValidTo    DATETIME2(0) NOT NULL
);
GO
 
ALTER TABLE dbo.Customers
SET
    (
        SYSTEM_VERSIONING = ON
        (HISTORY_TABLE = History.Customers)
    );
GO

Für die Speicherung der historischen Daten wird die Tabelle [history].[Customers] bereitgestellt. Zusätzlich wird für die Tabelle [dbo].[Customers] ein Trigger entwickelt, der nach der Aktualisierung die betroffenen Datensätze mit dem Namen des Bearbeiters kennzeichnet/aktualisiert.

CREATE TRIGGER dbo.trg_Customers_Update
ON dbo.Customers
FOR UPDATE
AS
    SET NOCOUNT ON;
 
    -- Update the [UpdateUser] with information about
    -- the user!
    UPDATE c
    SET    UpdateUser = 'Donald Duck'
    FROM   dbo.Customers AS C INNER JOIN inserted AS I
           ON (C.Id = I.Id)
 
    SET NOCOUNT OFF;
GO

Szenario

Das folgende Beispiel zeigt, wie Microsoft SQL Server den implementierten Trigger auf [dbo].[Customer] verarbeitet. Dazu wird in einer expliziten Transaktion zunächst der Datensatz mit der ID = 10 aktualisiert.

-- now the first record will be updated
BEGIN TRANSACTION;
GO
 
    UPDATE dbo.Customers
    SET    Name = 'db Berater GmbH'
    WHERE  Id = 10;
    GO
 
    SELECT DTL.resource_type,
           T.object_name,
           DTL.resource_description,
           DTL.request_type,
           DTL.request_mode,
           DTL.request_status
    FROM   sys.dm_tran_locks AS DTL
           OUTER APPLY
           (
               SELECT s.name + N'.' + t.name    AS object_name
               FROM   sys.schemas AS S INNER JOIN sys.tables AS T
                      ON (S.schema_id = T.schema_id)
               WHERE  T.object_id = DTL.resource_associated_entity_id
           ) AS T
    WHERE  DTL.request_session_id = @@SPID
           AND DTL.resource_type != N'DATABASE';
    GO
 
COMMIT TRANSACTION;
GO

Der zweite Teil der obigen Abfrage ermittelt die innerhalb der Transaktion gesetzten Sperren. Dadurch wird erkennbar, welche Objekte durch die Transaktion verwendet/blockiert werden.

Blocked resources 03

In Zeile 9 ist eine exklusive Schlüsselsperre erkennbar. Hierbei handelt es sich um den Datensatz in [dbo].[Customers] der für die Aktualisierung gesperrt wurde. Wesentlich interessanter jedoch ist die RID-Sperre (Rowlocator ID in einem HEAP) für zwei (!) Datenzeilen. Die exklusiven Sperren auf die Ressource 1:2264:2 und 1:2264:3 weisen darauf hin, dass ein Datensatz auf Datenseite 2264 in Slot 2 und ebenfalls in Slot 3 gesperrt wurden. Die Datenseite gehört zur Tabelle [history].[Customers]. Zwei Slots = zwei Datensätze. Microsoft SQL Server verarbeitet in der History Tabelle also zwei Datensätze! Einen noch genaueren Einblick in die Transaktion offenbart ein Blick in das Transaktionsprotokoll:

SELECT  Operation,
        Context,
        AllocUnitName,
        [Page ID],
        [Slot ID]
FROM    sys.fn_dblog(NULL, NULL)
WHERE   [Transaction ID] IN
        (
            SELECT  [Transaction ID]
            FROM    sys.fn_dblog(NULL, NULL)
            WHERE   [Transaction Name] = N'user_transaction'
        )
ORDER BY
        [Current LSN] ASC;
GO

TRAN_LOCKS_02

Die Transaktionsschritte sind chronologisch aufgeführt. Nachdem in Zeile 1 die Transaktion beginnt, wird zunächst ein Datensatz in [dbo].[Customers] geändert [LOP_MODIFY_ROW]. Diese Änderung führt zu einem neuen Eintrag in die Tabelle [history].[Customers] und wird durch die Operation [LOP_INSERT_ROWS] gekennzeichnet. Dieser Prozess wird automatisch durch die implementierte Technologie von “System Versioned Temporal Tables” initiiert. Nachdem der ALTER Datensatz in die History Tabelle eingetragen wurde, wird der benutzerdefinierte Trigger für UPDATE-Ereignisse gestartet und modifiziert den bereits geänderten Datensatz erneut [LOP_MODIFYING_COLUMNS] und erneut schlägt die Technologie von “System Versioned Temporal Tables” zu und trägt den vorherigen Datensatz in die History Tabelle ein. Zum Schluss wird die Transaktion geschlossen [LOP_COMMIT_XACT]. Ein Blick auf die gespeicherten Daten aus beiden Tabellen visualisiert die zuvor beschriebenen Prozessschritte:

;WITH T
AS
(
    SELECT  C.*
    FROM    dbo.Customers AS C
    WHERE   Id = 10
 
    UNION ALL
 
    SELECT  C.*
    FROM    history.Customers AS C
    WHERE   C.Id = 10
)
SELECT * FROM T
ORDER BY ValidFrom DESC;
GO

RECORDSOURCE_01

In Zeile 3 befindet sich der ursprüngliche Datensatz, dessen Name zunächst geändert wurde. Durch die Aktualisierung jedoch wurde dieser Datensatz erneut in die History Tabelle gespeichert und in Zeile 1 steht der letztendlich in [dbo].[Customers] gespeicherte Datensatz!

Lösungen?

Viele Datenbankanwendungen verwenden Trigger und die Entwickler haben viel Businesslogik in diese Trigger implementiert. Eine “einfache” Portierung der Triggerlogik in andere Prozesskanäle ist nicht schnell realisierbar. Welche Möglichkeiten bestehen also, dieses Dilemma zu lösen?

Verwendung von INSTEAD OF Trigger

Eine Idee wäre, den UPDATE-Prozess im Vorfeld abzufangen, die Daten zu manipulieren und dann in einem Update-Statement einzutragen. Während das oben beschriebene Problemszenario mit AFTER-Triggern arbeitet, sollte eine INSTEAD OF-Lösung den gewünschten Erfolg bringen. AFTER-Trigger werden abgefeuert, wenn der Datensatz bereits aktualisiert wurde (innerhalb der Transaktion) und somit die Änderungen bereits in das Transaktionsprotokoll eingetragen wurden. Ein INSTEAD OF Trigger wird ausgeführt, BEVOR die Datenmanipulation stattfindet. Für das Eintragen/Aktualisieren von Daten ist dann der Trigger selbst verantwortlich.

 CREATE TRIGGER dbo.trg_Customers_Update
ON dbo.Customers
INSTEAD OF UPDATE
AS
    SET NOCOUNT ON;
 
    -- Update the [UpdateUser] with information about
    -- the user!
    UPDATE C
    SET    C.Name = I.Name,
           C.Street = I.Street,
           C.ZIP = I.ZIP,
           C.City = I.City,
           C.UpdateUser = 'Donald Duck'
    FROM   dbo.Customers AS C INNER JOIN inserted AS I
           ON (C.Id = I.Id)
 
    SET NOCOUNT OFF;
GO

Das obige Codebeispiel zeigt, dass der vollständige UPDATE-Prozess durch den Trigger verwaltet wird. Versucht man jedoch, den Trigger zu implementieren, stößt man unweigerlich an die Einschränkungen von “System Versioned Temporal Tables”.

ERRORMESSAGE_01

System Versioned Temporal Tables erlauben keine INSTEAD OF Trigger. Diese Restriktion ist zwingend, da Temporal Tables gemäß ANSI 2011 Standard implementiert wurden. Würde ein INSTEAD OF Trigger zugelassen werden, bestünde die Möglichkeit, die Daten in der Tabelle [deleted] zu manipulieren und anschließend diese Daten in die History Tabelle zu leiten.

Verwendung von Stored Procedures

Aus mehreren Gründen empfiehlt sich die Verwendung von Stored Procedures. Sie fordert jedoch ein Umdenken bei den Entwicklern. Statt adhoc-Abfragen in der Applikation zu generieren, die DML-Operationen an die Datenbank senden, wäre die Verwendung von Stored Procedures von mehreren Vorteilen geprägt:

  • Abstraktionsschicht zwischen Anwendung und Daten
  • Implementierung von Geschäftslogik als Business-Schicht
  • Restriktion des Zugriffs auf die Daten
-- Implementation of logic as stored procedure
CREATE PROC dbo.proc_update_customer
    @Id     INT,
    @Name   VARCHAR(100),
    @Street VARCHAR(100),
    @ZIP    CHAR(5),
    @City   VARCHAR(100)
AS
    SET NOCOUNT ON;
 
    -- now the record will be updated with all
    -- information
    UPDATE  dbo.Customers
    SET     Name       = @Name,
            Street     = @Street,
            ZIP        = @ZIP,
            City       = @City,
            -- implementation of UpdateUser
            UpdateUser = ORIGINAL_LOGIN()
    WHERE   Id = @Id;
 
    SET NOCOUNT OFF;
GO

Zusammenfassung

Die Verwendung von Triggern in System Versioned Temporal Tables sollte auf jeden Fall vermieden werden. Durch Trigger, die nachträglich betroffene Datensätze manipulieren, wird ein nicht unerheblicher Overhead an Daten in der History Tabelle generiert. Statt auf die Implementierung von Triggern zu setzen, sollte die Verwendung von Stored Procedures in Betracht gezogen werden. Sie bietet neben der Eliminierung der zuvor genannten Nachteile von Triggern auch Sicherheitsaspekte, um die Daten der Tabellen nicht unmittelbar zu manipulieren.

Bisher veröffentlichte Artikel zu System Versioned Temporal Tables

Herzlichen Dank fürs Lesen!


HEAPS in Verbindung mit DELETE-Operationen

$
0
0

In einem Projekt wurde den Entwicklern gesagt, dass man grundsätzlich mit Heaps arbeiten solle, da durch die Verwendung eines Clustered Index viele Deadlocks verursacht worden sein. Daraufhin hat man für fast alle Tabellen in der Datenbank die geclusterten Tabellen wieder zu Heaps konvertiert. Die Deadlocks sind laut Aussage vollkommen verschwunden – jedoch hat man ein paar Dinge nicht beachtet, die sich nun nachteilig auf die Performance auswirken; und es sind nicht fehlende Indexe gemeint!

Einleitung

Ich persönlich favorisiere Heaps sofern es möglich ist; insbesondere DWH-Szenarien sind prädestiniert für Heaps. Meine Meinung über Heaps habe ich grundsätzlich überdacht, nachdem ich die Artikel “Unsinnige Defaults: Primärschlüssel als Clusterschlüssel” von Markus Wienand und Thomas Kejser “Clustered Index vs. Heap” gelesen habe. Grundsätzlich bieten Clustered Indexe in SQL Server sehr viele Vorteile (insbesondere in der Maintenance); jedoch sollte man nicht immer den “pauschalen” Empfehlungen folgen. Von daher empfehle ich meinen Kunden immer wieder mal, auch über die Alternative HEAP nachzudenken. Die Alternative muss aber – wie im vorliegenden Fall – gründlich überlegt sein und immer von drei Seiten betrachtet werden:

  • Analyse der Workloads
  • Analyse der SELECT-Statements
  • Analyse der Maintenance (Indexfragmentierung, Forwarded Records, DELETE und UPDATE-Verhalten)

Wenn alle Bedingungen “passen”, steht der Verwendung von Heaps nichts mehr im Wege. Im Vorliegenden Fall hat man leider nur die Option 2 im Blick gehabt und dabei vollkommen ausgeblendet, dass in der betroffenen Tabelle sehr viele Aktualisierungs- und Löschvorgänge stattfinden.

Testumgebung

Für die aus dem obigen Fall entstandenen Probleme wird eine Tabelle verwendet, die pro Datenseite einen Datensatz speichert. Diese Tabelle wird mit 20.000 Datensätzen gefüllt.

-- Create a HEAP for the demo
CREATE TABLE dbo.demo_table
(
    Id    INT        NOT NULL    IDENTITY (1, 1),
    C1    CHAR(8000) NOT NULL    DEFAULT ('Das ist nur ein Test')
);
GO
 
-- Now we fill the table with 20,000 records
SET NOCOUNT ON;
GO
 
INSERT INTO dbo.demo_table WITH (TABLOCK) (C1)
SELECT  TOP 20000
        text
FROM    sys.messages;

Sobald die Tabelle befüllt ist, sind insgesamt (inklusive IAM-Seite) 20.001 Datenseiten im Buffer Pool des SQL Servers.

-- what resource of the table dbo.demo_table are in the buffer pool now!
;WITH db_pages
AS
(
    SELECT  DDDPA.page_type,
            DDDPA.allocated_page_file_id,
            DDDPA.allocated_page_page_id,
            DDDPA.page_level,
            DDDPA.page_free_space_percent,
            DDDPA.is_allocated
            sys.dm_db_database_page_allocations
            (
                DB_ID(),
                OBJECT_ID(N'dbo.demo_table', N'U'),
                NULL,
                NULL,
                'DETAILED'
            ) AS DDDPA
)
SELECT  DOBD.file_id,
        DOBD.page_id,
        DOBD.page_level,
        DOBD.page_type,
        DOBD.row_count,
        DOBD.free_space_in_bytes,
        DP.page_free_space_percent,
        DP.is_allocated
FROM    sys.dm_os_buffer_descriptors AS DOBD
        INNER JOIN db_pages AS DP ON
        (
            DOBD.file_id = DP.allocated_page_file_id
            AND DOBD.page_id = DP.allocated_page_page_id
            AND DOBD.page_level = DP.page_level
        )
WHERE   DOBD.database_id = DB_ID()
ORDER BY
        DP.page_type DESC,
        DP.page_level DESC,
        DOBD.page_id,
        DOBD.file_id;

DM_OS_BUFFER_DECRIPTORS_01

Jede Datenseite des Heaps ist nahezu vollständig gefüllt. Nun werden 1.000 Datensätze aus der Tabelle mittels DELETE gelöscht.

-- Now we delete half of the records
SET ROWCOUNT 2000;
DELETE  dbo.demo_table
WHERE   Id % 2 = 0;

Die Analyse des Bufferpools zeigt das Dilemma beim Löschen von Datenseiten aus einem Heap…

DM_OS_BUFFER_DECRIPTORS_02

Entgegen der Erwartung, dass leere Datenseiten automatisch wieder an die Datenbank zurückgegeben (deallokiert) werden, stellt sich die Situation so dar, dass die leeren Datenseiten weiterhin von der Tabelle allokiert sind und im Buffer Pool vorhanden sind. Jede zweite Seite aus dem gezeigten Beispiel ist leer und beim Laden der Daten aus der Tabelle werden diese leeren Datenseiten mit in den Buffer Pool geladen. bei 1.000 Datensätzen macht das immerhin 8 MByte aus.

Lesen von Datenseiten im Heap

Wird ein Heap verwendet, so können Daten nur mit einem Table Scan ermittelt werden. Ein Table Scan bedeutet, dass immer die vollständige Tabelle gelesen werden muss und anschließend die gewünschten Datensätze aus der Ergebnismenge gefiltert werden müssen.

SELECT * FROM dbo.demo_table WHERE Id = 10 OPTION (QUERYTRACEON 9130);

Der Beispielcode generiert folgenden Ausführungsplan. Das TF 9130 wird verwendet, um den FILTER-Operator im Ausführungsplan sichtbar zu machen.

Execution_Plan_01

Faktisch ist ein Heap auf Grund seiner Definition nicht mit einem Ordnungskriterium versehen. Aus diesem Grund verhält sich der Heap wie ein Puzzle und jeder einzelne Stein muss verglichen werden, bis die gewünschten Steine gefunden werden. Ein Heap hat eine weitere Besonderheit, die es in einem Index nicht gibt; die einzelnen Datenseiten haben keine unmittelbare Verbindung zueinander.

HEAP Structure

Eine Datenseite in einem Heap ist isoliert und nur durch die IAM-Seite (Index Allocation Map) werden die vielen autarken Datenseiten miteinander in Verbindung gebracht. Da Microsoft SQL Server beim Lesen von Seite 110 z. B. nicht weiß, welche Seiten außerdem zur Tabelle gehören, muss – im Vorfeld – die IAM Datenseite gelesen werden. Auf dieser Seite stehen alle Datenseiten (max. 64.000), die zur Tabelle gehören. Mit dieser Information kann Microsoft SQL Server anschließend die einzelnen Datenseiten lesen. Diese Technik nennt sich Allocation Scan.

Löschen von Datensätzen in Heap

Wenn man versteht, dass ein Heap kein Ordnungskriterium besitzt, kann man sich vorstellen, was passieren würde, wenn während eines Löschvorgang von Transaktion 1 ein weiterer Vorgang Daten der Tabelle lesen möchte.

Transactions-Concurrency

In der Abbildung laufen zwei Transaktionen zeitversetzt. Transaktion 1 (T1) führt einen Löschvorgang aus während Transaktion 2 (T2) mittels SELECT die Daten für einen anderen Prozess liest. Auf Grund der Strukturen eines Heap müssen beide Prozesse zunächst die IAM Seite lesen. Sobald die IAM Seite gelesen wurde, können beide Prozesse mit dem sequentiellen Durchlaufen der Tabelle beginnen. Würde T1 die Daten und die Datenseite 36 löschen, würde T2 in einen Fehler laufen. Da T2 bereits die IAM Seite gelesen hat, weiß der Prozess, dass er die Datenseite 36 lesen muss. Die wäre aber bei einer Deallokation nicht mehr vorhanden! Aus diesem Grund belässt Microsoft SQL Server die Datenseite als allokierte (aber leere) Datenseite in der Struktur der Tabelle. Unabhängig davon, ob ein weiterer Prozess auf die Tabelle zugreift, ist dieses Verhalten das Standardverhalten von Microsoft SQL Server.

Deallokieren von leeren Datenseiten

Um leere Datenseiten wieder an die Datenbank zurückzugeben, gibt es vier Möglichkeiten:

  • Verwendung von Clustered Index statt Heap
  • Neuaufbau des HEAP mit ALTER TABLE
  • Löschen von Datensätzen mit einer exklusiven Tabellensperre
  • Lock Escalation beim Löschen von großen Datensatzmengen

Während Option 1 und 2 selbsterklärend sind, zeigen die nachfolgenden Beispiele, wie es sich mit exklusiven Sperren verhält:

Löschen von Datensätzen mit exklusiver Tabellensperre

Die einfachste Methode, Datensätze aus einem Heap zu löschen und den allokierten Platz wieder freizugeben, besteht darin, die Tabelle während des Löschvorgangs exklusiv zu sperren.  Der Nachteil dieses Verfahrens liegt jedoch darin, dass ein solches System nicht mehr skalieren kann. Solange ein Löschvorgang durchgeführt wird, können anderen Prozesse nicht auf die Tabelle zugreifen (Lesend und/oder Schreibend). Einzig mit Hilfe der optimistischen Isolationsstufe “READ COMMITTED SNAPSHOT ISOLATION” lässt sich der Lesevorgang unter intensiver Verwendung von Systemressourcen bewerkstelligen; dieses Verfahren soll jedoch in diesem Artikel nicht näher beleuchtet werden.

-- Now we delete 2000 records
BEGIN TRANSACTION;
GO
    DELETE dbo.demo_table WITH (TABLOCK)
    WHERE  Id <= 2000;

Sobald der Code durchgelaufen ist, sind von den ursprünglichen 20.000 Datenseiten nur noch 18.000 Datenseiten vorhanden.

-- what pages have been allocated by the table
SELECT  DDIPS.index_id,
        DDIPS.index_type_desc,
        DDIPS.page_count,
        DDIPS.record_count
FROM    sys.dm_db_index_physical_stats
(
    DB_ID(),
    OBJECT_ID(N'dbo.demo_table', N'U'),
    0,
    NULL,
    N'DETAILED'
) AS DDIPS

DM_DB_INDEX_PHYSICAL_STATS_01

Microsoft SQL Server kann mit Hilfe einer exklusiven Sperre auf der Tabelle gefahrlos die Datenseiten deallokieren, da während des Löschvorgangs kein Prozess auf die IAM-Seite zugreifen kann.

Transactions-Concurrency-X-Lock

Transaktion 1 (T1) beginnt mit dem Löschvorgang und setzt zunächst eine exklusive Sperre auf die Tabelle (in diesem Fall auf die IAM-Datenseite). Solange der Löschvorgang andauert, wird diese exklusive Sperre auf der Tabelle belassen. Während dieser Zeit muss Transaktion 2 (T2) warten. Sobald der Löschvorgang beendet ist, wird die Sperre auf der Tabelle aufgehoben und T2 kann (die nun aktualisierte IAM-Seite) lesen. Das Sperren der kompletten Tabelle hat zur Folge, dass Prozesse, in die die Tabelle involviert ist, nicht mehr skalieren können; die Prozesse müssen seriell bearbeitet werden.

-- output of aquired / released locks
DBCC TRACEON (3604, 1200, -1);
GO
 
-- delete 1,000 records
SET ROWCOUNT 2000;
DELETE dbo.demo_table WITH (TABLOCK)
WHERE  Id % 2 = 0;
GO
 
-- deactivate the output of locks
DBCC TRACEOFF (3604, 1200, -1);
GO

Das Codebeispiel macht die Sperren für die Transaktion sichtbar. Die nachfolgende Abbildung zeigt die gesetzten Sperren.

LOCKS_01

Der Löschvorgang fordert zunächst einen X-Lock auf die Tabelle (OBJECT: 8:245575913). Sobald die Tabellen erfolgreich gesperrt wurde, können Datenseiten und Datensätze gesperrt werden, um sie zu löschen. Durch die exklusive Sperre auf die Tabelle können keine weiteren Prozesse auf das Objekt zugreifen; die IAM ist “gesichert” und kann ohne Gefahren modifiziert werden.

Lock Escalation beim Löschen von großen Datensatzmengen

Das obige Beispiel hat gezeigt, dass man gezielt steuern kann, ob Datenseiten in einem Heap bei einem Löschvorgang wieder freigegeben werden sollen. Jedoch werden Datenseiten auch dann freigegeben, wenn die Tabelle durch den Prozess exklusiv blockiert wird. Als Lock Escalation wird der Vorgang beschrieben, bei dem viele Einzelsperren zu einer Gesamtsperre konvertiert werden. Diese Technik verwendet Microsoft SQL Server, um Ressourcen zu sparen. Der – grobe – Schwellwert für eine Sperrausweitung liegt bei 5.000 Einzelsperren. Wenn z. B. 10.000 Datensätze gelöscht werden sollen, muss Microsoft SQL Server 10.000 exklusive Sperren während der Transaktion halten. Jede Sperre konsumiert 96 Bytes. Bei 10.000 Datensätzen würden das 960.000 Bytes (~1 MB) an RAM während der Transaktion belegt werden. Aus diesem Grund wird ab einem bestimmten Schwellwert die EInzelsperre zu einer Komplettsperre (Partition oder Tabelle) eskaliert.

Das folgende Beispiel zeigt, wie sich Lock Escalation auf das Löschen von großen Datenmengen in einem HEAP auswirkt. Gegeben ist wieder die obige Tabelle mit 20.000 Datensätzen. Aus dieser Tabelle sollen – ohne weitere Tabellenhinweise – 10.000 Datensätze gelöscht werden.

DM_DB_INDEX_PHYSICAL_STATS_02

Da ein Datensatz 8 KBytes konsumiert, hat die Tabelle 20.000 Datenseiten allokiert. Um die Besonderheiten im Transaktionsprotokoll besser lokalisieren zu können, wird eine benannte Transaktion verwendet.

-- Now we delete half of the records
BEGIN TRANSACTION demo;
GO
 
DELETE dbo.demo_table
WHERE  Id % 2 = 0;
GO
 
COMMIT TRANSACTION demo;
GO

Nachdem 10.000 Datensätze gelöscht worden sind (OHNE Tabellenhinweise), stellt sich das Ergebnis der verbliebenen Datenseiten wie folgt dar.

DM_DB_INDEX_PHYSICAL_STATS_03

Die Zahl der verbliebenen Datenseiten ergibt – auf dem ersten Blick – überhaupt keinen Sinn. Die Erwartungshaltung bei diesem Löschvorgang wäre entweder alle 20.000 Datenseiten verbleiben im allokierten Zustand oder aber nur noch 10.000 Datenseiten. Die während des Löschvorgangs gesetzten Sperren stellen sich wie folgt dar:

DM_TRAN_LOCKS_01

20.000 allokierte Datenseiten – 6.876 gesperrte Datenseiten = verbliebene 13.124 Datenseiten. Die Frage, die sich in diesem Zusammenhang stellt: Warum werden 6.876 Datenseiten exklusiv gesperrt und nicht alle 10.000 Datenseiten. Ein Blick in das Transaktionsprotokoll liefert die Antworten zu diesem Verhalten.

DM_TRAN_LOCKS_02

Die erste Abbildung zeigt den Inhalt des Transaktionsprotokolls zu Beginn der Transaktion. Es ist gut zu erkennen, dass einzelne Datensätze gelöscht werden (AQUIRE_LOCK_X_RID). Da zu diesem Zeitpunkt keine exklusive Sperre auf der Tabelle liegt, verbleiben die Datenseiten weiterhin in der Tabelle.

DM_TRAN_LOCKS_03

Ab Zeile 3.126 im Transaktionsprotokoll wird es interessant. Insgesamt wurden – bis zu diesem Punkt – 3.124 Datensätze gelöscht, OHNE die Datenseiten wieder zu deallokieren! Ab Datensatz 3.125 findet eine Lock Escalation statt (Zeile 3126). Nun wird nicht mehr jede einzelne Datenzeile (RID) blockiert sondern die vollständige Tabelle (OBJECT). Wird bei gesperrter Tabelle ein Datensatz gelöscht, ist die Aktion “aufwändiger”:

  • Der Datensatz wird gelöscht (LOP_DELETE_ROWS)
  • Aktualisierung des Headers der betroffenen Datenseite (LOP_MODIFY_HEADER)
  • Freigabe der Datenseite in PFS (LOP_MODIFY_ROW | LCK_PFS)
  • Löschen der Zuordnung aus IAM (LOB_HOBT_DELTA)

Insgesamt werden die ersten Datensätze – bis zur Lock Escalation – aus den Datenseiten gelöscht ohne die Datenseiten zu deallokieren. Bei einer Lock Escalation (Tabelle wird gesperrt) werden ab diesem Moment die Datenseiten im dem Heap deallokiert.

Zusammenfassung

Heaps bieten viele Vor- und Nachteile in einem Datenbanksystem; man sollte vor der Implementierung von Heaps berücksichtigen, dass sie “pflegebedürftiger” sind als Clustered Indexe. Ein Heap reagiert in DML-Operationen komplett anders als ein Clustered Index. Ein Heap sollte daher nur verwendet werden, wenn:

  • Die Tabelle hauptsächlich Daten aufnimmt (INSERT)
    z. B. Protokoll-Tabellen
  • Die Tabelle autark ist und keine Referenz zu anderen Tabellen besitzt
  • Die Attribute des Heaps ausschließlich “Fixed Length” Attribute sind
    (Forwarded Records)

Wenn Daten aus einem Heap gelöscht werden müssen, dann muss man berücksichtigen, dass Microsoft SQL Server Datenseiten in einem Heap nicht automatisch wieder freigibt. Datenseiten werden nur deallokiert, wenn sichergestellt ist, dass die Tabelle nicht durch andere Prozesse gelesen werden kann; die Tabelle muss aslo exklusiv gesperrt sein! Durch die Bearbeitung von Tabellen mit exklusiven Sperren wird ein großer Vorteil von Microsoft SQL Server – Granularität und Concurrency – gewaltsam blockiert. Diese Besonderheiten gilt es bei der Arbeit mit Heaps zu beachten.

Herzlichen Dank fürs Lesen!

Parameter Sniffing – Lösungsansätze

$
0
0

Wer täglich mit Microsoft SQL Server arbeitet – sei es als DBA oder als Entwickler – wird sich schon mal mit dem Problem von Parameter Sniffing auseinandergesetzt haben. “Parameter Sniffing” bedeutet, dass Microsoft SQL Server beim Ausführen einer Stored Procedure/parametrisierten Abfrage den Übergabeparameter verwendet, um die Kardinalität des Wertes zu bestimmen und zukünftige Ausführungen der Abfrage auf Basis des ERSTEN Übergabeparameters durchführt. Die Kardinalität des Parameters fließt in die Bestimmung der Abfragestrategie mit ein. Die Abfragestrategie wird als Ausführungsplan im Plancache abgelegt. Dieser Artikel beschäftigt sich mit den Problemen, die sich aus dieser Arbeitsweise ergeben und zeigt mögliche Lösungsansätze.

Testumgebung

Um das Problem von Parameter Sniffing darzustellen, wird eine Testtabelle mit ca. 10.000 Datensätzen (je nach Version von Microsoft SQL Server) benötigt. Ebenfalls wird eine Stored Procedure für die Ausführung der standardisierten Abfragen verwendet.

Tabelle

Als Datengrundlage dient eine Tabelle mit dem Namen [dbo].[Mitarbeiter]. Aus Gründen der Vereinfachung wird ein Attribut mit einer Datenlänge von 1.000 Bytes verwendet um „Volumen“ zu generieren.

-- Erstellen der Demotabelle
CREATE TABLE dbo.Mitarbeiter
(
    Id         INT        NOT NULL IDENTITY(1,1),
    Name       CHAR(1000) NOT NULL,
    CostCenter CHAR(7)    NOT NULL
);
GO

-- Befüllen der Tabelle mit ca. 10.000 Datensätzen
INSERT INTO dbo.Mitarbeiter WITH (TABLOCK) (Name, CostCenter)
SELECT CAST(text AS CHAR(1000)),
       'C' + RIGHT(REPLICATE('0', 6) + CAST(severity AS VARCHAR(4)), 6)
FROM   sys.messages
WHERE  language_id = 1033;
GO

-- Erstellung der benötigten Indexe
CREATE UNIQUE CLUSTERED INDEX cuix_Mitarbeiter_Id ON dbo.Mitarbeiter (Id);
CREATE NONCLUSTERED INDEX nix_Mitarbeiter_CostCenter ON dbo.Mitarbeiter (CostCenter);
GO

Die Verteilung der Schlüsselwerte des Indexes [nix_Mitarbeiter_CostCenter] kann aus dem Histogramm der gespeicherten Statistiken entnommen werden:

-- Verteilung der Kostenstellen
DBCC SHOW_STATISTICS (N'dbo.Mitarbeiter', N'nix_Mitarbeiter_CostCenter') WITH HISTOGRAM;

histogramm_01
Die Analyse der Verteilung von Kostenstellen zeigt, dass es nur einen Datensatz gibt, der die Kostenstelle „C000013“ als Eintrag gespeichert hat während über 6.300 Einträge zur Kostenstelle „C000016“ vorhanden sind.

Prozedur

Für den Zugriff auf die Daten wird eine Stored Procedure erstellt, die als Parameter die gewünschte Kostenstelle bereitstellt.

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
    @CostCenter	CHAR(7)
AS
    SET NOCOUNT ON;
    SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter
    WHERE CostCenter = @CostCenter;
    SET NOCOUNT OFF;
GO

Problemstellung

Sobald Microsoft SQL Server ein SQL-Statement ausführt, wird der generierte Ausführungsplan im Plan Cache gespeichert, um beim nächsten Aufruf des SQL Statements wiederverwendet zu werden. Dieses Verfahren ist bei komplexen Abfragen ein gewünschter Effekt. Bei der ersten Ausführung der Prozedur mit verschiedenen Parametern werden unterschiedliche Ausführungspläne generiert.

EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000016';

executionplan_ci_scan_01

EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000013';

executionplan_key_lookup_01

Die unterschiedlichen Ausführungspläne kommen zustande, da Microsoft SQL Server unter anderem das zu generierende I/O berücksichtigt. Ein INDEX SEEK (wie z. B. mit Kostenstelle C000013) verursacht zusätzliche 3 I/O pro gefundenen Datensatz. Würde für die Suche nach der Kostenstelle C000016 dieser Ausführungsplan verwendet, so würden insgsamt > 19.000 I/O nur für die Key Lookups produziert werden. Ein TABLE SCAN verursacht lediglich 1.300 I/O.
Entscheidend für die Performance der Stored Procedure ist – und genau das ist das Problem von Parameter Sniffing – die initiale Ausführung der Prozedur. Das folgende Beispiel zeigt die beiden Ausführungspläne, wenn zu Beginn (Plan Cache ist leer!) die Abfrage auf die Kostenstelle „C000013“ ausgeführt wird. Anschließend (der Ausführungsplan ist nun im Plan Cache) wird die Stored Procedure mit dem Wert „C000016“ für die Kostenstelle ausgeführt:

EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000013';
GO
EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000016';
GO

executionplan_02

Bei der ersten Ausführung wird der Parameterwert „C000013“ von Microsoft SQL Server verwendet, um einen Ausführungsplan zu erzeugen. Dieser Ausführunsplan wird im Plan Cache gespeichert. Bei der zweiten Ausführung erkennt Microsoft SQL Server, dass der Ausführungsplan bereits vorhanden ist und verwendet ihn erneut. Hierbei kommt es dann jedoch zu einem gravierenden Nachteil; die Prozedur wurde unter der Annahme zwischengespeichert, dass nur 1 (!!!) Datensatz zurückgeliefert wird. Tatsächlich werden aber bei der zweiten Ausführung > 6.000 Datensätze geliefert. Ein Blick auf die Eigenschaften des gespeicherten Ausführungsplans verdeutlicht das Problem:

plan_parameters_01

Die Strategie des gespeicherten Ausführungsplans basiert auf dem „Parameter Compiled Value“ während die Ausführung durch den „Parameter Runtime Value“ geprägt ist. Ist der Ausführungsplan für die initiale Kostenstelle ideal gewesen, dreht er für die erneute Verwendung mit anderen Parameterwerten ins Gegenteil. Dieses Problem wird Parameter Sniffing genannt. Die Frage, die jeden Programmierer in einem solchen Fall umtreibt: Wie kann man das Problem minimieren/verhindern?

Lösungsansätze

Das Speichern eines Ausführungsplans ist ein essentieller Bestandteil in der Optimierungsphase von Microsoft SQL Server. Wird ein Ausführungsplan im Plan Cache gespeichert, kann Microsoft SQL Server bei der Ausführung der Prozedur sowohl den Kompiliervorgang als auch den Optimierungsvorgang überspringen und die Prozedur unmittelbar ausführen. Für die Prozedur im Beispiel mag ein RECOMPILE nicht ins Gewicht fallen; aber man sollte nicht nur die absolute Zeit berücksichtigen sondern auch im Fokus behalten, wie stark die Prozedur innerhalb der Applikation genutzt wird. Sind es nur ein paar hundert Aufrufe am Tag oder millionenfache Aufrufe – das kann einen nicht unerheblichen Einfluss auf die Performance der Anwendung haben. Aus diesem Grund müssen unterschiedliche Optimierungsmöglichkeiten in Betracht gezogen werden.

Neukompilierung

Das generelle Problem von Parameter Sniffing ist der gespeicherter Ausführungsplan. Ziel des ersten Lösungsansatzes ist es, das Speichern von Ausführungsplänen zu vermeiden oder aber – abhängig vom Parameterwert – einen idealen Plan zu finden.

Neukompilierung durch Applikation

Sofern eine direkte Einflussnahme in den Applikationscode möglich ist, gibt es zwei Ansätze, um eine Neukompilierung zu erzwingen.

Manipulation von Planattributen

Ein Ausführungsplan speichert viele Informationen. Unter den vielen Informationen (z. B. Ausführungstext, Strategie, etc…) gibt es sogenannte Planattribute. Bei diesen Planattributen unterscheidet man zwischen cacherelevanten Einstellungen und nichtrelevanten Einstellungen.

SELECT A.*
FROM    sys.dm_exec_query_stats AS S
        CROSS APPLY sys.dm_exec_sql_text (S.sql_handle) AS T
        CROSS APPLY
        (
           SELECT *
           FROM   sys.dm_exec_plan_attributes(S.plan_handle)
                  WHERE is_cache_key = 1
         ) AS A
WHERE    T.text LIKE '%dbo.Mitarbeiter_pro_Kostenstelle%'
         AND T.text NOT LIKE '%sys.dm_exec_sql_text%';

Der obige Code liefert alle cacherelevante Einstellungen, die Bestandtei des Ausführungsplans sind. Werden Einstelungen VOR der Ausführung der Prozedur geändert, wird ein neuer Ausführungsplan generiert, sofern nicht bereits ein Ausführungsplan vorhanden ist.

plan_attributes_01

-- Ändern der Verbindungseinstellungen
SET DATEFORMAT ymd;
EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000016';
SET DATEFORMAT dmy;

Manipulation des Aufrufs der Prozedur

Die Idee, Sessionkonfigurationen zur Laufzeit anzupassen, ist keine gute Lösung. Das obige Beispiel könnte sogar dazu führen, dass die Applikation nicht mehr korrekt läuft, wenn das Datum nicht in einem vorher bestimmten Format verwendet wird. Oft reicht es aus, den Aufruf unmittelbar anzupassen, damit eine Neukompilierung durchgeführt wird. Ebenfalls wird diese Lösung nur bedingt helfen, da der Ausführungsplan für alle Anwender gilt – und somit bei erneuter Ausführung auf die gleichen Parameter und Cachesettings trifft!
Mit dem folgenden T-SQL-Aufruf (gestartet innerhalb der Applikation) kann eine Neukompilierung der Stored Procedure erzwungen werden:

EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000013' WITH RECOMPILE;
GO
EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000016' WITH RECOMPILE;
GO

Durch die Option „WITH RECOMPILE“ auf Ebene der Prozedur wird die vollständige Prozedur neu kompiliert. Diese Technik ist nicht zu empfehlen, wenn die Prozedur sehr umfangreich ist und innerhalb der Prozedur komplexe Abfragen mit einer langen Optimierungsphase verwendet werden.

Neukompilierung in Prozedur

Wenn es an den Möglichkeiten mangelt, den Applikationscode zu manipulieren, bleibt als Alternativ die Optimierung IN der Prozedur. In diesem Fall muss die Neukompilierung durch die Prozedur selbst bestimmt werden. Hierzu gibt es zwei Lösungsansätze:

  • Neukompilierung der vollständigen Prozedur
  • Neukompilierung des betroffenen Statements

Bis einschließlich Microsoft SQL Server 2000 war eine Neukompilierung nur auf Ebene der Prozedur möglich; seit Microsoft SQL Server 2005 gibt es die Möglichkeit der Neukompilierung auf „Statementlevel“. Um eine Neukompilierung der vollständigen Prozedur zu erzwingen, ist der Hinweis unmittelbar im Prozedurkopf zu platzieren:

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
	@CostCenter	CHAR(7)
	WITH RECOMPILE
AS
	SET NOCOUNT ON;
	SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter WHERE CostCenter = @CostCenter;
	SET NOCOUNT OFF;
GO

Durch die Verwendung von „WITH RECOMPILE“ im Prozedurkopf wird die vollständige Prozedur bei jedem Aufruf neu kompiliert. Bei dieser Technik gilt es zu beachten, wie komplex die Prozedur ist und ob das Verhindern von Parameter Sniffing nicht durch lange Kompilier- und Optimierungszeiten erkauft wird.
Ist eine Prozedur sehr komplex und es lassen sich Statements isolieren, die Opfer von Parameter Sniffing sind, kann ein RECOMPILE seit Microsoft SQL Server 2005 auf einzelne Statements angewendet werden.

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
	@CostCenter	CHAR(7)
AS
	SET NOCOUNT ON;
	SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter WHERE CostCenter = @CostCenter
	OPTION (RECOMPILE);
	SET NOCOUNT OFF;
GO

Verwendung des Density Vectors

Wenn Microsoft SQL Server einen Ausführungsplan erstellt, gibt es zwei Möglichkeiten zur Bestimmung der geschätzten Anzahl von Datensätzen:

  • Verwendung des Histogramms
  • Verwendung des Density Vectors

Microsoft SQL Server kann das Histogramm eines Statistikobjekts nur verwenden, wenn zur Kompilierzeit des Befehls der Wert des Prädikats bekannt ist. Ist das nicht der Fall, muss Microsoft SQL Server vom Histogramm einer Statistik auf den Density Vektor ausweichen.

-- Density Vector der Statistiken
DBCC SHOW_STATISTICS (N'dbo.Mitarbeiter', N'nix_Mitarbeiter_CostCenter')
WITH STAT_HEADER;
DBCC SHOW_STATISTICS (N'dbo.Mitarbeiter', N'nix_Mitarbeiter_CostCenter')
WITH DENSITY_VECTOR;

density_vector_01

Die Abbildung zeigt sowohl den Header als auch den Density Vektor der Statistiken des Index „nix_Mitarbeiter_CostCenter“. Zur Berechnung der geschätzten Anzahl von Datensätzen wird der Wert aus [All Density] im Density Vector mit dem [Rows] aus dem Header multipliziert.
0,0625 * 8932 = 558,25
Ein Problem bei der Verwendung des Density Vektors ist der generische Ansatz, der mit diesen Informationen verfolgt wird. Der Density Vektor kann nur einen Mittelwert bilden. Wenn die Datengrundlage eine gleichmäßige Verteilung der Daten gewährleistet, ist der Density Vektor ein probates Hilfsmittel zur Bestimmung der geschätzten Rückgabemenge; aber dann wäre Parameter Sniffing kein Problem.
Die Demodaten haben einen heterogenen Verteilungsschlüssel; mal sind es extrem viele Datensätze („C000016“) während auf der anderen Seite deutlich weniger Datensätze vorhanden sind („C000013“). Um Microsoft SQL Server zu zwingen, den Density Vektor statt das Histogramm zu verwenden, gibt es zwei programmatische Möglichkeiten:

  • Verwendung einer neuen Variablen im Code der Prozedur
  • Verwendung von OPTION (OPTIMIZE FOR UNKNOWN)

Verwendung von Variablen

Die Kompilierung erfolgt auf Statement-Level. Die Verwendung einer zusätzlichen Variablen innerhalb der Prozedur führt dazu, dass Microsoft SQL Server nicht bestimmen kann, welchen Wert die Variable hat, wenn sie im SELECT-Statement angewendet wird.
Da ein Wert beim Kompilieren des SELECT-Statements nicht deterministisch ist, kann Microsoft SQL Server kein Histogramm verwenden sondern muss auf den Density Vektor ausweichen. Das führt dazu, dass Microsoft SQL Server für JEDE Ausführung den Mittelwert von 558,25 Datensätzen annimmt. Dieser Wert ist für die Kostenstelle C000013 deutlich zu hoch während der Wert für die Kostenstelle C000016 klar zu niedrig ist.

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
    @CostCenter	CHAR(7)
AS
    SET NOCOUNT ON;

    -- Deklaration einer neuen Variablen!
    DECLARE @myCostCenter CHAR(7) = @CostCenter;

    SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter
    WHERE CostCenter = @myCostCenter;

    SET NOCOUNT OFF;
GO

Die Abbildung zeigt die gespeicherten Ausführungspläne für die verwendeten Kostenstellen. In beiden Ausführungsplänen wird vom generischen Wert des Density Vektors als „geschätzte Zeilen“ ausgegangen.

executionplan_03

Verwendung von OPTION (OPTIMIZE FOR UNKNOWN)

Statt mit einer neuen Variablen in der Stored Procedure zu arbeiten, kann im Statement selbst die Option „OPTIMIZE FOR UNKNOWN“ verwendet werden. Wie es der Befehl bereits suggeriert, verhält sich die Anweisung so, als ob ihr der Wert der Prozedurvariablen zum Zeitpunkt der Kompilierung nicht bekannt ist. Diese Option verhält sich exakt wie die zuvor beschriebene Verwendung von neuen Variablen in der Stored Procedure; und erbt damit alle Vor- und Nachteilen dieser Technik.

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
    @CostCenter	CHAR(7)
AS
    SET NOCOUNT ON;

    SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter
    WHERE CostCenter = @CostCenter OPTION (OPTIMIZE FOR UNKNOWN);

    SET NOCOUNT OFF;
GO

Verwendung von Plan Guides

Mit Planhinweislisten kann die Leistung von Abfragen optimiert werden, wenn die Abfrage nicht unmittelbar angepasst werden kann. Planhinweislisten beeinflussen die Abfrageoptimierung, indem Abfragehinweise oder ein fester Abfrageplan für die Abfragen verfügbar gemacht werden. Um einen Abfrageplan zu erstellen, wird die Prozedur sys.sp_create_plan_guide verwendet. Wenn z. B. für die Beispielprozedur angegeben werden soll, dass der Ausführungsplan jedes Mal neu berechnet werden soll (RECOMPILE), müsste ein Planhinweis für die Prozedur folgendermassen implementiert werden:

EXEC sp_create_plan_guide
     @name = N'PG_Employee_By_CostCenter',
     @stmt = N'SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter WHERE CostCenter = @CostCenter;',
     @type = N'OBJECT',
     @module_or_batch = N'dbo.Mitarbeiter_pro_Kostenstelle',
     @params = NULL,
     @hints =  N'OPTION (RECOMPILE)';
GO

Die Verwendung von Planhinweislisten hat den Vorteil, dass Applikationselemente nicht geändert werden müssen. Es gibt viele Softwarehersteller, die – zu Recht – darauf verweisen, dass Anpassungen nicht erlaubt sind und somit ein Garantieanspruch verfällt. Nachteil der Planhinweislisten ist, dass diese Technik nicht in den Express-Editionen von Microsoft SQL Server zur Verfügung stehen.
Ob Microsoft SQL Server eine Planhinweisliste verwendet, kann ebenfalls in den Eigenschaften des Ausführungsplans überprüft werden:

usage_of_planguide

Verwendung von dynamischem Code

Dynamischer Code lässt sich in nicht immer verhindern; für die Lösung des Problems von Parameter Sniffing sollte die nachfolgende Lösung jedoch die letzte Alternative sein. Dynamischer Code bedeutet, dass die auszuführende Abfrage zunächst – dynamisch – in einer Variablen gespeichert wird und anschließend mit EXEC() ausgeführt wird. Die umgeschriebene Prozedur sieht anschließend wie folgt aus:

CREATE PROCEDURE dbo.Mitarbeiter_pro_Kostenstelle
    @CostCenter	CHAR(7)
AS
    SET NOCOUNT ON;

    DECLARE @sql_cmd NVARCHAR(256) = N'SELECT Id, Name, CostCenter FROM dbo.Mitarbeiter
WHERE CostCenter = ' + QUOTENAME(@CostCenter, '''');
    EXEC (@sql_cmd);

    SET NOCOUNT OFF;
GO

Mit der Verwendung von dynamischem SQL geht – neben Sicherheitsbedenken – ein weiterer nicht unerheblicher Nachteil einher, der von vielen Programmierern unterschätzt wird: Plan Cache Bloating!
Bei der Verwendung von dynamischem SQL werden Textfragmente miteinander kombiniert und ergeben so einen – neuen – auszuführenden SQL Befehl. Dieser SQL-Befehl wird dann mittels EXEC in einem eigenen Prozess ausgeführt. Durch die Konkatenation werden keine Variablen innerhalb des SQL-Befehls verwendet sondern ein „fertig designeder Code“ an die Engine gesendet; JEDER unterschiedliche SQL-Befehl wird mit einem eigenen Ausführungsplan im Plan Cache gespeichert!
Die Beispielprozedur wird mit drei unterschiedlichen Kostenstellen ausgeführt. Bei der Analyse der Ausführungspläne ist offensichtlich, dass Microsoft SQL Server für jedes Statement einen „individuellen“ Ausführungsplan erstellt.

EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000013';
GO
EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000016';
GO
EXEC dbo.Mitarbeiter_pro_Kostenstelle @CostCenter = 'C000019';
GO
SELECT  T.Text,
        QP.usecounts,
        QP.size_in_bytes,
        QP.objtype
FROM    sys.dm_exec_cached_plans AS QP
        CROSS APPLY sys.dm_exec_sql_text (QP.plan_handle) AS T
WHERE   T.text LIKE '%FROM dbo.Mitarbeiter%'
        AND T.text NOT LIKE '%sys.dm_exec_sql_text%';

Die Prozedur wurde insgesamt drei Mal ausgeführt und statt eines einzigen Ausführungsplans wurden drei Pläne erstellt.

plan_cache_bloating_01

Zusammenfassung

Parameter Sniffing ist bei ungleicher Verteilung von Daten eine Herausforderung für jeden SQL Entwickler. Es gibt keine allgemeine Lösung, die als „Silver Bullet“ alle Probleme löst. Sind – wie im verwendeten Beispiel – nur wenige Schlüsselwerte mit unterschiedicher Kardinalität vorhanden, kann die Verwendung von dynamischem Code eine gute Idee sein. Hierbeit gilt es jedoch, den Zugriff auf die Daten durch Views einzuschränken und keinen unmittelbaren Zugriff auf die Tabellen zu gewähren.
Sind die Datenmengen für die einzelnen Werte zu unterschiedlich und Sicherheit ist ein wichtiges Thema, dann scheidet eine Lösung mit dynamischem SQL aus.
Es verbleiben abschließend nur zwei Alternativen. Der Zugriff auf den Density Vekor wird für viele Situationen eine gute Alternative sein; es gibt jedoch Situationen, in denen diese Alternative nicht wirkt, da die verschiedenen Datenwerte zu große Abstände in der Datenmenge haben und somit den Density Vector als ungeeignetes Mittel erscheinen lassen.
Letzte Alternative ist – und bleibt – das Vermeiden einer Speicherung des Ausführungsplans. Nur so ist gewährleistet, dass für jeden einzelnen Wert ein geeigneter Ausführungsplan gewählt wird. Diese Lösung ist jedoch nur so lange ein adäquates Mittel, so lange die Kompilier- und Optimierungsphasen schnell und effizient sind. Wird für den Optimierungsvorgang mehr Zeit aufgewendet als für einen „schlechten“ Plan, sollte man eher den schlechten Plan in Kauf nehmen.

Lesenswert!

Herzlichen Dank furs Lesen!

Special Interest Group–SQL Server

$
0
0

Am 14.12.2016 fand zum 6. Mal das Treffen der S.I.G. – SQL Server Internals statt. Dieses – virtuelle – Treffen wurde wie immer aufgezeichnet und kann über den S.I.G.-eigenen Youtube-Channel abgerufen werden. Thema des Treffens war eines meiner Lieblingsthemen – HEAPS.

 

Nach einer kurzen Einführung in die Grundlagen von HEAPS beschreibe ich die Speicherstrukturen und erkläre sowohl die Funktion von IAM und PFS als Hotspots einer Tabelle. HEAPS sind deutlich pflegeintensiver und erfordern eine sorgfältige Planung von Tabellenstrukturen. Wer die internen Details von HEAPS besser kennenlernen möchte, ist mit diesem Video sicherlich ein ersten Schritt gegangen.

Viel Spaß beim Anschauen und Lernen.

Abfragen nicht mit Variablen testen

$
0
0

Häufig fällt auf, dass Datenbankentwickler ihre Abfragen mit Hilfe von Variablen testen. Die Programmiersprachen, wie z. B. C, C++, Basic und Java, besitzen ihre eigenen Variablen zur Aufnahme von Daten und diesen Lösungsansatz möchte man gerne in der Datenbank wiederverwenden. Der gravierende Unterschied zwischen beiden Lösungsansätzen liegt darin, dass Abfragen in Microsoft SQL Server diese Variablen zur Evaluierung der geschätzten Abfragekosten verwenden. Der folgende Artikel beschreibt, warum Variablen für Tests ungeeignet sind und zeigt Alternativen, wie man trotz der Verwendung von Variablen in Testabfragen zuverlässige Auswertungen durchführen kann.

Ausführung einer Abfrage

Bevor Microsoft SQL Server eine Abfrage ausführt, müssen verschiedene Schritte bearbeitet werden. Der Abfrageprozessor durchläuft drei Phasen, bevor ein Plan aus einer Abfrage erstellt wird. Zuerst analysiert und normalisiert der Abfrageprozessor die Statements. Dann kompiliert er den Transact SQL (T-SQL) Code. Schließlich optimiert er die SQL-Anweisung.

Parsing

Beim Parsing prüft der Abfrageprozessor die korrekte Syntax einschließlich der korrekten Schreibweise und der Verwendung von Schlüsselwörtern. Die Hauptfunktion besteht darin, zu prüfen, ob Tabellen und Spalten vorhanden sind, Metadaten über die Tabellen und Spalten geladen und Informationen über erforderliche (implizite) Konvertierungen zum Sequenzbaum hinzugefügt werden können.

Kompilierung

In der zweiten Phase sucht der Abfrageprozessor nach Anweisungen und führt die Anweisungen über einen T-SQL-Sprachcompiler aus. Diese Anweisungen (z. B. Variablendeklarationen und Zuweisungen, bedingte Verarbeitung mit IF und Iteration mit WHILE) sind nicht Teil der grundlegenden DML-Syntax (SELECT, INSERT, UPDATE und DELETE). Sie sind zusätzlichen Features, um die Funktionalität des SQL-Codes zu erweitern. Diese Anweisungen brauchen keine Optimierung, aber sie müssen vor der Ausführung kompiliert werden.

Optimierung

Die komplexeste Phase der Abfrageverarbeitung ist die Optimierung. Der Abfrageoptimierer bestimmt den idealen Ausführungsplan für jede SQL-Anweisung in einem Abfrage-Batch oder einer gespeicherten Prozedur. Die Optimierungsphase ist davon geprägt, in kürzester Zeit einen “besten Plan” zu ermitteln Mit den vielen Möglichkeiten – darunter drei Arten von Joins und eine beliebige Anzahl von Indizes auf jeder Tabelle – könnte die Auswertung aller möglichen Ausführungspläne länger dauern als die Ausführung der Abfrage.
Der Kompilierprozess ist der wesentliche Bestandteil VOR der Optimierung. Eine Kompilierung findet immer auf Ebene eines Statements statt, nicht für den gesamten Batch!

Elemente von Statistiken

Der Optimierungsprozess benötigt fundamentale Informationen über die zu erwartende Ergebnismenge. Nur mit einer guten Schätzung er zu erwartenden Datensätze lässt sich ein – aus Sicht von Microsoft SQL Server – optimaler Plan gestalten. Damit Microsoft SQL Server Informationen über die Anzahl und die Verteilung der Datenwerte erhält, werden Statistiken erstellt und gepflegt. Für die Ermittlung von zu erwartenden Datenmengen stehen Microsoft SQL Server zwei Bereiche in den Statistiken zur Verfügung.

Density Vector

Der Abfrageoptimierer verwendet den Density Vector, um Kardinalitätsschätzungen für Abfragen zu erweitern. Der Density Vector enthält einen Verdichtungskoeffizienten für jedes Präfix von Spalten im Statistikobjekt. Der Wert für eine Verdichtung berechnet sich aus 1 / Anzahl verschiedener Werte.

Histogramm

Das Histogramm berechnet die Häufigkeit des Vorkommens eines unterschiedlichen Wertes in einer Tabelle / Index. Der Abfrageoptimierer berechnet ein Histogramm für die Spaltenwerte in der ersten Schlüsselspalte des Statistikobjekts und wählt die Spaltenwerte aus, indem statistische Zeilenstichproben entnommen werden oder indem ein vollständiger Scan aller Zeilen in der Tabelle oder Sicht ausgeführt wird. Das Histogramm kann nur verwendet werden, wenn dem Abfrageprozessor zum Zeitpunkt der Kompilierung der zu suchende Wert bekannt ist!

Testumgebung

Für die nachfolgenden Tests ist eine Tabelle mit ca. 100.000 Datensätzen erstellt worden.

CREATE TABLE CustomerData
(
    Id     INT           NOT NULL    IDENTITY (1, 1),
    [Name] VARCHAR(100)  NOT NULL,
    CCode  CHAR(3)       NOT NULL,
    Street VARCHAR(100)  NOT NULL,
    ZIP    CHAR(5)       NOT NULL,
    City   VARCHAR(100)  NOT NULL
);
GO

CREATE UNIQUE CLUSTERED INDEX cuix_CustomerData_Id ON dbo.CustomerData (Id);
CREATE NONCLUSTERED INDEX nix_CustomerData_CCode ON dbo.CustomerData (CCode);
GO

Während der gruppierte Index hoch selektiv ist, ist der Index auf dem Ländercode eher nicht selektiv. Ein Blick in das Histogramm des Index zeigt die Verteilung der unterschiedlichen Ländercodes in der Tabelle.

DBCC SHOW_STATISTICS (N’dbo.CustomerData’, N’nix_CustomerData_CCode’);

DBCC SHOW_STATISTICS 01

Aus dem Histogramm ist ersichtlich, dass die Datenverteilung sehr verzerrt ist. Gibt es nur ca. 200 Kunden aus Österreich so sind es für Deutschland über 90.000 Kundenbeziehungen.

Testabfrage

Schaut man den Entwicklern über die Schulter, findet man relativ häufig Statements der folgenden Art:

DECLARE @CCode CHAR(3) = 'AU';

SELECT * FROM dbo.CustomerData
WHERE  CCode = @CCode
ORDER BY
       Name;
GO

Die Intention es Programmierers ist bereits in der ersten Zeile offensichtlich – durch einfaches Austauschen des Wertes für die Variable möchte er schnell erkennen, wie performant sich die Abfrage bei unterschiedlichen Abfragewerten verhält. Der Ausführungsplan für die obige Abfrage stellt sich wie folgt dar:

EXECUTION_PLAN_01

Auf den ersten Blick scheint der Ausführungsplan für die Abfrage ideal. Bedingt durch das “ORDER BY” muss ein SORT-Operator verwendet werden, der seine Daten aus dem gruppierten Index bezieht. Fraglich ist jedoch, warum Microsoft SQL Server für die Evaluierung der Daten einen SCAN über die vollständige Tabelle durchführen muss, obwohl lediglich 217 Datensätze zu erwarten sind.  Das Ausführungsprofil zeigt den Grund für dieses Verhalten.

EXECUTION_PROFILE_01

Obwohl das Histogramm 217 Datensätze ausweist, geht der Abfrageoptimierer von einer deutlich höheren Anzahl von Datensätzen aus; seine Schätzung betreffen 33% der gesamten Datenmenge. Zum Vergleich dazu wird die Abfrage mit einem explizit definierten Literal ausgeführt:

SELECT * FROM dbo.CustomerData
WHERE  CCode = 'AU'
ORDER BY
       Name;

EXECUTION_PLAN_02

Auch das Ausführungsprofil zeigt deutliche Unterschiede zur vorherigen Version!

EXECUTION_PROFILE_02

Vergleicht man die geschätzten Zeilen, ist auffällig, dass bei der Ausführung der Abfrage mit einem in der Abfrage definierten Literal die Schätzungen deutlich akkurater sind, als im vorherigen Beispiel. Doch woher kommen diese immensen Unterschiede. Die Erklärung liegt in der Vorgehensweise der Kompilierung einer Abfrage.
Im ersten Abfragebatch wird zunächst eine Variable deklariert und gekennzeichnet. Diese Anweisung wird von Microsoft SQL Server kompiliert und anschließend gleich wieder vergessen! Wird der zweite Teil des Batches ausgeführt, kennt Microsoft SQL Server den Wert der Variablen (für die Kompilierung des Statements) nicht mehr. Damit kann Microsoft SQL Server nicht nach einem geeigneten Wert im Histogramm des Statistikobjekts schauen und kann sich nur am Density Vector der Statistik orientieren.

Use_of_Density_Vector

Der abgebildete Workflow verdeutlicht das Problem bei der Verwendung von Variablen in einem Befehlsbatch. Die zweite Befehlszeile verwendet “eine” Variable @CCode, die zuvor initialisiert wurde. In der Kompilierphase betrachtet Microsoft SQL Server jedoch das Statement isoliert und somit weiß Microsoft SQL Server während des Kompilierens nicht, welchen Wert die Variable zur Laufzeit besetzt. Da kein Wert für die Variable vorhanden ist, kann Microsoft SQL Server nicht auf das Histogramm zugreifen – nach welchem Wert soll Microsoft SQL Server dann suchen? Vielmehr muss als Alternative der Density Vector verwendet werden. Die Abfrage verwendet ausschließlich das Attribut [CCode] als Prädikat. Für dieses Prädikat wird eine Verteilungsdichte von 1/3 = 0,33333 berechnet. Diese Dichte wird mit der Anzahl der vorhandenen Datensätze (ca. 10.0000) multipliziert und das Resultat ergibt die geschätzten Datensätze für die Ausgabe.

EXECUTION_PLAN_03

Problematischer bei der vorliegenden Beispiel-Abfrage ist jedoch der SORT-Operator. Bei einem SORT-Operator handelt es sich um einen sogenannten “STOP-Operator”. Ein STOP-Operator leitet die Daten nach Erhalt nicht automatisch weiter, sondern muss sie vor der Weiterleitung zwischenspeichern, um mit ihnen zu arbeiten. Der SORT-Operator speichert die Daten zunächst zwischen, um sie anschließend zu sortieren und dann an den vorherigen Operator weiterzuleiten. Dazu benötigen solche STOP-Operatoren aber eine Speicherreservierung. Die Höhe der Reservierung von Speicher hängt von den geschätzten Datenzeilen ab. Für das obige Beispiel wurden ca. 15 MB reserviert.

EXECUTION_PLAN_04

15 MB mögen auf den ersten Blick nicht als besonders viel erscheinen – aber was passiert, wenn 1.000 Anforderungen an den SQL Server gerichtet werden, die jeweils 15 MB benötigen. Unabhängig von der Anzahl der Anforderungen sollte Microsoft SQL Server schonend mit den Ressourcen umgehen und nur so viel Ressourcen binden, wie auch tatsächlich genutzt werden. Ein Blick auf die Abfrage mit einem Literal zeigt den deutlichen Unterschied.

EXECUTION_PLAN_05

Der Ausführungsplan für die Abfrage mit Literal mag komplexer aussehen; ist sie aber nicht. Die Abfrage ist höchst effizient, da sie – anders als bei Verwendung einer Variable – während der Kompilierphase den Wert für das Prädikat kennt und gezielt im Histogramm nach dem Wert suchen kann.

Use_of_Histogram

Da Microsoft SQL Server nun weiß, wie viele Datensätze an den Client zurückgeliefert werden müssen, kann von einer präziseren Planung bei der Erstellung des Ausführungsplans ausgegangen werden. Das spiegelt sich – neben einer optimaleren Strategie  – auch bei der Berechnung der zu verwendenden Ressourcen wider.

Lösung trotz Variablen?

Die meisten Programmierer möchten nicht gerne auf Variablen verzichten. Das Beispiel für diesen Artikel ist sehr übersichtlich; aber was ist mit einem SQL-Statement, dass über 50, 60 oder mehr Zeilen geht. Da möchte man nur ungern im Code nach den Variablen suchen und sie durch Literale ersetzen. Was also tun? Das Geheimnis liegt in der Ausführung des Statements mit Hilfe von sp_executesql! Mit Hilfe von sp_executesql ist es möglich, das Statement zu kapseln und MIT Variablen an den Parser von Microsoft SQL Server zu übergeben. Das Statement wird ähnlich behandelt wie eine Stored Procedure, der ein Parameterwert übergeben wird.

DECLARE @CCode CHAR(3) = 'AU';
DECLARE @stmt NVARCHAR(1000) = N'SELECT * FROM dbo.CustomerData
WHERE  CCode = @CCode
ORDER BY
       Name;';

DECLARE @vars NVARCHAR(64) = N'@CCode CHAR(3)';

EXEC sp_executesql @stmt, @vars, @CCode;
GO

Das obige Code-Beispiel generiert ein abschließendes SQL-Statement mit einer Variablen, die erst während des Kompilierens von sp_executesql übergeben wird. Dadurch kann Microsoft SQL Server den Wert “sniffen” und in der Kompilierphase verwenden. Der Wert ist bekannt und Microsoft SQL Server kann auf das Histogramm zugreifen. Aber VORSICHT ist hier angeraten – mit sp_executesql schaffen wir uns andere Probleme, die unter Umständen gravierender sein können: Parameter Sniffing. Über dieses Problem habe ich in meinem Artikel “Parameter Sniffing – Lösungsansätze” sehr ausführlich geschrieben.
Für das Testen eines SQL-Statements mit Hilfe von Variablen wäre es empfehlenswert, die Abfrage mit Hilfe von RECOMPILE jedes Mal neu kompilieren zu lassen. Dann wird auch der Parameter korrekt gelesen.

DECLARE @CCode CHAR(3) = 'AU';

SELECT * FROM dbo.CustomerData
WHERE  CCode = @CCode
ORDER BY
       Name
OPTION (RECOMPILE);
GO

Mehr Informationen?

Statistiken sind ein wichtiger Faktor bei der Optimierung von Abfragen. Sie zu verstehen, Ergebnisse zu analysieren und zu bewerten sind ein wesentlicher Bestandteil für das Verständnis der Operationen von Microsoft SQL Server. Das Thema Statistiken mit Fallbeispielen aus der täglichen Praxis ist zu einer meiner Lieblingssessions geworden, die ich überall in Europa vortrage. Unter anderem bin ich mit diesem Vortrag am 16.02.2017 um 10:45 auf der SQLKonferenz anzutreffen. Wer also Lust, Zeit und Interesse hat, darf sich sehr gerne hier für die größte 3-tägige deutsche SQL Server Konferenz in Darmstadt anmelden.

Herzlichen Dank fürs Lesen!

SARGable- und Non-SARGable-Abfragen

$
0
0

Ein Kunde beklagte sich über die schlechte Ausführungsgeschwindigkeit einer Funktion, die er von einem Programmierer erhalten hatte. Bei der Durchsicht des Codes der Funktion war das Problem schnell gefunden. Statt eines performanten Indexseek hat die Abfrage einen Indexscan durchgeführt. Ursache für die schlechte Performance war, dass die WHERE-Klausel keine SARGable Argumente verwendete.

Was bedeutet SARGable?

Hinter dem Begriff “SARGable” verbirgt sich ein Akronym für “Search ARGumentable Query”. Dieser Ausdruck sagt aus, dass ein RDBMS-System mit Hilfe eines Prädikats in der Lage ist, Werte innerhalb eines Index zu finden. Dabei kann das RDBMS-System gezielt nach den Werten innerhalb des Index mit Hilfe einer SEEK-Operation suchen. Wenn als Prädikat (WHERE-Klausel) eine Transformation der Werte einer Spalte durchführen muss, spricht man von “NONSARGable” Abfragen. In diesem Fall kann das RDBMS-System einen Index nicht gezielt durchsuchen sondern muss jeden Eintrag transformieren und mit dem Prädikat vergleichen. Eine SEEK-Operation ist dann nicht mehr möglich und es muss eine SCAN-Operation verwendet werden.

Testumgebung

Für die Demonstration der unterschiedlichen Verfahren dient eine einfache Tabelle, in der Mitarbeiter und deren Kostenstellen gespeichert werden.

Tabelle

CREATE TABLE dbo.Employees
(
    ID            INT        NOT NULL,
    [Name]        CHAR(1000) NOT NULL,
    [CostCenter]  CHAR(5)    NOT NULL
);
GO
 
CREATE NONCLUSTERED INDEX nix_Employees_CostCenter ON dbo.Employees (CostCenter);
GO

Die Tabelle besitzt ~12.500 Datensätze.

Distribution der Daten

Die Beispieldaten in der indexierten Spalte [CostCenter] sind wie folgt verteilt

DBCC SHOW_STATISTICS (N'dbo.Employees', N'nix_Employees_CostCenter') WITH HISTOGRAM;

DATA_DISTRIBUTION_01

Für die nachfolgenden Beispiele sollen alle Mitarbeiter der Kostenstellen Cxxxx dienen.

Funktion

Die vom Programmierer zur Verfügung gestellte Funktion hatte den – korrespondierend zum Beispiel – folgenden Inhalt:

CREATE FUNCTION dbo.GetEmployeesByCostCenter
    (@CostCenterPrefix CHAR(1))
RETURNS TABLE
AS
RETURN
(
    SELECT  Id, Name, CostCenter
    FROM    dbo.Employees
    WHERE   SUBSTRING(CostCenter, 1, 1) = @CostCenterPrefix
);

Die Funktion erwartet als Parameter lediglich das Präfix der gewünschten Kostenstellen. Der Programmierer hat die Funktion SUBSTRING verwendet, um aus dem gespeicherten Wert das Präfix zu filtern.

Demonstrationen

Ursprüngliche Funktion

Die Abfrage nach Daten der Kostenstellen, die mit “C” beginnen (160 Datensätze) generieren den folgenden Ausführungsplan.

SELECT * FROM dbo.GetEmployeesByCostCenter('C');

EXECUTION_PLAN_01

Die Abfrage verwendet einen TABLE SCAN und kann den Index auf [CostCenter] nicht verwenden. Ein Blick in die Eigenschaften des Operators zeigt das Problem. Statt einen performanten INDEX SEEK verwendet Microsoft SQL Server für ALLE Datensätze einen transformierten Wert, der aus der Spalte [ConstCenter] resultiert. Das erste Zeichen aus [CostCenter] wird extrahiert. Anschließend kann dieser extrahierte Wert mit dem Übergabeparameter verglichen werden. Da erst JEDER Wert transformiert werden muss, ist ein INDEX SEEK nicht möglich.

DATA_SET_01

Die obige Abbildung zeigt, wie sich die Abfrage verhält. Das Ergebnis der [Transformation] wird mit dem Präfix verglichen, das der Funktion übergeben wird. Alle Datensätze, deren Transformation der Spalte [CostCenter] ein “C” ergeben, werden an den Client zurückgeliefert. Dazu ist es aber erforderlich, erst für ALLE Datensätze die Transformation durchzuführen.

Modifizierte Funktion

Um Microsoft SQL Server zu veranlassen, einen Index effektiv zu nutzen, darf ein Attribut nicht durch eine Funktion oder durch Operationen geändert werden, bevor das Ergebnis ausgewertet werden soll. Das Ziel muss sein, dass unmittelbar im Wert selbst gesucht werden soll. Die Funktion wurde in der Auswertung des Prädikats geringfügig geändert – im Ergebnis kann Microsoft SQL Server einen performanten INDEX SEEK verwenden.

CREATE FUNCTION dbo.GetEmployeesByCostCenter
    (@CostCenterPrefix CHAR(1))
RETURNS TABLE
AS
RETURN
(
    SELECT  Id, Name, CostCenter
    FROM    dbo.Employees
    WHERE   CostCenter >= @CostCenterPrefix
            AND CostCenter < CHAR(ASCII(@CostCenterPrefix) + 1)
);

EXECUTION_PLAN_02

Zusammenfassung

Es ist unumgänglich, bei der Suche nach Optimierungsmöglichkeiten Details innerhalb eines Ausführungsplans zu beachten. Bei Einschränkungen mittels WHERE-Klausel sollte immer darauf geachtet werden, eine SEEK-Einschränkung (Predicate) zu erzielen. Funktionen und Operationen von Attributen verhindern in der Regel diesen Optimierungsschritt.

Herzlichen Dank fürs Lesen!

Optimierung von LIKE-Suche

$
0
0

Immer wieder kommt es vor, dass trotz guter Indexierung ein Index nicht optimal genutzt werden kann, da die Suchmuster eine optimale Verwendung eines Index verhindern. Eher durch Zufall bin ich auf einen interessanten Artikel von Fabricio Lima gestoßen, der eine – wie ich finde – interessante Lösung präsentiert, um sogenannte “Wildcard-Suchen” zu optimieren. Hierbei macht er sich den Umstand zu Nutze, dass der CPU-Anteil bei Verwendung von SQL Sortierungen deutlich reduziert wird und somit Abfragen mit Wildcards schneller ausgeführt werden.

Grundlagen der Sortierung

Eine Grundfunktion innerhalb von Datenbanken ist die Festlegung von Sortierungen für die Daten. Eine Sortierung gibt die Bitmuster an, die die jeweiligen Zeichen in einem Datensatz darstellen. Sortierungen legen außerdem die Regeln fest, nach denen Daten sortiert und verglichen werden. SQL Server unterstützt das Speichern von Daten mit unterschiedlichen Sortierungen in einer Datenbank. Bei Nicht-Unicode-Spalten gibt die Sortierungseinstellung die Codepage für die Daten und die Zeichen an, die dargestellt werden können. Bei der Auswahl von Sortierungen kann zwischen SQL-Sortierungen und Windows-Sortierungen ausgewählt werden. Bei der Verwendung einer WINDOWS-Sortierung werden Nicht-Unicode-Spalten mit dem gleichen Algorithmus verarbeitet, wie Unicode-Spalten. Bei der Verwendung von SQL-Sortierungen wird ein anderes Verfahren für Nicht-Unicode-Spalten verwendet. Basis für diese Vergleiche sind interne “Sortierungen”.

Testumgebung

Für die Durchführung der Tests wird eine Datenbank mit der WINDOWS-Sortierung [Latin1_General_CI_AS] erstellt. Diese Sortierung berücksichtigt keine Groß- und Kleinschreibung und kann für die Speicherung von Nicht-Unicodedaten (ASCII) und Unicodedaten verwendet werden.

Datenbank

-- Create the database with a unicode collation
CREATE DATABASE [demo_db]
ON PRIMARY
(
    NAME        = N'demo_db',
    FILENAME    = N'S:\MSSQL13.SQL_2016\MSSQL\DATA\demo_db.mdf',
    SIZE        = 500MB,
    FILEGROWTH  = 0MB
)
LOG ON
(
    NAME        = N'demo_log',
    FILENAME    = N'S:\MSSQL13.SQL_2016\MSSQL\DATA\demo_db.ldf',
    SIZE        = 500MB,
    FILEGROWTH  = 0MB
)
COLLATE Latin1_General_CI_AS;

Testtabelle

In der Datenbank wird eine Tabelle angelegt, die eine Spalte verwendet, in der ausschließlich Nicht-Unicodedaten gespeichert werden (VARCHAR, CHAR)

SELECT  message_id,
        language_id,
        severity,
        is_event_logged,
        CAST(text AS VARCHAR(2048)) A 
INTO    dbo.messages
FROM    sys.messages;

Die Tabelle enthält ungefähr 280.000 Datensätze (Microsoft SQL Server 2016). Das Attribut ist das einzige Textattribut.

DATA-STRUCTURE-01

Stored Procedure

Für die Ausführung der Abfragen – insbesondere für Lastvergleiche – wird eine Gespeicherte Prozedur verwendet, die ein Suchmuster als Parameter erwartet. Weiterhin wird durch eine Variable gesteuert, ob die Standardsortierung der Datenbank oder eine SQL-Sortierung verwendet werden soll.

CREATE PROC dbo.GetMessages
    @SearchString      VARCHAR(20),
    @use_sql_collation BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
 
    IF @use_sql_collation = 0
        SELECT * FROM dbo.messages
        WHERE  text LIKE @SearchString;
    ELSE
        SELECT * FROM dbo.messages
        WHERE  text COLLATE SQL_Latin1_General_CP1_CI_AS LIKE @SearchString;
 
    SET NOCOUNT OFF;
END

Bei der Auswahl der geeigneten SQL-Sortierung wurde darauf geachtet, dass – um unterschiedliche Ergebnisse zu vermeiden – gleiche Einstellungen für Groß- und Kleinschreibung sowie die Berücksichtigung von Akzenten verwendet wurden!

Testszenario

Für eine möglichst präzise Auswertung der Laufzeiten wird vor der Ausführung der Prozedur mit unterschiedlichen Parameterwerten die Ausgabe der Ausführungszeiten aktiviert!

SET STATISTICS TIME ON;
GO

EXEC dbo.GetMessages @SearchString = '%error%', @use_sql_collation = 0;
GO
PRINT '===================================================================';
GO
EXEC dbo.GetMessages @SearchString = '%error%', @use_sql_collation = 1;
GO
 
SET STATISTICS TIME OFF;

Bei der ersten Ausführung wird die WINDOWS-Sortierung verwendet, die für die Datenbank als Standardsortierung definiert wurde (Latin1_General_CI_AS). Beim zweiten Aufruf wird die Verwendung der SQL-Sortierung (siehe Code in Prozedur) forciert. Der Unterschied in den Laufzeiten ist beeindruckend.

EXECUTION-TIME-01

Für die Ausführung der Abfrage unter Verwendung der WINDOWS Sortierung benötigt Microsoft SQL Server (13.0.1772.0) auf einem Laptop mit 16 GB RAM und 4 Cores ca. 5 Mal länger als bei Verwendung einer SQL-Server-Sortierung. Die nachfolgende Abbildung zeigt die Ausführung der Prozedur mit 10 Clients unter Verwendung der WINDOWS-Sortierung und anschließender Ausführung mit der SQL-Server Sortierung.

CPU-Auslastung

Die mit Hilfe von PerfMon aufgezeichneten CPU-Auslastungen zeigen im Zeitraum der ersten 15 Sekunden (Blau) eine permanente Prozessorauslastung während der Ausführung der Prozedur unter Verwendung der WINDOWS-Sortierung. Die anschließend erneut von 10 Prozessen ausgeführte Prozedur unter Verwendung der SQL-Server Sortierung zeigt ebenfalls einen hohen – aber kurzen – Konsum von Prozessorzeiten. Dieses Verhalten spiegelt sich auch in der Ausführungszeit wider:

  • 15,491 Sekunden für Ausführung von 10 Clients mit WINDOWS-Sortierung
  • 2,570 Sekunden für Ausführung von 10 Clients mit SQL-Server-Sortierung

Grund für dieses Verhalten

Bei der Verwendung von WINDOWS-Sortierungen wird der Vergleich von Nicht-Unicode-Spalten mit den gleichen Algorithmen durchgeführt wie der Vergleich von Unicode-Spalten. Bei der Verwendung von SQL-Server Sortierungen wird eine andere Vergleichssemantik für Nicht-Unicode-Spalten verwendet, die auf einer einfacheren Sortierordnung basiert. Sortierregeln für Unicode-Spalten sind komplexer als Regeln für Nicht-Unicode-Spalten. Die Komplexität wird bereits durch den erweiterten Zeichensatz begründet. Nicht-Unicode-Spalten kennen nur den ASCII-Zeichensatz mit 255 unterschiedlichen Zeichen!

Zusammenfassend gilt:

  • Wenn Nicht-Unicode-Spalten in Microsoft SQL Server mit einer WINDOWS-Sortierung verwendet werden, unterliegen die Vergleichsregeln / Sortierungsregeln IMMER den Regeln für Unicodedaten
  • Wenn Nicht-Unicode-Spalten in Microsoft SQL Server mit einer SQL-Server Sortierung verwendet werden, unterliegen die Vergleichsregeln / Sortierungsregeln den – einfacheren – Regeln für Nicht-Unicode-Spalten

Der Unterschied wird spürbar bei rozessorgebundenen Operationen bedingt durch den Overhead bei den Vergleichsoperationen und/oder Textmanipulationen. Mehr Details zu diesem Thema finden sich in dem von Microsoft veröffentlichten Artikel “Comparing SQL Collations to WINDOWS Collations”.

Besonderheiten

Die Lösung hat ihren Charme – aber es gilt: “There is no lunch for free!”. Den Prozessorvorteil erkauft man sich möglicherweise mit höheren Speicherzuteilungen wenn explizit mit einer angegebenen Sortierung (COLLATE) gearbeitet wird. Die nachfolgenden Beispiele zeigen, wie sich die Abfragen in Bezug auf ihre Schätzungen und den damit verbundenen Speicherzuteilungen verhalten.

SELECT *
FROM   dbo.messages
WHERE  text LIKE '%error%'
ORDER BY
       text
OPTION
(
    QUERYTRACEON 2363,
    QUERYTRACEON 3604
);

Die Abfrage verwendet – um eine Speicherzuteilung zu erzwingen – eine ORDER BY-Klausel. Der Ausführungsplan für die Abfrage unter Verwendung der WINDOWS-Sortierung sieht wie folgt aus:

EXECUTION-PLAN-01

Microsoft SQL Server schätzt, dass ~4.000 Datensätze zurückgeliefert werden. Für die Sortierung wird ein Speicherbedarf für den Sort-Operator von 6,5 MB berechnet. Bei der Berechnung der zu erwartenden Datensätze kann Microsoft SQL Server nicht auf das Histogramm des Statistikobjekts selbst zugreifen. Wenn ein Statistikobjekt für eine Textspalte (char, varchar, nchar, nvarchar, varchar(max), nvarchar(max), text, oder ntext) erstellt wird, werden Statistiken über Zusammenfassungen von Zeichenfolgen (string summary statistics) erstellt, um die Kardinalitätsschätzungen für Abfrageprädikate, die den LIKE-Operator verwenden, zu verbessern. Zusammenfassungen von Zeichenfolgen werden getrennt vom Histogramm gespeichert! Ein Blick in die Ausgabe des “Estimation Process” zeigt, wie Microsoft SQL Server die geschätzte Anzahl von Datensätzen berechnet.

STATISTICS_INTERNAL_01

Die Kalkulation der geschätzten Zeilen basiert auf Statistiken über die Zusammenfassung von Zeichenfolgen. Microsoft SQL Server verwendet einen “Trie” für die Evaluierung der Daten. Ein Trie oder Präfixbaum ist eine Datenstruktur, die in der Informatik zum Suchen nach Zeichenketten verwendet wird. Die Selektivität ergibt sich aus den “string summary statistics”.

Anders jedoch sieht es jedoch aus, wenn explizit eine Sortierung für das Prädikat angegeben wird.

SELECT *
FROM   dbo.messages
WHERE  text COLLATE SQL_Latin1_General_CP1_CI_AS LIKE '%error%'
ORDER BY
    text COLLATE SQL_Latin1_General_CP1_CI_AS
OPTION
(
    QUERYTRACEON 2363,
    QUERYTRACEON 3604
);

Der zugehörige Ausführungsplan zeigt erhebliche Abweichungen vom vorherigen Beispiel.

EXECUTION-PLAN-02

Die Kalkulation der geschätzten Zeilen ergibt ein höheres Ergebnis. Dieses Ergebnis wirkt sich auf die Speicherzuteilung aus. Braucht die erste Abfrage lediglich 6,5 MB hat sich die Speicherzuteilung bei der Verwendung einer expliziten Sortierung um den Faktor 10 erhöht. Diese Erhöhung basiert auf den höheren Schätzungen für die Anzahl der zurückzuliefernden Datensätze. Auch hier hilft ein Blick auf den “Estimation Process”.

STATISTICS_INTERNAL_02

Konnte Microsoft SQL Server ohne die explizite Angabe einer Sortierung noch auf “string summary statistics” zugreifen, ist das mit der Verwendung einer expliziten Sortierung nicht mehr möglich. Die Sortierung sorgt dafür, dass sich die Schätzung ähnlich einer “non-sargable” Abfrage verhält. Für die Suche der betreffenden Datenzeilen müssen zunächst die Werte in der Textspalte in die angegebenen Sortierung konvertiert werden, bevor sie ausgewertet werden können.
In diesem Fall kann Microsoft SQL Server kein Histogramm (und somit auch keine string summary statistics) verwenden sondern wendet einen festen Filterwert (CSelCalcFixedFilter) für die Berechnung der auszugebenden Datensätze. Aus diesem Faktor ergibt sich die Anzahl der zu liefernden Datensätze: 278.696 * 0,09 = 25.082,6. Der “Vorteil” des geringeren CPU-Verbrauchs geht mit einem – möglichen – höheren Konsum des Speichers einher.

Herzlichen Dank fürs Lesen!

Isolationsstufe SERIALIZABLE im Detail

$
0
0

Eine Frage in einem englischsprachigen Forum für Microsoft SQL Server motivierte mich, diesen Artikel über die verschiedenen ISO-Isolationsstufen zu schreiben. Dieser Artikel beschäftigt sich mit der restriktivsten Isolationsstufe – SERIALIZABLE. In einem zuvor geschriebenen Artikel habe ich das Sperrverhalten von SELECT-Statements in den verschiedenen Isolationsstufen beschrieben. Dieser Artikel befasst sich nicht mit den Grundlagen von Isolationsstufen sondern behandelt die besonderen Merkmale.

Isolationsstufe SERIALIZABLE

Die Isolationsstufe SERIALIZABLE stellt als restriktivste Stufe sicher, dass alle Aktionen von Transaktionen sich nicht gegenseitig beeinflussen. Der Anwender kann sicher sein, dass die Transaktion in jedem Fall einen gültigen Zustand besitzt. Es werden alle Anomalien des Mehrbenutzerbetriebes verhindert. Der Einsatz bietet sich für Transaktionssysteme an, die Daten häufig verändern und nicht für den Bereich der Datenanalyse eingesetzt werden. Nur in der Isolationsstufe SERIALIZABLE sind sogenannte Bereichssperren möglich. Eine Bereichssperre verhindert, dass während der aktiven Transaktion in den ausgewählten Bereich weitere Datensätze durch andere Transaktionen eingefügt werden oder bestehende Datensätze der ausgewählten Datenmenge durch andere Transaktionen geändert werden können.

Testumgebung

Für diese Artikelreihe wird eine Tabelle [dbo].[CustomerOrderDetails] verwendet. In dieser Tabelle befinden sich 3.000.000 Datensätze. Der gruppierte Primärschlüssel erstreckt sich über zwei Attribute ([Order_Id], [Position]). Weitere Indexe sind für die Tabelle nicht vorhanden.

CREATE TABLE dbo.CustomerOrderDetails
(
    Order_Id    INT               NOT NULL,
    Position    INT               NOT NULL,
    Article_Id  INT               NOT NULL,
    Quantity    NUMERIC(10, 2)    NOT NULL,
    Unit        CHAR(5)           NOT NULL,
    Price       SMALLMONEY        NOT NULL,
    Currency    CHAR(3)           NOT NULL,

    CONSTRAINT pk_CustomerOrderDetails_Order_Id PRIMARY KEY CLUSTERED
    (
        Order_Id ASC,
        Position ASC
    )
);
GO

Sperrverhalten bei pessimistic locking

Standardmäßig verwenden Datenbanken in Microsoft SQL Server das sogenannte “pessimistic locking”. Hierbei handelt es sich um ein Sperrverhalten, bei dem ein schreibender Zugriff einen lesenden Zugriff auf die gleiche Ressource verhindert. Die zweite Transaktion darf den Datensatz nicht eher lesen, bevor er nicht von der ersten Transaktion wieder freigegeben wird; ansonsten entstehen Dirty Reads.

Gleichzeitige Lesen durch zwei Transaktionen

Das erste Beispiel zeigt, wie sich das Leseverhalten in der restriktiven Isolationsstufe SERIALIZABLE verhält. Dazu werden in Transaktion 1 (T1) alle Bestelldetails aus der Bestellung 1001 angezeigt. In Transaktion 2 wird ebenfalls diese Bestellung abgefragt.

--Transaktion 1!
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
GO

BEGIN TRANSACTION;
GO
    SELECT * FROM dbo.CustomerOrderDetails
    WHERE  Order_Id = 1001;
    GO
 
    SELECT resource_type,
           resource_description,
           request_mode,
           request_type,
           request_status
    FROM   sys.dm_tran_locks
    WHERE  resource_database_id = DB_ID()
           AND resource_type <> N'DATABASE';
    GO

Achtung – die Transaktion 1 ist noch nicht bestätigt und somit noch offen!

T1 - Order 1001

Die Abbildung zeigt im Ergebnis 3 Bestelldetails für die Bestellung 1001; schaut man in die Sperrliste, fällt auf, dass 4 (!) Datensätze (Keys) gesperrt sind. Microsoft SQL Server verwendet in der Isolationsstufe SERIALIZABLE Bereichssperren, wenn ein Suchkriterium nicht eindeutig ist. Da nur nach dem Prädikat [Order_Id] gesucht wurde, kann nicht sichergestellt werden, dass nur ein Datensatz zurückgeliefert wird. Microsoft SQL Server verhindert durch Bereichssperren, dass zwischen den gefundenen Datensätzen weiteren Datensätze eingefügt werden können. Ein Blick auf die gesperrten Datensätze verdeutlicht dieses Verhalten.

SELECT *
FROM   dbo.CustomerOrderDetails
WHERE  %%lockres%% IN
       (
        SELECT resource_description
        FROM   sys.dm_tran_locks
        WHERE  request_session_id = @@SPID
               AND resource_type = N'KEY'
       )
ORDER BY
       Order_Id,
       Position;
GO

Die Abfrage liefert alle Datensätze, die in der geöffneten Transaktion blockiert werden.

RecordSet-01

Die Abgrenzungen zeigen die Ergebnismenge der Abfrage. Microsoft SQL Server verhindert auf Grund der Isolationsstufe SERIALIZABLE, dass während der Ausführung der Transaktion weitere Bestelldetails für die Bestellung 1001 erfasst werden können (Position > 3). Aus diesem Grund muss der erste Datensatz der Bestellung 1002 ebenfalls gesperrt werden; er repräsentiert die untere Grenze des Bereichs, in dem keine weiteren Daten eingefügt werden dürfen.

In einer zweiten Transaktion werden ebenfalls Informationen zur Bestellung 1001 in der Isolationsstufe SERIALIZABLE abgerufen.

T2 - Order 1001

Die Abbildung zeigt, dass beide Transaktionen ohne Einschränkungen auf die Daten zugreifen können. Ein lesender Zugriff verhindert nicht einen anderen lesenden Zugriff auf die angeforderten Ressourcen.

Schreibender Zugriff…

Der schreibende Zugriff erfordert einen exklusiven Sperrmechanismus. Für Microsoft SQL Server bedeutet diese Einschränkung, dass ein Prozess so lange mit dem Schreiben warten muss, bis ein exklusiver Zugriff auf die Ressourcen möglich ist. Der schreibende Zugriff ist in zwei Situationen möglich:

während Daten gelesen werden

Wenn Transaktion 1 die Daten mit der einschränkenden Isolationsstufe SERIALIZABLE liest, werden Ressourcen erst dann freigegeben, wenn die Transaktion beendet ist. Während die Transaktion geöffnet ist, müssen alle Datensätze, die sich im Bereich der Ausgabe befinden, eine S-Sperre beibehalten. Was macht Transaktion 2, wenn sie Daten ändern möchte, die in diesem Bereich vorhanden sind?

—Transaktion 2
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
GO

BEGIN TRANSACTION;
GO
    UPDATE dbo.CustomerOrderDetails
    SET    Price = Price * 1.10
    WHERE  Order_Id = 1001;
    GO

Die Ausführung der Abfrage wird verzögert, bis Transaktion 1 vollständig abgeschlossen ist.

T2 - UPDATE Order 1001

Transaktion 2 versucht, eine exklusive Bereichssperre (RangeX-X) auf den ersten Datensatz anzuwenden. Dieser Datensatz (wie auch alle weiteren Datensätze) sind durch Transaktion 1 mit einer Bereichssperre belegt. Aus diesem Grund muss Transaktion 2 warten. Die Isolationsstufe von Transaktion 2 ist hierbei vollkommen irrelevant. Entscheidend ist in der vorliegenden Situation, dass eine exklusiver Sperre nicht mit einer lesenden Sperre kompatibel ist.

während Daten geändert werden

Wie schaut es jedoch aus, wenn Transaktion 1 Änderungen an Datensätzen durchführt, während eine weitere Transaktion die Daten ebenfalls ändern möchte?

—Transaktion 1
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
GO
 
BEGIN TRANSACTION;
GO
    UPDATE dbo.CustomerOrderDetails
    SET    Price = Price * 1.10
    WHERE  Order_Id = 1001;
    GO
 
    SELECT request_session_id,
           resource_type,
           resource_description,
           request_mode,
           request_type,
           request_status
    FROM   sys.dm_tran_locks
    WHERE  resource_database_id = DB_ID()
           AND resource_type <> N'DATABASE'
    ORDER BY
           request_session_id;
    GO

In Transaktion 1 werden für alle Bestelldetails aus Bestellung 1001 die Preise um 10% erhöht. Die Transaktion ist noch nicht abgeschlossen!

T1 - UPDATE Order 1001

Die Transaktion hält exklusive Bereichssperren auf 4 Datensätze (RangeX-X). Wenn eine zweite Transaktion ebenfalls Daten der Bestellung 1001 anpassen möchte, ergibt sich ein Konflikt, der nicht aufgelöst werden kann.

T2 - UPDATE Order 1001 - 02

Das Verhalten von Transaktion 2 ist logisch. Da keine exklusive Sperre gesetzt werden kann, muss Transaktion 2 warten, bis Transaktion 1 die Auswahl von Datensätzen abgeschlossen hat. Interessant jedoch ist in diesem Zusammenhang, wie sich Transaktion 2 verhält, wenn nicht Bestellung 1001 geändert werden soll sondern Bestellung 1002. Wenn man die Sperren auf den betroffenen Datensätzen vergleicht, wird man feststellen, dass Transaktion 1 erneut eine Sperre auf die erste Position von Bestellung 1002 gesetzt hat.

T2 - UPDATE Order 1002

Bei dem Versuch, Daten der Bestellung 1002 zu aktualisieren, muss Transaktion 2 erneut warten. Transaktion 1 muss den ersten Datensatz von Bestellung 1002 blockieren, um zu verhindern, dass ein neuer Datensatz für Bestellung 1001 mit der Position 4 eingefügt wird (Bereichssperre). Der Einfluss der Sperre erstreckt sich aber nicht nur auf die Bestellung 1001 sondern auch auf den ersten Datensatz der Bestellung 1002.

Lesender Zugriff…

während Daten gelesen werden

Diese Art des konkurrierenden Zugriffs ist nicht durch explizites Sperrverhalten der Transaktionen untereinander gekennzeichnet. Beide Transaktionen verwenden eine Bereichssperre für die Auswahl von Daten. Ein lesender Prozess blockiert keinen anderen lesenden Prozess!

T2 - SELECT Order 1001 - 01

während Daten geschrieben werden

Sobald Daten geschrieben werden, verändert sich diese kooperative Arbeitsweise bei mehreren Transaktionen. Die nachfolgende Abbildung zeigt die Sperren, die von Transaktion 1 (SPID: 52)  während einer Aktualisierung gesetzt werden.

T1 - UPDATE Order 1001 - 02

Will ein weiterer Prozess (SPID: 53) auf die gleichen Orderdetails zugreifen, wird versucht eine lesende Bereichssperre (RangeS-S) zu setzen. Diese Sperre ist nicht kompatibel mit einer exklusiven Sperre und muss warten.

T1 - UPDATE Order 1001 - 03

Zusammenfassend kann man sagen, dass sich in der Isolationsstufe SERIALIZABLE im Rahmen mehrerer Transaktionen nichts ändert. Der Unterschied ist, dass Microsoft SQL Server die Sperren nicht auf einzelne Datensätze anwendet, sondern immer eine Bereichssperre verwendet, um zu vermeiden, dass zwischen den bestehenden Datensätzen neue Datensätze hinzugefügt werden.

Sperrverhalten bei optimistic locking

Mit Microsoft SQL Server 2005 wurde zum ersten Mal “optimistic locking” in die Datenbankwelt von Microsoft implementiert. Von optimistic locking spricht man bei Verfahren, die einen parallelen Zugriff von mehreren Benutzern auf denselben Datensatz konfliktarm und ohne Inkonsistenzen regeln. Microsoft hat mit SQL Server 2005 die Option ALLOW_SNAPSHOT_ISOLATION und READ_COMMITTED_SNAPSHOT implementiert. Das Festlegen der READ_COMMITTED_SNAPSHOT-Option ermöglicht Zugriff auf versionierte Zeilen mit der READ COMMITTED-Isolationsstufe. Wenn die READ_COMMITTED_SNAPSHOT-Option auf OFF festgelegt ist, muss die Snapshot-Isolationsstufe für jede Sitzung explizit festgelegt werden, um auf versionierte Zeilen zuzugreifen. Für die nachfolgenden Beispiele wird die READ_COMMITTED_SNAPSHOT-Isolationsstufe für die Testdatenbank aktiviert.

ALTER DATABASE CustomerOrders SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

Gleichzeitiges Lesen durch zwei Transaktionen

Zu diesem Szenario bedarf es nicht vieler Worte. Wie bereits im ersten Beispiel demonstriert, ist ein gleichzeitiges Lesen von mehreren Transaktionen ohne weiteres möglich. Solange die Sperrmuster zueinander kompatibel sind, verhalten sich die Sperren kooperativ.

Schreibender Zugriff

Beim schreibenden Zugriff ändert sich das Verhalten der unterschiedlichen Transaktionen. Hierbei kommt es jedoch darauf an, welcher Art die “führende” Transaktion ist. Abhängig davon, ob Transaktion 1 schreibend oder lesend die Daten blockiert, verhält sich Transaktion 2 unterschiedlich.

während Daten gelesen werden

Wenn Transaktion 1 die Daten mit der einschränkenden Isolationsstufe SERIALIZABLE liest, werden Ressourcen erst dann wieder freigegeben, wenn die Transaktion vollständig abgeschlossen ist. So lange wird auf alle Datensätze, die sich im Bereich der Ausgabe befinden, eine RangeS-S-Sperre gehalten. Was macht Transaktion 2, wenn sie Daten ändern möchte, die in diesem Bereich vorhanden sind?

—Transaktion 2
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
GO
 
BEGIN TRANSACTION;
GO
    UPDATE dbo.CustomerOrderDetails
    SET    Price = Price * 1.10
    WHERE  Order_Id = 1001;
    GO

T2 - UPDATE Order 1001 - 03

Die Abbildung zeigt das Sperrverhalten beider Transaktionen. Während Transaktion 1 (SPID: 55) eine Bereichssperre gesetzt hat, muss Transaktion 2 (SPID: 52) darauf warten, eine Bereichssperre für einen exklusiven Zugriff zu erhalten. Bei “optimistic locking” kann ein lesender Prozess also auch weiterhin einen schreibenden Prozess blockieren.

während Daten geschrieben werden

Ähnlich verhält es sich, wenn Transaktion 1 Daten ändert. In diesem Fall hilft optimistic locking nicht, da eine exklusive Bereichssperre auf die betroffenen Daten gesetzt wird. Transaktion 2 kann erst dann schreibend auf die Daten zugreifen, wenn Transaktion 1 die Sperren wieder löst.
Entscheidend bei diesem Szenario ist – unabhängig davon, ob optimistic locking aktiviert ist – dass JEDES andere Isolationslevel ausser READ COMMITTED die Verwendung von optimistic locking ausser Kraft setzt!

CHECK-Einschränkungen (Plausibilitätsprüfungen)

Wenn man das Sperrverhalten im Isolationsmodus SERIALIZABLE in den oben gezeigten Beispielen genauer untersucht, weiß man, dass eine Sperre auf den den ersten Datensatz der nächsten Bestellung (1002) erforderlich ist, weil Microsoft SQL Server verhindern möchte, dass eine weitere Position (4) eingetragen wird. Wie verhält sich Microsoft SQL Server, wenn mittels CHECK-Einschränkung bereits eine Restriktion vorhanden ist, die nachfolgende Einträge verhindert?

Zunächst wird für die Testtabelle festgelegt, dass maximal 3 Positionen pro Bestellungen erlaubt sind:

ALTER TABLE dbo.CustomerOrderDetails
ADD CONSTRAINT chk_Position_Max CHECK (Position <= 3);
GO

Lässt man obige anschließend erneut eine Abfrage für Bestellung 1001 laufen, stellt man fest, dass Microsoft SQL Server weiterhin den ersten Datensatz von Bestellung 1002 sperrt. Das Verhalten erklärt sich dadurch, dass transaktionale Isolationsstufen KEINE Regeln, die auf Ebene von Metadaten festgelegt werden, berücksichtigt. Transaktionen orientieren sich am ACID-Modell und müssen – gemäß ihrer Bestimmung – einen konsistenten Status gewährleisten.

Herzlichen Dank fürs Lesen!


Read Committed Snapshot Isolation und hohe Anzahl von version_ghost_record_count

$
0
0

Bereits zum 3. Mal wurde ich zu einem Notfalleinsatz gerufen, bei dem ein Microsoft SQL Server völlig unerwartet einen Performanceeinbruch hatte. Die üblichen Verdächtigen (Parameter Sniffing, Statistiken, …) konnten nach kurzer Prüfung ausgeschlossen werden. Die weitere Suche brachte dann eine interessante Ursache zu Tage, die ich so bisher noch nie gesehen habe. Wenn eine Datenbank mit READ COMMITTED SNAPSHOT ISOLATION arbeitet, sollte man seinen Applikationscode (auch in SQL Server) gründlich testen!

Read Committed Snapshot Isolation

Das Problem in jedem Datenbanksystem, mit dem sich ein Programmierer auseinandersetzen muss, ist die Behandlung von „Gleichzeitigkeit“ von vielen Anwendern. In einer Datenbank werden tausende von gleichzeitigen Befehlen zum Auswählen und Bearbeiten von Datensätzen ausgeführt. Microsoft SQL Server muss diese Anforderungen serialisieren und die Anforderungen nacheinander abarbeiten. Bei der Bearbeitung von Datensätzen (Auswählen oder Bearbeiten) verwendet Microsoft SQL Server Sperren, um die Ressourcen während des Zugriffs vor anderen Prozessen zu schützen. Hierbei spielt insbesondere der Schutz von Schreibzugriffen eine wichtige Rolle. Bearbeitet z. B. in einer Lagerverwaltung ein Mitarbeiter die Lagermenge eines Produkts (UPDATE), dann muss Microsoft SQL Server sicherstellen, dass zur gleichen Zeit dieser Datensatz nicht von einem anderen Prozess gelesen werden darf.

Read Committed Workflow #1

Transaktion 1 beginnt um 18:34:50, den Datensatz Nr. 10 zu bearbeiten während Transaktion 2 nur eine Sekunde später versucht, den Datensatz Nr. 10 zu lesen. Transaktion 1 verwendet eine exklusive Sperre (X) auf dem Datensatz. Dadurch wird Transaktion 2 daran gehindert, den Datensatz zu lesen. Dieses Verhalten nennt sich pessimistisches Sperrverhalten und ist das Standardverhalten in der Isolationsstufe „READ COMMITTED„. Um dieses Sperrverhalten zu umgehen, wurde mit Microsoft SQL Server 2008 zum ersten Mal READ COMMITTED SNAPSHOT ISOLATION (RCSI) eingeführt. Der Name „Momentaufnahme“ definiert, dass alle Abfragen in der Transaktion auf dieselbe Version (Momentaufnahme) der Datenbank zurückgehen, die auf dem Zustand der Datenbank zum Zeitpunkt des Beginns der Transaktion basiert. Wenn READ_COMMITTED_SNAPSHOT auf ON festgelegt wird, verwendet Datenbankmodul die Zeilenversionsverwaltung, um jede Anweisung mit einer hinsichtlich der Transaktionen konsistenten Momentaufnahme der Daten so darzustellen, wie sie zu Beginn der Anweisung vorhanden waren. Es werden keine Sperren verwendet, um die Daten vor Updates durch andere Transaktionen zu schützen.

READ COMMITTED SNAPSHOT ISOLATION #2

Sobald RCSI für eine Datenbank aktiviert ist, wird zu Beginn der Transaktion 1 eine Kopie der Datenseite in TEMPDB abgelegt. Wenn anschließend Transaktion 2 Daten aus der Datenseite lesen möchte, wird der Inhalt aus der Kopie gelesen. Dadurch wird sichergestellt, dass Transaktion 2 die gültigen Daten zu Beginn der Transaktion liest und somit einen konsistenten Zustand erhält. Viele Entwickler von konkurrierenden Systemen verwenden gerne diese Alternative, um somit z. B. Deadlock-Situationen zu umgehen. Das diese Lösung nicht immer die „Silver Bullet“ ist, die alle Probleme lösen kann, soll das nachfolgende Beispiel demonstrieren.

Testumgebung

Alle Systeme, auf denen ich die Probleme im Zusammenhang mit RCSI gefunden habe, verwendeten einen ähnlich gelagerten Workload. Dabei geht es um relativ kleine Tabellen (<=75.000 Datensätze), die durch eine sehr hohe Anzahl von INSERT-und LÖSCH-Vorgängen aufgefallen sind. Der Inhalt dieser Tabellen waren „rotierende“ Daten, die z. B. in einer Lagerverwaltung verwendet werden. Z. B. werden in der Automobilindustrie Statusinformationen über die Karosserieträger an jeder Stelle auf der Fertigungsstrecke gesammelt. Damit kann man feststellen, an welcher Stelle im Produktionszyklus die Karosserie aktuell ist. Sobald der Fertigungsprozess abgeschlossen ist, kann der Statusdatensatz aus dem System gelöscht werden. Das Verhalten mit Hilfe einer Testtabelle simuliert werden. Dabei werden jedoch nur neue Datensätze eingetragen und alte Datensätze gelöscht:

Rotating data

Die Abbildung zeigt die Abfolge des Workloads. Als Anfangsbestand werden n Datensätze verwendet. Anschließend wird in einer Endlosschleife jeweils ein neuer Datensatz hinzugefügt und anschließend ein Bestandsdatensatz nach dem FIFO-Prinzip aus der Tabelle gelöscht.Dieses System verdeutlicht den Workload, wie er z. B. in automatisierter Lagersoftware oder der Automobilindustrie verwendet wird.

Aktivieren von RCSI für Demo-Datenbank

Zunächst muss die Datenbank für RCSI vorbereitet werden. Am einfachsten geht das mit T-SQL, wie der nachfolgende Code zeigt:

-- Use the demo database and activate RCSI
USE master;
GO
 
ALTER DATABASE demo_db SET READ_COMMITTED_SNAPSHOT ON;
GO
 
USE demo_db;
GO

Erstellen der Demotabelle mit ein paar Beispieldatensätzen

Nachdem die Datenbank für RCSI präpariert wurde, kann mit der Testtabelle fortgefahren werden. Um schnell und effizient ein paar Tausend Datensätze zu generieren, verwende ich das folgende Skript:

IF OBJECT_ID(N'dbo.demo_table', N'U') IS NOT NULL
    DROP TABLE dbo.demo_table;
    GO
 
CREATE TABLE dbo.demo_table
(
    ID    INT       NOT NULL    IDENTITY (1, 1),
    C1    CHAR(100) NOT NULL
);
GO

INSERT INTO dbo.demo_table (C1)
SELECT TOP (1000)
       CAST(TEXT AS CHAR(100)) AS C1
FROM   sys.messages
WHERE  language_id = 1031;
GO
 
CREATE UNIQUE CLUSTERED INDEX cuix_demo_table_Id
ON dbo.demo_table (Id);
GO

Sobald die Tabelle erstellt ist, wird die folgende Abfrage ausgeführt, um die Effizienz des Index auf [Id] zu prüfen:

SELECT MAX(Id) FROM dbo.demo_table;

Die Anzahl der Datenseiten sowie der Datenmenge ist überschaubar und das Experiment kann beginnen.

-- How many data pages and ghost records do we have?
SELECT  page_count,
        avg_page_space_used_in_percent,
        record_count,
        version_ghost_record_count
FROM    sys.dm_db_index_physical_stats
        (
            DB_ID(),
            OBJECT_ID(N'dbo.demo_table', N'U'),
            1,
            NULL,
            N'DETAILED'
        )
WHERE   index_level = 0;
GO

image

Starten des Workloads

Mit dem folgenden Code wird der oben beschriebene Workload in einem neuen Abfragefenster gestartet. Hierbei wird zunächst eine Transaktion geöffnet und anschließend wird fortlaufend am Ende der Tabelle ein neuer Datensatz eingefügt. Nach 10 ms wird der Datensatz mit der kleinsten [ID] aus der Tabelle entfernt. Die Besonderheit in diesem Workload (und genau das war auch der Fehler in allen 3 Vorfällen!) ist, dass die Transaktion während des gesamten Laufs (in einem Fall mehrere Tage) geöffnet bleibt!

BEGIN TRANSACTION;
GO
 
    -- Insert new record into dbo.Customers
    WHILE (1 = 1)
    BEGIN
        -- wait 10 ms before each new process
        WAITFOR DELAY '00:00:00:010';
 
        INSERT INTO dbo.demo_table(C1)
        SELECT C1
        FROM   dbo.demo_table
        WHERE  Id = (SELECT MIN(Id) FROM dbo.demo_table);
 
        -- Wait 10 ms to delete the first record from the table
        WAITFOR DELAY '00:00:00:010';
 
        -- Now select the min record from the table
        DELETE dbo.demo_table WHERE Id = (SELECT MIN(Id) FROM dbo.demo_table);
    END

Alle 10 ms wird innerhalb einer expliziten Transaktion für die Simulation der erste Datensatz am Ende neu eingefügt. Weitere 10 ms später wird der älteste Datensatz gelöscht. Dadurch ergibt sich ein rotierendes System, wie es in den drei Vorfällen angewendet wurde. Diese Transaktion lief für insgesamt 3 Minuten. In einem weiteren Fenster wurde eine simple Aggregation zu Beginn und zum Ende der Demo ausgeführt. Überraschend war das produzierte IO am Ende des Tests!

-- result of the I/O with RCSI
SELECT MAX(ID) FROM dbo.demo_table;
GO
 
-- result of the I/O without RCSI
SELECT MAX(ID) FROM dbo.demo_table WITH (NOLOCK);
GO

IO Output after Workload

Der erste Output zeigt eine deutliche Erhöhung des IO während die Ausführung in der READ UNCOMMITTED Isolationsstufe die erwarteten 2-3 IO produziert. Über die Laufzeit hat sich die Anzahl der allokierten Datenseiten wie folgt verändert:

no_of_version_ghost_record_count

Der lineare Anstieg der Anzahl von version_ghost_record_count ist zu erwarten, da die Zeitintervalle für die Verarbeitung der Daten nicht variiert.

Grund für dieses Verhalten

Der Grund für dieses Verhalten ist einfach und schnell erläutert; der Workload für die „Rotation der Daten“ verursacht permanent neue Kopien von Datenseiten, die innerhalb der Transaktion bearbeitet werden. Würden nicht kontinuierlich am Ende der Tabelle neue Datensätze eingefügt werden, wäre die Anzahl der Datenseiten im Version Store auf die existierende Datenmenge begrenzt. Die Aggregationsfunktion in Verbindung mit RCSI muss jedes Mal den Version Store berücksichtigen, um einen Wert zurückzuliefern. Wird die Abfrage jedoch in der Isolationsstufe READ UNCOMMITTED ausgeführt, kann RCSI nicht greifen (wird nur in Verbindung mit READ COMMITTED verwendet!) und es entstehen Dirty Reads. In diesem Fall kann Microsoft SQL Server auf den Index zugreifen, ohne den Version Store zu berücksichtigen.

Lösung des Problems

Der Fehler in allen beobachteten Fällen liegt nicht bei Microsoft SQL Server sondern am implementierten Workload. Da die Transaktionen nur zu beginn explizit implementiert wurden, bleibt die Transaktion geöffnet und der Version Store kann nicht freigegeben werden. Jeder Vorgang muss – sofern eine explizite Transaktion im Spiel ist – mit einem COMMIT TRANSACTION beendet werden. Nur dann wird der Version Store wieder freigegeben und die version_ghost_records entfernt.

Vielen Dank fürs Lesen!

Zugriffssteuerung mit Hilfe von LOGON-Triggern

$
0
0

Ein Kunde kam mit einer Anforderung auf uns zu, den Zugriff zu einen Microsoft SQL Server zu limitieren. Die Einschränkung sollte aber nicht auf einem Anmeldenamen basieren, sondern auf dem Namen bestimmter Applikationen. Die Einschränkung sieht vor, dass ausgewählte Mitarbeiter des Entwicklungsteams mit Hilfe von Microsoft SQL Server Management Studio auf den Server zugreifen dürfen, während das Business nur mit Hilfe einer selbst entwickelten Applikation auf die Daten des Microsoft SQL Servers zugreifen dürfen. Auf dem ersten Blick scheint es hierfür keine Lösung zu geben; aber seit der Version von Microsoft SQL Server 2005 (SP2) gibt es die Möglichkeit, LOGON Trigger zu verwenden.

Was sind LOGON Trigger?

Logon-Trigger lösen eine Reaktion als Antwort auf ein LOGON-Ereignis aus, wenn eine Benutzersitzung mit einer Instanz von SQL Server erstellt wird und nachdem die Authentifizierungsphase der Anmeldung abgeschlossen ist. Aus diesem Grund werden alle Meldungen, die aus dem Trigger stammen und normalerweise den Benutzer erreichen (z. B. Fehlermeldungen und Meldungen aus der PRINT-Anweisung) zum SQL Server Fehlerprotokoll umgeleitet.

Business Case

Die Anforderungen im vorliegenden Fall sind trivial: Alle Mitarbeiter dürfen mittels einer Applikation auf die Datenbanken in Microsoft SQL Server zugreifen. Für gelegentliche Tests / Fehlersuche ist es einem kleinen Kreis von Entwicklern gestattet, mit Hilfe von Microsoft SQL Server Management Studio unmittelbar auf den Datenbankserver zuzugreifen. Da die berechtigten Anwender von jedem ihnen zur Verfügung stehenden Computer auf die Datenbanken zugreifen müssen, ist eine Zugriffsbeschränkung über IP-Adressen nicht möglich. Ebenfalls wurde von Seiten des Kunden der Zugang über einen Jumphost aus internen Gründen wieder verworfen worden.

Lösung

Als Lösung kommt ein LOGON-Trigger zum Tragen, der nach der Anmeldung überprüft, mit welcher Applikation der Anwender auf den Datenbankserver zugreifen möchte. Da ein LOGON-Trigger immer erst NACH der Anmeldung ausgeführt wird, sind Sitzungsdetails bereits in der Systemview [sys].[dm_exec_sessions] gespeichert worden. Mit Hilfe dieser Informationen lässt sich auslesen, mit welchem Programm der Benutzer auf den Datenbankserver zugreift.

SELECT session_id,
       host_name,
       program_name
FROM   sys.dm_exec_sessions
WHERE  is_user_process = 1
       AND session_id = @@SPID;

Die Abfrage liefert für die aktuelle Sitzung neben der Session ID und den Hostnamen noch das Attribut [program_name]. In diesem Attribut wird in der Regel der Programmname gespeichert. Leider ist damit jedoch ein kleiner Nachteil verbunden, der weiter unten beschrieben wird.
Program Name of active session

Wenn ein Zugang mit Hilfe von Microsoft SQL Server Management Studio initiiert wird, so wird der Name des Programms in der Verbindung mit übermittelt. Somit kann identifiziert werden, wenn jemand versucht, sich mit Hilfe von SSMS auf den Server zu verbinden. Da nur einem begrenzter Kreis von Mitarbeitern diese Option zur Verfügung steht, wird zunächst eine Sicherheitsrolle [SSMSAccess] als Serverrolle eingerichtet. Alle Mitarbeiter (Domänenkonten, Domänengruppen, SQL Konten), die Mitglied dieser Gruppe oder sysadmin sind, erhalten mittels SSMS einen Zugang zum Microsoft SQL Server. Der Versuch anderer Mitarbeiter wird abgelehnt.

CREATE SERVER ROLE [SSMSAccess] AUTHORIZATION [sa];

Jeder Anwender, der einen Zugang mittels SSMS erhalten soll, muss Mitglied dieser Serverrolle sein!

Im Anschluss wird ein Servertrigger implementiert, der den Wert in program_name für die aktive session_id überprüft.

CREATE TRIGGER connection_application
ON ALL SERVER
FOR LOGON
AS
BEGIN
    IF EXISTS
    (
        SELECT * FROM sys.dm_exec_sessions
        WHERE  session_id = @@SPID
        AND PROGRAM_NAME() LIKE '%SQL Server Management Studio%'
        AND
        (
            IS_SRVROLEMEMBER(N'SSMSAccess') = 0
            AND IS_SRVROLEMEMBER(N'sysadmin') = 0
        )
    )
    BEGIN
        RAISERROR (N'You are not allowed to connect to this server with SSMS!', 11, 1) WITH NOWAIT;
        ROLLBACK;
    END
END;
GO

Will ein Benutzer mit SQL Server Management Studio auf den Server zugreifen, muss er entweder Mitglied der benutzerdefinierten Serverrolle oder aber sysadmin sein. Ansonsten wird mittels RAISERROR ein Eintrag im Fehlerprotokoll von SQL Server hinterlassen.

login_test_no_access

Der Benutzer mit dem Anmeldenamen [login_test] ist weder Mitglied der Gruppe [SSMSAccess] noch sysadmin. Aus diesem Grund schlägt die Anmeldung fehl.

Failed_Login

Hintertür?

Diese Möglichkeit der Kontrolle mag auf den ersten Blick gut sein. Dennoch gibt es eine Hintertür, mit der man dennoch in das System gelangt. Das Problem ist der “Connection String”, der von einer Applikation generiert wird, wenn eine Verbindung zum Microsoft SQL Server aufgebaut wird. Die nachfolgende Abbildung zeigt den Connection String, der von Microsoft Access generiert wird, wenn ein Zugriff auf die Datenbank erfolgt:

Access-ConnectionString

Die Option “APP” wird von Microsoft SQL Server als [program_name] in sys.dm_exec_sessions hinterlegt.

Program Name of Access Session

Dieser Teil des Connection String kann manipuliert werden! In Microsoft SQL Server Management Studio können die Elemente der Verbindungszeichenfolge explizit angegeben werden. Dadurch lässt sich der [program_name] manipulieren und ein Zugang wäre möglich.

Manipulation Verbindungszeichenfolge

Herzlichen Dank fürs Lesen!

Leere Seiten in HEAP ermitteln

$
0
0

Wer mich kennt, weiß, dass ich ein großer Fan von HEAPS bin. HEAPS zeigen gegenüber gruppierten Indexen bei bestimmten Workloads bessere Leistungsverhalten. Neben den vielen Vorteilen haben HEAPS auch Nachteile. Ein deutlicher Nachteil ist, dass HEAPS allokierte Datenseiten bei Löschoperationen nicht unmittelbar freigeben. Daraus ergibt sich – manchmal – die Erfordernis, festzustellen, wie viele Datenseiten in einem HEAP nicht gefüllt sind und somit durch einen REBUILD wieder freigegeben werden können.

Löschoperationen in HEAP

In einem vorherigen Artikel habe ich Löschoperationen detailliert beschrieben, wenn es sich dabei um einen HEAP handelt. Weitere Informationen dazu findet man im Artikel “HEAPS in Verbindung mit DELETE-Operationen” vom 12.08.2016. Ein HEAP gibt leere Datenseiten nicht frei, wenn nicht sichergestellt werden kann, dass anderen Prozessen der Zugriff auf die IAM-Seite(n) verweigert werden kann. Möchte man Datenseiten während einer Löschoperation in einem HEAP unmittelbar freigeben, muss die Löschoperation mit Hilfe eines TABLOCK-Hinweises exklusiv abgesichert sein.

Ermittlung der vorhandenen Datenseiten

Um festzustellen, welche Datenseiten von einem Objekt (Tabelle, Index) allokiert sind, gibt es seit Microsoft SQL Server 2012 die Systemfunktion [sys].[dm_db_database_page_allocations]. Wer mit einer älteren Version von Microsoft SQL Server arbeitet, kann mit Hilfe von DBCC IND einen Blick in Struktur der allokierten Datenseiten werfen. Jedoch werden mit DBCC IND keine Informationen zur Belegung der Datenseiten ausgegeben! Nachfolgend wird eine Tabelle [dbo].[messages] erstellt und mit ca. 12.000 Datensätzen gefüllt.

SELECT *
INTO  dbo.messages
FROM  sys.messages
WHERE language_id = 1033;
GO

Die Tabelle besitzt keine Indexe und ist ein einfacher HEAP.

PFS-Seite

HEAPS haben gegenüber einem gruppierten Index eine Besonderheit, die ausschließlich bei HEAPS auftritt; der Füllgrad einer Datenseite wird in der PFS-Datenseite gespeichert! Wenn neue Datensätze in einem HEAP gespeichert werden, muss in der PFS-Datenseite nach einer Datenseite gesucht werden, die ausreichend Platz für die Speicherung der Daten in der Tabelle bereitstellt. Sobald ein neuer Datensatz in die Tabelle eingetragen wird, wird im Anschluss die PFS-Seite mit einem prozentualen Wert aktualisiert, der beschreibt, zu wieviel Prozent die Datenseite gefüllt ist. Hierbei können nur 5 mögliche Prozentwerte gespeichert werden:

Prozentwert Beschreibung
NULL / 0% Die allokierte Datenseite ist leer
50% Die allokierte Datenseite ist mit 4.030 Bytes gefüllt.
80% Die allokierte Datenseite ist mit 6.424 Bytes gefüllt.
95% Die allokierte Datenseite ist mit ~7.629 Bytes gefüllt
100% Die Datenseite ist voll

Werden neue Datensätze in einem HEAP gespeichert, findet eine Aktualisierung der PFS-Seite nur dann statt, wenn der neue Datensatz einen der obigen Schwellwerte überschreitet. Speichert man in einer Tabelle z. B. Datensätze mit einer Gesamtlänge von 1.000 Bytes, so wird beim Eintragen des ersten Datensatzes die PFS-Seite mit 50% aktualisiert. Wird ein zweiter Datensatz eingetragen, ist eine weitere Aktualisierung nicht notwendig, da die Gesamtmenge immer noch unter 50% liegt. Erst beim 5. Datensatz wird erneut die PFS-Seite aktualisiert und der Füllgrad wird mit 80% angegeben.

Ermittlung des Füllgrads

Der Füllgrad ist ein wesentlicher Bestandteil einer jeden Datenseite in einem HEAP. Mit Hilfe der oben genannten Systemview kann ermittelt werden, zu wieviel Prozent eine Datenseite gefüllt ist. Nachdem die Testtabelle erstmalig befüllt wurde, ist jede Datenseite vollständig gefüllt!

SELECT allocated_page_page_id,
       page_free_space_percent
FROM   sys.dm_db_database_page_allocations
       (
         DB_ID(),
         OBJECT_ID(N'dbo.messages', N'U'),
         0,
         NULL,
         N'DETAILED'
       )
WHERE  page_level = 0
       AND is_iam_page = 0;
GO

image

Sobald Datensätze aus der Tabelle gelöscht werden, werden die Einträge in der PFS-Seite aktualisiert!

DELETE dbo.messages
WHERE message_id %2 = 0;
GO

Da die Tabelle nicht exklusiv gesperrt ist, werden allokierte Datenseiten nicht wieder freigegeben. Nach dem Löschvorgang stellt sich die Datenverteilung prozentual wie folgt dar:

image

Es ist erkennbar, dass die PFS-Seite für alle betroffenen Datenseiten des HEAP aktualisiert wurde. Mit dem nächsten Löschbefehl werden alle Datensätze aus den ersten 2 Datenseiten gelöscht:

DELETE TOP (40) 
FROM dbo.messages;
GO

image

Wenn eine Datenseite vollständig leer ist, wird NULL als Eintrag gespeichert. Die Datenseite besitzt keine weiteren Daten mehr; verbleibt aber allokiert. Mit der folgenden Abfrage kann man feststellen, wie viele Datenseiten leer sind.

SELECT page_free_space_percent,
       COUNT_BIG(*) AS num_of_pages,
       SUM
       (
         CASE WHEN page_free_space_percent IS NULL
              THEN 0
              ELSE CAST(page_free_space_percent AS INT)
         END * 8060.0 / 100
       ) AS allocated_bytes
FROM   sys.dm_db_database_page_allocations
       (
         DB_ID(),
         OBJECT_ID(N'dbo.messages', N'U'),
         0,
         NULL,
         N'DETAILED'
       )
WHERE  page_level = 0
       AND is_iam_page = 0
GROUP BY
       page_free_space_percent;
GO

image

Die Auswertung ist nicht zu 100% akkurat, da sie – auf Grund der Speicherverfahren für HEAPS – einige Schwächen zeigt:

  • Die Anzahl der Pages mit dem Eintrag NULL kann bis zu 7 weitere LEERE Datenseiten anzeigen. Das hängt damit zusammen, dass Microsoft SQL Server ab der 9 allokierten Datenseite keine MIXED EXTENTS mehr verwendet, sondern UNIFORM EXTENTS. Daraus resultiert, dass bei Allokation von mindestens einer Datenseite in einem UNIFORM EXTENT 7 weitere Datenseiten allokiert werden, die jedoch noch nicht allokiert sind.
  • Die Prozentzahlen täuschen über den wahren Füllgrad hinweg. Ein Füllgrad von z. B. 50% besagt lediglich, dass eine Datenmenge von 1 Byte bis 4.030 Bytes auf der Datenseite gespeichert wird. Um die genaue Datenmenge zu evaluieren, muss JEDE Datenseite explizt untersucht werden. Das ist für eine kleine Tabelle noch vertretbar; handelt es sich jedoch um mehrere Millionen Datenseiten, dauert die Evaluierung zu lang.

Herzlichen Dank fürs Lesen!

Temporal Tables und INSTEAD OF-Trigger

$
0
0

Mit der Einführung von System Versioned Temporal Tables wurde für die Programmierer ein Weg geschaffen, um eigene Historisierungslösungen ad acta zu legen. In grauer Vorzeit verwendete man entweder Trigger oder Stored Procedures für die Entwicklung einer eigenen Historisierungslösung. Die Möglichkeiten dieser Lösungen waren beschränkt und unter Umständen sehr fehleranfällig. Viele Entwickler haben den Wunsch, im Datensatz den Benutzer zu speichern, der zuletzt Änderungen vorgenommen hat. Üblicherweise kann das nur mit Hilfe eines UPDATE-Triggers geschehen. Das AFTER-Trigger erhebliche Probleme in System Versioned Temporal Tables verursachen können, habe ich im Artikel “Temporal Tables – Verwendung von Triggern” bereits beschrieben. INSTEAD OF Trigger sind in System Versioned Temporal Tables nicht erlaubt. Was also tun? Dieser Artikel beschreibt eine Lösung, in der INSTEAD OF-Trigger dennoch zum Erfolg führen.

Testumgebung

Für die nächsten Beispiele wird erneut die Tabelle [dbo].[Customer] in leicht abgewandelter Form verwendet. Die Tabelle besitzt ein Attribut mit dem Namen [UpdateUser]. Dieses Attribut soll bei jeder Aktualisierung automatisch mit dem Namen des Bearbeiters aktualisiert werden.

-- Create a dedicated schema for the history data
IF SCHEMA_ID(N'history') IS NULL
    EXEC sp_executesql N'CREATE SCHEMA [history] AUTHORIZATION dbo;';
    GO

-- Create the System Versioned Temporal Table
CREATE TABLE dbo.Customers
(
    Id         INT          NOT NULL    IDENTITY (1, 1),
    Name       VARCHAR(100) NOT NULL,
    Street     VARCHAR(100) NOT NULL,
    ZIP        CHAR(5)      NOT NULL,
    City       VARCHAR(100) NOT NULL,
    UpdateUser SYSNAME      NOT NULL DEFAULT (ORIGINAL_LOGIN()),
    ValidFrom  DATETIME2(0) GENERATED ALWAYS AS ROW START NOT NULL DEFAULT ('2016-01-01T00:00:00'),
    ValidTo    DATETIME2(0) GENERATED ALWAYS AS ROW END NOT NULL DEFAULT ('9999-12-31T23:59:59'),

    CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id),
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
GO

-- Create the history table
CREATE TABLE history.Customers
(
    Id         INT          NOT NULL    IDENTITY (1, 1),
    Name       VARCHAR(100) NOT NULL,
    Street     VARCHAR(100) NOT NULL,
    ZIP        CHAR(5)      NOT NULL,
    City       VARCHAR(100) NOT NULL,
    UpdateUser SYSNAME      NOT NULL,
    ValidFrom  DATETIME2(0) NOT NULL,
    ValidTo    DATETIME2(0) NOT NULL
);
GO

INSTEAD OF -Trigger sind weder bei der aktuellen noch bei der Verlaufstabelle zulässig, um zu verhindern, dass die DML-Logik ungültig wird. INSTEAD OF-Trigger setzen die Standardaktionen der auslösenden Anweisung (INSERT, UPDATE oder DELETE) außer Kraft. Ein INSTEAD OF-Trigger kann definiert werden, der eine Fehler- oder Wertüberprüfung für eine oder mehrere Spalten ausführt und anschließend weitere Aktionen ausführt, bevor der Datensatz eingefügt wird.
INSTEAD OF-Trigger können sowohl für Tabellen als auch für Sichten definiert werden.

Die Möglichkeit, mit System Versioned Temporal Tables und INSTEAD OF Triggern zu arbeiten, besteht darin, nicht direkt auf die Tabelle zuzugreifen sondern mit Hilfe einer View die Daten der zugrunde liegenden Tabelle zu bearbeiten!

-- Erstellen einer View für Tabelle dbo.Customers
CREATE OR ALTER VIEW dbo.v_Customers
AS
    SELECT Id, Name, Street, ZIP, City, UpdateUser
    FROM dbo.Customers;
GO

-- Erstellen eines INSTEAD OF Triggers für die View
CREATE OR ALTER TRIGGER dbo.trg_v_Customers_Update
ON dbo.v_Customers
INSTEAD OF UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE C
    SET    C.Name = I.Name,
           C.Street = I.Street,
           C.ZIP = I.ZIP,
           C.City = I.City,
           C.UpdateUser = SUSER_SNAME()
    FROM   dbo.Customers AS C INNER JOIN inserted AS I
           ON (C.Id = I.Id)

    SET NOCOUNT OFF;
END
GO

Sobald die View angelegt und der Trigger implementiert wurde, kann auch “dynamisch” der Eintrag für das Attribut [UpdateUser] gesetzt werden.

UPDATE dbo.v_Customers
SET    Name = 'db Berater GmbH',
       Street = 'Bahnstr. 33',
       ZIP = '64390',
       City = 'Erzhausen'
WHERE  Id = 10;
GO

Herzlichen Dank fürs Lesen!

Bisher veröffentlichte Artikel zu System Versioned Temporal Tables

Temporal Tables – lang laufende Transaktionen

$
0
0

Im englischsprachigen Forum für Microsoft SQL Server kam eine Frage auf, in der es darum ging, dass zwei explizite Transaktionen den gleichen Datensatz / Datensätze einer System Versioned Temporal Table ändern wollen. Dabei kommt es zu Konflikten, die schwierig zu lösen sind. Bei den Überlegungen zu einer adäquaten Lösung war schnell klar, wo das Problem liegt – nicht aber, wie man dieses Problem am einfachsten lösen kann. Mit diesem Artikel möchte ich eine Lösungsmöglichkeit anbieten, die mich selbst nicht wirklich überzeugt. Dennoch ist es aus meiner Sicht die – aktuell – einzige Lösung für das Problem.

Datum für Gültigkeitsbereich eines Datensatzes

Um das nachfolgende Problem zu verstehen, muss man das Konzept der System Versioned Temporal Tables in Bezug auf historische Datensätze verstehen. Wenn durch eine DML-Operation ein Datensatz in einer Temporal Table geändert wird, muss der ursprüngliche Datensatz mit einem Datumsbereich gespeichert werden, in dem er gültig gewesen ist. Hierzu verwendet Microsoft SQL Server IMMER die Startzeit der Transaktion! Das folgende Beispiel zeigt eine aus diesem Umstand resultierende Problematik:

CREATE TABLE dbo.Customers
(
    Id        INT          NOT NULL    IDENTITY (1, 1),
    Name      VARCHAR(100) NOT NULL,
    Street    VARCHAR(100) NOT NULL,
    ZIP       CHAR(5)      NOT NULL,
    City      VARCHAR(100) NOT NULL,
    ValidFrom DATETIME2(0) GENERATED ALWAYS AS ROW START NOT NULL DEFAULT ('2016-01-01T00:00:00'),
    ValidTo   DATETIME2(0) GENERATED ALWAYS AS ROW END NOT NULL DEFAULT ('9999-12-31T23:59:59'),

    CONSTRAINT pk_Customers_ID PRIMARY KEY CLUSTERED (Id),
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
);
GO

CREATE TABLE history.Customers
(
    Id        INT          NOT NULL    IDENTITY (1, 1),
    Name      VARCHAR(100) NOT NULL,
    Street    VARCHAR(100) NOT NULL,
    ZIP       CHAR(5)      NOT NULL,
    City      VARCHAR(100) NOT NULL,
    ValidFrom DATETIME2(0) NOT NULL,
    ValidTo   DATETIME2(0) NOT NULL
);
GO

Problemstellung

Das eingefügte Video (Bitte etwas Nachsicht – ist mein erster Versuch, den Blog mit Videos zu versehen!) zeigt das Problem, wenn eine Transaktion zu lang läuft und erst zum Schluss die gewünschte Änderung an einem Datensatz durchführt.

Transaktion 1


-- TRANSACTION 1 starts a transaction which 
-- changes the affected record after 10 sec.
BEGIN TRANSACTION;
GO
    -- what is the start time of the transaction
    SELECT DTAT.transaction_begin_time
    FROM   sys.dm_tran_current_transaction AS DTCT
           INNER JOIN sys.dm_tran_active_transactions AS DTAT
           ON (DTCT.transaction_id = DTAT.transaction_id);

    RAISERROR (N'This is my loooong running process...', 0, 1) WITH NOWAIT;
    DECLARE    @I INT = 10
    WHILE @I > 0
    BEGIN
        RAISERROR (N'%i seconds to go...', 0, 1, @I) WITH NOWAIT;
        WAITFOR    DELAY '00:00:01'
        SET @I -= 1;
    END

    UPDATE dbo.Customers
    SET    Name = 'This is me'
    WHERE  Id = 10;
COMMIT TRANSACTION;
GO

Transaktion 2

-- TRANSACTION 2 starts a transaction and immediately
-- after the start it changes the affected record.
BEGIN TRANSACTION;
GO
    -- what is the start time of the transaction
    SELECT CAST(DTAT.transaction_begin_time AS DATETIME2(0))
    FROM   sys.dm_tran_current_transaction AS DTCT
           INNER JOIN sys.dm_tran_active_transactions AS DTAT
           ON (DTCT.transaction_id = DTAT.transaction_id);

    UPDATE dbo.Customers
    SET    Name = 'This is me'
    WHERE  Id = 10;
COMMIT;

SELECT * FROM dbo.Customers FOR SYSTEM_TIME ALL
WHERE    Id = 10;
GO

Zunächst wird Transaktion 1 (links) gestartet. In diesem Code ist eine Verzögerung von 10 Sekunden implementiert, die ein “laaaaaanges” Laufzeitverhalten in der Prozedur simuliert. Während Transaktion 1 läuft, wird im rechten Fenster Transaktion 2 gestartet. In Transaktion 2 wird eine Änderung an Datensatz 10 durchgeführt, während Transaktion 1 noch läuft. Sobald Transaktion 1 den gleichen Datensatz ändern möchte, wird die Transaktion mit einem Fehler beendet!

image

Ursache für den Abbruch ist Transaktion 2! Bei der Speicherung von Daten in der Verlaufstabelle muss ein Datum angegeben werden, BIS zu dem ein Datensatz in der ursprünglichen Tabelle gültig gewesen ist. Als validen Zeitstempel verwendet Microsoft SQL Server den Beginn einer Transaktion. T1 beginnt um 13:45:27.810; eine Änderung des Datensatzes findet aber erst 10 Sekunden später statt. Transaktion 2 beginnt um 13:45:33.000 (5 Sekunden später) und führt unmittelbar eine Änderung am Datensatz ID = 10 durch. Sobald Transaktion 1 den gleichen Datensatz ändern möchte, kommt es zu einem zeitlichen Konflikt, da Microsoft SQL Server versucht, den “früheren” Zeitstempel zu verwenden. Dadurch entstehen inkonsistente Daten in der Verlaufstabelle. Aus diesem Grund schlägt die Transaktion fehl und es findet ein Rollback statt.

Lösung

Um das Problem der Verzögerung zu lösen, muss der zu ändernde Datensatz zu Beginn der Transaktion blockiert werden. Nur so ist es möglich, eine weitere Transaktion daran zu hindern, den Datensatz zu ändern, während in Transaktion 1 andere Operationen ausgeführt werden.

-- TRANSACTION 1 starts a transaction which 
-- changes the affected record after 10 sec.
BEGIN TRANSACTION;
GO
    -- what is the start time of the transaction
    SELECT DTAT.transaction_begin_time
    FROM   sys.dm_tran_current_transaction AS DTCT
           INNER JOIN sys.dm_tran_active_transactions AS DTAT
           ON (DTCT.transaction_id = DTAT.transaction_id);

    -- Block the affected record with a X-Lock
     SELECT * FROM dbo.Customers WITH (UPDLOCK, HOLDLOCK)
     WHERE    Id = 10;

    RAISERROR (N'This is my loooong running process...', 0, 1) WITH NOWAIT;
    DECLARE    @I INT = 10
    WHILE @I > 0
    BEGIN
        RAISERROR (N'%i seconds to go...', 0, 1, @I) WITH NOWAIT;
        WAITFOR    DELAY '00:00:01'
        SET @I -= 1;
    END

    UPDATE dbo.Customers
    SET    Name = 'This is me'
    WHERE  Id = 10;
COMMIT TRANSACTION;
GO

Der Code von Transaktion 1 wurde minimal geändert, unmittelbar nach Beginn der Transaktion wird der Datensatz mittels eines UPDLOCK und HOLDLOCK innerhalb der Transaktion gesperrt. Somit kann Transaktion 2 nicht auf den Datensatz zugreifen sondern muss warten, bis Transaktion 1 beendet ist.

Bisher veröffentlichte Artikel zu System Versioned Temporal Tables

Herzlichen Dank fürs Lesen!

Zentralen Verwaltungsserver in Verwaltung aufnehmen

$
0
0

Wer schon mal mit einem “Zentralen Verwaltungsserver” in Microsoft SQL Server gearbeitet hat, wird dieses Feature nicht mehr missen wollen. Insbesondere in sehr großen Serverlandschaften erleichtert es die Arbeit ungemein, da mit wenigen Handgriffen Anpassungen an den verwalteten Servern durchgeführt werden können. Zentrale Verwaltungsserver speichern eine Liste von SQL Server -Instanzen, die in ein oder mehrere Gruppen unterteilt sind. Alle Aktionen wirken sich auf alle Server in der Servergruppe aus.

Eine – bedauerliche – Einschränkung besteht jedoch, seit dieses Feature mit Microsoft SQL Server 2008 zur Verfügung gestellt wurde: “Der Zentrale Verwaltungsserver” kann nicht selbst in die Gruppe der verwalteten Server hinzugefügt werden!

image

Der Versuch, den Server [NB-LENOVO-I\SQL_2016] in die Liste der verwalteten Server hinzuzufügen, schlägt fehl, da Microsoft SQL Server überprüft, ob es sich bei dem Server um den Zentralen Verwaltungsserver handelt. Die Fehlermeldung ist selbst erklärend. Mit einem kleinen Trick kann man aber diese Barriere knacken und den Zentralen Verwaltungsserver in die Liste übertragen.

Hinweis

Die hier gezeigte Lösung ist nicht offiziell von Microsoft freigegeben! Trotz einiger Recherche und Tests ist nicht auszuschließen, dass es zu Problemen kommen kann. Alle von mir ausgeführten Tests (Multi-Server-Abfrage, Policy-Checks, …) verliefen ohne Beanstandungen. Die Analyse der Datenstruktur sowie der Prozeduren ergaben keine Einschränkungen/Probleme. Dennoch der Hinweis, die gezeigte Technik selbst AUSFÜHRLICH zu testen!

Hintergrundinformationen

msdb

Microsoft verwaltet einen SQL Server im Kern auch nur durch die Verwendung von Tabellen, Sichten und Stored Procedures. Insbesondere die Datenbank MSDB ist voll von solchen Verwaltungselementen. Die Funktionalität eines “Zentralen Verwaltungsservers” wird ebenfalls durch Objekte in der Systemdatenbank MSDB bereitgestellt. Hierzu hat das Team von Microsoft folgende Objekte in MSDB implementiert:

image

Systemtabellen

Microsoft SQL Server benötigt für einen “Zentralen Verwaltungsserver” lediglich zwei Tabellen. In der Tabelle [dbo].[SYSMANAGEMENT_SHARED_SERVER_GROUPS_INTERNAL] werden die logischen Verwaltungsgruppen gespeichert, unter denen man seine SQL Server abspeichert. Die Tabelle [dbo].[SYSMANAGEMENT_SHARED_REGISTERED_SERVERS_INTERNAL] verwaltet die Namen der zu verwaltenden SQL Server.

Views

Die Views haben einen “schützenden” Charakter. Es gibt für die Verwaltungsserver zwei Datenbankgruppen, mit denen die registrierten SQL Server verwaltet werden können:

Ausschließlich die Benutzergruppe [ServerGroupReaderRole] besitzt eine SELECT-Berechtigung auf die beiden Views. Damit ist gewährleistet, dass jedes Mitglied dieser Gruppe die zu verwaltenden SQL Server sehen kann.

Stored Procedures

Die Vielzahl von Stored Procedures für die Verwaltung eines Verwaltungsservers sollen hier nicht detailliert beschrieben werden; aus den Namen geht deutlich hervor, welche Funktion diese Prozeduren haben. Ausschließlich Mitglieder der Datenbankrolle [ServerGroupAdministratorRole] besitzen das Recht, die Prozeduren auszuführen.

Eine Stored Procedure wird dennoch etwas genauer unter die Lupe genommen. Hierbei handelt es sich um die Prozedur [dbo].[SP_SYSMANAGEMENT_ADD_SHARED_REGISTERED_SERVER]. Diese Prozedur wird aufgerufen, sobald ein neuer Server zum Verwaltungsserver hinzugefügt werden soll. Gibt man als Namen den Verwaltungsserver selbst ein, wird der oben beschriebene Fehler ausgelöst!

[dbo].[SP_SYSMANAGEMENT_ADD_SHARED_REGISTERED_SERVER]

Es soll nicht der vollständige Code der Prozedur hier beschrieben werden. Vielmehr ist wichtig, wie Microsoft SQL Server überprüft, welcher SQL Server hinzugefügt werden soll und wie darauf reagiert wird!

IF (UPPER(@@SERVERNAME collate SQL_Latin1_General_CP1_CS_AS) = UPPER(@server_name collate SQL_Latin1_General_CP1_CS_AS))
BEGIN
    RAISERROR (35012, -1, -1)
    RETURN (1)
END

INSERT INTO [msdb].[dbo].[sysmanagement_shared_registered_servers_internal]
(server_group_id, name, server_name, description, server_type)
VALUES
(@server_group_id, @name, @server_name, @description, @server_type)

Wenn der eigene Servername (@@SERVERNAME) identisch ist mit dem hinzuzufügenden Namen, wird der Fehler 35012 ausgeführt und die Prozedur beendet. Sollte die Überprüfung erfolgreich sein, wird in die Tabelle [dbo].[SYSMANAGEMENT_SHARED_REGISTERED_SERVERS_INTERNAL] der Eintrag vorgenommen. Na ja, was Microsoft SQL Server kann, kann ich als sysadmin auch.

In meinem Beispiel ist die Instanz [NB-LENOVO-I\SQL_2016] als Zentraler Verwaltungsserver eingerichtet worden. Diese Instanz dem Verwaltungsserver hinzuzufügen, wurde mit der obigen Fehlermeldung beendet. Also wird die Instanz direkt in die Systemtabelle eingetragen! In meiner Testumgebung liefert die Abfrage der beiden Systemtabellen folgende Informationen:

image

Die Instanz [NB-LENOVO-I\SQL_2017] befindet sich unmittelbar im Hauptknoten der Verwaltung während die Instanz [NB-LENOVO-I\SQL_2008] in der Gruppe [Produktion] gespeichert wurde. Soll also die Instanz [NB-LENOVO-I\SQL_2016] ebenfalls in der Gruppe [Produktion] gespeichert werden, muss das folgende T-SQL-Skript ausgeführt werden:

INSERT INTO dbo.sysmanagement_shared_registered_servers_internal
(server_group_id, name, server_name, description, server_type)
VALUES
(
    6,
    N'NB-LENOVO-I\SQL_2016',
    N'NB-LENOVO-I\SQL_2016',
    N'Zentraler Verwaltungsserver',
    0
);

Et voilà!

Vielen Dank fürs Lesen!

Warum man IMMER vollständig qualifizierte Objekte in T-SQL verwenden soll

$
0
0

Aktuell untersuche ich bei einem Kunden eine Applikation, dessen Performance optimiert werden soll. Bei der Durchsicht des Codes ist mir aufgefallen, dass die Programmierer eine einfache Notation für die Aufrufe von Prozeduren oder für die Generierung von SQL-Abfragen verwendet haben.

EXEC myproc;
GO
SELECT * FROM myTable;
GO

Leider hält sich dieser Programmierstil hartnäckig obwohl er für Microsoft SQL Server eher kontraproduktiv ist. Der folgende Artikel zeigt die Nachteile dieses Stils. Vielleicht nimmt sich ja der eine oder andere Programmierer die Beispiele zu Herzen und ändert seinen Programmierstil.

Ausführungsprozess von Microsoft SQL Server

Wenn eine Abfrage an Microsoft SQL Server gesendet wird, wird zunächst im Standardschema des Benutzers nach einem entsprechenden Objekt gesucht wird. Befindet sich das gewünschte Objekt im Standardschema des Benutzers, so wird auf dieses Objekt verwiesen. Sollte jedoch das angeforderte Objekt nicht im Standardschema liegen, sucht Microsoft SQL Server im [dbo]-Schema der Datenbank nach dem gewünschten Objekt. Sobald Microsoft SQL Server die gewünschten Objekte für die Ausführung der Abfrage gefunden hat, wird ein Ausführungsplan erzeugt. Der Ausführungsplan ist die Basis der Execution Engine!

Testumgebung

Für die nachfolgenden Demos werden zwei Benutzer in der Datenbank erzeugt. Der Benutzer [User1] verwendet als Standardschema [dbo] während [User2] ein eigenes Schema verwendet.

CREATE USER [USER1] WITHOUT LOGIN WITH DEFAULT_SCHEMA = [dbo];
CREATE USER [USER2] WITHOUT LOGIN WITH DEFAULT_SCHEMA = [User2]
GO 
CREATE SCHEMA [User2] AUTHORIZATION [USER2];
GO

image
Nachdem die Benutzer angelegt wurden, wird eine Testtabelle [dbo].[messages] angelegt, auf die beide Benutzer lesenden Zugriff besitzen.

SELECT TOP (1) *
INTO  dbo.messages
FROM  sys.messages
WHERE language_id = 1033;
GO

GRANT SELECT ON dbo.messages TO USER1;
GRANT SELECT ON dbo.messages TO USER2;
GO

Demonstration

Ausführung ohne vollständig qualifizierte Objektreferenz

Im ersten Beispiel führen beide Datenbankbenutzer ein SELECT aus, ohne dabei auf das Schema zu referenzieren. Allen Beispielen geht voraus, dass der Plancache vorher gelöscht wird!

EXECUTE AS USER = 'USER1';
GO
SELECT USER_NAME() AS [User], * FROM messages;
GO
REVERT;
GO

EXECUTE AS USER = 'USER2'
GO
SELECT USER_NAME() AS [User], * FROM messages;
GO
REVERT;
GO

image
Jeder Benutzer der Datenbank erhält das gleiche Ergebnis. Scheinbar gibt es also keine Probleme bei der Anwendung der obigen Technik. Dennoch gilt es, zu berücksichtigen, dass Microsoft SQL Server für die Ausführung einer Abfrage einen Ausführungsplan benötigt. Ein Blick in den Plancache nach der Ausführung der Abfrage durch beide Datenbankbenutzer zeigt, welche Probleme sich – im Hintergrund – ergeben!

SELECT cp.plan_handle ,
       cp.usecounts ,
       cp.size_in_bytes ,
       cp.cacheobjtype ,
       st.text
FROM   sys.dm_exec_cached_plans cp
       CROSS APPLY sys.dm_exec_sql_text(plan_handle) st
WHERE  st.dbid = DB_ID()
       AND st.text NOT LIKE '%exec_cached_plans%';

image
Microsoft SQL Server speichert zwei Ausführungspläne mit jeweils 40 KByte ab. In heutigen Serverlandschaften sicherlich kein größeres Problem; aber neben Speicherbedarf darf man nicht unterschätzen, dass die Kompilierung CPU-Zeit konsumiert.

Ausführung mit vollständig qualifizierter Objektreferenz

Das gleiche Beispiel wird nun mit vollständig qualifizierten Objekten durchgeführt. Hierbei adressieren beide Benutzer das Referenzobjekt mit dem Schemanamen.

EXECUTE AS USER = 'USER1';
GO
SELECT USER_NAME() AS [User], * FROM <strong>dbo</strong>.messages;
GO
REVERT;
GO

EXECUTE AS USER = 'USER2'
GO
SELECT USER_NAME() AS [User], * FROM <strong>dbo</strong>.messages;
GO
REVERT;
GO

Schaut man anschließend in den Plancache, wird man überrascht feststellen, dass nur noch ein Ausführungsplan gespeichert wurde.
image

Der Plancache

Viele Anwendungsentwickler wissen, dass Microsoft SQL Server vor der Ausführung einer neuen Abfrage einen Ausführungsplan generiert, der dann im Plancache abgespeichert wird. Kleinste Änderungen am SQL-Text führen dazu, dass ein weiterer Plan erstellt und abgespeichert werden muss. Im ersten Beispiel ist der Text exakt identisch für beide Benutzer und dennoch wurden zwei Ausführungspläne gespeichert. Microsoft SQL Server muss noch weitere Informationen zu einem Ausführungsplan speichern, die eine Rolle für die “Eindeutigkeit” des Ausführungsplans spielen.
Neben dem Ausführungstext speichert Microsoft SQL Server weitere Informationen im Plancache, die bestimmen, ob ein Ausführungsplan “eindeutig” identifizierbar ist ist. Diese Informationen befinden sich in den Planeigenschaften, die mit der Systemfunktion [sys].[dm_exec_plan_attributes] abgerufen werden können.  Die nachfolgende Abfrage zeigt die Planeigenschaften für beide weiter oben ausgeführten Abfragen, die ohne voll qualifizierten Objekte aufgerufen wurden.

SELECT pq.*
       ,s.name
FROM   sys.dm_exec_cached_plans cp
       CROSS APPLY sys.dm_exec_plan_attributes(plan_handle) pq
       CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS st
       INNER JOIN sys.schemas AS s
       ON
       (
           s.schema_id = CAST(pq.value AS INT)
           AND pq.attribute = N'user_id'
       )
WHERE  st.dbid = DB_ID()
       AND st.text LIKE '%messages%'
       AND st.text NOT LIKE '%exec_cached_plans%'
       AND pq.is_cache_key = 1;
GO

image
Die Abbildung zeigt Unterschiede der beiden Abfragen. Das Attribut [user_id] unterscheidet sich in beiden Abfragen. Der Begriff [user_id] ist hier etwas unglücklich gewählt; handelt es sich doch eigentlich nicht um einer [user_id] sondern um eine [schema_id]. Während der Benutzer [USER2] das Standardschema [User2] verwendet, greift [USER1] auf das Schema [dbo] als Standard zu. Diese Informationen werden im Plancache abgespeichert. Da sie sich für beide Prozesse unterscheiden, muss Microsoft SQL Server zwei Ausführungspläne generieren und speichern.

Zusammenfassung

Dass die Verwendung von qualifizierten Objekten nicht nur freundlicher zu lesen ist sondern auch umständliche Suchen des SQL Servers nach dem geeigneten Objekt vermieden werden, sind nur einige Vorteile. Viel deutlicher wird jedoch der immense Vorteil durch die Wiederverwendung von Abfrageplänen, da sie nicht mehrfach im Plancache hinterlegt werden müssen. Die Abfragen können also optimiert ausgeführt werden und der Speicher von SQL Server dankt es auch noch.

Herzlichen Dank fürs Lesen!


Eigentümer von Datenbank geändert – wer hat’s getan?

$
0
0

Als DBA ist man verantwortlich für viele alltägliche Dinge, die die Stabilität und die Sicherheit der zu betreuenden Microsoft SQL Server betreffen. Nicht alles hat man sofort im Fokus und manchmal kommt es vor, dass Dinge bereits passiert sind und der DBA mit den Auswirkungen der Änderungen konfrontiert wird. Wird z. B. der Eigentümer einer Datenbank geändert, kann es passieren, dass die Anwendung nicht mehr läuft. Diese Art der Programmierung sehe ich sehr häufig und dann stellt sich die Frage, wer hat diese Anpassungen vorgenommen. Standardmäßig speichert Microsoft SQL Server diese Informationen nicht; aber es gibt eine Lösung, die jeder DBA mit wenig Aufwand implementieren kann – ÜBERWACHUNGEN. Dieser Artikel beschreibt an Hand eines konkreten Problems die Funktionsweise der ÜBERWACHUNGS-Komponente in Microsoft SQL Server.

SQL Server Audit

Unter einem AUDIT versteht man die Überwachung einer Instanz von Microsoft SQL Server oder einer einzelnen Datenbank. Sie umfasst die Nachverfolgung und Protokollierung von Ereignissen, die während des Betriebs von Microsoft SQL Server auftreten. Überwachte Ereignisse können in die Ereignisprotokolle oder Überwachungsdateien geschrieben werden. Unterschiedliche Überwachungsebenen für Microsoft SQL Server garantieren eine lückenlose Protokollierung, die von gesetzlichen oder standardspezifischen Anforderungen für die Installation abhängig sind. Für Server können Überwachungsaktionsgruppen instanzweise aufgezeichnet werden. Für Datenbanken können Überwachungsaktionsgruppen oder Überwachungsaktionen pro Datenbank aktiviert werden. Alle Editionen ab SQL Server 2016 SP1 unterstützen Überwachungen auf Datenbankebene. Zuvor war die Überwachung auf Datenbankebene der Enterprise, Developer und Evaluation Edition vorbehalten.

Eine AUDIT-Implementierung besteht aus zwei Komponenten. Je nach Anwendungsfall unterscheidet man zwischen Server-Audits (zu finden in [Security] –> [Audits] auf Serverebene) oder Datenbank-Audits (zu finden in [Security] –> [Database Audit Specifications] auf Datenbankebene).

Überwachung

Jedes Audit benötigt als Grundlage für die Speicherung von Ereignissen eine Überwachung, die die generelle Konfiguration der Überwachung (Speicherort, Dateigröße, …) beinhaltet.

image

Sobald eine Überwachung konfiguriert wurde, kann diese Überwachung dazu verwendet werden, Überwachungsspezifikationen zu verwalten und die Informationen in der konfigurierten Datei oder im WINDOWS Ereignisprotokoll ist zu empfehlen, da es nicht manipuliert werden werden. Dateien können leicht von einem DBA gelöscht oder manipuliert werden.

Das Konto, unter dem der SQL Server -Dienst ausgeführt wird, muss über die Berechtigung zum Generieren von Sicherheitsüberwachungen verfügen, um in das Windows-Sicherheitsprotokoll schreiben zu können. Hierzu muss dem SQL Server-Dienstkonto die Vollberechtigung zum Zugriff auf die Registrierungsstruktur HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Security vergeben werden!

Weitere Informationen zu AUDITS finden sich hier:

Problemstellung

Die Änderung des Eigentümers einer Datenbank kann weitreichende Folgen haben. Wenn eine Applikation (siehe Sharepoint) den Eigentümer überprüft und voraussetzt, dass z. B. ein Applikationskonto der Eigentümer sein muss, wird die Anwendung entweder überhaupt nicht oder fehlerhaft gestartet. Die Änderung des Eigentümers einer Datenbank wird nicht von Microsoft SQL Server protokolliert; ein AUDIT wäre somit ein ideales Einsatzgebiet für diese Anforderungen.

Erstellen einer Überwachung

Um ein AUDIT zu implementieren, muss zunächst ein Überwachungsobjekt implementiert werden. Das kann entweder über die GUI erfolgen oder aber mit Hilfe von T-SQL automatisiert werden.

USE [master]
GO

CREATE SERVER AUDIT [Monitor_DatabaseOwnerChange]
TO FILE
(
    FILEPATH = N'F:\TraceFiles'
    ,MAXSIZE = 128 MB
    ,MAX_FILES = 1
    ,RESERVE_DISK_SPACE = OFF
)
WITH
(
    QUEUE_DELAY = 1000
    ,ON_FAILURE = CONTINUE
);
GO

ALTER SERVER AUDIT [Monitor_DatabaseOwnerChange]
WITH (STATE = ON);
GO

Das obige Skript erstellt eine Überwachung mit dem Namen “Monitor_DatabaseOwnerChange”. Alle entdeckten Vorfälle werden in einer Datei abgespeichert, die in F:\TraceFiles abgespeichert wird. Der Name der Datei wird von Microsoft SQL Server selbst verwaltet! Hat die Datei die maximale Größe von 128 MB erreicht, wird sie überschrieben. Microsoft SQL Server speichert die Überwachung zunächst im RAM, die Änderungen werden aber alle 1000ms in die Datei geschrieben. Im Falle eines Fehlers soll die Überwachung aber nicht gestoppt werden sondern weiterhin versuchen, die Aufzeichnungen durchzuführen.

Erstellen einer Überwachungsspezifikation

Sobald die Überwachung erstellt wurde, kann die Überwachungsspezifikation erstellt werden. Microsoft SQL Server stellt verschiedene Audit-Aktionsgruppen zur Verfügung, um die Überwachung granular zu gestalten. Die Überwachung kann auf Serverebene und Datenbankebene erfolgen. Eine Übersicht der möglichen Überwachungsgruppen findet man hier: https://docs.microsoft.com/de-de/sql/relational-databases/security/auditing/sql-server-audit-action-groups-and-actions. Die möglichen Aktionen sind vielfältig und – aus meiner Sicht – teilweise recht unübersichtlich. Liest man sich durch den Dschungel der Möglichkeiten, findet man die Aktionsgruppe “DATABASE_OBJECT_OWNERSHIP_CHANGE_GROUP”. In der Hilfe zu Microsoft SQL Server heißt es zu dieser Gruppe:

“Das Ereignis wird ausgelöst, wenn der Besitzer für Objekte im Datenbankbereich geändert wird. Das Ereignis wird für eine Objektbesitzänderung in einer beliebigen Datenbank auf dem Server ausgelöst.”

Die Überwachungsspezifikation kann – wie bereits weiter oben beschrieben, mit Hilfe der GUI erstellt werden. In T-SQL ist es deutlich schneller und einfacher zu realisieren:

CREATE SERVER AUDIT SPECIFICATION [DatabaseOwnerChange]
FOR SERVER AUDIT [Monitor_DatabaseOwnerChange]
ADD
(
    DATABASE_OWNERSHIP_CHANGE_GROUP
)
WITH (STATE = ON);
GO

image

Überprüfung der implementierten Überwachung

Nachdem die Überwachung implementiert wurde, wird es Zeit, die Funktionalität zu überprüfen. Dazu ändere ich für eine Testdatenbank den Eigentümer.

ALTER AUTHORIZATION ON DATABASE::CustomerOrders TO [NB-LENOVO-I\Uwe];
GO

Auswertung mit SQL Server Management Studio

Mit Hilfe von SQL Server Management Studio lassen sich Protokolleinträge leicht anzeigen und auswerten. Hierzu wird einfach mit der rechten Maustaste auf die zuvor implementierte Überwachung geklickt und der Befehl [View Audit Logs] zeigt in einem Dialog die gesammelten Einträge chronologisch geordnet an.

image

Das Problem der Benutzeroberfläche besteht jedoch darin, dass immer ALLE Datensätze angezeigt werden. Sie können zwar gefiltert werden, dennoch sind es immer mehrere Schritte, die benötigt werden, um die Informationen auszuwerten. Eine “eigene” Verwendung der Ergebnisse ist aus der GUI heraus nicht möglich.

Auswertung mit DMF

Wer die Arbeit mit der GUI scheut, dem steht eine Systemfunktion zur Verfügung, mit der es möglich ist, die Protokolldatei ebenfalls auszulesen. Mit Hilfe von sys.fn_get_audit_file besteht die Möglichkeit der Ausgabe mit Hilfe von T-SQL. Die Funktion kann natürlich auch verwendet werden, um das Ergebnis z. B. in den Reporting Services oder anderer Clientsoftware zu verwenden. Die Berechtigungen für die Verwendung der Funktion sind jedoch sehr hoch gesteckt – es wird die Berechtigung CONTROL SERVER benötigt.

SELECT  event_time,
        session_id,
        server_principal_name,
        target_server_principal_name,
        database_name,
        statement
FROM    sys.fn_get_audit_file
        (
            'F:\TraceFiles\Monitor_DatabaseOwnerChange_*.sqlaudit',
            DEFAULT,
            DEFAULT
        )
WHERE   action_id = 'TO'
ORDER BY event_time DESC;

Das obige Listing gibt alle Überwachungen aus, bei denen der Eigentümer einer Datenbank geändert wurde. Neben der Information über den Zeitpunkt wird die Session, in der der Befehl abgesetzt wurde sowie folgende Informationen ausgegeben:

  • Anmeldename des ausführenden Kontos (server_principal_name)
  • Neuer Eigentümer der Datenbank (target_server_principal_name)
  • Betroffene Datenbank (database_name)
  • Ausgeführtes Statement (statement)

image

Zusammenfassung

Überwachungen sind ein unerlässliches Mittel, wenn es um Sicherheitsstrategien für Microsoft SQL Server geht. In einer Zeit, in der Daten eine immer wichtigere Rolle im täglichen Geschäft spielen, sollte der Schutz dieses Guts nicht zu “lax” behandelt werden. Microsoft SQL Server. Mit Hilfe von sehr granularen Überwachungsmöglichkeiten zeigt Microsoft SQL Server einmal mehr, dass Sicherheit sehr ernst genommen wird.

Vielen Dank fürs Lesen!

Inside the engine – Erstellung eines Fremdschlüssels

$
0
0

In einem Forumsbeitrag auf msdn wurde gefragt, wie genau Microsoft SQL Server vorgeht, wenn eine Fremdschlüsselbeziehung implementiert werden muss. Insbesondere ging es dabei um die Frage, welche Sperren Microsoft SQL Server setzt und welchen Einfluss diese Sperren auf die Performance haben. Ich habe mich etwas genauer mit den “Internals” beschäftigt und in diesem Artikel zusammengefasst.

Fremdschlüssel

Definition

Fremdschlüssel werden in Relationalen Datenbanksystemen implementiert, um die referenzielle Integrität von Datensätzen aus unterschiedlichen Tabellen gewähren zu können. So können in einer Auftragstabelle keine Aufträge erfasst werden, wenn es dazu keinen passenden Kunden gibt. Eine Fremdschlüssel-Einschränkung muss nicht notwendigerweise mit einer PRIMARY KEY-Einschränkung in einer anderen Tabelle verknüpft sein. Sie kann so definiert werden, dass sie auf die Spalten einer UNIQUE-Einschränkung in einer anderen Tabelle verweist.

Implementierung

Ein Fremdschlüssel ist eine Einschränkung; sie betrifft die Metadaten der Tabelle selbst. Deshalb ist die Implementierung immer mit einem ALTER TABLE verbunden.

ALTER TABLE dbo.CustomerOrders
ADD CONSTRAINT fk_Customers_Id
FOREIGN KEY (Customer_Id)
REFERENCES dbo.Customers(Id);
GO

Das Codebeispiel erstellt eine Fremdschlüssel-Einschränkung auf der Tabelle [dbo].[CustomerOrders] zur Tabelle [dbo].[Customers] über die Attribute [Customer_Id] zu [Id]. Damit ist gewährleistet, dass keine Aufträge erfasst werden können, wenn keine passende [ID] in der Tabelle [dbo].[Customers] vorhanden ist.

Insight the engine

Wie behandelt Microsoft SQL Server diese Anfrage intern? Dazu gib es zwei Fragestellungen:

  • Welche Ressourcen werden während der Erstellung einer Fremdschlüssel-Einschränkung blockiert?
  • Welche Schritte müssen intern ausgeführt werden, um eine Fremdschlüssel-Einschränkung zu implementieren?

Ressourcen

Um festzustellen, welche Ressourcen während der Implementierung blockiert werden, lässt man die Erstellung des Fremdschlüssels in einer Transaktion laufen. So können die gesetzten Sperren untersucht werden.

BEGIN TRANSACTION;
GO
    ALTER TABLE dbo.CustomerOrders
    ADD CONSTRAINT fk_Customers_Id
    FOREIGN KEY (Customer_Id)
    REFERENCES dbo.Customers(Id);
    GO

    SELECT DTL.resource_type,
           DTL.request_mode,
           DTL.request_type,
           DTL.request_status,
           CASE resource_type
                WHEN N'OBJECT' THEN OBJECT_NAME(resource_associated_entity_id)
                WHEN N'KEY' THEN OBJECT_NAME(P.object_id)
                ELSE NULL
           END AS resource_object
    FROM   sys.dm_tran_locks AS DTL
           LEFT JOIN sys.partitions AS P
           ON DTL.resource_associated_entity_id = P.hobt_id
    WHERE  request_session_id = @@SPID;
    GO

image

Microsoft SQL Server setzt [SCH-M] (Schema Modification) Sperren sowohl auf die betroffenen Tabellen als auch auf das Fremdschlüsselobjekt. Sperren des Typs [Sch-M] werden von Microsoft SQL Server verwendet, wenn auf Objekte DDL-Vorgänge ausgeführt werden. Während einer [Sch-M]-Sperre werden gleichzeitige Zugriffe auf die Tabelle verhindert.

Die Schlüsselsperren [KEY] werden im obigen Kontext verwendet, um die Schemaänderungen in den internen Systemobjekten zu speichern.

sys.sysschobjs Jede Zeile stellt ein Objekt in der Datenbank dar.
sys.syssingleobjrefs Enthält eine Zeile für jeden allgemeinen N-zu-1-Verweis.

Workload

Bei der Implementierung einer Fremdschlüssel-Einschränkung vermutet man eine Menge Arbeit für Microsoft SQL Server; dem ist aber nicht so. Die folgende Abbildung zeigt die Transaktionsschritte, die während der Implementierung ausgeführt werden.

SELECT [Current LSN],
       Operation,
       Context,
       [Lock Information]
FROM   sys.fn_dblog(NULL, NULL)
ORDER BY
       [Current LSN] ASC;
GO

image

Was auf dem ersten Blick umständlich und kompliziert aussieht entpuppt sich bei genauer Betrachtung der Einzelschritte als logisch:

  • Nachdem die Transaktion geöffnet wurde (Zeile 4) wird eine Schema-Modifikationssperre auf das Objekt [dbo].[CustomerOrders] gesetzt.
  • Sobald die Sperre erfolgreich gesetzt worden ist, kann für den Fremdschlüssel ein Eintrag in die Tabelle [sys].[sysschobjs] gesetzt werden. Erst, wenn der Name des Fremdschlüsselobjekts eingetragen ist, ist die interne ID für das Objekt bekannt.
  • Ist die ID bekannt, kann das Objekt mit einer SCH-M Sperre für Zugriffe von außen geschützt werden (Zeile 7).
  • Anschließend werden die Informationen zur Fremdschlüsseleinschränkung in die Systemtabelle eingetragen werden (Zeile 8 – 11).
    Es sind nicht mehrere Eintragungen sondern die Systemtabelle besitzt mehrere Indexe (4!).
    image
    Im ersten Schritt wurde der existierende Eintrag für [dbo].[CustomerOrders] verändert (Status); anschließend wurde das Fremdschlüsselobjekt hinzugefügt.
  • Sind alle Informationen zum Fremdschlüssel erfasst, wird die Tabelle [dbo].[Customers] blockiert (Zeile 12)
  • In den Zeilen 13 – 19 werden erneut Daten in die Systemtabellen [sys].[syssingleobjrefs] geschrieben.
    image
  • Nachdem alle Informationen über die Fremdschlüsselbeziehung manifestiert wurden, können die Sperren wieder aufgehoben werden (Zeile 20 – 25)
  • bevor dann die Transaktion beendet wird (Zeile 27)

Zusammenfassung

Die Implementierung eines Fremdschlüssels behandelt Microsoft SQL Server sehr effizient. Wichtigste Voraussetzung für die schnelle Implementierung eines Fremdschlüssels ist das Setzen einer SCH-M-Sperre auf die betroffenen Objekte, dann ist der Rest nur noch ein Kinderspiel.

Vielen Dank fürs Lesen!

Inside the engine – Feste Typenlänge wird variable Typenlänge

$
0
0

In einem Forenbeitrag der deutschen msdn SQL Server Foren wurde ein Problem beschrieben, bei dem die nachträgliche Konvertierung eines Attributs mit fester Zeichenlänge dazu führt, dass in der Ausgabe der Daten die Informationen mit Leerzeichen aufgefüllt werden. Das es sich hierbei um ein “normales” Verhalten von Microsoft SQL Server handelt, beschreibt der nachfolgende Artikel.

Teststellung

Für die Demonstration des oben beschriebenen Verhaltens wird eine Tabelle mit ~280.000 Datensätzen erstellt

USE demo_db;
GO

SELECT language_id,
       message_id,
       severity,
       CAST(LEFT(text, 20) AS CHAR(100)) AS 
INTO   dbo.messages
FROM   sys.messages;

Die Tabelle besitzt ein Attribut “TEXT” mit einer festen Zeichenlänge von 100 Zeichen. Bei solchen Attributen füllt Microsoft SQL Server automatisch das Attribut mit Leerzeichen, bis der Eintrag 100 Zeichen lang ist.

SELECT page_type_desc
       allocated_page_file_id,
       allocated_page_page_id
FROM   sys.dm_db_database_page_allocations
       (
           DB_ID(),
           OBJECT_ID(N'dbo.messages', N'U'),
           0,
           NULL,
           N'DETAILED'
       );
GO

Mit dem obigen T-SQL-Code werden alle allokierten Datenseiten der neu angelegten Tabelle ausgegeben und ein Blick in die Datenseite 360 zeigt, wie die Datensätze gespeichert werden.

image

DBCC TRACEON (3604);
DBCC PAGE (0, 1, 360, 1);
GO

image

Der markierte Bereich zeigt den Hexwert 0x20, der ein Leerzeichen repräsentiert. Microsoft SQL Server füllt den verbleibenden Platz mit Leerzeichen auf, damit 100 Zeichen in das Attribut geschrieben werden. Feste Datenlängen werden sehr häufig verwendet, um “Forwarded Records” in Heaps oder “Page Splits” in gruppierten Indexen zu vermeiden, wenn die Attribute häufig mit unterschiedlichen Datenlängen aktualisiert werden müssen. Entscheidet man sich nachträglich, aus Attributen mit fester Datenlänge Attribute mit variabler Datenlänge zu machen, kann es zu Überraschungen kommen, wie das nächste Beispiel zeigt.

Los geht’s!

Das Attribut “TEXT” in der Beispieltabelle wird mit Hilfe von DDL-Befehlen vom Datentypen CHAR zu einem VARCHAR geändert.

ALTER TABLE dbo.messages
ALTER COLUMN  VARCHAR(100);
GO

Sperren während der Operation

Da es sich um DDL-Operationen handelt, muss Microsoft SQL Server sicherstellen, dass während der Aktualisierung niemand auf die Tabelle zugreift. Das wird mit Hilfe einer SCH-M-Sperre gewährleistet.

SELECT OBJECT_NAME(resource_associated_entity_id) AS object_name,
       request_mode,
       request_type,
       request_status
FROM   sys.dm_tran_locks
WHERE  request_session_id = @@SPID
       AND resource_type = N'OBJECT';
GO

image

Transaktionsprotokoll

Interessant ist ein Blick in das Transaktionsprotokoll um festzustellen, welche Schritte genau Microsoft SQL Server durchführen musste, um die Typenkonvertierung abzuschließen. Da es sich bei der Testtabelle um einen HEAP handelt, werden nur die Aktionen, die sich auf den HEAP beziehen, summiert.

SELECT Context,
       Operation,
       COUNT_BIG(*)
FROM   sys.fn_dblog(NULL, NULL)
WHERE  Context = N'LCX_HEAP'
GROUP BY
       Context,
       Operation;
GO

image

Hervorzuheben bei der Untersuchung des Transaktionsprotokolls ist die Anzahl der “LOP_INSERT_ROWS” sowie “LOP_FORMAT_PAGES”. Die Erklärung dazu folgt etwas weiter unten!

Microsoft SQL Server ändert 275.370 Datensätze in der Tabelle (LOP_MODIFY_ROW); man könnte also vermuten, dass die Leerzeichen aus dem Attribut entfernt wurden. Ein Blick auf die Datenseiten zeigt jedoch, dass die Anpassungen sich nicht auf die Leerzeichen, sondern auf die Zeilenstruktur auswirken!

Strukturanpassungen

image

Interessant ist bei der Untersuchung der Datenseite, dass sich die Größe des Datensatzes fast verdoppelt hat – insgesamt ist der Datensatz von ursprünglich 114 Bytes auf 218 Bytes gewachsen. Es ist zu erkennen, dass Microsoft SQL Server den “ursprünglichen” Wert des Attributs “TEXT”
als neuen Wert am Ende eingefügt hat!

Um zu verstehen, warum Microsoft SQL Server einen so hohen Overhead bei einer Strukturänderung benötigt, muss man die interne Struktur eines Datensatzes verstehen. Dieser Artikel bezieht sich nur auf die Besonderheiten, die für für das Verstehen der Operation relevant sind! Wer mehr Informationen über die Anatomie eines Datensatzes benötigt, dem kann ich den Artikel “Inside the Storage Engine: Anatomy of a record” von Paul Randal (SQLSkills) empfehlen!

Eine genaue Betrachtung des Recordheaders zeigt, dass mehr als nur Daten verändert wurden. Die ersten 4 Bytes einer Datenzeile beschreiben den Typen des Datensatzes sowie das Offset zum NULL-Bitmap!

image

Die Abbildung zeigt einen Datensatz VOR der Umwandlung des Datentypen und einen Datensatz NACH der Umwandlung des Datentypen. Man kann erkennen, dass das erste Byte geändert wurde (aus 0x10 wird 0x30).

0x10 = Bit 4 = Datensatz besitzt ein NULL-Bitmap

0x20 = Bit 5 = Datensatz besitzt Attribute mit variablen Datentypen

Bytes 3 und 4 bestimmen das Offset des NULL-Bitmaps, das im vorliegenden Fall identisch ist (0x6F00 = 111). Ein Blick auf die 111. Position zeigt einen weiteren Unterschied!

image

Die markierte Position (2 Bytes) zeigt die Anzahl der Spalten im Datensatz. Microsoft SQL Server hat nicht den Inhalt des bestehenden Attributs ersetzt, sondern eine weitere Spalte hinzugefügt! Ein Blick auf den Dateninhalt zeigt diese Änderung ebenfalls.

image

Microsoft SQL Server hat das ursprüngliche Attribut “TEXT” als DROPPED gekennzeichnet und den Inhalt des Datensatzes als NEUE Spalte hinzugefügt. Dabei wurde jedoch nicht der gekürzte (VARCHAR) Wert genommen sondern der vollständige ursprüngliche Wert. Durch das Wachstum der Länge des Datensatzes ist eine Kettenreaktion in Gang gesetzt worden, die die Logeinträge für LOP_FORMAT_PAGE und LOP_INSERT_ROWS erklären.

Als der Datentyp des Attributs geändert wurde, musste der Datensatz erweitert werden. Die Erweiterung führt dann dazu, dass der geänderte Datensatz nicht mehr auf die Datenseite passt und somit ein FORWARDED RECORD (die Tabelle ist ein HEAP) generiert wird.

SELECT OBJECT_NAME(object_id) AS table_name,
       index_id,
       index_type_desc
       forwarded_record_count
FROM   sys.dm_db_index_physical_stats
       (
           DB_ID(),
           OBJECT_ID(N'dbo.messages', N'U'),
           0,
           NULL,
           N'DETAILED'
);
GO

image

Die Anzahl der FORWARDED_RECORDS entspricht exakt der Anzahl der “eingefügten” Datensätze. Die Nachteile von FORWARDED RECORDS habe ich im Artikel “Forwarded Records intern” detailliert beschrieben. Das gleiche Verhalten ist auch in einem gruppierten Index zu beobachten – nur werden in einem gruppierten Index keine FORWARDED RECORDS generiert, sondern PAGE SPLITS führen zu einem fragmentierten Index.

Zusammenfassung

Microsoft SQL Server belässt die Leerzeichen in einem Attribut mit fester Zeichenlänge, wenn zu einem Datentypen mit variabler Zeichenlänge gewechselt wird. Dieser Wechselt geht IMMER einher mit FORWARDED RECORDS oder PAGE SPLITS und es ist empfehlenswert, nach der Änderung des Datentypen Indexe neu aufzubauen, die dieses Attribut verwenden.

Herzlichen Dank fürs Lesen!

Zugriffssteuerung mit Hilfe von LOGON-Triggern

$
0
0

Ein Kunde kam mit einer Anforderung auf uns zu, den Zugriff zu einen Microsoft SQL Server zu limitieren. Die Einschränkung sollte aber nicht auf einem Anmeldenamen basieren, sondern auf dem Namen bestimmter Applikationen. Die Einschränkung sieht vor, dass ausgewählte Mitarbeiter des Entwicklungsteams mit Hilfe von Microsoft SQL Server Management Studio auf den Server zugreifen dürfen, während das Business nur mit Hilfe einer selbst entwickelten Applikation auf die Daten des Microsoft SQL Servers zugreifen dürfen. Auf dem ersten Blick scheint es hierfür keine Lösung zu geben; aber seit der Version von Microsoft SQL Server 2005 (SP2) gibt es die Möglichkeit, LOGON Trigger zu verwenden.

Was sind LOGON Trigger?

Logon-Trigger lösen eine Reaktion als Antwort auf ein LOGON-Ereignis aus, wenn eine Benutzersitzung mit einer Instanz von SQL Server erstellt wird und nachdem die Authentifizierungsphase der Anmeldung abgeschlossen ist. Aus diesem Grund werden alle Meldungen, die aus dem Trigger stammen und normalerweise den Benutzer erreichen (z. B. Fehlermeldungen und Meldungen aus der PRINT-Anweisung) zum SQL Server Fehlerprotokoll umgeleitet.

Business Case

Die Anforderungen im vorliegenden Fall sind trivial: Alle Mitarbeiter dürfen mittels einer Applikation auf die Datenbanken in Microsoft SQL Server zugreifen. Für gelegentliche Tests / Fehlersuche ist es einem kleinen Kreis von Entwicklern gestattet, mit Hilfe von Microsoft SQL Server Management Studio unmittelbar auf den Datenbankserver zuzugreifen. Da die berechtigten Anwender von jedem ihnen zur Verfügung stehenden Computer auf die Datenbanken zugreifen müssen, ist eine Zugriffsbeschränkung über IP-Adressen nicht möglich. Ebenfalls wurde von Seiten des Kunden der Zugang über einen Jumphost aus internen Gründen wieder verworfen worden.

Lösung

Als Lösung kommt ein LOGON-Trigger zum Tragen, der nach der Anmeldung überprüft, mit welcher Applikation der Anwender auf den Datenbankserver zugreifen möchte. Da ein LOGON-Trigger immer erst NACH der Anmeldung ausgeführt wird, sind Sitzungsdetails bereits in der Systemview [sys].[dm_exec_sessions] gespeichert worden. Mit Hilfe dieser Informationen lässt sich auslesen, mit welchem Programm der Benutzer auf den Datenbankserver zugreift.

SELECT session_id,
       host_name,
       program_name
FROM   sys.dm_exec_sessions
WHERE  is_user_process = 1
       AND session_id = @@SPID;

Die Abfrage liefert für die aktuelle Sitzung neben der Session ID und den Hostnamen noch das Attribut [program_name]. In diesem Attribut wird in der Regel der Programmname gespeichert. Leider ist damit jedoch ein kleiner Nachteil verbunden, der weiter unten beschrieben wird.
Program Name of active session

Wenn ein Zugang mit Hilfe von Microsoft SQL Server Management Studio initiiert wird, so wird der Name des Programms in der Verbindung mit übermittelt. Somit kann identifiziert werden, wenn jemand versucht, sich mit Hilfe von SSMS auf den Server zu verbinden. Da nur einem begrenzter Kreis von Mitarbeitern diese Option zur Verfügung steht, wird zunächst eine Sicherheitsrolle [SSMSAccess] als Serverrolle eingerichtet. Alle Mitarbeiter (Domänenkonten, Domänengruppen, SQL Konten), die Mitglied dieser Gruppe oder sysadmin sind, erhalten mittels SSMS einen Zugang zum Microsoft SQL Server. Der Versuch anderer Mitarbeiter wird abgelehnt.

CREATE SERVER ROLE [SSMSAccess] AUTHORIZATION [sa];

Jeder Anwender, der einen Zugang mittels SSMS erhalten soll, muss Mitglied dieser Serverrolle sein!

Im Anschluss wird ein Servertrigger implementiert, der den Wert in program_name für die aktive session_id überprüft.

CREATE TRIGGER connection_application
ON ALL SERVER
FOR LOGON
AS
BEGIN
    IF EXISTS
    (
        SELECT * FROM sys.dm_exec_sessions
        WHERE  session_id = @@SPID
        AND PROGRAM_NAME() LIKE '%SQL Server Management Studio%'
        AND
        (
            IS_SRVROLEMEMBER(N'SSMSAccess') = 0
            AND IS_SRVROLEMEMBER(N'sysadmin') = 0
        )
    )
    BEGIN
        RAISERROR (N'You are not allowed to connect to this server with SSMS!', 11, 1) WITH NOWAIT;
        ROLLBACK;
    END
END;
GO

Will ein Benutzer mit SQL Server Management Studio auf den Server zugreifen, muss er entweder Mitglied der benutzerdefinierten Serverrolle oder aber sysadmin sein. Ansonsten wird mittels RAISERROR ein Eintrag im Fehlerprotokoll von SQL Server hinterlassen.

login_test_no_access

Der Benutzer mit dem Anmeldenamen [login_test] ist weder Mitglied der Gruppe [SSMSAccess] noch sysadmin. Aus diesem Grund schlägt die Anmeldung fehl.

Failed_Login

Hintertür?

Diese Möglichkeit der Kontrolle mag auf den ersten Blick gut sein. Dennoch gibt es eine Hintertür, mit der man dennoch in das System gelangt. Das Problem ist der “Connection String”, der von einer Applikation generiert wird, wenn eine Verbindung zum Microsoft SQL Server aufgebaut wird. Die nachfolgende Abbildung zeigt den Connection String, der von Microsoft Access generiert wird, wenn ein Zugriff auf die Datenbank erfolgt:

Access-ConnectionString

Die Option “APP” wird von Microsoft SQL Server als [program_name] in sys.dm_exec_sessions hinterlegt.

Program Name of Access Session

Dieser Teil des Connection String kann manipuliert werden! In Microsoft SQL Server Management Studio können die Elemente der Verbindungszeichenfolge explizit angegeben werden. Dadurch lässt sich der [program_name] manipulieren und ein Zugang wäre möglich.

Manipulation Verbindungszeichenfolge

Herzlichen Dank fürs Lesen!

Leere Seiten in HEAP ermitteln

$
0
0

Wer mich kennt, weiß, dass ich ein großer Fan von HEAPS bin. HEAPS zeigen gegenüber gruppierten Indexen bei bestimmten Workloads bessere Leistungsverhalten. Neben den vielen Vorteilen haben HEAPS auch Nachteile. Ein deutlicher Nachteil ist, dass HEAPS allokierte Datenseiten bei Löschoperationen nicht unmittelbar freigeben. Daraus ergibt sich – manchmal – die Erfordernis, festzustellen, wie viele Datenseiten in einem HEAP nicht gefüllt sind und somit durch einen REBUILD wieder freigegeben werden können.

Löschoperationen in HEAP

In einem vorherigen Artikel habe ich Löschoperationen detailliert beschrieben, wenn es sich dabei um einen HEAP handelt. Weitere Informationen dazu findet man im Artikel “HEAPS in Verbindung mit DELETE-Operationen” vom 12.08.2016. Ein HEAP gibt leere Datenseiten nicht frei, wenn nicht sichergestellt werden kann, dass anderen Prozessen der Zugriff auf die IAM-Seite(n) verweigert werden kann. Möchte man Datenseiten während einer Löschoperation in einem HEAP unmittelbar freigeben, muss die Löschoperation mit Hilfe eines TABLOCK-Hinweises exklusiv abgesichert sein.

Ermittlung der vorhandenen Datenseiten

Um festzustellen, welche Datenseiten von einem Objekt (Tabelle, Index) allokiert sind, gibt es seit Microsoft SQL Server 2012 die Systemfunktion [sys].[dm_db_database_page_allocations]. Wer mit einer älteren Version von Microsoft SQL Server arbeitet, kann mit Hilfe von DBCC IND einen Blick in Struktur der allokierten Datenseiten werfen. Jedoch werden mit DBCC IND keine Informationen zur Belegung der Datenseiten ausgegeben! Nachfolgend wird eine Tabelle [dbo].[messages] erstellt und mit ca. 12.000 Datensätzen gefüllt.

SELECT *
INTO  dbo.messages
FROM  sys.messages
WHERE language_id = 1033;
GO

Die Tabelle besitzt keine Indexe und ist ein einfacher HEAP.

PFS-Seite

HEAPS haben gegenüber einem gruppierten Index eine Besonderheit, die ausschließlich bei HEAPS auftritt; der Füllgrad einer Datenseite wird in der PFS-Datenseite gespeichert! Wenn neue Datensätze in einem HEAP gespeichert werden, muss in der PFS-Datenseite nach einer Datenseite gesucht werden, die ausreichend Platz für die Speicherung der Daten in der Tabelle bereitstellt. Sobald ein neuer Datensatz in die Tabelle eingetragen wird, wird im Anschluss die PFS-Seite mit einem prozentualen Wert aktualisiert, der beschreibt, zu wieviel Prozent die Datenseite gefüllt ist. Hierbei können nur 5 mögliche Prozentwerte gespeichert werden:

Prozentwert Beschreibung
NULL / 0% Die allokierte Datenseite ist leer
50% Die allokierte Datenseite ist mit 4.030 Bytes gefüllt.
80% Die allokierte Datenseite ist mit 6.424 Bytes gefüllt.
95% Die allokierte Datenseite ist mit ~7.629 Bytes gefüllt
100% Die Datenseite ist voll

Werden neue Datensätze in einem HEAP gespeichert, findet eine Aktualisierung der PFS-Seite nur dann statt, wenn der neue Datensatz einen der obigen Schwellwerte überschreitet. Speichert man in einer Tabelle z. B. Datensätze mit einer Gesamtlänge von 1.000 Bytes, so wird beim Eintragen des ersten Datensatzes die PFS-Seite mit 50% aktualisiert. Wird ein zweiter Datensatz eingetragen, ist eine weitere Aktualisierung nicht notwendig, da die Gesamtmenge immer noch unter 50% liegt. Erst beim 5. Datensatz wird erneut die PFS-Seite aktualisiert und der Füllgrad wird mit 80% angegeben.

Ermittlung des Füllgrads

Der Füllgrad ist ein wesentlicher Bestandteil einer jeden Datenseite in einem HEAP. Mit Hilfe der oben genannten Systemview kann ermittelt werden, zu wieviel Prozent eine Datenseite gefüllt ist. Nachdem die Testtabelle erstmalig befüllt wurde, ist jede Datenseite vollständig gefüllt!

SELECT allocated_page_page_id,
       page_free_space_percent
FROM   sys.dm_db_database_page_allocations
       (
         DB_ID(),
         OBJECT_ID(N'dbo.messages', N'U'),
         0,
         NULL,
         N'DETAILED'
       )
WHERE  page_level = 0
       AND is_iam_page = 0;
GO

image

Sobald Datensätze aus der Tabelle gelöscht werden, werden die Einträge in der PFS-Seite aktualisiert!

DELETE dbo.messages
WHERE message_id %2 = 0;
GO

Da die Tabelle nicht exklusiv gesperrt ist, werden allokierte Datenseiten nicht wieder freigegeben. Nach dem Löschvorgang stellt sich die Datenverteilung prozentual wie folgt dar:

image

Es ist erkennbar, dass die PFS-Seite für alle betroffenen Datenseiten des HEAP aktualisiert wurde. Mit dem nächsten Löschbefehl werden alle Datensätze aus den ersten 2 Datenseiten gelöscht:

DELETE TOP (40) 
FROM dbo.messages;
GO

image

Wenn eine Datenseite vollständig leer ist, wird NULL als Eintrag gespeichert. Die Datenseite besitzt keine weiteren Daten mehr; verbleibt aber allokiert. Mit der folgenden Abfrage kann man feststellen, wie viele Datenseiten leer sind.

SELECT page_free_space_percent,
       COUNT_BIG(*) AS num_of_pages,
       SUM
       (
         CASE WHEN page_free_space_percent IS NULL
              THEN 0
              ELSE CAST(page_free_space_percent AS INT)
         END * 8060.0 / 100
       ) AS allocated_bytes
FROM   sys.dm_db_database_page_allocations
       (
         DB_ID(),
         OBJECT_ID(N'dbo.messages', N'U'),
         0,
         NULL,
         N'DETAILED'
       )
WHERE  page_level = 0
       AND is_iam_page = 0
GROUP BY
       page_free_space_percent;
GO

image

Die Auswertung ist nicht zu 100% akkurat, da sie – auf Grund der Speicherverfahren für HEAPS – einige Schwächen zeigt:

  • Die Anzahl der Pages mit dem Eintrag NULL kann bis zu 7 weitere LEERE Datenseiten anzeigen. Das hängt damit zusammen, dass Microsoft SQL Server ab der 9 allokierten Datenseite keine MIXED EXTENTS mehr verwendet, sondern UNIFORM EXTENTS. Daraus resultiert, dass bei Allokation von mindestens einer Datenseite in einem UNIFORM EXTENT 7 weitere Datenseiten allokiert werden, die jedoch noch nicht allokiert sind.
  • Die Prozentzahlen täuschen über den wahren Füllgrad hinweg. Ein Füllgrad von z. B. 50% besagt lediglich, dass eine Datenmenge von 1 Byte bis 4.030 Bytes auf der Datenseite gespeichert wird. Um die genaue Datenmenge zu evaluieren, muss JEDE Datenseite explizt untersucht werden. Das ist für eine kleine Tabelle noch vertretbar; handelt es sich jedoch um mehrere Millionen Datenseiten, dauert die Evaluierung zu lang.

Herzlichen Dank fürs Lesen!

Viewing all 109 articles
Browse latest View live