Quantcast
Channel: ppedv Team Blog - SQL
Viewing all 110 articles
Browse latest View live

SQL Server 2016 – CTP2

$
0
0

Nach langer Zeit wieder mal eine Artikel von mir.. der mich besonders erfreut. SQL Server 2016 ist nun endlich als Download in der CTP 2 Version verfügbar. Den Link zum Download finden Sie hier: https://www.microsoft.com/en-us/evalcenter/evaluate-sql-server-2016.

SQL 2016 Logo

Natürlich erwartet man von einer neuen Version viele schöne neue Dinge.. und ich muss sagen, enttäuscht worden bin ich nicht:

Hier mal eine kleine Liste mit Neuigkeiten des Datenbankmoduls, die für mich am spannendsten sind:

  • Live Query Plan / Statistik

    Ausführungspläne live beobachten. Pro Operator Sekunden und Status in %.

  • Query Store

    Historisierung der Abfragepläne, Performance Data, Erkennung teurer bzw. auch schlechter werdenden Pläne). Nicht mehr umständlich den flüchtigen Plancache per T-SQL analysieren müssen.

  • Availability Groups

    nun auch für Standard Edition – yeah!! limitiert auf 2 Knoten

  • Polybase

    eigtl aus Parallel Data Warehouse stammend. Unstrukturierte Daten direkt aus dem Datenbankmodul abfragen

  • Stretch Database

      “Warme” Daten bleiben auf dem System, kalte können automatisch in Azure Datenbanken abgelegt werden

  • JSon Support

    keine umständlichen Programmierung mehr mit CLR, sondern nativer Support

  • Row Level Security
    Mittels einer Securitypolicy werden bei Abfragen auf Tabellen nur noch die Zeilen ausgegeben, die per Richtlinie erlaubt sind. Die Richtlinie selbst besteht aus ein Funktion, die das Filterprädikat vorgibt und einer der einer Tabelle zugewiesenen Richtlinie. Die Daten werden anschliessend nur noch maskiert ausgegen. Also keine Sicht oder ähnliches Notwendig.

  • Always Encrypted
    Nicht zu verwechseln mit TDE, aber gleicher Gedanke. Daten sind nie im Plaintext einsehbar, sondern auch bei Abfragen und im Netzwerkprotokoll verschlüsselt.

  • InMemory Verbesserungen

    Columnstore Index auf inMemory Tables—Wau!!
    benutzerdefinierte Skalarwertfunktionen. 
    ALTER TABLE  Juhuu! Ändern des Bucket count, hinzufügen, Ändern, Löschen von Spalten oder contraints
    ALTER PROCEDURE für native kompilierte Prozeduren
    Fremdschlüsselbeziehungen auf inMemory Tabellen .. endlich kann man von DB Design sprechen!

  • Columnstore Index

    Non-Clustered IX nun auch updatebar
    Auch bei einem Clustered Columnstore IX zusätzlich B-Tree Indizes möglich
    Gefilterte Indizes
    IX Reorganize supportet
    Snapshot und Read Cmommited Snapshot Isolation Support

Die CTP2 macht bereits einen sehr gute Eindruck und enthält viele Neuerungen, die einerseits längst überfällig waren, wie Rowlevel Security oder Always Encrypted (der Administrator kann die Daten nicht mehr im plain text sehen) oder auch sinnvolle Ergänzungen, die einem das Leben mit SQL Server leichter machen. Weiter so!


SQL Server 2016 Schritt für Schritt–Installation und First Look

$
0
0

SQL Server 2016.. habe ich schon erwähnt, dass ich den ziemlich cool finde?

Wollen wir mal Schritt für Schritt den neuen SQL Server unter die Lupe nehmen.

Installation

SQL Server 2016 läßt sich wie gewohnt installieren. Allerdings fallen natürlich ein paar Dinge gleich ins Auge:

Zu den Instanzfeatures zählt nun PolyBase.

clip_image001

Allerdings muss dazu auch die Java Plattform installiert werden..

clip_image002

Die Idee, bereits bei der Installation, die best practice Gedanken umzusetzen, wurde bereits sehr gut in SQL den vorherigen Versionen umgesetzt. Trennung der Daten von den Logfiles und Tempdb eigenes Laufwerk. Nun wird ebenfalls gleich empfohlen, der tempdb mehrere Datein zuzuweisen..

clip_image003

Ansonsten bleibt des Setup identisch.

Database

Auf den ersten Blick fällt dann auch gleich auf, dass Eigenschaften einer Datenbank eín wenig zugelegt haben. So läßt sich der QueryStore in den Settings der DB aktivieren.

clip_image005

Interessanterweise ist Mirroring immer noch Bestandteil des SQL Server 2016. Ein Wegfall würde mir allerdings nicht mehr allzu schwer fallen, da AlwaysOn Availability Groups nun auch in der Standard Version enthalten sind. (Begrenzt auf 2 Knoten)

clip_image007

Innerhalb der Datenbank finden sich nun die neuen Features, wie External Ressources für External Tables, sprich Polybase, Query Store, der die Top Queries bzw schlecht performanden Queries anzeigen kann.

clip_image009

image

QueryStore

Anstatt mühseelig den Plancache per TSQL abzufragen bzw. die Pläne sogar zu archivieren und zu analysieren, steht nun der QueryStore zur Verfügung.

image

Mit verschiedenen vorgefertigten Berichten, kommt man so den teuersten, oder sich verschlechternden Abfragen auf die Schliche.

image

Temporal Tables


Versionierung der Daten via Temporal Tables: Ohne großen Aufwand , den verlauf der Daten speichern.

image

Live Query Statsistics

Wunderbar zu beobachten, an welcher Stelle die Abfrage gerade arbeitet. Pro Operator die Anzeige der Dauer in Sekunden und % Abgeschlossen.

image

Super.., oder?

Nächstes Artikel:Temporal Tables.

Schritt für Schritt: SQL Server 2016 – temporal tables

$
0
0

Nein, bitte nicht verwechseln: temporal tables haben nichts zu tun mit temporary tables table variables und dergleichen. Im Gegenteil, die Daten  einer temporal table bleiben bestehen – auch nach Neustart. Also was sollte denn dann der Zweck sein? Kurz und einfach gesagt, stellen temporal tables den historischen Verlauf von Datensätze dar. In SQL auch "System versioning" genannt.

Das Prinzip dahinter ist schnell erklärt: Tabellen, für die ein Verlauf der Datensätze organisiert werden soll, benötigen zwingend 2 Spalten vom Typ datetime2, die den Gültigkeitszeitraum eines Datensatzes markieren. Zu diesen Tabellen wird außerdem eine weitere Tabellen benötigt, die über dasselbe Tabellenschema verfügt und den historischen Verlauf der Datensätze speichert.

Weder die Tabelle mit den historischen Werten, noch die Funktion, welche eine Änderung der Daten abfängt und die "alten" Datensätze in die Verlaufstabelle schreibt müssen tatsächlich selbst geschrieben bzw. angelegt werden.

Wollen wir mal einen Blick darauf werfen..

System_Versioning aktivieren

Das Aktivieren des SYSTEM_VERSIONING geschieht als Erweiterung des CREATE TABLE Statement.

Create table contacts 
(
Cid intidentityprimarykey,
Lastname varchar(50),
Firstname varchar(50),
Birthday date,
Phone varchar(50),
email varchar(50),
ValidFrom datetime2 Generated always asrow start notnull,
ValidTo datetime2 Generated always asrowendnotnull,
Period for system_time(Startdatum, Enddatum)
)

with (system_Versioning = ON (History_table=dbo.Contactshistory))
  

Essentiell sind natürlich die Zeile, die den Gültigkeitsbereich definieren. ValidFrom gibt dabei den Zeitpunkt an, seit wann dieser Datensatz in der Form existiert und ValidTo bis wann er gültig ist. Im Falle der Originaltabelle wird das datum auf das Jahr 9999 gesetzt, was für die meisten einen gewissen beruhigenden Charakter darstellt. Die Spaltennamen dürfen sind frei wählbar. Die Option GENERATED ALWAYS kann auch mit dem Merkmal HIDDEN verwendet werden. Das ist dann erforderlich, wenn Sie die beiden Spalten für den Gültigkeitsbereich standardmäßig immer versteckt halten wollen. Zumindest thereotisch.. in der aktuellen CTP2 wurde das wohln noch nicht implementiert und quittiert daher mit einem Syntaxfehler.

ValidFrom datetime2 GENERATED ALWAYS ASROW START HIDDEN notnull,
ValidTo datetime2 GENERATED ALWAYS ASROWEND HIDDEN notnull,


PERIOD gibt die zu verwendenden datetime2 Spalten für den Gültigkeitsbereich an

Period for system_time(ValidFrom, ValidTo)

..und folgende Zeile definiert die Versionstabelle. Diese kann bereits vorher erstellt worden sein. Falls nicht wird Sie in dem moment erstellt. Gibt man gar keine Versionstabelle so weist der Name folgendes Muster aus: MSSQL_TemporalHistoryFor_<objectid>

with (system_Versioning = ON (History_table=dbo.Contactshistory))

Und so sieht das Ergebnis im SSMS aus:


Natürlich kann auch nachträglich eine Tabelle als SYSTEM_VERSIONING initialisiert werden.


Aktivierung des SYSTEM_VERSIONING bei bestehenden Tabellen

Die Initialisierung von temporal tables bei bestehenden Tabellen vollzieht sich ebenfalls rel. Einfach und ist mit obiger synteax auch recht gut naschvollziehbar:

Fall 1: Eine Tabelle hat bereits, warum auch immer, zwei Spalten die für den Gültigkeitsbereich stehen:

CREATE
TABLE Demo2

(

SP1 intidentityprimarykey,
SP2 int,
StartFrom datetime2notnull, EndTo datetime2notnull

);

--Aktivierung der PERIOD

ALTER
TABLE demo1
ADD PERIOD FOR SYSTEM_TIME(StartFrom,EndTo)

--Aktivierung des SYSTEM_VERSIONING

ALTER
TABLE demo1
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.demohistory, DATA_CONSISTENCY_CHECK = ON))
 

Fall 2: Die Spalten für das SYSTEM_VERSIONING sind noch nicht vorhanden..

--Alternativ, wenn die Start und End-Zeitstempelspalten noch nicht vorhanden sind

CREATE
TABLE Demo3
(
SP1 intidentityprimarykey, SP2 int
)
 

ALTER
TABLE demo3
ADD PERIOD FOR SYSTEM_TIME (StartFrom, EndTo),
StartFrom datetime2 GENERATED ALWAYS ASROW START NOTNULLDEFAULTGETUTCDATE(),
EndTo datetime2 GENERATED ALWAYS ASROWENDNOTNULLDEFAULTCONVERT(DATETIME2,'9999.12.31');
     

ALTER
TABLE demo3
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.demo3history, DATA_CONSISTENCY_CHECK = ON))
 

Arbeiten mit temporal tables

Nun wollen wir mal mit dem SYSTEM_VERSIONING arbeiten. Zuerst ein paar Datensätze zum Spielen

insert
into Contacts
(Lastname,Firstname,Birthday, Phone, email)

select'Kent', 'Clark','3.4.2010', '089-3303003', 'clarkk@krypton.universe'

insert
into Contacts
(Lastname,Firstname,Birthday, Phone, email)

select'Wayne', 'Bruce','3.4.2012', '08677-3303003', 'brucew@gotham.city'

Und nun die Änderungen, die zu einer Versionierung der Datensätze führt

update contacts set email ='wer@earth.de'where cid = 1
update contacts set Phone ='w3434'where cid = 1
update contacts set Lastname ='Wayne'where cid = 1

--etwas später

update contacts set email ='asas@earth.de'where cid = 1
update contacts set Phone ='w34sasaa34'where cid = 2
update contacts set Lastname ='Smith'where cid = 1

Das Ergebnis:

select*from contacts
s
elect*from ContactsHistory


In der Originaltabelle findet man logischer Weise den aktuell gültigen Datensatz wieder. Die Zeitstempelspalten geben den Bereich an, seit wann der Datensatz gütlig ist. In den Verlaufstabellen dagegen, befindet jeder geänderte Datensatz – ebenfalls mit den Zeitstempelspalten für seine Lebensdauer.

Versionen finden

Das Praktische wollen wir natürlich nicht außer Acht lassen: Die Suche nach einer bestimmten Version.

Entweder man frägt die temporal table direkt ab:

--Abfrage auf Versiossverlauf

select
*
from contactshistory

where

    Startdatum >='2015-06-28 09:17:04'

    and

    Enddatum <='2015-06-28 09:17:50'


Das ist natürlich dann empfehlenswert, wenn man den Verlauf eines Datensatzes sehen möchte.

Sofern nur die letzte gütlige Version zu einem Zeitpunkt gesucht wird, kann man das auch direkt von der Originaltabelle erfahren.

--Coole Abfrage: direkt auf Originatabelle

select
*
from contacts

    FOR SYSTEM_TIME ASOF'2015-06-27 15:49:11.1448833'
    where cid =1


Nachträgliches Ändern von SYSTEM_VERSIONING Tabellen

Haben wir was vergessen? Ach ja: kann man versionierte Tabellen auch nachträglich ändern? Klar! System_Versioning ausschalten, Änderungen auf beiden Tabellen ausfürhren, SYSTEM_VERSIONING wieder einschalten. Guggste..

--SYSTEM_VERSIONING deaktiveren

Alter
Table contacts set (system_versioning= off)

--Beliebige Spalten auf beiden Seiten hinzufügen

Alter
Table contacts add Fax varchar(50)

Alter
Table contactshistory add Fax varchar(50)

--und wieder einschalten

ALTER TABLE contacts

SET (system_versioning=on (History_table=dbo.Contactshistory))

Deaktivieren des SYSTEM_VERSIONING bzw Löschen der Verlaufstabelle


Und was, wenn ich die Versionierung nicht mehr haben will. Dann SYSTEM_VERSIONING ausschalten und Tabellen jeweils löschen.

alter
table Contacts Set (system_versioning=OFF)

GO

drop
table contacts

drop
table Contactshistory

Randnotizen

Abgesehen Davon, dass ich teporal tables extrem einfach in der Anwendung finde, bleiben noch ein paar Details zu erwähnen übrig.

  • Temporal tables sind per default zeilenkomprimiert
  • Die Originaltabllen müssen einen Primary key haben
  • Löscht man den Clustered IX der temporal Tabelle, dann kann man auch Clustered Columnstore IX verwenden –absolutly perfect! 12 Punkte!!
  • Geht nicht bei Filetables
  • Kein truncate table bei SYSTEM_VERSIONING = ON
  • Kein instead of Trigger
  • Support für AlwaysOn

Coole Sache..

Demnächst mehr mit Schritt für Schritt: SQL 2016

SQL Geography Datentyp Entfernung kalkulieren

$
0
0

Stolpere grad über ein Problem mit net und der Spatial Datentypen. Damit kann an Orte definieren und deren Entfernung berechnen. Im Microsoft SQL Server wird der Breitengrad und Längengrad, also Latitude und Longitude gespeichert. Gleiches erhält man bei Nutzung von Bing oder Google Maps als Koordinaten.

image

Nun zwei Varianten der Berechnung der Entfernung per SQL Statement. Hier wird die Distanz zwischen zwei Punkten über eine Kugel gerechnet. Die Funktion STLength liefert die direkte Verbindung und damit einen kleineren Wert.

SQL  Point ( Lat, Long, SRID )

   1:  DECLARE @g geography;
   2:  DECLARE @h geography;
   3:   
   4:  SET @g = geography::Point(48.206501 ,16.378343, 4326);
   5:  SET @h = geography::Point(48.16988, 12.830674, 4326);
   6:  SELECT @h.STDistance(@g);

Ergebnis 263789,75902209

Für Entity Framework oder auch SQLCommand muss man einen String umwandeln um dann per DbGeography.FromText die Koordinaten zu übergeben. Im Kern wird dazu dieses TSQL Statement genutzt.

   1:  DECLARE @g geography;
   2:  DECLARE @h geography;
   3:  SET @g = geography::STGeomFromText('POINT(48.206501 16.378343)', 4326);
   4:  SET @h = geography::STGeomFromText('POINT(48.16988 12.830674)', 4326);
   5:  SELECT @h.STDistance(@g);
Ergebnis 392552,427707221

Das erste Ergebnis ist korrekt und beim zweiten offensichtlich Längengrad an erster Stelle zu nehmen. Die Reihenfolge ist also Längengrad und Breitengrad als vertauscht.

Schritt für Schritt: SQL 2016 - Dynamic Data Masking

$
0
0

clip_image002

Es weihnachtet! Gerade bekam ich von einer Kollegin Plätzchen angeboten mit der Größe eines Diskus und ca 2,5 Milliarden Kalorien. Ich sehe schon das Hüftgold anrollen, oder wie die Franzosen es liebevoll nennen: „poignées d'amour“. Der Brite übrigens love handles. Was mich zu dem Schluss bringt, dass wir in good old germany scheinbar ein Problem mit unseren Körperzonen haben.

clip_image004

Solche Problemzonen gibt’s aber nicht nur zu den Feiertagen und auch nicht nur um die Hüfte. Im konkreten Fall spreche ich vom Hüftspeck Marke „geheime Daten“.

Bisher war es gelinde gesagt, etwas umständlich Leseberechtigten den Zugriff auf bestimmte Spalten zu nehmen oder die Daten zu verschlüsseln. Mit SQL Server 2016 (CTP 3.1) wird die Geschichte schon deutlich einfacher.

Wir wäre es denn, wenn wir ohne zusätzlichen Code, die Daten – je nach Berechtigung – verschlüsselt oder unverschlüsselt sehen könnten? Das hört sich für mich nach einem ziemlich coolen Securtiyfeature an. Das sogenannte Dynamic Data Masking – kurz DDM.

Maskierfunktionen

SQL Server 2016 stellt für DDM folgende Funktionen zur Verfügung:

  • · Default():
    Gibt bei Textdatentypen entweder XXXX aus oder weniger, falls das Feld eine geringer Größe aufweist
    Bei numerischen Daten wird eine 0 ausgegeben.
    Bei Datumsfeldern das entgegen der Doku der 1.1.1900
  • · Email():
    maskiert Emailadressen in folgender Form: mXXXX@XXXX.com. Lediglich der erste Buchstabe der Emailadresse wird korrekt ausgegeben, der Rest wird „ausgext“ und mit der Domäne .com versehen
  • · Random():
    Für numerische Datentypen eine Zufallsmaskierung, bei der Zufallszahlen innerhalb des Angegeben Start- und Endwertes zurückgegeben werden
  • · Custom():
    Hier können sie ein Prä- und Suffix angeben und in welcher Form der Teil dazwischen dargstellt werden soll. Wie etwa partial(2,“XXX“,3) die ersten 2 und die letzten 3 Zeichen korrekt ausgibt. Der Teil dazwischen mit dem gewünschten string maskiert wird.

Wollten wir nicht die Problemzonen entfernen? Nun denn, wie geht’s?

Anlegen der Demotabelle

 

Create database DataMasking;
GO

use DataMasking;
GO

CREATE TABLE Angestellte
(AngID int IDENTITY PRIMARY KEY,
Vorname varchar(100) MASKED WITH (FUNCTION = 'partial(1,"XXXXXXX",0)') NULL,
Nachname varchar(100) NOT NULL,
tel varchar(12) MASKED WITH (FUNCTION = 'default()') NULL,
Email varchar(100) MASKED WITH (FUNCTION = 'email()') NULL,
Gebdatum date MASKED WITH (FUNCTION='default()') NULL,
Kennwort varchar(100) MASKED WITH (FUNCTION='partial(5,"XXXXXXX",0)') NULL
);
GO

Arbeiten mit Dynamic Data Mask

Ich sehe die Speckröllchen schon schwinden. Sie auch? In der Tabelle wurden bereits die Maskierungen angegeben. Natürlich kann jederzeit eine maskierte/unmaskierte Spalte hinzugefügt werden.

ALTER TABLE Angestellte
ADD HireDate date MASKED WITH(FUNCTION='default()') NULL;

Oder natürlich auch eine bestehende geändert werden:

ALTER TABLE Angestellte
ADD Gehalt money MASKED WITH(FUNCTION='random(1,12)') NULL;

ALTER TABLE Angestellte
ALTER COLUMN Gehalt MASKED WITH(FUNCTION=’default()') NULL;

..und wenn man möchte bekommt man sie auch wieder los

ALTER TABLE Angestellte
ALTER COLUMN DROP MASKED;

Ergebnisse. Wir wollen Ergebnisse sehen! OK .. guggst du..:

Fügen wir zunächst mal ein paar Daten ein:

INSERT Angestellte (Vorname, Nachname, Tel, Email, GebDatum, Kennwort )
VALUES
('Robert', 'Lindner', '08676-9876', 'robert@mi6.com','5.1.2012','145@qwe'),
('Hans', 'Blatter', '089-984576', 'hans@blatter.de', '6.6.2014','sdfsf324543'),
('Sepp', 'Beckenbauer', '08677-455646', 'derKaiser@fbc.de','7.7.2000','flkjfd@lkdfdl');

Kurze Abfrage:

clip_image006

Haben sie etwas Anderes erwartet? Wir haben das Recht die Daten zu lesen. Für Benutzer mit reinen Leserechten sieht das schon anders aus:

Legen wir einen Benutzer MrX an und statten ihn mit Leserechte aus:

CREATE USER MrX WITHOUT LOGIN;
GRANT SELECT ON Angestellte TO MrX;
EXECUTE AS USER = 'MrX';

SELECT * FROM Angestellte;
REVERT;

clip_image008

Hier kommt der Aha-Effekt. Keine Codeänderung, nur von den Rechten des Benutzers abhängig! Super Sache. Speckröllchen ohne Diät weg. Da schmeckt mir die Schweinshaxe wieder..

Wer Recht hat, hat Recht

Natürlich können wir Benutzern das leserechte auf maskierte Daten geben.. und wieder nehmen:

--Rechte geben..

GRANT UNMASK TO MrX;
EXECUTE AS USER = 'MrX';
SELECT * FROM Angestellte;
REVERT;

--und Rechte wieder nehmen

REVOKE UNMASK TO Mrx;

Und welches recht benötigt man, um GRANT UNMASK bzw REVOKE UMASK vergeben zu können?

Die notwendige Rechte dazu sind: ALTER ANY MASK und ALTERTABLE.

Plan B

Ich nehme an, dass den meisten TSQL Schreiberlingen auch die Problemzone der Funktionen bekannt sind. Nachdem eine Funktion zuständig ist, um die Daten zu maskieren, sollte doch bei Abfragen eine Suche per Index zu einem Scan führen. Das passiert allerdings tatsächlich nicht. Die Daten indiziert werden und gesucht werden. Lediglich die Ausgabe wird verschlüsselt.

Bei Tests auf ca 20 Mio Zeilen ergaben sich keine nennenswerte Unterschiede. Für testzwecke habe ich eine Datumsspalte einmal maskiert und einmal unmaskiert in der Tabelle. Auf beide Spalten wurde die gleichen Indizes angelegt. Non Clustered mit Included Columns, so dass ein Covered Index entstehen müßte.

Das Ergebnis:

In beiden Fällen, ob Benutzer mit Leserecht oder auch ohne bekommen wir einen IX Seek.

clip_image010

Die Statistikmessungen kommen zum selben Ergebnis:

clip_image012

Auch im Falle einer größeren Menge (1 MIo Ergebniszeilen) ändert daran nichts. Auch keine höhere CPU Last.

Cool… noch L

Was geht ab, Mann

Oder was geht eben nicht ab.. Mit folgenden Limits müssen wir aktuell in der CTP 3.1 leben..

Kein Support von DDM für

  • · Encrypted columns (Always Encrypted)
  • · FILESTREAM
  • · COLUMN_SET

Ebenfalls sinnlos ist für Mrx die Daten zu kopieren. Mittels eines

SELECT * into Angestellte2 FROM Angestellte;

bekommt man lediglich maskierte Daten in die Zieltabellen. Die Maskierfunktionen werden nicht übernommen.

Ein Update wird MrX zweifelsohne gelingen, aber die Daten wird er wegen der Maskierfunktion nicht lesen können.

Der „Bug“ aus früheren CTPs, dass ein Konvertieren in einen von DDM nicht unterstützten Datentyp, die Daten im Klartext anzeigt, war in der CTP 3.1 bisher nicht mehr nachzuvollziehen. Vor allem, da jetzt alle wichtigen Datentypen supported sind.

Ohje - Weiter Problemzonen

Nun schien doch alles in Ordnung zu sein? Doch genau dort wo wir hurra auf 10 Kilo weniger geschrieben haben, enau dort liegt auch der Hund begraben. (Wieso begräbt eigentlich immer den Hund?)

Schauen wir uns das mal an…:

EXECUTE AS USER = 'MrX';
select * from angestellte where Gehalt between 30001 and 50001
select * from angestellte where gehalt = 50000
REVERT

select * from angestellte where Gehalt between 30001 and 50001
REVOKE UNMASK TO Mrx

clip_image014

Achnee.., oder? Doch! Wer fragt bekommt Antwort. Logischerweise muss es möglich sein die Daten abfragen und korrekt Ergebnisse zu bekommen. Tja das Resultat heißt hier im billigsten Fall: Mit Ausdauer und Fleiß komm ich auch an die Gehälter…

 

Fazit: Tolles Feature, das mit Betracht eingesetzt werden sollte.  So sind erratbare bzw schätzbare Felder rel. leicht herauszubekommen. Dagegen ist das Unkenntlichmachen von Daten wie Email , Kreditkartennummern extrem easy geworden. Also mal schauen, was da noch so kommt in der Finalversion..

 

Viel Spaß damit!

Lambda Expressions und Linq

$
0
0

In einem letzten Artikel (Lambda Expressions) habe ich mich mit dem Zusammenhang zwischen Delegates und Lambda Expressions beschäftigt.
Das dort erlernte Wissen möchte ich heute mit einem Praxisbeispiel bei der Verwendung von Linq anwenden.

Zuerst mal: Was ist Linq?

Stark vereinfacht ausgedrückt ist Linq eine Klassenbibliothek, die uns Erweiterungs Methoden für das Interface IEnumerable und somit für alle Klassen, welche das Interface, implementieren (List<T>, Array, ObservableCollection<T>, usw…) bereitstellt. Damit werden Abfragen auf Collections in C# sehr einfach.
Linq steht für Language integrated query.

Es gibt vier verschiedene Linq Bibliotheken:
- Linq to Object (Arrays und Collections)
- Linq to DataSet (Abfragen auf DataTables in DataSets)
- Linq to XML (Abfragen auf XML Dokumente)
- Linq to SQL (Abfragen auf SQL-Server)

Der große Mehrwert von Linq liegt darn, dass man nicht zwischen den Bibliotheken unterscheiden muss und die Syntax bei allen vieren identisch ist.

Auch MSDN liefert viele Informationen zu Linq.

Was hat Linq jetzt mit Lambda Expressions zu tun?

Betrachten wir zuerst mal die Syntax einer der wichtigsten Methoden von Linq:

publicstaticIEnumerable<TSource> Where<TSource>(thisIEnumerable<TSource> source,Func<TSource, bool> predicate
)

Mit dem Wissen meiner bereits oben genannten Blogeinträge (Lambda Expressions und Erweiterungs Methoden) können wir diese Methode jetzt sehr leicht verstehen.

Erster Parameter:thisIEnumerable<TSource> source
Wir sehen anhand des Schlüsselwort “this”, dass es sich um eine Erweiterungsmethode handelt, welche sich auf den Typ IEnumerable<T> bezieht.

Zweiter Parameter: Func<TSource, bool> predicate
Der zweite Parameter verrät uns, dass es sich um ein delegate handelt. Passen würde jede Methode, welche ein Objekt vom Typ TSourceübernimmt und einen booleschen Wert zurückliefert.
TSource ist natürlich genau der Typ, von dem auch die abzufragende Collection ist.

string[] namen = newstring[] { "Sepp", "Klaus", "Karina", "Magdalena", "Judith" };IEnumerable<string> result = namen.Where(name => name.StartsWith("K"));

Wie man gut erkennen kann, kann hier für den Delegate-Parameter eine Lambda Expression übergeben werden.

Beispiele für weitere Linq-Abfragen: 101 Linq Samples

Power BI Reports in SQL Server Reporting Services On-Premises

$
0
0

Im Oktober auf dem PASS Summit 2016 hat Microsoft die Technical Preview von Power BI Reports in SQL Server Reporting Services (SSRS) On-Premises vorgestellt und veröffentlicht. Somit schafft Microsoft einen großen Mehrwert für viele Kunden, welche nicht in die Cloud gehen “können”. Eine neue Vorlage, mit dem Stand Januar 2017- “Vorschau der Power BI Report-Funktionalität in SQL Server Reporting Services” ist nun verfügbar. In diesem Beitrag stelle ich Ihnen diesen Stand aus dem Azure-Marketplace vor. Ein weiterer Artikel wird folgenden, indem auf Basis vom SQL Server 2016 SP1 das gleiche Preview vorgestellt wird.

Die folgende Umgebung ist nur für die Entwicklung und das Testen lizenziert, nicht für die Produktion. Es handelt sich um eine vorkonfigurierte Umgebung, die alle SQL Server-Komponenten (Berichtsdienste, Datenbankmodule, Analysis Services), Client-Tools (Power BI Desktop, Mobile Report Publisher, Berichts-Generator, SQL Server-Datentools) und Demoinhalte enthält.

Filter nach “Power BI”:

image

Auswahl und bestätigen des Technical Preview:

image

Basis Konfiguration:

image

Anmerkung: Es wird eine Tabular oder Multidimensional SSAS Instance unterstützt.

Konfiguration Storage:

image

Anmerkung: Habe die vorgeschlagene VM nicht geändert, jedoch bei Storage unter “Performance” von Premium auf Standard umgestellt. Da sonst auch bei ausgeschalteter VM, kosten für den Premium-Storage anfallen.

Zusammenfassung bestätigen:

image

Nutzungsregeln bestätigen:

image

 

Nach der Bereitstellung und dem Connect, können Sie über das Icon “SSRS Preview” sofort starten:

image 

image

 

Drei Power-BI Reports stehen zum testen zur Verfügung:

image

image

image

image

Weitere Blogartikel zu den technischen Details werden folgen.

Viel Erfolg und Spaß beim Testen!

SQL – Datum, Format() und Convert()

$
0
0


Womit kann man jeden Programmierer zum Weinen bringen? Richtig: wenn es ums Datum geht. In SQL ist das aber gar nicht so dramatisch und sicher nichts, wovor man Angst haben muss. Wir sehen uns in diesem Beitrag an, wie man in SQL mit Datumsfunktionen arbeiten, das Datum im gewünschten Format ausgeben und welche Datentypen man dafür verwenden kann.

 

Die gebräuchlichsten Datumsfunktionen in SQL




Mit diesen Funktionen kommt man schon ziemlich weit. Wer sich damit noch nicht zufrieden geben will: Eine Komplettübersicht über alle Datumsfunktionen findet Ihr in der Microsoft-Dokumentation.


SYSDATETIME() und GETDATE() – aktuelles Datum und Uhrzeit

Mit SYSDATETIME()und GETDATE() könnt Ihr das aktuelle Datum abfragen; dabei ist GETDATE() auf mehrere Millisekunden und SYSDATETIME() auf Nanosekunden genau. Ein weiterer nicht unerheblicher Unterschied ist der verbrauchte Speicherplatz; SYSDATETIME() verbraucht dabei 6-8 Bytes (entsprechend weniger, wenn die Millisekunden oder gar die Zeit gar nicht benötigt wird), GETDATE() braucht 8 Bytes.

Wer jetzt meint, Speicherplatz ist billig und auf das bisschen kommt es wohl auch nicht an: die Menge machts. Bei potenziell Millionen von Einträgen kann die konsequente Verwendung von optimalen Datentypen sehr wohl einen Unterschied machen. Aber das ist ein anderes Thema.

  

DAY(), MONTH(), YEAR() und DATEPART() – Datumsanteile abfragen

Mit DAY(),MONTH() oder YEAR()kann man sich den entsprechenden Datumsanteil als Integer ausgeben lassen; ebenso mit DATEPART(). Bei letzterem muss allerdings noch spezifiziert werden, welchen Datumsanteil man denn gerne hätte, und abhängig vom Datentyp des verwendeten Datums lassen sich bis zu Nanosekunden ausgeben.




DATEPART(day,date),DATEPART(month,date) oderDATEPART(year,date) führen zum exakt gleichen Ergebnis wie DAY(date),MONTH(date) oder YEAR(date).

 

DATENAME() – Wochentag oder Monatsname

Mit DATENAME()kann man sich den Namen eines Wochentages oder den Monatsnamen ausgeben lassen. Für andere Ausgaben macht DATENAME() nicht viel Sinn, da wir für alle anderen wieder Zahlen herausbekommen.



DATEADD() – Daten addieren

Mit DATEADD()kann man eine Zeitspanne zum angegebenen Datum hinzuaddieren. Wird eine negative Zahl eingegeben, wird diese Zeitspanne vom angegebenen Datum abgezogen.




DATEDIFF() – Differenz zwischen zwei Daten

Mit DATEDIFF()kann man eine Datumsdifferenz zwischen zwei Daten ermitteln. Abhängig von der Abfrage bekommt man einen Integer-Wert zurück, der für die Differenz in der angegebenen Größenordnung (Jahre, Tage, …) steht. Liegt das Enddatum vor dem Startdatum, ist der Ausgabewert negativ.





Datepart Parameter – was wollen wir abfragen?

 

Damit DATEPART(),DATEADD() und DATEDIFF()funktionieren, müssen wir genau angeben, welchen Datumsteil (datepart) wir abfragen wollen.

Die folgende Tabelle zeigt die wichtigsten Intervalle, die für die Datumsabfragen verwendet werden können.



Leider gibt es dazu ein paar Abweichungen, wenn wir das Datum individueller formatieren wollen; dazu kommen wir noch im Abschnitt „Format“.

 

Die vollständige Liste der datepart- Angaben findet Ihr in derMicrosoft-Dokumentation.


FORMAT() – wie soll etwas ausgegeben werden?

 

Das Problem mit dem Datum, das die bereits erwähnten Verzweiflungsausbrüche bei Programmierern auslöst, sind die vielen unterschiedlichen Formate, in denen das Datum in unterschiedlichen Regionen der Welt dargestellt wird.

Da gibt es YYYY-DD-MM, YYYY-MM-DD, YYYY/MM/DD, DD.MM.YYYY und noch eine ganze Reihe anderer Varianten. Meistens wird die Formatierung des Datums, das aus der Datenbank kommt, clientseitig oder in einem Zwischenschritt, beispielsweise mit C# erledigt. Wenn es aber unbedingt serverseitig passieren soll, haben wir mehrere Möglichkeiten.

 

Die SQL Server Funktion FORMAT() erlaubt uns, sehr individuelle Formatierungen durchzuführen. FORMAT() funktioniert unter anderem mit den Datentypen int,float, money– und eben auch mit Datumsformaten (date, time, datetime, datetime2). Der Datentyp des zurückgegebenen Wertes bei FORMAT() ist nvarchar (oder NULL).


Für einen vollständigen Überblick über alle akzeptierten Datentypen könnt Ihr einen Blick in die Microsoft-Dokumentation werfen.

Die einfachste Variante sieht so aus:



FORMAT() und Datum

Wir geben einen Wert ein, der in ein bestimmtes Format gebracht werden soll, gefolgt vom gewünschten Format. Das geht auch mit Datum…




…allerdings müssen wir das Datum irgendwo auslesen, entweder aus einer Datumsfunktion oder aus einer Tabellenspalte, die ein Datum enthält; die Angabe eines einzelnen konkreten Datums würde hier nicht funktionieren, da dies als Text (varchar) interpretiert wird.

In der Praxis arbeiten wir aber normalerweise ohnehin mit Daten, die aus einer Tabelle oder einer Datumsfunktion kommen, und nicht mit einzelnen, händisch eingegebenen Daten.





FORMAT() und Variablen

Für die Anwendung  von FORMAT()ist auch die Verwendung von Variablen hervorragend geeignet:




Über eine Variable könnte theoretisch auch wieder ein konkretes Datum angegeben werden, da ja hier auch ein Datentyp definiert wird (nicht empfehlenswert):



FORMAT() und der culture-Parameter

Optional kann noch ein weiterer Parameter (culture) eingegeben werden, mit dem sich eine regionsbezogene Formatierung erreichen lässt:




Das kleine 'd' im format-Parameter bedeutet, dass das Datum in Zahlen ausgegeben wird; bei großem 'D' würde der Monat und (je nach regionaler Konvention) auch der Wochentag ausgeschrieben werden, z.B.:



Eine vollständige Übersicht über unterstützte culture-Codes gibt es in der Microsoft-Dokumentation. (Diese Übersicht ist derzeit nur auf Englisch verfügbar, aber die Tabelle ist selbsterklärend.)


Da in vielen Regionen die gleichen Formate für die Datumsanzeige verwendet werden, macht es keinen Sinn, sämtliche verfügbaren culture-Codes anzugeben. Im Gegenteil, das wäre eine massive unnötige Belastung für die Abfrage.




Wer es noch genauer wissen möchte: Die Microsoft-Dokumentation zum Thema FORMAT() findet Ihr hier.



CONVERT() und CAST() – umwandeln von Datentypen

 

Mit CONVERT() und CAST() ist es möglich, einen Datentyp in einen anderen zu verwandeln.

Wir sehen uns hier allerdings nur die Fälle an, die für unser Thema „Datum“ relevant sind.

Mit beiden Anweisungen lässt sich ein Datentyp in einen anderen konvertieren; allerdings ist die Syntax bei CONVERT() und CAST() ein wenig anders und CONVERT() bietet noch die zusätzliche Option von sogenannten Styles.

 

CAST() und Datum

Wollen wir mittels CAST() ein Datum als Text (also etwa als varchar oder nvarchar) darstellen (oder umgekehrt), sieht das so aus:


Es ist möglich, mittels CAST() einen Datumswert als Text darzustellen oder umgekehrt. Mit CAST() allein haben wir allerdings keinen Einfluss darauf, wie unser Datum nun dargestellt wird. Geben wir eine Länge (length) an, beispielsweise varchar(20), müssen wir außerdem noch darauf achten, dass sich das, was wir darstellen wollen, in dieser Anzahl an Zeichen überhaupt ausgeht.

Der umgekehrte Weg, Text in ein Datum umzuwandeln, wäre nicht empfehlenswert (obwohl es theoretisch möglich ist), denn hier haben wir das Problem, dass wir nicht sicher sein können, welcher Wert als Tag oder Monat interpretiert wird. Glücklicherweise kommt dieser Fall in der Praxis so gut wie nicht vor, da wir ja normalerweise Werte aus einer Tabelle auslesen und nicht einzelne Daten händisch eingeben.

 

CONVERT() und Datum

Auch CONVERT() konvertiert einen Datentyp in einen anderen, wie der Name schon sagt, allerdings haben wir hier noch die Möglichkeit, einen sogenannten Style anzugeben, der zugleich auch für ein bestimmtes Format sorgt.




CONVERT(), Datum und der Style-Parameter

Den Style geben wir über eine Zahl ein. Liegt diese Zahl über 100, wird das Jahrhundert mit angegeben (yyyy), sonst nur die Jahreszahl (yy). Damit das nicht zu einfach wird, gibt es natürlich auch hier ein paar Ausnahmen.

In der folgenden Tabelle seht Ihr einen Überblick über einige gebräuchliche Standards und die dazugehörigen Style-Angaben.



Einen vollständigen Überblick über alle Standards findet Ihr wieder in der Microsoft-Dokumentation.


Mit all diesem Wissen ausgerüstet sollte es Euch jetzt leichter fallen, Eure Datumsangaben in das von Euch gewünschte Format zu bringen.

Viel Spaß beim Ausprobieren, und vielleicht sehen wir uns ja in einem unserer Kurse zum Thema SQL!


Was ist eigentlich... MAXDOP?

$
0
0

Die gute oder die schlechte Nachricht zuerst? Ok, die gute: Das Grundprinzip von MAXDOP ist recht schnell erklärt. Die schlechte? Ach ja: Es gibt keine Einheitsformel, keine immer richtige Lösung. Die „richtige Lösung“ müssen wir durch Testen von Fall zu Fall finden. Aber erstmal langsam von Anfang an.

 

Das Grundprinzip von MAXDOP


MAXDOP steht für Maximal Degree of Parallelism und bedeutet nichts anderes, als dass mehrere CPUs parallel, also mehr oder weniger gleichzeitig, an einer Abfrage arbeiten dürfen. Mehr oder weniger ist ein wichtiger Punkt – die Aufgabenverteilung erfolgt nämlich nicht gleichmäßig.

Warum sollten überhaupt mehrere CPUs eine Abfrage bearbeiten? Ist das gut oder schlecht? Zur Veranschaulichung verwenden wir die Jelly Bean-Analogie.

Die Jelly Bean-Analogie

Stell dir vor, du müsstest eine Handvoll Jelly Beans zählen.


1,2,3,…9… schaffst du locker. Aber wie sieht’s damit aus:


Ein ganzer Haufen von Jelly Beans… Klar, schaffst du auch – aber es wird eine ganze Weile dauern. Geht doch viel schneller, wenn wir die Arbeit aufteilen – hol dir ein paar Freunde, und jeder bekommt ein paar Jelly Beans, die er/sie zählen soll.


Wahrscheinlich geht das schneller – aber jetzt haben wir einen zusätzlichen Arbeitsvorgang: Jeder muss sagen, wie viele Jelly Beans er/sie hat und wir müssen die einzelnen Summen zu einer Gesamtsumme addieren. Dieser zusätzliche Arbeitsschritt kostet wieder ein bisschen Zeit.

Außerdem haben nicht alle gleich viele Jelly Beans. (Wenn wir das schon vorab wüssten, bräuchten wir sie ja nicht mehr zu zählen.) Das heißt auch, dass einer schneller mit dem Zählen fertig ist und einer langsamer – aber um zu einer Endsumme zu kommen, müssen wir auf den Langsamsten warten.

 

Jelly Beans und SQL-Abfragen

…und was genau hat das jetzt mit MAXDOP zu tun?

Abhängig davon, wieviel Information bei einer Abfrage verarbeitet werden muss, kann es Sinn machen, die Arbeit auf mehrere CPUs aufzuteilen. Dabei heißt „mehr“ leider nicht immer auch gleich „besser“.

Wenn uns 1000 Freunde helfen, die Jelly Beans zu zählen, dann dauert es auch recht lange, bis wir alle Einzelsummen eingesammelt haben, um sie addieren zu können.


Mit den CPUs und den Abfragen ist es ähnlich: Die einzelnen Ergebnisse wieder miteinander zu verknüpfen braucht Zeit – und auch CPUs müssen auf den Langsamsten warten (also den, der die größte Datenmenge bewältigen muss).

 

 

MAXDOP im SSMS

Ok, wo also finde ich dieses mysteriöse MAXDOP-Setting?


Im Microsoft SQL Server Management Studio (SSMS) klicken wir im Object Explorer mit Rechtsklick auf unsere Datenbank. Dann öffnet sich ein Fenster mit Optionen:


Mit Klick auf den letzten Menüpunkt, Properties:


Wir wählen Advanced aus und scrollen zu „Parallelism“ hinunter:


Unter dem Reiter „Parallelism“ finden wir den Eintrag „Max Degree of Parallelism“… oder MAXDOP. Treffer!

 

MAXDOP Settings

Das Default-Setting für MAXDOP ist 0. Das ist etwas missverständlich, denn es heißt NICHT, wie man vielleicht vermuten würde, dass Null, also kein Parallelismus erlaubt ist, sondern es heißt vielmehr, dass alle verfügbaren CPUs verwendet werden dürfen!

Will man keinen Parallelismus zulassen, müsste man hier 1 einstellen, d.h. es darf nur ein CPU verwendet werden. In manchen Fällen kann das Sinn machen.

Für eine sinnvolle Beschränkung gibt man hier die gewünschte Anzahl an CPUs an, die man für eine Abfrage zur Verfügung stellen will, also beispielsweise 2 oder 4.

Wird Hyper-Threading verwendet, stehen uns hier doppelt so viele Scheduler zur Verfügung, wie physikalisch CPUs eingebaut sind. Als Richtwert könnte man es hier mit der Hälfte der verfügbaren Scheduler versuchen. Also beispielsweise 8 Scheduler -> MAXDOP: 4.

Wird NUMA verwendet, wird es noch komplexer. Hier wird empfohlen, nicht mehr als die Anzahl von Cores in einem NUMA-Node zu verwenden. Das ist aber schon wieder ein anderes Thema – die Microsoft-Dokumentation zum Thema findet sich hier.


MAXDOP in eine Query integrieren

Wir können eine MAXDOP-Anweisung auch direkt an eine Query anhängen – Vorsicht: das gilt nur für die jeweilige Query und überschreibt das Server-MAXDOP-Setting.

Das würde dann beispielsweise so aussehen:

USE AdventureWorks2017

GO

SELECT*

FROM Sales.SalesOrderDetail

ORDERBY ProductID

OPTION (MAXDOP 2)

GO


Cost Threshold for Parallelism

Indirekt können wir MAXDOP auch über die Cost Threshold for Parallelism beeinflussen. Dieses Setting finden wir unter Advanced> Parallelismüber dem Max Degree of Parallelism. Jede Abfrage hat bestimmte „Kosten“. Da es für diesen Kostenwert keine Einheit gibt, wird er oft scherzhaft als SQL-Dollar bezeichnet. (Tatsächlich soll es sich dabei um die Zeit handeln, die diese Abfrage auf einem bestimmten Computer eines bestimmten Microsoft Mitarbeiters in den 90ern gedauert hätte.)

Per Default steht das Setting für Cost Threshold for Parallelism auf 5; die meisten Queries haben aber weitaus höhere „Kosten“. Ist 5 eingestellt, dürfen die meisten Abfragen gleich Parallelismus verwenden; setzt man diesen Schwellwert höher, beispielsweise auf 50, dürfen nur kostspieligere Abfragen auch parallel arbeiten.

Auch das erfordert Tests und einen Blick in die Execution Plans– wieviel kosten unsere Abfragen so im Schnitt? Danach können wir uns bei der Einstellung der Cost Threshold richten.

 

 

Wie können wir testen?

Ok, jetzt wissen wir, wo wir das MAXDOP-Setting verändern können. Wie können wir aber testen, was es uns bringt?

Zum einen brauchen wir dazu eine Datenbank mit einer realistischen Befüllung mit Daten – klar: Wenn wir nur mit wenigen Testdaten Abfrage-Zeiten auslesen wollen, wird das Ergebnis massiv verfälscht.

Zum anderen müssen wir nur ein bisschen durch das SSMS navigieren.

 

Statistik einschalten

Die Statistik einzuschalten ist schon einmal eine gute Idee:

setstatisticsio,timeon

Damit erhalten wir, wenn wir eine Query ausführen, zusätzliche Informationen im „Messages-Tab“ des Ergebnisfensters:


Wir können hier ablesen, wie lange es gedauert hat, bis eine Abfrage ausgeführt wurde (elapsed time), sowie die „CPU time“. Ist die CPU-Time höher als die Geschwindigkeit der gesamten Abfrage, kam offenbar Parallelismus zum Einsatz: Die CPU-Time sagt uns, wie lange mehrere CPUs in Summe an der Abfrage gearbeitet haben.

Beim Testen mit unterschiedlichen MAXDOP-Einstellungen geben uns diese Werte wertvolle Hinweise darauf, mit welchen Einstellungen wir im jeweiligen Fall am besten unterwegs sind.

 

Execution Plan verwenden

Der Execution Plan gibt uns eine Übersicht darüber, wie denn unsere Abfrage so abläuft. Auch hier finden wir Informationen über MAXDOP. Wir können einen Estimated oder Actual Execution Plan verwenden; wie der Name schon sagt, bekommen wir bei ersterem eine informierte Schätzung, bei letzterem eine Analyse der ausgeführten Abfrage. Aber Achtung: die Erstellung des Execution Plans kostet auch etwas Zeit – die Abfragezeit wird dadurch also leicht verfälscht!

Tastenkombination

Einschalten können wir die Pläne über Tastenkombination, Menütabs oder Toolbar; mit STRG + L bekommen wir den Estimated, mit STRG + M den Actual Execution Plan.

Menüleiste

In der Menüleiste wählen wir „Query“ und können uns dort zwischen Estimated oder Actual Execution Plan entscheiden:


Toolbar

In der Toolbar gibt es zwei Icons, die wir für Estimated oder Actual Execution Plan anklicken können:



Execution Plan lesen

Ein Execution Plan für eine (nicht besonders gute) Abfrage könnte etwa so aussehen:


Dabei sagen uns die Doppelpfeile im gelben Feld, dass hier Parallelismus verwendet wurde, und unter „Parallelism“ sehen wir, dass das Zusammenführen der Informationen (Gather Streams) in diesem Fall 19% unserer Rechenzeit verbraucht hat! Vielleicht war in diesem Fall Parallelismus doch nicht ganz so schlau.

Wenn wir im Execution Plan mit der Maus über das Select gehen, sehen wir unter „Estimated Subtree Cost“ die geschätzten Kosten der Abfrage:


… und gleich darüber können wir auch die aktuelle MAXDOP-Einstellung sehen.

 

Properties

Wenn wir im Execution Plan unsere Abfrage auswählen und dann auf F4 drücken, öffnet sich das Properties-Fenster. Hier können wir unter anderem sehen, wie viele Zeilen von jedem CPU gelesen werden mussten.


In diesem Fall liegt ein Unterschied von fast 10.000 Zeilen zwischen dem „Fleißigsten“ und dem „Faulsten“. Entsprechend gravierender können die Unterschiede bei Abfragen aus größeren Datenmengen werden (und entsprechend länger muss auf den Langsamsten gewartet werden).

 

Von Fall zu Fall entscheiden

Wie eingangs schon erwähnt, gibt es keine Einheitslösung. Wo der „sweet spot“ liegt, müssen wir jeweils durch Testen herausfinden. Im Execution Plan können wir sehen, ob Parallelismus verwendet wurde; in der Statistik können wir uns ansehen, wieviel CPU-Zeit die Query verbraucht hat und wie lange die gesamte Query gedauert hat.

Wie viele CPUs verwendet werden dürfen, können wir leicht einstellen; die Herausforderung ist vielmehr, zu entscheiden, wie viele verwendet werden sollten.

Auch eine Überlegung wert: das Verhältnis zwischen CPU-Time und Query-Time. Nicht in jedem Fall ist das Ziel, die Zeit der Abfrage um noch ein paar Millisekunden schneller zu machen – wenn dadurch die CPU-Zeit wesentlich in die Höhe schießt, bedeutet das, dass zwar meine Abfrage recht schnell ist, dass aber möglicherweise nicht mehr genug Ressourcen für andere Prozesse zur Verfügung stehen. Ich hätte dann also eine Abfrage optimiert, blockiere aber alle anderen währenddessen.

 

Weiterführende Infos, MAXDOP für Fortgeschrittene sozusagen, gibt es z.B. in diesem Blogartikel von Matteo Lorini.

Viel Spaß beim Tes… nein, das traue ich mich nicht zu sagen… viel Spaß beim Ausprobieren!

Und vielleicht sehen wir uns ja in einem unserer Kurse zumThema SQL!
























































Was ist eigentlich... ein Clustered Index?

$
0
0

Ihr beschäftigt euch mit SQL und wollt wissen, was ein Index ist und wofür wir das brauchen? Dann seid ihr hier richtig. In diesem Artikel schauen wir uns an, was ein clustered index (gruppierter Index) ist, wie wir einen erstellen und wie er funktioniert.

 

Suchen wie im Telefonbuch

Dieses Beispiel ist für alle geeignet, die alt genug sind, um noch zu wissen, wie ein gutes altes Telefonbuch verwendet wurde (alle anderen: stellt euch ein Lexikon vor… oder irgendein Buch… eines mit richtigen Seiten aus Papier 😉 ).

Angenommen, wir suchen nach der Telefonnummer von Bruce Wayne im Telefonbuch von Gotham City. Angenommen, dieses Telefonbuch hat 1000 Seiten. Kein Problem für uns, denn wir wissen ja, ein Telefonbuch ist alphabetisch erst nach Nachnamen, dann nach Vornamen geordnet. Wir blättern also zu „W“, streifen kurz mal über „Wayne, Alex“ und „Wayne, Amanda“ und schon haben wir „Wayne, Bruce“ gefunden.

 

Wie speichert die Datenbank?

Jetzt stellt euch aber mal vor, die Einträge im Telefonbuch wären nicht alphabetisch geordnet, sondern in der Reihenfolge, in der die Leute dort zugezogen sind. Bei einer so alteingesessenen Familie wäre „Wayne, Bruce“ wohl relativ weit am Anfang zu finden, aber wo genau auf diesen 1000 Seiten? Entsprechend länger dauert es, bis wir etwas gefunden haben.

Aber genau so speichert die Datenbank die Datensätze ab; die Reihenfolge ist mehr oder weniger zufällig, gespeichert wird dort, wo gerade Platz ist.



Dabei sprechen wir von einem „heap“ – die Einträge werden sozusagen alle auf einem „Haufen“ gespeichert.

Die Datenbank müsste also alle Einträge nach „Wayne, Bruce“ absuchen, denn selbst, wenn sie den gewünschten Eintrag zufällig gleich als zweiten findet, kann sie ja nicht sicher sein, dass es nicht irgendwo noch einen anderen Einwohner mit dem gleichen Namen gibt.

Erstellen wir dazu einmal eine Beispieltabelle:


In diese Tabelle fügen wir die oben genannten Personen mit absolut authentischen Telefonnummern ein. Mit einem SELECT-Statement geben wir jetzt den Inhalt der Tabelle aus und lassen uns zugleich den Actual Execution Plan anzeigen:


Die eigentliche Ausgabe ist erwartungsgemäß unspektakulär:


Viel spannender ist für uns ein Blick in den Execution Plan:


Hier sehen wir, dass, obwohl wir nur eine einzige Person abgefragt haben, ein sogenannter Table Scan stattgefunden hat – das bedeutet, dass die gesamte Tabelle gescannt, also abgesucht wurde!

Bei unseren 12 Einträgen hat das natürlich keine erkennbaren Auswirkungen auf die Performance; bei ein paar Millionen Einträgen sieht das aber gleich ganz anders aus.

Wie funktioniert ein Clustered Index?

Beim Telefonbuch können wir uns darauf verlassen, dass, wenn wir „Wayne, Bruce“ unter „W“ gefunden haben, nicht noch andere Personen dieses Namens irgendwo anders versteckt sind. Wir müssen also wesentlich weniger absuchen (nur die Einträge unter „W“) und kommen somit schneller zu einem Ergebnis.

Diese Funktion erfüllt bei der Datenbank der Clustered Index. Er sorgt dafür, dass die Einträge auch in einer (vom Index bestimmten) Reihenfolge abgespeichert werden. Wir suchen also sozusagen nur unter „W“.

 

Datenspeicherung in der Datenbank

Die Daten werden auf sogenannten Seiten (pages) gespeichert. Auf einer Seite werden rund 8KB (8060 bytes) gespeichert. Ein Index sorgt dafür, dass die Daten in einer bestimmten Reihenfolge abgespeichert werden, und nicht als heap. Wir bekommen also einen Verweis auf die Seiten, auf denen unsere gewünschten Daten liegen, und müssen nicht mehr alle absuchen.

 

Navigation mit Index

Wären jetzt unsere Daten alphabetisch sortiert (was ein Index für uns übernehmen kann), könnte die Datenbank also nach „Wayne, Bruce“ suchen, wie wir im Telefonbuch: vorblättern bis „W“, A-V müssen gar nicht erst durchsucht werden.

 

Das kann man sich etwa so vorstellen:


Somit können wir zu der Seite mit den gesuchten Informationen navigieren, ohne alle Seiten lesen zu müssen. Ausgehend vom root node gehen wir über die intermediate nodes zu den eingentlichen data pages:


Die SQL-Anweisung, um einen Clustered Index zu erstellen, ist keine Hexerei:


Wenn wir jetzt noch einmal mittels SELECT nach Bruce Wayne suchen, sieht unser Execution Plan gleich anders aus:


Jetzt wurde kein Table Scan mehr ausgeführt, sondern ein Clustered Index SEEK, eine indizierte Suche.

 

Primary Key und Clustered Index

Im Relationalen Datenbankmodell haben Tabellen normalerweise einen Primary Key (Hauptschlüssel). Dieser erstellt eigentlich klammheimlich im Hintergrund einen solchen Clustered Index für uns – auf der Spalte des Hauptschlüssels. In vielen Fällen ist das auch sinnvoll; will man die Datenbank optimieren, muss man allerdings von Fall zu Fall abwägen.

Eine Tabelle kann mehrere Indizes haben, davon aber nur einen Clustered Index (min Nonclustered Indexes, also nichtgruppierten Indizes setzen wir uns in einem anderen Artikel auseinander).

Daher sollte schon bei der Planung überlegt werden, ob es im jeweiligen Fall sinnvoll ist, dass der Clustered Index über dem Hauptschlüssel liegt, oder ob man ihn lieber über eine andere Spalte setzen möchte.

Viel Spaß beim Ausprobieren, und vielleicht sehen wir uns ja in einem unserer Kurse zum Thema SQL!




Untergruppierungen mit OVER PARTITION BY

$
0
0

Gruppieren von SQL-Abfragen kennen wir von Aggregatfunktionen wie SUM() oder AVG(). Mit einem GROUP BY geben wir an, dass die Summe oder der Mittelwert pro einer bestimmten Spalte berechnet werden soll. Wollen wir aber auch Spalten ausgeben, die nicht vom GROUP BY erfasst werden, wird es spannend. Dafür gibt es aber auch eine einfache Lösung: OVER (PARTITION BY …).

 

„Normale“ Aggregatfunktionen

Aggregatfunktionen erlauben uns, Summen und Mittelwerte zu berechnen, zu zählen oder den größten oder kleinsten Wert auszugeben.

Die am häufigsten verwendeten Aggregatfunktionen (aggregate functions) sind AVG(), COUNT(), MAX(), MIN() und SUM().





Dabei können wir uns einen einzelnen Wert ausgeben lassen, zum Beispiel einen Mittelwert, oder mehrere Werte, nämlich den Mittelwert pro einer bestimmten Spalte – oder noch weitere Untergruppierungen (Partitionen) erstellen.


 

Einen einzelnen Wert ausgeben

Sehen wir uns das anhand der Northwind Beispieldatenbank an: Hier gibt es eine Tabelle Orders (Bestellungen); darin scheinen die Frachtkosten (Freight) auf.


Wir können einen Mittelwert aller Frachtkosten berechnen:


Dann bekommen wir genau den einen Wert zurück…


… oder, viel wahrscheinlicher: wir können die mittleren Frachtkosten pro… ausgeben.

 

Mittelwert pro

Dazu verwenden wir GROUP BY. Wir gruppieren den Mittelwert zum Beispiel pro Land, in das geliefert wird (ShipCountry), oder pro Frächter (ShipVia), oder PRO Kunde (CustomerId).


Da es in der Northwind Datenbank 3 Frächter gibt, bekommen wir hiermit 3 Treffer zurück und den jeweiligen mittleren Frachtkostenwert pro Frächter (ShipVia).


Aggregatfunktionen und mehr als zwei Spalten

Wenn wir aber mehr als diese beiden zusammengehörigen Spalten ausgeben wollen, wird es spannend.

In manchen Fällen kann es eventuell Sinn machen; wir wollen zum Beispiel die mittleren Frachtkosten pro Kunde und Frächter:




Plötzlich haben wir statt der drei Ausgabezeilen 239; 91 Kunden, von denen 89 schon etwas bestellt haben und somit Frachtkosten generiert haben, und manche Kunden, die nicht von allen drei Frachtunternehmen beliefert worden sind.


Dadurch bekommen wir auch Mehrfachausgaben; die CustomerId steht so oft da, wie dieser Kunde von unterschiedlichen Frächtern beliefert wurde, und die Frächter (ShipVia) werden bei jedem Kunden, den sie beliefert haben, aufgeführt.

Auch Mehrfachausgaben mit Aggregatsfunktion und GROUP BY können Sinn machen. Im Beispiel mit den Frächtern und Kunden könnten wir hier vergleichen, welcher Frächter (ShipVia) im Schnitt zu welchem Kunden (CustomerId) am günstigsten liefert.


 

Wenn Mehrfachausgaben bei Aggregatsfunktionen zum Problem werden

Was aber, wenn wir mehr Informationen in der Ausgabetabelle wollen? Beispielsweise möchten wir auch noch die OrderId (Bestellnummer) ausgeben:


Dadurch bekommen wir 830 Zeilen zurück – also alle Bestellungen aus der Orders-Tabelle. Die Datenmenge wäre noch längst kein Problem – aber jetzt haben wir keinen Mittelwert mehr, denn die durchschnittlichen Frachtkosten pro Bestellung sind einfach die Frachtkosten. Wir bekommen also das gleiche Ergebnis, wie wenn wir gar kein AVG() abgefragt hätten.



Untergruppierungen – OVER PARTITION BY

Wenn wir trotzdem einen Mittelwert ausgegeben haben wollen, können wir das mit Hilfe einer Untergruppierung lösen.

Dabei verzichten wir auf das GROUP BY und verwenden stattdessen direkt nach der Aggregatfunktion ein OVER:



Im OVER erstellen wir eine Partition, eine Untergruppierung, in der wir jetzt angeben, pro welcher Spalte wir die mittleren Frachtkosten berechnet haben wollen.




Wir erhalten wieder 830 Ausgabezeilen (so viele, wie Bestellungen, OrderIds vorhanden sind). Mit OVER PARTITION BY erhalten wir aber so auch die mittleren Frachtkosten pro Frächter (ShipVia).

Für unser Beispiel bedeutet das, dass in jeder Ausgabezeile auch der mittlere Frachtkostenwert pro Frächter (ShipVia) dabeisteht. Da es insgesamt nur drei Frächter gibt, sind das jetzt drei verschiedene Werte, die sich entsprechend oft wiederholen.

Wollen wir einen mittleren Frachtkostenwert pro Kunde und Frächter, können wir auch das im OVER PARTITION BY angeben:



Somit haben wir wieder ein ähnliches Ergebnis wie im Beispiel weiter oben mit GROUP BY, nur, dass wir hier eben auch weitere Spalten angeben können (OrderId), die jetzt nicht mehr an die Untergruppierung im PARTITION BY gekoppelt sind.


In Zeile 1 und 6 beispielsweise entspricht der mittlere Frachtkostenwert (AvgFreight) wieder genau dem Frachtkostenwert (Freight) – diesmal ist das kein Fehler; Kunde ALFKI wurde eben nur einmal von Frächter #2 und einmal von Frächter #3 beliefert. Überall, wo ein Frächter einen Kunden mehr als einmal beliefert hat, steht in der Spalte AvgFreight nun der mittlere Frachtkostenwert dieses Kunden (CustomerId) beim entsprechenden Frächter (ShipVia).

 

Anwendungsfälle

OVER PARTITION BY ist überall dort praktisch, wo Mehrfachausgaben eines Wertes erlaubt sind, mehr Spalten als nur die Wert pro Spalte X in der Ausgabe gefragt sind und Aggregatfunktionen zum Einsatz kommen.

So lässt sich zum Beispiel der Gehaltsdurchschnitt pro Abteilung ausgeben, oder der Notendurchschnitt pro Klasse in einem größeren Kontext.

Auch für mehrere Aggregatfunktionen in einem SELECT Statement ist OVER PARTITION BY empfehlenswert.


So könnten wir in unserem Beispiel etwa die mittleren Frachtkosten pro Kunde und Frächter, die Frachtkostensumme pro Kunde und den höchsten Frachtkostenwert pro Kunde und Frächter, sowie beliebig viele andere Spalten aus der Orders-Tabelle ohne GROUP BY ausgeben:



Ergebnis:





Ich hoffe, Ihr habt ein wenig Appetit bekommen, Euch weiter mit SQL auseinanderzusetzen, und vielleicht sehen wir uns ja einmal in einem unserer Kurse zum Thema SQL!






Was ist eigentlich... ROLLBACK und COMMIT?

$
0
0

In diesem Artikel beschäftigen wir uns mit SQL Transaktionen. Was ist eine Transaktion? Wofür braucht man das überhaupt? Wie wird sie verwendet? Und natürlich: Was ist eigentlich ROLLBACK und COMMIT?

 

Transaktion

Als SQL Einsteiger befasst man sich normalerweise zunächst mit SELECT-Statements. Wir lernen, Informationen aus Datenbanken zu holen. Das brauchen wir meist zuerst. In einem der nächsten Lernschritte erfahren wir dann, wie wir Informationen, die sich in der Datenbank befinden, verändern können.

Das Verändern von Informationen in der Datenbank gehört zum Tagesgeschäft – in einer Kundendatenbank beispielsweise verändert sich ständig irgendetwas: Jemand ändert den Namen, die Adresse oder Telefonnummer; der Kundenstatus ändert sich oder die offenen Beträge.

Dass wir die Daten in der Datenbank ändern können, ist ganz selbstverständlich – es bringt aber auch Gefahren mit sich. Was, wenn zwei Sachbearbeiter/innen gerade gleichzeitig die gleichen Daten verändern wollen? Was, wenn jemand gerade Daten verändert, während jemand anderer sie gleichzeitig abfragen möchte? Haben wir dann schon die aktuellen Daten oder noch die alten? Was, wenn das System abstürzt, während gerade jemand eine Überweisung tätigen möchte? Steht dann beim Kunden, das Geld wurde schon abgebucht und bei der Bank, es sei noch ausständig?

Damit uns so etwas nicht passieren kann, brauchen wir die Transaktion. Sie sorgt dafür, dass eine Transaktion, wie beispielsweise das Überweisen eines Betrages, entweder ganz oder gar nicht ausgeführt wird. Weiters stellt sie sicher, dass auf Daten, die gerade bearbeitet werden, nicht von anderer Seite her zugegriffen werden kann.

 

ROLLBACK und COMMIT am Beispiel UPDATE

Sehen wir uns das am Beispiel UPDATE an. Wir wollen einen Namen in einer Datenbank ändern. Unsere Kundendatenbank sieht derzeit so aus:


Minnie wird das Kundenkonto übernehmen, und somit haben wir nicht viel zu tun, wir müssen nur den Vornamen von Mickey auf Minnie umändern. Dafür brauchen wir nur ein einfaches UPDATE:


Gleich darauf werfen wir noch einen Blick in unsere Tabelle und stellen fest, O Graus! uns ist ein Fehler unterlaufen. Wir haben damit alle Vornamen in „Minnie“ umgeändert:


Und das war es dann. Wir haben keine Möglichkeit mehr, das rückgängig zu machen. Wenn wir jetzt nicht fünf, sondern fünftausend Kunden haben (oder mehr), können wir nur noch hoffen, dass es irgendwo ein Backup von unserer Datenbank gibt – aber auch das wäre katastrophal, denn dann würden wir nicht nur unseren Fehler zurücksetzen, sondern alle Änderungen, die seither vorgenommen wurden.

Die Transaktion kann uns unter anderem vor diesem Horrorszenario schützen. Solange wir die Tranksaktion nicht mit einem COMMIT abgeschlossen haben, ist an der Datenbank noch nichts passiert, und wir können auch wieder zurücksetzen.

Wenn unser einfaches UPDATE innerhalb einer Transaktion durchgeführt wird, sieht das so aus:

Oh nein!, wir haben den gleichen Fehler gemacht, wie zuvor… aber jetzt haben wir zwei Vorteile: Einerseits bekommen wir beim Ausführen einen Hinweis, dass das nicht ganz stimmen kann:


Wenn wir nur einen Vornamen ändern wollten, wie kann es dann fünf Zeilen betreffen? Hoffentlich bemerken wir hier den Fehler.

Andererseits haben wir jetzt dank der Transaktion die Möglichkeit, alles noch einmal rückgängig zu machen: dafür gibt es das ROLLBACK.


Damit ist dann nocht nichts passiert, nichts in der Datenbank verändert worden.

Erst, wenn wir ein COMMIT machen, greift die Veränderung (und hoffentlich korrigieren wir vorher unseren Fehler).



Blockieren von Daten in Verwendung

Weiters werden Daten, auch ganze Tabellen, die gerade in Verwendung sind, blockiert. So einen Fall haben wir zum Beispiel, wenn wir gerade Zugtickets oder Kinoplätze reservieren wollen. Wenn jemand anderer gerade die Plätze ausgewählt hat, stehen sie uns nicht zur Verfügung – und zwar so lange nicht, bis derjenige die Plätze entweder gekauft hat (erst dann werden sie uns als bereits besetzt angezeigt) oder der Verkauf abgebrochen wurde und wir uns die letzte Reihe unter den Nagel reißen können.


Das passiert auch bei unserem simplen Beispiel – solange unser UPDATE nicht mit dem COMMIT abgeschlossen wurde, hat auch sonst niemand Zugriff auf die Daten.


Andere stecken solange in der Warteschleife (selbst, wenn sie nur eine Tabellenabfrage machen und nichts verändern wollen):



Savepoints: Transaktionen verschachteln

Transaktionen können auch miteinander verschachtelt werden, oder mit anderen Worten, wir können innerhalb einer Transaktion Savepunkte erstellen, zu denen zurückgesetzt werden kann. In diesem Fall vergeben wir auch Namen für die Transaktionen, um eben zu einem bestimmten Punkt zurücksetzen zu können.

In unserem einfachen Beispiel könnte das etwa so aussehen:


Die verschachtelten Transaktionen beginnen wir mit SAVE TRAN statt mit BEGIN TRAN um einen Savepoint zu erstellen.

Versuchen wir, eine verschachtelte Transaktion zurückzusetzen, wenn wir versehentlich mit BEGIN TRAN versucht haben, eine Verschachtelung zu erstellen, bekommen wir die Fehlermeldung, dass keine Transaktion dieses Namens gefunden wurde und die Transaktion wird ausgeführt!

 


Transaktionsnamen und Transaktionslog

Damit wir bei mehreren Verschachtelungen feststellen können, welche wir denn jetzt zurücksetzen (oder ausführen) möchten, brauchen unsere Transaktionen Namen. Auch wenn wir mit dem Transaktionslog arbeiten, ist es hilfreich, wenn unsere Transaktionen wiedererkennbare Namen haben, um sie leichter einordnen zu können:


Das Transaction-Log können wir zum Beispiel so aufrufen:

Der erste Wert in der Klammer steht dabei für starting log sequence number (LSN), der zweite Wert steht für die ending LSN. NULL, NULL bedeutet einfach alles.

Auch nach bestimmten Transaktionen können wir suchen, wenn wir dem SELECT noch ein WHERE mitgeben:



Viel Spaß beim Ausprobieren, und vielleicht sehen wir uns ja einmal in einem unserer Kurse zum Thema SQL!










SQL - verschiedene nichtgruppierte Indizes

$
0
0

Wir unterteilen in SQL in zwei große Gruppen von Indizes: gruppierte und nicht gruppierte (Engl.: clustered und nonclustered). Zum Thema wozu brauchen wir einen Index und was sind gruppierte Indizes gibt es bei uns schon einen anderen Blogartikel hier. In diesem Artikel befassen wir uns mit nicht gruppierten Indizes und verschiedenen Varianten von Indizes.

Nicht gruppierter Index (nonclustered index)

 Während ein Clustered Index für die physikalische Speicherung der Daten in einer bestimmten Reihenfolge auf dem Datenträger verantwortlich ist, erstellt uns ein Nonclustered Index einen Verweis darauf, wo die gewünschten Daten zu finden sind.

Das kann man vergleichen mit einem Index in einem Lexikon – wir sehen dort nach, wo ein bestimmtes Thema oder ein bestimmter Fachbegriff zu finden sind, und im Index steht die gewünschte Seite. Dann können wir direkt diese Seite aufschlagen und müssen nicht alle Seiten des Lexikons durchblättern, um unser Thema zu finden.

Im Lexikon kostet es natürlich auch ein bisschen Zeit, im Index den richtigen Eintrag zu finden; trotzdem sind wir damit viel schneller, als wenn wir das gesamte Lexikon lesen müssten.

Genauso ist es bei der Datenbank; und genauso spart uns der Index beim Suchen in der Datenbank jede Menge Zeit.

Ein Beispiel:

Wir haben eine Kundendatenbank und suchen den Kunden namens „Manfred“. Ohne Index müssten wir die gesamte Datenbank durchsuchen, denn selbst, wenn wir schon einen gefunden haben, es könnte ja mehrere Manfreds geben.

Dank des Index müssen wir aber jetzt nicht mehr alle Seiten durchsuchen; wir müssen nur wissen, mit welchen Namen die jeweiligen Seiten beginnen. Da „Manfred“ vor „Paul“, aber nach „Inge“ gespeichert ist, brauchen wir weder die Seiten vor „Inge“, noch die Seiten ab „Paul“ zu durchsuchen:


Pro Seite, auf der unsere Kunden gespeichert sind, brauchen wir einen Eintrag auf einer Zwischenseite (Intermediate Node). Bei unserem simplen Beispiel müssten wir nur 2 Seiten durchsuchen (das Intermediate Node und die „Inge“-Seite). Abhängig von der Anzahl der notwendigen Intermediate Nodes schrumpft die Anzahl der zu durchsuchenden Seiten aber auch bei einer echten Datenbank ganz gewaltig.

 

Nonclustered Index erstellen

 Die Anweisung, um einen Nonclustered Index zu erstellen, ist simpel:


Dabei darf man das „NONCLUSTERED“ sogar komplett weglassen – wenn wir nicht explizit angeben, dass wir einen „CLUSTERED“ Index erstellen wollen, wird automatisch ein nonclustered Index erstellt.

Oft wird beim Vergeben des Namens ein IX_ für Index vorangestellt – dabei handelt es sich allerdings um eine Konvention, die der besseren Lesbarkeit dient.

Pro Tabelle dürfen wir theoretisch 999 Indizes setzen. Wir brauchen also keine Angst zu haben, eine Maximalzahl von erlaubten Indizes zu überschreiten. Aber Vorsicht: Indizes machen zwar das Lesen (Suchen) schneller, aber das Schreiben (UPDATE etc.) langsamer. Zu viele können also auch wieder zum Problem werden.

Von den nichtgruppierten Indizes gibt es einige Variationen, die wir hier kurz vorstellen:


Zusammengesetzter Index (multicolumn index)

Manchmal brauchen wir einen Index, der mehrere Spalten abdeckt; zum Beispiel wollen wir in unserer Kundendatei nach Stadt und Postleitzahl geordnet suchen.

In diesem Fall geben wir einfach mehrere Spalten beim Erstellen des Index an:


Theoretisch könnten wir bis zu 16 Spalten in einen solchen zusammengesetzten Index aufnehmen (da stellt sich aber die Frage, wieviel Sinn das noch macht).

 

Index mit eingeschlossenen Spalten (index with included columns)


Wenn wir mehrere Spalten abdecken wollen, gibt es eine bessere Möglichkeit, die bei der Abfrage nicht alle Ebenen belastet: den Index mit eingeschlossenen Spalten. Ziel ist es, einen Lookup zu vermeiden (der recht hohe Kosten verursachen kann).


Wenn in dem Index alle Spalten enthalten sind, die in der Abfrage vorkommen, spricht man außerdem von einem abdeckenden Index (covering index).

Im Beispiel oben sehen wir auch ein UNIQUE in unserem CREATE-Statement. Das bedeutet, dass die Spalten eindeutig sein müssen, wie das zum Beispiel bei unserer OrderId der Fall ist. Man spricht dann auch von einem eindeutigen Index (unique index).

Gefilterter Index (filtered Index)


Wir wissen beispielsweise, dass wir aus unserer Kundendatenbank häufig Abfragen mit einer ganz bestimmten WHERE-Clause machen; zum Beispiel wollen wir immer Daten zu unseren Kunden in Deutschland abfragen. Dann können wir einen gefilterten Index erstellen, der nicht über alle Länder, sondern eben nur über Deutschland gelegt wird:



Generell können wir uns merken, dass Indizes das Auffinden von Daten erleichtert, unsere Abfragen also normalerweise schneller werden. Gleichzeitig müssen Indizes aber auch gewartet werden; wollen wir Veränderungen in der Datenbank vornehmen, also etwas einfügen, weglöschen oder umschreiben, müssen auch die Indizes aktualisiert werden – in den meisten Fällen machen Indizes daher das Schreiben langsamer.

In einem etwas fortgeschritteneren Artikel werden wir uns dann auch mit dem Columnstore Index beschäftigen.


Viel Spaß beim Ausprobieren, und vielleicht sehen wir uns ja in einem unserer Kurse zum Thema SQL Abfragen!


















SQL pages - Datenbankspeichereinheiten erklärt

$
0
0

Ein häufiger Irrglaube ist, dass man Datenbankabfragen allein durch kleine magische Anpassungen an der SQL-Abfrage selbst um ein Vielfaches schneller machen kann. Natürlich kann man hier durch gewisse Kniffe ein wenig nachhelfen; die eigentliche Datenbankoptimierung beginnt allerdings wesentlich früher, nicht erst bei den Abfragen.

Die Optimierung beginnt tatsächlich bei der optimalen Nutzung der pages, der Speichereinheiten der Datenbank. Je mehr pages durchsucht werden müssen, desto länger dauert die Abfrage. Je ungünstiger der zur Verfügung stehende Platz genutzt wird, desto mehr Seiten werden benötigt und müssen durchsucht werden und desto länger dauert die Abfrage.

Also sehen wir uns einmal die Architektur der Datenbank etwas genauer an.

Die grundlegende Speichereinheit einer Datenbank ist eine page (Seite). Auf einer solchen Seite befinden sich zum Großteil die eigentlichen, in der Datenbank abgespeicherten Informationen; um diese Informationen aber effizient finden zu können, bedarf es auch weiterer, sogenannter Metainformationen.

Diese Metainformationen kann man sich vorstellen wie den Index in einem Lexikon; Indizes in Datenbanken funktionieren sehr ähnlich. Man bekommt einen Verweis darauf, wo sich der gesuchte Fachbegriff befindet, und kann direkt zur entsprechenden Seite blättern, ohne das gesamte Lexikon lesen zu müssen. Auch Indizes in der Datenbank ermöglichen uns ein schnelleres Finden von Informationen durch Verweise, wo die Daten abgespeichert sind – und auch diese zusätzliche Information muss irgendwo abgespeichert werden.

Die Metadaten werden noch einmal weiter untergliedert, abhängig davon, um welche Art von Information es sich handelt (beispielsweise Indexeinträge, oder eine Information, wieviel freier Speicherplatz noch vorhanden ist).

Eine page besteht aus einem page header, den eigentlichen Daten und dem row offset.


 

 

Im Header finden sich z.B. die Informationen dazu, welche Art von Daten hier gespeichert wird und welche Indizes verwendet werden (Index ID).

Im Row-Offset befindet sich die Information, welche Information in welcher Zeile zu finden ist (wieder vergleichbar mit dem Index im Lexikon), was ein schnelleres Auffinden von Daten ermöglicht. Die Offset-Informationen am Ende jeder Seite sind in umgekehrter Reihenfolge zu den Zeilen abgespeichert.

 

Insgesamt besteht eine Seite (page) aus 8192 Byte. Davon entfallen 96 Byte auf denpage header, maximal 8060 Byte auf eine einzelne Zeile. Ein row offset Eintrag belegt 2 Byte. Für jede Zeile kommen noch einmal 7 Byte an Overhead hinzu.


 

 

Eine Zeile beansprucht also den Platz für den eigentlichen Eintrag (abhängig von Datentyp und Anzahl Zeichen) + 7 Byte an overhead + 2 Byte für row offset. Daraus ergibt sich, wie viele Zeilen auf einer Seite Platz haben.

Je besser die Seiten befüllt sind, desto weniger Seiten müssen insgesamt für das Ergebnis durchsucht werden. Wäre ein einziger Datensatz 4100 Byte lang, würde nur ein einziger Eintrag auf eine Seite passen (zwei würden mit 8200 Byte den Speicherplatz der Seite überschreiten)! Und diese wäre noch dazu nur knapp über die Hälfte befüllt. Wir würden unsinnig viel Speicherplatz verbrauchen und die Abfragen wären unerträglich langsam.

 

Genaugenommen fängt die Optimierung also bei intelligent gewählten Datentypen an!

 

8 Seiten (pages) ergeben einen sogenannten Block (engl.: Extent), eine Einheit zur Verwaltung des Speicherplatzes. Bei rund 8 KB pro page ergibt das also 64 KB pro Block. Das macht 16 Blöcke pro 1 MB.

 

Solche und weitere Themen im Bereich der Datenbankoptimierung gibt es auch in unserem zweitägigen Kurs SQL Server – Abfragen und Indizes optimieren.

 


Komforteinstellungen für SSMS – Tipps für Settings

$
0
0

In diesem Artikel werden einige nützliche Settings aufgelistet, die einem das Leben mit SSMS leichter machen.

 

Sprache

 


 

Beginnend mit einer Grundsatzentscheidung: der Spracheinstellung für SSMS. Empfohlene Sprache: Englisch. Viele Begriffe sind so übersetzt, dass sie in der Übersetzung kaum wiederzuerkennen sind, was das Verständnis erschwert, wenn man mit den üblicherweise englischsprachigen Fachbegriffen vertraut ist. Auch in großen Unternehmen kann es die Kommunikation mit möglicherweise nicht-deutschsprachigen Kollegen erschweren, eine anderssprachige Version von SSMS zu verwenden.

„Geh mal zum 3. Menüpunkt von links und nimm dann das 6. von oben…“

So möchte man nicht mit jemandem kommunizieren müssen, weil dort der Solution Explorer Projektmappenexplorer heißt.

Die Einstellungen für die verwendete Sprache findet man unter Tools -> Options… -> Environment -> International Settings.

 

Wenn es hier die gewünschte Sprache schon gibt, dann einfach auswählen und SSMS neu starten. Wenn es die gewünschte Sprache hier nicht gibt, dann kann man sich SSMS in dieser Sprache herunterladen (die Neuinstallation ist in wenigen Sekunden erledigt).

Den Download für die derzeit aktuelle Version 18.6 gibt es direkt bei Microsoft.

Für andere Versionen einfach runterscrollen; die Sprachauswahl gibt es jeweils nicht beim Download-Button selbst, sondern wenn man weiterscrollt.

 


 

Settings zur besseren Orientierung in SSMS

Zeilennummerierung

 

   


Ohne Zeilennummerierung kommen wir kaum mehr aus. Jede Fehlermeldung sagt uns, in welcher Zeile der Fehler aufgetreten ist. Ein Kollege möchte, dass man sich eine bestimmte Zeile noch einmal anschaut (oder Code beginnend bei einer bestimmten Zeile). Standardmäßig sind die Zeilennummern aber (bei einem frisch installierten SSMS) ausgeschaltet.

Zu finden unter Tools -> Options -> Text Editor -> All Languages -> General -> Line Numbers.

 


 

Scroll Bar in „Map Mode“

 



 

Die vertikale Scrollbalken in SSMS kann zu einer Code-Vorschau werden; bewegt man den Mauszeiger darüber, gibt es dabei eine Lupenfunktion, so dass man sich den Code an dieser Stelle ansehen und, wenn man den gewünschten Abschnitt gefunden hat, direkt dorthin springen kann.

 


Diese Einstellung findet man unter Tools -> Options… -> Text Editor -> All Languages -> Scroll Bars -> Use map mode for vertical scroll bar.
 

In dieser Art von Scrollbalken bekommen wir auch Farbindikatoren: Grün bedeutet dabei nicht, dass etwas richtig, sondern nur, dass es bereits abgespeichert wurde. Gelb ist kein Warnhinweis, sondern heißt, dieser Bereich wurde (noch) nicht gespeichert. Rot steht wie gewohnt für einen Fehler, und blau zeigt, wo aktuell das Einschaltungszeichen (Caret) steht.

 


 

Word Wrap

 



 

Mit Word Wrap verhindern wir, dass ein Kommentar in einer Zeile fortläuft und einen horizontalen Scrollbalken erzeugt. Abhängig von der Größe unseres Ausgabebildschirmes ist auch im SSMS nur so viel in einer Zeile, wie sich eben ausgeht – trotzdem können wir mit einem einzeiligen Kommentar arbeiten. Mit der Word Wrap-Einstellung merkt sich das SSMS, was alles zu dem Kommentar gehört.

 


Wie man sieht, verändert diese Einstellung auch die Zeilennummerierung nicht. SSMS merkt sich, dass dieser Kommentar eigentlich in einer Zeile steht.

Die Zeilenumbruchszeichen sind optional und können, falls unerwünscht, abgewählt werden.

 

Status Bar

 


Auch die Einstellungen für die Statusleiste lassen sich bearbeiten. Zum einen können wir festlegen, was denn überhaupt in der Statusleiste (unten) angezeigt werden soll, zum anderen können wir eine Farbe festlegen.

Unter Tools -> Options… -> Text Editor -> Editor Tab and Status Bar können diese Einstellungen vorgenommen werden; soll etwas nicht angezeigt werden, kann man den entsprechenden Punkt auf „false“ setzen.

 


 

Eine Möglichkeit, die Farben für die Statusleiste anzupassen, gibt es direkt beim Verbinden mit dem Server: Wenn man auf Connect -> Options… klickt, kann man „Use custom color“ auswählen, und dann die gewünschte Farbe mittels Colorpicker einstellen.


 

 

Reine Spielerei? Nur ein bisschen…

Ein realitätsnaher Anwendungsbereich für unterschiedliche Farben ergibt sich bei der Arbeit mit unterschiedlichen Servern. So kann man auf den ersten Blick anhand der Farben gleich erkennen, auf welchem Server man sich gerade befindet.

 

Ich hoffe, das erleichtert Euch die Arbeit ein bisschen! Solche Tipps und mehr gibt es in unseren SQL-Kursen.


 

 


Nützliche Tastenkombinationen für SSMS

$
0
0

Tastenkombinationen helfen uns, flüssiger und effizienter zu arbeiten. Manche Tastenkombinationen, wie das bekannte Copy&Paste, sind beinahe universell einsetzbar, andere sind applikationsabhängig. In manchen Fällen, wie bei SSMS, gibt es so viele verschiedene Tastenkombinationen, dass es wenig Sinn macht, alle auswendig zu lernen, da man einige nur recht selten braucht.

In diesem Artikel fassen wir einige der nützlichsten Tastenkombinationen für SSMS zusammen.

Am Ende des Artikels gibt es noch eine Kurzübersicht über alle hier vorgestellten Tastenkombinationen.

 

Ausführen (Execute)

Zum Ausführen einer Query haben wir gleich drei Möglichkeiten:

F5

STRG + E

ALT + X

Alle drei Varianten führen den markierten Teil einer Query aus, oder die gesamte Query, wenn nichts markiert wurde.

 

Neues Queryfenster

STRG + N

Diese Tastenkombination öffnet ein neues Queryfenster. Das bringt uns aber nur etwas, wenn wir die Query nicht abspeichern wollen, nicht für eine, die in ein bestehendes Projekt übernommen werden soll. In diesem Fall kann ich aber empfehlen, doch ausnahmsweise mit der Maus zu arbeiten: Wenn wir im Solution Explorer mit Rechtsklick auf „Query“, „New Query“ auswählen, ist die Query bereits an dem Speicherort, wo sie hinsoll, abgelegt. Öffnen wir ein neues Queryfenster mit der Tastenkombination oder über die Menüleiste, müssen wir beim Abspeichern nicht nur erst den Dateipfad angeben, die Query liegt auch erstmal unter „Miscellaneous“ und nicht unter „Queries“ und muss im Solution Explorer manuell verschoben werden.

Wollen wir eine Query in einem bestehenden Projekt anlegen, sind wir ausnahmsweise mit Klicken schneller.

 

Resultfenster

STRG + R

Damit blenden wir das Resultfenster ein bzw. aus. Ist das Resultfenster plötzlich verschwunden, keine Panik, wir müssen die ganze langsame Abfrage nicht noch einmal ausführen; STRG+R blendet und das Resultfenster wieder ein. Umgekehrt wollen wir manchmal wieder mehr von unserer Query sehen, ohne ständig das Resultfenster größer oder kleiner ziehen zu müssen; dabei hilft uns diese Tastenkombination.

 

Copy with Headers

STRG + UMSCHALT (SHIFT) + C

Auch aus anderen Anwendungen bekannt funktioniert natürlich STRG + C für Copy; im Resultfenster angewendet wird damit allerdings nur der Tabelleninhalt kopiert. Möchte man auch die Spaltenüberschriften kopieren (um das Ergebnis beispielsweise in eine Excel-Tabelle zu übernehmen), dann geht das mit dieser Tastenkombination.

 

Vorschläge anzeigen lassen

STRG + Leertaste

IntelliSense funktioniert auch in SSMS relativ zuverlässig. Wenn man einmal keine Vorschläge angezeigt bekommt bzw. die Vorschläge weggeklickt hat und sie wieder anzeigen möchte, dann mit dieser Tastenkombination.

 

Toggle zwischen den letzten Einträgen im Zwischenspeicher

STRG + UMSCHALT (SHIFT) + V

Wir können nicht nur mit STRG + V einfügen, was wir eben kopiert oder ausgeschnitten haben, sondern wir können mit STRG + UMSCHALT + V auch in umgekehrter Reihenfolge die Einträge auswählen, die wir davor kopiert hatten (last in first out-Prinzip).

 

Finden und Ersetzen

STRG + F

Damit können wir bestimmte Zeichenfolgen (Text oder auch Zahlen oder Sonderzeichen) finden.

STRG + G

Damit finden und springen wir in eine bestimmte Zeile (hilfreich, wenn wir beispielsweise an die Stelle springen wollen, an der uns eine Fehlermeldung angezeigt wird).

STRG + H

Auch nicht ganz so intuitiv, wie man sich das vielleicht wünschen würde: Mit dieser Tastenkombination können wir im Code die gesuchten Einträge finden und durch neue ersetzen.

STRG + END

Damit springen wir ganz zum Ende des aktuellen Queryfensters.

STRG + POS1

Damit springen wir ganz an den Anfang des aktuellen Queryfensters.

 

Markieren

UMSCHALT (SHIFT) + ENDE

Mit dieser Tastenkombination markieren wir alles von der aktuellen Caret-Position bis zum Ende der Zeile.

UMSCHALT (SHIFT) + POS1

Mit dieser Tastenkombination markieren wir alles von der aktuellen Caret-Position bis zum Anfang der Zeile.

UMSCHALT (SHIFT) + Pfeiltaste nach oben/unten

Mit dieser Tastenkombination markieren wir alles von der aktuellen Caret-Position bis zur nächsten Zeile darüber oder darunter.

 

Groß-/Kleinschreibung

STRG + UMSCHALT (SHIFT) + U

Mit STRG + SHIFT + U (U für „uppercase“) wird der markierte Text in Großbuchstaben geschrieben. Das bezieht sich nur auf den Text im Queryfenster, selbstverständlich hat das keine Auswirkung auf die Textausgabe oder gar auf die Datenbank.

STRG + UMSCHALT (SHIFT) + L

Die andere Variante: L für „lowercase“ verwandelt das Markierte in Kleinbuchstaben. Auch das betrifft selbstverständlich nur den Text im Editor.

 

Lesezeichen (Bookmarks) setzen

STRG + K

Irgendwann ist die Anzahl an Tastenkombinationen erreicht, die man nur mit STRG und Buchstaben errreichen kann; SSMS kennt aber wesentlich mehr. Also kam man auf die Idee mit STRG + K. Drücken wir STRG + K, erscheint links unten in der Statusleiste die Meldung „(CTRL + K) was pressed. Waiting for second key of chord…“

 

Wir drücken also STRG + K, halten STRG weiterhin gedrückt, und jetzt kommt das eigentliche Kommando dazu.

STRG + K, STRG + K

Damit erstellen wir in der aktuellen Zeile ein Lesezeichen, erkennbar an einem kleinen schwarzen Fähnchensymbol links außen neben der Zeile.

Zwischen diesen Lesezeichen können wir leicht hin- und hernavigieren:

STRG + K, STRG + N

Damit springen wir zum nächsten („next“) Bookmark (oder eben Lesezeichen).

STRG + K, STRG + P

Damit springen wir zum vorherigen („previous“) Bookmark.

Ein nicht mehr benötigtes Lesezeichen können wir wieder mit STRG + K, STRG + K entfernen.

 

Kommentare

STRG + K, STRG + C

STRG + K drücken, STRG gedrückt halten und C eingeben: Damit wird das Markierte auskommentiert. C steht dabei für „comment“.

STRG + K, STRG + U

Damit können wir das wieder rückgängig machen: U wie „uncomment“ verwandelt ein Kommentar wieder in Normaltext.

 

 

Für diejenigen, denen die englischen Abkürzungen geläufiger sind:

STRG = CTRL

UMSCHALT = SHIFT

 

 

Schnelle Übersicht:

 

Tastenkombination

Ausgeführte Aktion

F5

STRG + E

ALT + X

Markiertes wird ausgeführt (oder die gesamte Query, wenn nichts markiert wurde)

STRG + F

Finden von Zeichen (Text, Zahlen etc.) im Code

STRG + Leertaste

Vorschläge anzeigen

STRG + N

Öffnet neues Queryfenster

STRG + R

Ein-/Ausblenden des Resultfensters

STRG + UMSCHALT + C

Copy with Headers

STRG + UMSCHALT + V

Toggle zwischen den letzten Einträgen im Zwischenspeicher

STRG + G

Finden und Springen zu bestimmter Zeile

STRG + H

Finden und Ersetzen

STRG + ENDE

Springen ans Ende des aktuellen Queryfensters

STRG + POS1

Springen an den Anfang des aktuellen Queryfensters

UMSCHALT + ENDE

Markieren bis zum Ende der Zeile

UMSCHALT + POS1

Markieren bis zum Anfang der Zeile

UMSCHALT + Pfeiltaste

Markieren bis zur Zeile darüber oder darunter

STRG + UMSCHALT + U

Markiertes in Großbuchstaben („uppercase“)

STRG + UMSCHALT + L

Markiertes in Kleinbuchstaben („lowercase“)

STRG + K, STRG + K

Lesezeichen (Bookmark) setzen oder entfernen

STRG + K, STRG + N

Zum nächsten Lesezeichen (Bookmark) springen

STRG + K, STRG + P

Zum vorherigen Lesezeichen (Bookmark) springen

STRG + K, STRG + C

„comment“ – Markiertes auskommentieren

STRG + K, STRG + U

„uncomment“ – Markiertes nicht mehr als Kommentar darstellen

 


Solche und weitere Tipps gibt es auch in unseren Kursen zum Thema SQL!

Datenbank-Pages – die missverstandenen 8060 Byte

$
0
0

Im Allgemeinen kann man sich merken, dass eine Seite (page), die Datenbankspeichereinheit, rund 8 KB Speicherplatz für unsere Daten hat. Wenn man mit einem ungefähren Wert zufrieden ist, dann passt das so. Wer es aber ein bisschen genauer wissen möchte, ist irgendwann schon auf die Zahl 8060 Byte gestoßen.

Wenn man dann 8192 – 8060 (Daten) – 96 (header) rechnet, bleiben einem verwirrende 36 Byte übrig. Das liegt daran, dass diese Rechnung falsch ist.

Fälschlicherweise wird oft davon ausgegangen, dass wir maximal 8060 Byte zum Abspeichern unserer Daten auf einer Seite (page) zur Verfügung hätten – aber wenn wir es ganz genau nehmen, ist das nicht korrekt.

8060 Byte ist die Anzahl an Byte, die eine Zeile (ein Datensatz) maximal haben darf. Auf eine Seite passen tatsächlich noch ein paar wenige Byte mehr.

Sehen wir uns die Seite einmal genauer an:

Insgesamt sind es 8192 Byte; davon entfallen 96 Byte fix auf den page header. Jede Zeile erzeugt 7 Byte overhead und 2 Byte row offset. Bei einem einzelnen Datensatz hätten wir also theoretisch 8087 Byte für Daten zur Verfügung – der darf aber maximal 8060 Byte haben, 27 Byte würden in diesem Fall einfach leer bleiben.

Theoretisch können wir eine Page aber zu 100% befüllen; wenn wir mindestens zwei Datensätze haben.

8192 – 96 (page header) – 4 (2 x 2 row offset) – 14 (2 x 7 overhead) = 8078

Bei zwei Datensätzen dürften wir insgesamt 8078 Byte für Daten auf einer einzelnen Page verwenden. Bei mehr Datensätzen wird es um jeweils 9 Byte (2 row offset + 7 overhead) pro Zeile weniger.

Die 8060 Byte würden also theoretisch mit 4 Datensätzen zu jeweils 2015 Byte stimmen, wenn wir eine Seite zu 100% befüllen wollen.

8060 (2015 x 4 Daten) + 8 (4 x 2 row offset) + 28 (4 x 7 overhead) + 96 (page header) = 8192

Wenn wir mehr als 4 Datensätze haben, bleibt für die Daten entsprechen weniger Platz.

In der Theorie würde die Rechnung bei 100 Datensätzen, die auf eine Seite passen sollen, so aussehen:

8192 – 200 (100 x 2 row offset) – 700 (100 x 7 overhead) – 96 (page header) = 7196

Wir hätten also 7196 Byte für Daten zur Verfügung. Das würde bedeuten, jeder unserer Datensätze dürfte noch 71 Byte lang sein; 96 Byte würden freibleiben. Wären unsere Datensätze länger als 71 Byte, würden keine 100 davon mehr auf eine Seite passen.

(In der Praxis würde die Datenbank allerdings ab einer Befüllung von 81% entscheiden, es ist Zeit für eine neue Seite, es sei denn, man arbeitet mit Clustered Index und fillfactor oder verwendet ein einziges INSERT statement.)

Meistens wäre für uns die umgekehrte Überlegung interessanter: Mein Datensatz ist n Byte lang, wieviele davon passen auf eine Seite?

Die unerklärlichen übrigbleibenden 36 Byte aus der ersten Rechnung ergeben sich aus einer falschen Annahme. Fix reserviert sind nur die 96 Byte für den header, 2 Byte row offset pro Zeile und 7 Byte overhead pro Zeile.

 

Genug Theorie – sehen wir uns das Ganze mit Datenbankabfragen an:

Test: Passen tatsächlich mehr als 8060 Byte an Daten auf eine Seite?

Laut der Rechnung von oben haben wir maximal 8078 Byte zur Verfügung:

8192 – 96 (page header) – 4 (2 x 2 row offset) – 14 (2 x 7 overhead) = 8078

 

Wir erstellen eine Testdatenbank:

 

CREATEDATABASE PageTestDB

 

USE PageTestDB

 

Wir brauchen auch eine Tabelle.

Wir wissen bereits, dass ein Datensatz (eine Zeile) maximal 8060 Byte haben darf; um die 8078 Byte nützen zu können, brauchen wir also zwei Datensätze zu je 4039 Byte (8078 / 2).

 

CREATETABLE Test0(Testtext char(4039))

INSERTINTO Test0(Testtext)

VALUES('ABC'),('DEF')

 

Zwar haben wir jeweils nur 3 Zeichen eingefügt, aber durch die Verwendung des Datentyps char werden die nicht verbrauchten Byte mit Leerzeichen aufgefüllt.

 

Jetzt müssen wir die PageID herausfinden:

 

DBCC TRACEON(3604)

DBCC IND('PageTestDB','Test0',-1)

 

 

Wir sehen, dass 1 Datapage mit der ID 264 erstellt worden ist:

 

Bei der Page mit der PagePID 80 handelt es sich um eine IAM-Page (Index Allocation Map); wir sehen bei IAMPID dass unsere Daten-Seite dieser IAM-Page zugeordnet ist. Die für uns interessante Seite ist die mit der ID 264.

Jetzt sehen wir uns im Header an, wieviele Einträge auf unserer Seite sind und wie viel Speicherplatz noch frei ist – wenn wir insgesamt nur 8060 Byte belegen dürften, müsste ja der zweite Eintrag auf einer neuen Seite abgespeichert sein.

 

DBCCPAGE('PageTestDB', 1, 264)

 

Zwei Informationen aus dem Header sind für uns interessant:

 

m_slotCnt = 2

m_freeCnt = 0

 

m_slotCnt sagt uns, wie viele Einträge auf der Seite sind (es sind tatsächlich beide unserer Einträge hier abgespeichert) und m_freeCnt sagt uns, wie viele Byte noch frei sind (keine).

Auch die Auslastung der Seite in Prozent können wir noch abfragen, wenn wir es noch nicht glauben wollen:

 

DBCC SHOWCONTIG('Test0')

 

Auch mit diesem Befehl können wir feststellen, zu wieviel Prozent unsere Seite beschrieben ist und wie viel freier Speicherplatz noch besteht:

 

 

Theoretisch haben wir auf einer Seite (page) also 8078 Byte für unsere Daten zur Verfügung. In der Realität ist es aber tatsächlich weniger. Obwohl ein Datensatz in den meisten Fällen deutlich weniger als 4039 Byte beansprucht, wird der Platz, den wir zur Verfügung haben, pro Zeile um 9 Byte kleiner – denn pro Zeile fallen 2 Byte row offset und 7 Byte overhead an.

 

Viel Spaß beim Austesten (ich weiß doch, dass man so etwas selbst probieren muss)!

Vielleicht sehen wir uns ja in einem unserer Kurse zum Thema SQL!

 

Was sind eigentlich… SET-Operatoren? UNION, INTERSECT und EXCEPT mit Beispielen

$
0
0

Als SQL-Einsteiger kommt man recht schnell an den Punkt, wo man Informationen aus mehr als einer Tabelle abfragen möchte, und dafür lernen wir zunächst einmal JOINS. Dabei geht es aber darum, Verknüpfungen über übereinstimmende Felder zu erstellen und die Ergebnismenge über die Art der Verknüpfung einzuschränken.

Manchmal wollen wir jedoch nicht notwendigerweise zusammenhängende Daten miteinander vergleichen oder listenartig ausgeben – und dafür verwenden wir die Set-Operatoren.

SET-Operatoren in T-SQL sind UNION, UNION ALL, INTERSECT und EXCEPT.

Wenn jemand schon einmal von einem MINUS-Operator gehört hat: stimmt, den gibt es auch, aber nicht bei uns in SQL Server. MINUS erfüllt beispielsweise in Oracle die gleiche Funktion wie EXCEPT in den meisten anderen großen Datenbanksystemen.

Die Grundfunktionen sind schnell erklärt:

Mit UNION können wir Ergebnisse aus zwei (theoretisch) voneinander unabhängigen SELECT-Statements untereinander ausgeben (dazu gleich mehr), INTERSECT zeigt uns, welche Übereinstimmungen es in den beiden Abfragen gibt und EXCEPT sagt uns, wo die Unterschiede liegen. Das müssen wir uns selbstverständlich noch etwas näher ansehen.

 

UNION und UNION ALL

 

Wir wollen beispielsweise eine Liste aller Kontaktpersonen und deren Telefonnummern ausgeben. Für dieses erste Beispiel verwenden wir die Northwind Beispieldatenbank.

Kontaktpersonen gibt es in der Customers- und in der Suppliers-Tabelle.

 

Mit einem JOIN würden wir keine Liste der Kontaktpersonen bekommen, sondern vier Spalten, jeweils eine für ContactName und Phone aus der Customers-Tabelle und jeweils eine für ContactName und Phone aus der Suppliers Tabelle. Weiters hätten wir mit einem INNER JOIN das Problem, dass nur die Kunden, die schon etwas bestellt, und nur die Supplier, die schon etwas geliefert haben, in unserer Übersicht aufscheinen würden. Und wir müssten fünf Tabellen miteinander verjoinen, von denen wir die meisten gar nicht brauchen. Also keine gute Idee.

UNION kann das für uns erledigen:

Wir machen ein SELECT-Statement für die Informationen, die uns an der Customers-Tabelle interessieren, und das Gleiche für die Suppliers-Tabelle.

Beides wird mit dem UNION-Operator verknüpft:


SELECT       ContactName

           , Phone

FROM Customers

UNION

SELECT       ContactName

           , Phone

FROM Suppliers

 

Wir erhalten eine Spalte mit allen ContactNames und eine mit allen Telefonnummern, unabhängig von der Tabelle, aus der sie stammen.

Dabei wird die Spaltenüberschrift aus der ersten Abfrage beibehalten. Wir dürfen hier auch ein ALIAS vergeben. Wären die Spaltenüberschriften in der zweiten Abfrage anders, würden sie die ersten nicht überschreiben.

 

Ein paar Dinge gibt es zu beachten:

  1. Wir müssen in allen beteiligten SELECT-Statements die gleiche Anzahl an Spalten verwenden, wobei wir allerdings fehlende Spalten mit NULL oder mit eigenem Text auffüllen dürften.
  2. In eine Spalte dürfen nur gleiche oder implizit konvertierbare Datentypen. Selbstverständlich können wir auch in die entsprechenden Datentypen konvertieren, wenn wir das aus irgendeinem Grund unbedingt so wollen.
  3. UNION macht auch ein DISTINCT; Mehrfacheinträge werden also nicht ausgegeben.
  4. ORDER BY kann nicht vor dem UNION-Befehl kommen. Wir können aber das Gesamtergebnis nach einer bestimmten Spalte ordnen.

 

Zu Punkt 1: Gleiche Anzahl an Spalten

Das funktioniert nicht:


SELECT       CustomerID

           , ContactName

           , Phone

FROM Customers

UNION

SELECT       ContactName

           , Phone

FROM Suppliers


Im oberen SELECT-Statement werden drei, im unteren nur zwei Spalten abgefragt. Wir bekommen eine Fehlermeldung.

Folgendes wäre möglich:

 

SELECT       CustomerID

           , ContactName

           , Phone

FROM Customers

UNION

SELECT       NULL

           , ContactName

           , Phone

FROM Suppliers


Dabei müssten wir aber von Fall zu Fall die Sinnhaftigkeit, eine Spalte mit NULL aufzufüllen, abwägen.

 

Zu Punkt 2: gleiche Datentypen

Das funktioniert nicht:

SELECT       CustomerID

           , ContactName

           , Phone

FROM Customers

UNION

SELECT       SupplierID

           , ContactName

           , Phone

FROM Suppliers


Wir haben zwar nun die gleiche Anzahl der Spalten und auf den ersten Blick sieht alles gut aus, allerdings ist im Falle der Northwind Datenbank die CustomerID ein nchar(5), die SupplierID hingegen ein int. Somit dürfen die beiden nicht in derselben Spalte stehen, es sei denn, wir würden die SupplierID explizit in einen String-Datentyp konvertieren.

 

Zu Punkt 3: UNION macht ein DISTINCT

Wenn wir eine Liste von irgendetwas ausgeben wollen, sind Mehrfacheinträge oft unerwünscht. UNION verhindert diese; wenn wir Mehrfacheinträge aber angezeigt bekommen möchten, verwenden wir statt UNION ein UNION ALL.

Wenn wir in einem konkreten Fall 100%ig sicher sein können, dass es dort keine Mehrfacheinträge gibt, können wir auch mit UNION ALL arbeiten, wenn wir keine Mehrfacheinträge wollen, denn UNION ALL ist etwas schneller. UNION muss ja im Hintergrund für uns noch eine Überprüfung anstellen, ob Mehrfacheinträge vorhanden sind und diese dann ausschließen. Dieser Arbeitsschritt fällt bei UNION ALL weg.

 

Zu Punkt 4: UNION und ORDER BY

Wollen wir unser Ergebnis nach einer bestimmten Spalte ordnen, können wir das tun, aber das ORDER BY gilt für alles, was mit dem UNION- oder UNION ALL-Operator verknüpft wurde. Nur im oberen Teil ein ORDER BY oder im ersten und zweiten SELECT-Statement nach einer anderen Reihenfolge sortiert funktioniert nur unter Zuhilfenahme von temporären Tabellen oder Subselect (Subquery, Unterabfrage).

So dürfte ein ORDER BY mit UNION aussehen:


SELECT       ContactName

           , Phone

FROM Customers

UNION

SELECT       ContactName

           , Phone

FROM Suppliers

ORDERBY ContactName


Damit ordnen wir alphabetisch nach Kontaktnamen.

 

An einem stark vereinfachten Beispiel können wir am besten sehen, wie UNION, UNION ALL, INTERSECT und EXCEPT funktionieren:

 

Wir erstellen eine Mini-Test-Datenbank mit zwei „Tabellen“ mit jeweils einer Spalte:


CREATEDATABASE Demo

 

USE Demo

 

 

CREATETABLE A(Spalte1 int)

 

CREATETABLE B (Spalte1 int)

 

Diese beiden Tabellen befüllen wir mit ein paar Werten:


INSERTINTO A(Spalte1)

VALUES (NULL),(1),(2),(1)

 

INSERTINTO B(Spalte1)

VALUES (NULL),(1),(3),(1)

 

In unserer Tabelle A befinden sich jetzt die Werte 1, 2, nochmals 1 und NULL.

In unserer Tabelle B befinden sich jetzt die Werte 1, 3, nochmals 1 und NULL.

Wir können also das Verhalten der Se-Operatoren bei Mehrfacheinträgen, unterschiedlichen Werten, gleichen Werten und leeren Werten testen.

 

Wie verhält sich UNION?


SELECT*

FROM A

UNION

SELECT*

FROM B


Unser Ergebnis zeigt:

 


Wir erhalten alle Einträge aus der (einzigen) Spalte unserer Tabellen, NULL wird ausgegeben, und da UNION auch ein DISTINCT macht, wird NULL und 1 nur einmal angezeigt.

 

Wie verhält sich UNION ALL?


SELECT*

FROM A

UNIONALL

SELECT*

FROM B

 

Unser Ergebnis zeigt:

 


Alles wird ausgegeben, auch wenn ein Wert schon irgendwo vorkommt. Der Wert 1 steht sogar viermal da, denn er steht zweimal in Tabelle A und zweimal in Tabelle B.

 

Wie verhält sich INTERSECT?


SELECT*

FROM A

INTERSECT

SELECT*

FROM B

 

Unser Ergebnis zeigt:

 


INTERSECT zeigt uns nur die Werte (oder auch NULL), die in beiden Tabellen vorkommen. Auch hier wird ein DISTINCT gemacht. Und die Werte 2 und 3, die jeweils nur in einer Tabelle vorkommen, werden nicht ausgegeben.

 

Wie verhält sich EXCEPT?


SELECT*

FROM A

EXCEPT

SELECT*

FROM B

 

Unser Ergebnis zeigt:

 


EXCEPT zeigt uns das, was in Tabelle A, aber nicht in Tabelle B vorkommt. Wollen wir wissen, was in Tabelle B, aber nicht in Tabelle A vorkommt, drehen wir einfach die Reihenfolge der Abfrage um.

 

Das und noch viel mehr gibt es auch in unseren Kursen zumThema SQL– inzwischen wünsche ich viel Spaß beim Ausprobieren!

 


SQL – subtile Unterschiede in Serverfunktionen Teil 1

$
0
0

Oft hat man schon teilweise jahrelang mit einer bestimmten Serverfunktion gearbeitet, um dann durch Zufall herauszufinden, es gibt auch eine andere, die zumindest scheinbar genau das Gleiche erledigt. Ein paar dieser AHA-Erlebnisse und einen kurzen Überblicküber Ähnlichkeiten und Unterschiede gibt es hier:

Plus oder CONCAT? CONCAT oder CONCAT_WS? PARSE, CAST oder CONVERT? Ein Kurzüberblick über die Unterschiede und einfache Beispiele.


+, CONCAT und CONCAT_WS

 

Oft wird zum Zusammenfügen von Strings einfach ein Plus-Zeichen verwendet. In vielen Fällen bekommen wir damit auch exakt das gleiche Ergebnis heraus, wie mit CONCAT. Wo liegen also die Unterschiede?

 

Zum Testen erstellen wir erst eine Demo-Tabelle mit Vorname, Nachname und Titel und geben ein paar Testwerte ein:

CREATETABLE demo(FirstName varchar(25), LastName varchar(25), Title varchar(10))

 

INSERTINTO demo(FirstName, LastName, Title)

VALUES        ('John','Doe',NULL),

              ('Jane','Doe','Dr.'),

              ('Robin','Hood','Mr.')

Wollen wir nun Vor- und Nachname in einer Spalte als FullName ausgeben, könnten wir das Plus-Zeichen oder CONCAT verwenden:

SELECT FirstName +' '+ LastName AS FullName

FROM demo

 

     -- oder:

 

SELECTCONCAT(FirstName,' ', LastName)AS FullName

FROM demo

Wir haben hier auch jeweils ein Leerzeichen zwischen Vor- und Nachname eingefügt.

In beiden Fällen ist unser Ergebnis gleich:

Wollen wir aber den Titel mit ausgeben, sieht es etwas anders aus:

SELECT Title +' '+ FirstName +' '+ LastName AS FullName

FROM demo

Ergebnis:

 

Jede mathematische Operation mit NULL führt wieder zu NULL. Da für den ersten Eintrag kein Titel existiert, führt unser „NULL + Text“ zu NULL.

Mit CONCAT funktioniert es, ist aber noch nicht ideal:

SELECTCONCAT(Title,' ', FirstName,' ', LastName)AS FullName

FROM demo

Ergebnis:

 

Immerhin steht der Text da; schaut man allerdings genauer hin, fällt auf, dass unser John Doe etwas eingerückt ist, denn hier haben wir davor ein Leerzeichen eingefügt. Unsere CONCAT-Funktion hat außerdem den Nachteil, dass wir zwischen jeden Eintrag händisch ein Leerzeichen setzen müssen.

Dafür gibt es eben noch CONCAT_WS. Das „WS“ steht für „with separator“; wir können damit also einmal ein Trennzeichen angeben, dass dann zwischen alle einzelnen Einträge gesetzt wird:

SELECTCONCAT_WS(' ', Title, FirstName, LastName)AS FullName

FROM demo

Ergebnis:

Damit haben wir vor „John“ kein Leerzeichen, denn die Leerzeichen werden nur als Trennzeichen gesetzt. NULL wird korrekt als „nicht vorhanden“ eingestuft und bekommt somit auch keinen Separator.

Auch, wenn wir mehrere Einträge durch einen Separator trennen wollen, empfiehlt sich die Verwendung von CONCAT_WS, um nicht jedes Trennzeichen händisch angeben zu müssen; der gewünschte Separator wird nur einmal am Anfang angegeben:

SELECTCONCAT_WS(' ','Ich','weiß,','dass','ich','nichts','weiß.')AS Zitat

Ergebnis:

 


PARSE, CAST oder CONVERT?

 

CAST und CONVERT sind die beiden schon länger bekannten Serverfunktionen zum Konvertieren eines Datentyps in einen anderen. Die Syntax bei den beiden ist etwas anders:

 

-- mit CAST:

SELECTCAST('123'ASint)+ 2 AS Ergebnis

 

-- mit CONVERT

SELECTCONVERT(int,'123')+ 2 AS Ergebnis


Beide geben aber das gleiche Ergebnis aus:

Welche der beiden zu bevorzugen ist, ist eine der vielen Streitfragen in SQL, bei der auch mit Begriffen wie bessere Lesbarkeit argumentiert wird (und das sieht jeder ein bisschen anders). Auch die Performance kann situationsabhängig variieren und muss von Fall zu Fall getestet werden; die Performance-Unterschiede zwischen den beiden scheinen aber größtenteils vernachlässigbar zu sein.

Zwei erwähnenswerte Unterschiede gibt es zwischen diesen beiden Serverfunktionen:

  1. CAST ist ANSI, CONVERT ist serverspezifisch.
  2. CONVERT akzeptiert einen Style-Parameter, CAST nicht.

 

Über den Style-Parameter von CONVERT können wir beim Arbeiten mit Datum dafür sorgen, dass es in menschenleserlichem Format ausgegeben wird. Setzen wir als Style-Parameter 104, erhalten wir das Datum in deutscher Schreibweise:

SELECTCONVERT(varchar,GETDATE(), 104)AS Datum

Ergebnis:

 

 CAST allein kann das nicht; mit CAST können wir nur Datentypen konvertieren, keinen Style angeben.

 

PARSE ist ein jüngerer Verwandter der beiden. Auch mit PARSE können wir Datentypen konvertieren, und im ersten Moment scheint es so, als würden PARSE und CAST gleich funktionieren.

PARSE ist allerdings mit Vorsicht zu genießen. Ein Auszug aus der Microsoft-Dokumentation gibt uns einige Gründe an, warum: 

„Verwenden Sie PARSE nur, um eine Zeichenfolge in ein Datum und eine Uhrzeit oder in eine Zahl zu konvertieren. Für die allgemeine Typkonvertierungen sollten Sie auch weiterhin CAST oder CONVERT verwenden. Bedenken Sie, dass die Analyse des Zeichenfolgenwerts mit gewissen Leistungseinbußen verbunden ist.“

Übersetzt: Wenn überhaupt, dann ist es auch noch langsamer als die anderen beiden.

Und:

 „Für PARSE muss die .NET Framework-Common Language Runtime (CLR) vorhanden sein.“

 

Genaueres gibt es natürlich in der Microsoft-Dokumentation.


Wozu gibt es PARSE dann überhaupt? PARSE kennt auch einen Culture-Parameter (zumindest, wenn CLR vorhanden ist). Das kann für das Datum praktisch sein, oder für Text zu Zahl:

SELECTPARSE('€123,45'ASmoneyUSING'de-DE')AS Preis

Ergebnis:

 

CAST hingegen würde bei diesem Beispiel versagen:

SELECTCAST('€123,45'ASmoney)AS Preis

Ergebnis:

 

Trotzdem: Wenn wir es nicht für irgendeinen Sonderfall brauchen, sollten wir lieber bei CAST und CONVERT bleiben.

Auch mit FORMAT können wir über den Culture-Parameter eine schöne Datums-Formatierung erreichen, allerdings kann FORMAT für uns keine Konvertierung vornehmen. Mehr zum Thema Datum und Datumsformatierung gibt es hier.

 

Mehr dazu gibt es auch in unseren Kursen!

In der Zwischenzeit: Viel Spaß beim Ausprobieren!

SQL - Subtile Unterschiede in Serverfunktionen Teil 2

$
0
0

Oft hat man schon teilweise jahrelang mit einer bestimmten Serverfunktion gearbeitet, um dann durch Zufall herauszufinden, es gibt auch eine andere, die zumindest scheinbar genau das Gleiche erledigt. Ein paar dieser AHA-Erlebnisse und einen kurzen Überblick über Ähnlichkeiten und Unterschiede gibt es hier:

Im ersten Teil haben wir uns angesehen, worin der Unterschied zwischen der Verwendung des Plus-Zeichens und der CONCAT-Funktion bzw. CONCAT und CONCAT_WS besteht, bzw. wofür und wie PARSE, CAST und CONVERT verwendet werden.

 

In diesem Teil beschäftigen wir uns mit ISNULL vs. COALESCE, CHARINDEX vs. PATINDEX und TRANSLATE vs. REPLACE.

 

ISNULL und COALESCE

 

Sowohl ISNULL als auch COALESCE sollen uns helfen, etwas in unsere Textausgabe zu schreiben, wenn in einem Feld kein Wert enthalten ist (der Eintrag also NULL ist).

Wo liegen die Unterschiede? Eine kleine Testtabelle kann uns helfen:

CREATETABLE Hunde(Id intidentity, Hund nvarchar(30), Hunderasse nvarchar(30), Fell nvarchar(30))

 

INSERTINTO Hunde(Hund, Hunderasse, Fell)

VALUES     ('Tasso','DSH',NULL),

           ('Waldi','Dackel','kurz'),

           ('Mina',NULL,'mittellang'),

           ('Luna',NULL,NULL)


Wir fragen die Hunderasse ab, und für den Fall, dass keine eingetragen ist, können wir etwas eintragen, z.B. „Mischling“:

-- mit ISNULL:

SELECTISNULL(Hunderasse,'Mischling')AS Hunderasse

FROM Hunde

 

-- mit COALESCE:

SELECTCOALESCE(Hunderasse,'Mischling')AS Hunderasse

FROM Hunde


Bei beiden Abfragen erhalten wir das gleiche Ergebnis:


Aber angenommen, wir wollen keinen Text eingeben, sondern beispielsweise die Kategorie 1, dann sieht das Ergebnis plötzlich anders aus.

-- mit ISNULL:

SELECTISNULL(Hunderasse, 1)AS Hunderasse

FROM Hunde


Ergebnis:


Erwartungsgemäß wird NULL mit unserem Eintrag (1) ersetzt.

Bei COALESCE bekommen wir allerdings eine Fehlermeldung:

 -- mit COALESCE:

SELECTCOALESCE(Hunderasse, 1)AS Hunderasse

FROM Hunde

 

Ergibt: „Conversion failed when converting the nvarchar value 'DSH' to data type int.

 

Daran erkennen wir schon einen ersten Unterschied zwischen den beiden Funktionen. ISNULL evaluiert den ersten Ausdruck, in unserem Fall ein nvarchar, und entscheidet, dass man 1 doch auch zu Text machen kann.

COALESCE evaluiert alle Ausdrücke und entscheidet sich für die höchste Präzedenz (Rangfolge; engl.: precedence). In diesem Fall „gewinnt“ der Integerwert.

Mehr zum Thema Rangfolge von Datentypen gibt es in derMicrosoft-Dokumentation.


Noch einen anderen Unterschied zwischen den beiden Funktionen können wir ganz leicht an unserem Beispiel beobachten. ISNULL kann nur eine, COALESCE mehrere Spalten überprüfen.

ISNULL ersetzt alle NULL innerhalb der ausgewählten Spalte mit unserem selbstdefinierten Eintrag; COALESCE kann diese Überprüfung für mehrere Spalten ausführen. Dabei wird der zweite Ausdruck übernommen, wenn im ersten NULL steht, der dritte, wenn in den beiden ersten NULL steht und so weiter. Das ist möglich, weil COALESCE im Hintergrund ein CASE-Statement verwendet.

 

Bei unserem Hundesalon-Beispiel würden wir mit ISNULL eine Fehlermeldung bekommen, wenn wir mehr als 2 Parameter übergeben:

 SELECTISNULL(Hunderasse, Fell,'unbekannt')AS Fell
     , Fell
     , Hunderasse
FROM Hunde


Fehlermeldung: The isnull function requires 2 argument(s).

 

Im SSMS wird unser SELECT-Statement auch gleich unterwellt und auch der Tooltip weist uns darauf hin, dass hier nur 2 Parameter (2 arguments) erwartet werden.

Mit COALESCE dürfen wir so viele Spalten überprüfen, wie wir wollen:

SELECTCOALESCE(Fell, Hunderasse,'unbekannt')AS Fell

     , Fell

     , Hunderasse

FROM Hunde


Das bedeutet, wenn keine Felllänge eingetragen ist, dann soll einfach die Hunderasse ausgegeben werden, wenn weder Felllänge noch Hunderasse eingetragen sind, dann soll „unbekannt“ ausgegeben werden:


Beide haben natürlich ihre Berechtigung, und man muss von Fall zu Fall entscheiden, welche der beiden Funktionen dafür besser geeignet ist. Haben wir aber eine Anforderung, die sich mit beiden Funktionen lösen lässt, dann besser mit ISNULL: Im direkten Vergleich ist ISNULL etwas schneller, da im Hintergrund kein CASE abläuft.

 



CHARINDEX und PATINDEX



Wieder zwei von jenen Funktionen, die recht ähnlich klingen und ähnlich, aber eben nicht gleich, verwendet werden. Tatsächlich sind sie für unterschiedliche Anwendungsbereiche und Lösungen gedacht.

 

Mit CHARINDEX suchen wir nach dem ersten Vorkommnis eines bestimmten Zeichens.

 SELECT CHARINDEX('a','Donaudampfschifffahrtsgesellschaft')AS [Stelle von "A"]

Ergebnis:


In „Donaudampfschifffahrtsgesellschaft“ kommen noch weitere „a“ vor, uns wird von CHARINDEX aber nur die erste Stelle angegeben, an der das gesuchte Zeichen (oder auch die gesuchte Zeichenfolge) auftritt.

Was CHARINDEX im Gegensatz zu PATINDEX noch zu bieten hat, ist ein dritter, optionaler Parameter: start_location. Wir können angeben, ab welcher Stelle gesucht werden soll. Uns interessieren beispielsweise nicht die As in „Donaudampfschiff“, wir wollen aber feststellen, ob es danach noch weitere gibt, dann können wir angeben, dass nach „a“ erst ab der Stelle 17 gesucht werden soll:

SELECTCHARINDEX('a','Donaudampfschifffahrtsgesellschaft', 17)AS [Stelle von "A"]


Ergebnis: 18 (das „a“ in „fahrt“).

 

Auch mit PATINDEX können wir nach unserem „a“ suchen. Allerdings müssen wir das ein wenig anders schreiben. Wenn wir es genauso machen, wie bei CHARINDEX, geht es schief:

SELECTPATINDEX('a','Donaudampfschifffahrtsgesellschaft')AS ["A" mit PATINDEX]


 Ergebnis:


Es scheint so, als käme hier kein A vor.

Das liegt daran, dass unser CHARINDEX nach genau diesem Zeichen sucht, PATINDEX hingegen nach einem Pattern – PATINDEX macht ein LIKE! Wir hätten also mit der Anweisung oben nach genau „a“ und sonst nichts gesucht. Wollen wir das erste „a“ innerhalb von „Donaudampfschifffahrtsgesellschaft“ finden, sollten wir CHARINDEX verwenden. Wollen wir es aus irgendeinem Grund unbedingt mit PATINDEX lösen, müssten wir das so schreiben:

SELECTPATINDEX('%a%','Donaudampfschifffahrtsgesellschaft')AS ["A" mit PATINDEX]


Auch damit bekommen wir wieder die erste Stelle heraus, an der sich das gesuchte Zeichen befindet (4).

Dafür kann PATINDEX aber etwas, was CHARINDEX nicht kann: Richtig – nach Patterns suchen. Da PATINDEX ein LIKE zugrunde liegt, können wir hier Wildcards verwenden.


Wir suchen entweder ein A oder ein I, das vor einem F kommt:

SELECTPATINDEX('%[ai]f%','Donaudampfschifffahrtsgesellschaft')


Ergebnis: 14 (das „i“ in „schifffahrt“).




TRANSLATE und REPLACE



Mit diesen beiden Funktionen lassen sich Zeichen austauschen/ersetzen. Hier kann es wieder einmal im ersten Moment so aussehen, als würden beide Funktionen gleich arbeiten…

 SELECT     TRANSLATE('Ship','shi','cla')AS [Translate]
       ,REPLACE('Ship','shi','cla')AS [Replace]


Ergebnis: 


Gleiches Ergebnis? Ja, aber nur zufälligerweise bei diesem konkreten Beispiel. Eine winzige Änderung zeigt, dass sich die beiden Funktionen doch anders verhalten:

SELECTTRANSLATE('Ship','his','lac')AS [Translate]
     ,REPLACE('Ship','his','lac')AS [Replace]



Ergebnis:


TRANSLATE ersetzt jeden Buchstaben aus dem zweiten Parameter mit dem korrelierenden Buchstaben aus dem 3. Parameter, also in unserem Fall „h“ mit „l“, „i“ mit „a“ und „s“ mit „c“ – unabhängig davon, wo im Input-String sie vorkommen.

REPLACE sucht nach der genauen Zeichenfolge „his“, und würde sie mit „lac“ ersetzen; da diese Zeichenfolge aber in „Ship“ nicht vorkommt, wird der ursprüngliche String ausgegeben.

Einen weiteren Unterschied sehen wir, wenn Parameter 2 und 3 unterschiedlich viele Zeichen enthalten:

SELECTTRANSLATE('Ship','sh','cla')AS [Translate]


 

TRANSLATE wirft in diesem Fall eine Fehlermeldung aus: The second and third arguments of the TRANSLATE built-in function must contain an equal number of characters.

 

REPLACE ersetzt die Zeichen „sh“ durch „cla“… mit einem mehr oder weniger erfolgreichen Ergebnis:

SELECTREPLACE('Ship','sh','cla')AS [Replace]


Ergebnis:


Vorsicht besonders bei längeren Zeichenfolgen oder mehreren Wörtern – TRANSLATE ersetzt alle gefundenen Zeichen; REPLACE nur die exakte Zeichenfolge. Das kann mit TRANSLATE auch katastrophale Folgen haben:

SELECT     TRANSLATE('Wolfgang Amadeus Mozart','Amadeus','Norbert')AS [Translate]     
              ,REPLACE('Wolfgang Amadeus Mozart','Amadeus','Norbert')AS [Replace]



Ergebnis:


REPLACE sollte also verwendet werden, um exakte Zeichenfolgen zu ersetzen; TRANSLATE eignet sich für das Auswechseln aller angegebenen Zeichen, egal, wo innerhalb der Search-Expression sie stehen.


Mehr zum Thema SQL gibt es in unseren Kursen!


In der Zwischenzeit wünsche ich wie immer viel Spaß beim Ausprobieren!




Viewing all 110 articles
Browse latest View live