SQL tips

af Joakim Dalby

 

SQL er et deklarativt forespørgsels pro­gram­me­rings­sprog, d.v.s. man angiver, hvad man ønsker at få ud af databasen fremfor at be­skri­ve, hvordan man finder det ønskede i databasen. Deklarerer betyder angiver.

»Declarative language describes what should be done, not how it should be done.«

Donald D. Chamberlin og Raymond F. Boyce er designere af den originale SQL-sprogspecifikation i 1974, læs gerne deres artikel.

 

Læs mere om historikken omkring SQL i artiklen Remembering E.F. Codd.

 

Læs mere om database design og SQL i Database-Håndbogen.

 

Læs mere om SQL funktioner i SQL database functions og i nederst venstre hjørne klik download pdf, og se gode illustrationer.

 

Mange tips på nettet   sqlshack   Statisticsparser   sqlperformance   Sql tools

 

Sommarskog   Cathrine Wilhelmsen  

 

Ola Hallengren Maintenance Solution   David Schanzer Toolbox   Brent Ozar blitz

 

Microsoft How to maintain azure sql indexes and statistics

 

Dynamisk SQL Dynamic sql   Dynamic search conditions   Arrays in sql  

 

System stored procedures   Metadata queries   Round function

 

Sql server execution plans for query  Improve sql query performance  Index usage

 

The Data Loading Performance Guide

 

Reduce deadlocks with a clustered index

 

Undgå: Warning: Null value is eliminated by an aggregate or other SET operation.

Anvend: SET ANSI_WARNINGS OFF

 

SQL Server eksempel databaser: pubs, northwind, adventure works, wide world og et datamart eksempel polaris.

 

T-SQL står for Transact-SQL Structured Query Language til bl.a. @variabel, #tabel og flow control i en stored procedure samt dynamisk sql sætninger der bliver ud­ført og execute on the fly.

 

Types of SQL Commands

·  DDL – Data Definition Language e.x. CREATE TABLE, ALTER TABLE, TRUNCATE TABLE.

·  DML – Data Manipulation Language e.x. INSERT INTO, UPDATE, DELETE.

·  DCL – Data Control Language e.x. GRANT: Give user’s access privileges to database.

·  DQL – Data Query Language e.x. SELECT.

·  TCL – Transaction Control Language e.x. SAVE TRANSACTION @SavePoint,

BEGIN TRANSACTION, COMMIT TRANSACTION, ROLLBACK TRANSACTION.

 

Anbefalet options i SQL Server Management Studio  Se options.

Får man denne fejlmelding ved design af en tabel i SQL Server Management Studio: »Saving changes is not permitted. The changes that you have made require the following tables to be dropped and re-created. You have either made changes to a table that can't be re-created or enabled the option Prevent saving changes that require the table to be re-created.« så vælg i menuen Tools, Options, åben noden Designers, klik på noden Table and Database Designers og fjern flueben i Prevent saving changes that require the table re-creation.

 

Inline Primary key, Index, References and Default creation tabel eksempel

 

CREATE TABLE dbo.Employee

(

 EmployeeId int IDENTITY(1,1) NOT NULL

   CONSTRAINT PK_dbo_Employee PRIMARY KEY CLUSTERED,

 Name nvarchar(100) NOT NULL INDEX IX_dbo_Employee_Name NONCLUSTERED (Name),

 Title nvarchar(100) NULL

   CONSTRAINT UK_Title UNIQUE NONCLUSTERED (Title ASC), -- unique key

 Code nvarchar(10) NOT NULL CONSTRAINT CK_Code CHECK (Code IN('Good','Great')),

 BirthDate date NULL

   CONSTRAINT CK_BirthDate CHECK (BirthDate <= cast(getdate() as date)),

 Age tinyint NULL

   CONSTRAINT CK_Age CHECK (Age IS NULL OR (Age IS NOT NULL AND Age >= 18)),

 DepartmentId INT NOT NULL

   CONSTRAINT FK_dbo_Employee_Department FOREIGN KEY (DepartmentId)

     REFERENCES dbo.Department (DepartmentId),

 IsEmployed bit NOT NULL

   CONSTRAINT DF_dbo_Employee_IsEmployed DEFAULT ((0)),

 InsertTime_UTC_meta datetime2(3) NOT NULL

   CONSTRAINT DF_dbo_Employee_InsertTime_UTC_meta DEFAULT (sysutcdatetime())

) ON [PRIMARY]

GO

 

Ovenstående contraint danner automatisk et indeks ellers kan det gøres:

CREATE UNIQUE NONCLUSTERED INDEX UIX_Title_dbo_Employee_Title ON dbo.Employee

 (Title ASC) WHERE (Title IS NOT NULL) -- accepts rows with duplicate null value

 ON [PRIMARY]                          -- act as an unique key (UK)

GO

 

Tilføjelse af en ny kolone/felt (der angives ikke add column):

 

ALTER TABLE dbo.Employee

ADD UpdateTime_UTC_meta datetime2(3) NULL

GO

 

Tilføjelse af en default værdi til en eksisterende kolonne/felt:

 

ALTER TABLE dbo.Employee

ADD CONSTRAINT DF_dbo_Employee_UpdateTime_UTC_meta

    DEFAULT (sysutcdatetime()) FOR UpdateTime_UTC_meta

GO

 

Tilføjelse af en ny kolonne/felt med default der udfylder alle rækkerne automatisk:

 

ALTER TABLE dbo.Employee

ADD UpdateTime_UTC_meta datetime2(3) NOT NULL

    CONSTRAINT DF_dbo_Employee_UpdateTime_UTC_meta DEFAULT (sysutcdatetime())

GO

 

Tilføjelse af en primærnøgle til en tom tabel, feltet skal være udfyldt, not null:

 

ALTER TABLE dbo.Employee

ALTER COLUMN EmployeeId int NOT NULL

GO

 

ALTER TABLE dbo.Employee

ADD CONSTRAINT PK_dbo_Employee PRIMARY KEY CLUSTERED (EmployeeId) ON [PRIMARY]

GO

 

Tilføjelse af en primærnøgle til en fyldt tabel, feltet skal være udfyldt, not null:

 

ALTER TABLE [fact].[Salg]

ADD [FactSalgId] int IDENTITY(1,1) NOT NULL

GO

 

ALTER TABLE [fact].[Salg]

ADD CONSTRAINT PK_fact­_Salg PRIMARY KEY CLUSTERED ([FactSalgId]) ON [PRIMARY]

GO

 

Alternativ til unikt indeks er en constraint hvor NULL kravet kan ikke kan opsættes og UK_Title bliver vist under Indexes under tabellen dbo.Employee i Management Studio, fordi der uden et indeks vil blive udført en tablescan som tager tid for at undersøge om indsatte titel findes i forvejen i tabellen:

 

ALTER TABLE dbo.Employee

ADD CONSTRAINT UK_Title UNIQUE NONCLUSTERED(Title) -- unique key (UK)

GO

 

Slet primærnøgle reglen:

 

ALTER TABLE [dbo].[Employee]

DROP CONSTRAINT [PK_dbo_Employee] WITH ( ONLINE = OFF )

GO

 

Slet en kolone/felt:

ALTER TABLE [dbo].[Employee] DROP COLUMN EmployeeId

 

Alternativ til constraint angivelse er efter at tabellen er blevet kreeret og med et tjek af eksisterende rækker med data i tabellen:

 

ALTER TABLE [dbo].[Employee]  WITH CHECK ADD  CONSTRAINT [CK_Code] CHECK  (([Code]='Great' OR [Code]='Good'))

GO

 

ALTER TABLE [dbo].[Employee] CHECK CONSTRAINT [CK_Code]

GO

 

ALTER TABLE [dbo].[Employee]  WITH CHECK ADD  CONSTRAINT [FK_dbo_Employee_Department] FOREIGN KEY([DepartmentId])

REFERENCES [dbo].[Department] ([DepartmentId])

GO

 

ALTER TABLE [dbo].[Employee] CHECK CONSTRAINT [FK_dbo_Employee_Department]

GO

 

Klassisk opstilling for kreering af en tabel med nogle af ekstra regler

 

CREATE TABLE [dbo].[Employee]

(

  [EmployeeId] [int] IDENTITY(1,1) NOT NULL,

  [Name] [nvarchar](100) NOT NULL,

  [Title] [nvarchar](100) NULL,

  [Code] [nvarchar](10) NOT NULL,

  [BirthDate] [date] NULL,

  [Age] [tinyint] NULL,

  [DepartmentId] [int] NOT NULL,

  [IsEmployed] [bit] NOT NULL CONSTRAINT [DF_dbo_Employee_IsEmployed]  DEFAULT ((0)),

  [InsertTime_UTC_meta] [datetime2](3) NOT NULL

  CONSTRAINT [DF_dbo_Employee_InsertTime_UTC_meta]  DEFAULT (sysutcdatetime()),

  CONSTRAINT [PK_dbo_Employee] PRIMARY KEY CLUSTERED

    (

       [EmployeeId] ASC

    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,

           ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],

  CONSTRAINT [UK_Title] UNIQUE NONCLUSTERED

    (

       [Title] ASC -- unique key (UK)

    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,

           ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

 

Andet eksempel på navngivning med prefix PK_, IX_, UIX_, DF_ og hvor

schemaname og tablename og columnname indgår i navngivningen:

 

CREATE TABLE [base].[TradingPhases]

(

       [TradingPhases_Id] [bigint] IDENTITY(1,1) NOT NULL,

       [DatetimeOfOrder_UTC] [date] NOT NULL,

       [SequenceNumber] [bigint] NOT NULL,

       [FinancialInstrument] [varchar(12)] NOT NULL,

       [TradingVenueMIC] [varchar(20)] NULL,

       [InsertTime_UTC_meta] [datetime2](3) NOT NULL,

       [RecordSource_meta_SourceFile_Log_Id] [int] NOT NULL,

 CONSTRAINT [PK_base_TradingPhases] PRIMARY KEY CLUSTERED

 (

       [TradingPhases_Id] ASC

 )

)

GO

 

CREATE NONCLUSTERED INDEX [IX_base_TradingPhases_RecordSource_meta_SourceFile_Log_Id] ON [base].[TradingPhases]

(

       [RecordSource_meta_SourceFile_Log_Id] ASC

)

GO

 

CREATE UNIQUE NONCLUSTERED INDEX [UIX_base_TradingPhases_Multiple]

ON [base].[TradingPhases]

(

       [DatetimeOfOrder_UTC] ASC, -- unique key (UK).

       [SequenceNumber] ASC,

       [FinancialInstrument] ASC

)

INCLUDE([TradingVenueMIC]) -- to cover an extra column to avoid keylookup to table.

GO

 

ALTER TABLE [base].[TradingPhases]

  ADD CONSTRAINT [DF_base_TradingPhases_InsertTime_UTC_meta] 

  DEFAULT (sysutcdatetime()) FOR [InsertTime_UTC_meta]

GO

 

Alle objekter så som tabel, felt/kolonne, primærnøgle, fremmednøgle reference, default værdi og indeks skal have et sigende navn til senere opdatering via et sql script. I SQL Server Management Studio under Object Explorer kan der ud for en tabel klikkes på + tegnet og objekttyper folder sig ud. Eksempelvis under tabellens Constraints vises default værdi objekter, og på et navn som DF__Employee_­_IsEmpl__2176D992 kan der højreklikkes og vælges Rename og indtaste et mere sigende navn på objektet for en default værdi til et felt.

 

Forslag til andre navngivning

 

Table name

har altid et schema name f.eks.:

dbo.Kunde

crm.Kampagne

salg.Pris

Start med Stort bogstav og sigende navn, undgå mellemrum ellers brug [].

CamelCase og/eller _ understreg.

Prefix i tabelnavne for sortering og/eller placering i schema.

Column name

Start med Stort bogstav og sigende navn, undgå mellemrum ellers brug [].

CamelCase og/eller _ understreg.

Jeg sætter ikke konsekvent tabelnavn som prefix til felt navnet, men undgår også at have et Status felt, så hellere LeveranceStatus, StatusForVare.

Table alias

Start med lille bogstav

brug aldrig mellemrum og [].

Common table expression CTE

;WITH

Start med lille bogstav

brug aldrig mellemrum og [].

View name

har altid et schema name

Start med Stort bogstav

Views som grænseflade oven på tabel/tabeller og for levering af data til anden database/system.

Undgå schemabinding.

Prefix i viewnavn og/eller placering i schema f.eks. kaldet v, vw, dim, fact, m.fl.

Synonyms name

Start med prefix Syn_ eller suffix _Syn

Bruges primært ved linked servers tabel og tabel fra anden database på samme server eller andre objekter.

Schema name

Start med lille bogstav som i dbo f.eks. efter emne som kunde, borger, salg, dimAktuel, dimCurrent, factHistorical.

Schema er praktisk ved brugerrettig­hedsstyring og placering af tabeller i forskellige filgrupper.

Function name

har altid et schema name

Start med Stort bogstav

Bruges primært internt i databasen til returnering af værdi eller dataset til stored procedure, view, ad hoc query.

Stored procedure name

har altid et schema name

Start med Stort bogstav

Bruges primært som grænseflade for input og output af data og beregning. Jeg kan ikke lide prefix usp, jeg an­giver et suffix der fortæller hvad hovedhandlingen er f.eks.: _Insert, _InsertUpdate, _Delete især når stored procedure navnet svarer til den tabel der bliver påvirket af handlingen.

Sequence name

har altid et schema name

Start med prefix Seq_ eller suffix _Seq

Bruges til et fortløbende nummer på tværs af flere tabeller.

 

Tabelnavn: Sagstype som svarer til et forretningsbegreb.

Tabelnavn: SagType fordi Type er en egenskab/felt/kolonne til forretningsbegrebet Sag og det er valgt at beholde kolonnenavnet Type i tabellen Sag, selvom Type er et meget generisk og generelt udtryk i forhold til Sagstype.

 

Nedenfor ser jeg hurtigt, at det er en tabel OrderDetails der joins med en CTE der er defineret ovenover select delen, ligeså er table alias holdt som forkortelse med små bogstaver. Jeg forsøger at undgå brug af [] ved at undgå mellemrum men omkring reserverede ord kan [] være praktisk så ordets farve er neutral:

 

SELECT [Year] = Year(OrderDate)

FROM dbo.OrderDetails od

     INNER JOIN max_OrderID mx ON mx.OrderID = od.OrderID

 

Metadata og dokumentation i hovedet af en stored procedure

CREATE PROC [mart].[Customer_Mart_Dim_Customer_Fact_Sales_Load]

AS

/*

============================================================

 Author:      Joakim Dalby

 Create date: 2008-02-07

 SSIS-package:Load_Customer_Mart

 SQL job:     Load_Customer_Mart_ every_hour

 Description: Moving data for customer dimension and

              sales fact from Sales_EDW to Customer_Mart.

 Changelog:

   2008-08-16 Joakim Dalby, add a vat calculation.

   2009-10-30 Charlotte Jensen, add a CustomerType.

   2014-06-05 Flemming Bohrs, change the revenue calculation.

============================================================

*/

SET NOCOUNT ON

 

Changelog or Revision History.

 

Password - adgangskode

Send ikke er password fra en applikation til en database for at kontrollere rigtigheden, fordi passwordet kan måske blive aflæst af en hacker.

I stedet applikationen UI vil hash password som sendes til databasen, og i databasen bliver det gemte password også hashed, og de to hash værdier sammenlignes i databasen f.eks. i en stored procedure. Password forlader aldrig databasen og det er kun en dba med hans rettigheder som kan se passwordet.

 

Dato søgning

Find de ordrer som er efter 14 dage fra Ordredato, eller sagt på en anden måde, at Ordredato er mere end 14 dage gammel i forhold til dagsdato:

DATEDIFF(DAY, Ordredato, GETDATE()) > 14

DATEDIFF(DAY, Fradato, Tildato) hvor Fradato normalt kommer før Tildato.

 

SELECT DATEDIFF_BIG(second, '0001-01-01 00:00:00.0000000', '2020-10-20 23:59:59.9999999');

    giver 63.738.835.199 sekunder.

SELECT DATEDIFF_BIG(day, '0001-01-01 00:00:00.0000000', '2020-10-20 23:59:59.9999999');

    giver 737.717 dage.

 

I SQL 2000 er der kun datetime og afhængig af gemning i et felt f.eks. bruges Getdate() så gemmes dato og klokkeslæt eksempelvis: 2013-12-01 12:24:14.000 hvilket kræver en konvertering af datofeltet ved udsøgning på en dato uden klok­ke­slæt eksempelvis:

WHERE CONVERT(char(10), Ordredato, 126) = '20131201'     ååååmmdd

eller dagsdato som også konverteres

WHERE CONVERT(char(10), Ordredato, 126)=CONVERT(char(10), Getdate(), 126)

 

Når en bruger indtaster en dato i et felt af datetime sættes klokkeslæt til midnat eksempelvis 2013-12-01 00:00:00.000 kan en dato direkte udsøges eksempelvis WHERE Ordredato = '20131201'

eller dagsdato som først konverteres

WHERE Ordredato = CONVERT(char(10), Getdate(), 126)

 

WHERE Ordredato BETWEEN '20131201' AND '20131231' søger med klokkeslæt 00:00:00.000 d.v.s. en Ordredato 2013-12-31 12:24:14.000 medtages ikke, så

WHERE CONVERT(char(10),Ordredato,126) BETWEEN '20131201' AND '20131231'

WHERE Ordredato >= '20131201' AND Ordredato < '20140101' så medtages til og med 2013-12-31 23:59:59.999. Til og med i går: WHERE Ordredato < Getdate().

 

Ordredato er et datetime felt i databasen med klokkeslæt f.eks.

2017-12-03 13:32:45.123 som er søndag eftermiddag.

DECLARE @FraDato as date = '20171127'

DECLARE @TilDato as date = '20171203'

WHERE Ordredato >= @FraDato AND Ordredato < DATEADD(d, 1, @TilDato)

                                                          -- d.v.s. < 20171204 00:00:00.000

WHERE Ordredato BETWEEN @FraDato AND DATETIMEFROMPARTS(YEAR(@TilDato),

MONTH(@TilDato), DAY(@TilDato), 23, 59, 59, 999)

 

I SQL 2008 kom date datatypen, og den indeholder ikke klokkeslæt, så udsøg­ning bliver mere simpel:

WHERE Ordredato = '20131201'   eller  '2013-12-01' d.v.s. yyyymmdd

Eller dagsdato som først konverteres, da Getdate fortsat giver et klokkeslæt:

WHERE Ordredato = CAST(Getdate() AS date)

 

Når Ordredato er en datetime2(7) d.v.s. indeholder måske et klokkeslæt som ikke er midnat, og jeg ønsker alle rækker med dagsdato:

WHERE CAST(Ordredato AS date) = CAST(Getdate() AS date)

Eller et kendt interval, her december 2013 uanset klokkeslæt i Ordredato:

WHERE CAST(Ordredato AS date) BETWEEN '20131201' AND '20131231'

Bedst for svartiden / performance fordi feltet Ordredato ikke bliver casted:

WHERE Ordredato BETWEEN '2013-12-01 00:00:00.0000000' AND

                        '2013-12-31 23:59:59.9999999'

 

Første dato i gældende måned (1.): FirstDateOfMonth

·       DECLARE @dt date = DATEADD(day, 1-DAY(GETDATE()), GETDATE());

      SELECT @dt;

·       CAST(DATEADD(day, -DAY(GETDATE()) + 1, GETDATE()) AS date)

·       CAST(GETDATE() - DAY(GETDATE()) + 1 AS date)

·       CAST(FORMAT(GETDATE(),'yyyyMM01') AS DATE)

·       DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)

·       DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)

Eller den 1. om fire måneder:

CAST(DATEADD(month, 4, GETDATE() - DAY(GETDATE()) + 1) AS date)

 

Sidste dato i gældende måned (28., 29., 30., 31.): LastDateOfMonth

·       CAST(DATEADD(d, -DAY(GETDATE()), DATEADD(m, 1, GETDATE())) AS date)

·       CAST(DATEADD(m, 1, GETDATE()) - DAY(GETDATE()) AS date) 

·       DATEADD(day, -1, DATEADD(month, DATEDIFF(month, 0, GETDATE()) + 1, 0))

·       EOMONTH(GETDATE())

 

Første dato i gældende kvartal: FirstDateOfQuarter

CONVERT(date,DATEADD(quarter, DATEDIFF(quarter, 0, GETDATE()), 0))

 

Sidste dato i gældende kvartal: LastDateOfQuarter

CONVERT(date,DATEADD(day,-1,DATEADD(quarter,DATEDIFF(quarter,0,GETDATE())+1,0)))

 

Første dato i gældende uge: FirstDateOfWeek

SELECT DATEADD(wk, DATEDIFF(d, 0, GETDATE()) / 7, 0)     eller

SET DATEFIRST 1

SELECT CAST(GETDATE() - DATEPART(dw, GETDATE()) + 1 AS date)

 

Sidste dato i gældende uge: LastDateOfWeek

SELECT DATEADD(wk, DATEDIFF(d, 0, GETDATE()) / 7, 6)     eller

SET DATEFIRST 1

SELECT CAST(GETDATE() + (7 - DATEPART(dw, GETDATE())) as date)

 

Abbreviations: d = day, m = month, wk = week, dw = weekday, qq = quarter.

DATEDIFF bruger altid søndag som den første dag i ugen for at sikre, at funktionen er deterministisk.

 

Heltal dato yyyymmdd konverteret til ægte dato yyyy-mm-dd

SELECT CONVERT(date, '20130207', 23)

SELECT CONVERT(date, '20130702', 23)

 

SELECT CONVERT(date, CAST(OrderDate AS char(10)), 23)

 

Ægte dato konverteret til heltal dato yyyymmdd

SELECT CONVERT(char(8), Getdate(), 112)                         -- giver streng

SELECT CONVERT(int,CONVERT(char(8), Getdate(), 112))    -- giver heltal

 

Dato variabel eller felt som heltal yyyymmdd til en streng:

DECLARE @Birthdate as date = '1966-02-07'

SELECT CONVERT(char(8), @Birthdate, 112),

            CONVERT(char(8), OrderDate, 112)

 

Gældende klokkeslæt hh:mm:ss fås

SELECT CONVERT (varchar, Getdate(), 8)

SELECT CONVERT (time(0), Getdate(), 24)

 

Udregn en alder på en person (human age):

(yyyyMMdd - yyyyMMdd) / 10000 = forskel i fulde år ved datoer som heltal.

DECLARE @Birthdate as date = '1966-02-07'

SELECT

(CONVERT(int,CONVERT(char(8), Getdate(), 112)) - CONVERT(int,CONVERT(char(8), @Birthdate, 112))) / 10000

 

SQL Date Format Examples using CONVERT Function

 

Første dato og Sidste dato ud fra et heltal på formen ååååmm / yyyymm

DECLARE @yyyymm int = 202002

DECLARE @FirstDate AS date

DECLARE @LastDate AS date

-- Anvend en Kalender tabel:

SELECT @FirstDate = MIN([Dato]), @LastDate = MAX([Dato])

FROM [dbo].[Kalender]

WHERE ÅrMåned = @yyyymm

SELECT FirstDate = @FirstDate, LastDate = @LastDate

-- Anvend en algoritme:

SET @FirstDate = CONVERT(date, CONCAT(@yyyymm,'01'))

SET @LastDate = EOMONTH(CONVERT(date, CONCAT(@yyyymm,'01')))

SELECT FirstDate = @FirstDate, LastDate = @LastDate

 

FirstDate

LastDate

2020-02-01

2020-02-29

 

SET @LastDate = DATEFROMPARTS(LEFT(@yyyymm,4), RIGHT(@yyyymm,2),

                DAY(EOMONTH(DATEFROMPARTS(LEFT(@yyyymm,4), RIGHT(@yyyymm,2),1))))

 

SQL Server har forskellig format for en dato med Select eller Print:

Få sekunderne med ved en konvertering til en streng:

SELECT CONVERT(VARCHAR,GETDATE(),121) -- 2013-02-01 14:23:44.340

SELECT CONVERT(VARCHAR,GETDATE(),126) -- 2013-02-01T14:23:44.340

Fordi denne her giver kun timer og minutter:

SELECT CONCAT(GETDATE(),NULL) -- Feb  1 2018  2:23PM

Så undgå at bruge den ved dannelse af en hashbyte, gør i stedet dette:

SELECT CONCAT(CONVERT(VARCHAR,GETDATE(),126),';')

 

Formatter en dato og andre dato operationer

declare @date datetime = getdate() -- '2016-12-31 23:59:59.998'

select @date                                   -- giver 2016-12-31 23:59:59.998

print @date                                     -- giver Dec 31 2016  11:59PM

print cast(format(@date,'yyyy0101') as date)   -- giver 2016-01-01

print cast(format(@date,'yyyyMM01') as date)  -- giver 2016-12-01

print cast(format(@date,'yyyy0101') as int)      -- giver 20160101

print cast(format(@date,'yyyyMMdd') as int)     -- giver 20161231

print cast(format(@date,'yyyyMM') as int)        -- giver 201612

print format(@date,'yyyyMMddThh:mm:ss.fff','en-US') --20161231T11:59:59.997

print format(@date,'yyyyMMddTHH:mm:ss.fff','en-US') --20161231T23:59:59.997

 

Find debitor rækken hvor datoen er i perioden mellem FraDato og TilDato:

WHERE DebitorID = @Debitor AND

@date >= FraDato AND @date <= TilDato   ELLER:

FraDato <= @date AND TilDato >= @date   ELLER:

@date BETWEEN FraDato AND TilDato

 

Ofte er perioden mellem FraDato og TilDato ikke et lukket interval, fordi datoen i feltet TilDato ikke må medtages, f.eks. ved Kimball type 2 dimension:

WHERE DebitorID = @Debitor AND

@date >= FraDato AND @date < TilDato   ELLER:

FraDato <= @date AND TilDato > @date

d.v.s. between kan ikke anvendes her.

 

En nyttig funktion som giver en ægte dato (date) til at arbejde videre med:

DATEFROMPARTS (2015, 10, 11) giver 2015-10-11 d.v.s. 11. oktober 2015.

DATEADD(day, 10, DATEFROMPARTS (2015, 10, 11)) har lagt 10 dage til.

 

DECLARE @yyyyddmm AS varchar(100) = '20223010'

DECLARE @date AS date

SET @date = DATEFROMPARTS(LEFT(@yyyyddmm,4),RIGHT(@yyyyddmm,2),SUBSTRING(@yyyyddmm,5,2))

SELECT @date

 

Eller som datetime hvor du også bestemmer klokkeslæt f.eks. dato for et år siden med klokkeslæt 00:00:00.000 fra dagens begyndelse:

DATETIMEFROMPARTS(YEAR(GETDATE())-1, MONTH(GETDATE()), DAY(GETDATE()), 0, 0, 0, 0)

Ordredato er en datetime med mange forskellige klokkeslæt f.eks. 10:12:34.521 og der ønskes alle rækker siden dato for et år siden med klokkeslæt fra dagens begyndelse:

WHERE Ordredato >= DATETIMEFROMPARTS(YEAR(GETDATE())-1, MONTH(GETDATE()),

                                DAY(GETDATE()), 0, 0, 0, 0)

 

Eller siden 1. januar sidste år:

WHERE Ordredato >= DATETIMEFROMPARTS(YEAR(GETDATE())-1, 1, 1, 0, 0, 0, 0)

 

Ønskes dato og klokkeslæt som datetime findes denne funktion, hvis jeg har alle delene i int variable: DATETIMEFROMPARTS (@Year, @Month, @Day, @Hour, @Minute, @Seconds, @Milliseconds)

 

Eller kun som klokkeslæt til datatypen time:

TIMEFROMPARTS (@Hour, @Minute, @Seconds, @Fractions, @Precision)

 

SELECT CURRENT_TIMESTAMP giver samme tidspunkt som Getdate().

 

En dato med date data typen ønskes lavet om til første og sidste klokkeslæt:

DECLARE @FraDato date = '2010-02-04'

DECLARE @TilDato date = '2010-02-07'

SELECT CAST(@FraDato AS datetime)

-- 2010-02-04 00:00:00.000

SELECT CAST(@FraDato AS datetime2(7))

-- 2010-02-04 00:00:00.0000000

SELECT DATEADD(millisecond,(-2), CAST(DATEADD(day,1,@TilDato) AS datetime))

-- 2010-02-07 23:59:59.997

SELECT DATEADD(nanosecond,(-50), CAST(DATEADD(day,1,@TilDato) AS datetime2(7)))

-- 2010-02-07 23:59:59.9999999

 

Altid rart at kunne få fat i nogle bestemte datoer ud fra et felt, getdate eller en va­ria­ble, f.eks. første dato i året, første dato sidste år o.s.v.:

 

DECLARE @dato date = '2016-02-07'

SELECT DATEADD(year,DATEDIFF(year,0,@dato),0)    -- 2016-01-01 00:00:00.000

SELECT DATEADD(year,DATEDIFF(year,0,@dato)-1,0)  -- 2015-01-01 00:00:00.000

SELECT DATEADD(year,DATEDIFF(year,0,@dato),-1)   -- 2015-12-31 00:00:00.000

SELECT DATEADD(year,DATEDIFF(year,0,@dato)+1,-1) -- 2016-12-31 00:00:00.000

SELECT DATEADD(year,DATEDIFF(year,0,@dato)+1,0)  -- 2017-01-01 00:00:00.000

SELECT DATEADD(year,DATEDIFF(year,0,@dato)+2,-1) -- 2017-12-31 00:00:00.000

 

Fordi SQL Server har givet DateAdd disse betydninger:

SELECT DATEADD(year,0,0)                         -- 1900-01-01 00:00:00.000

SELECT DATEADD(year,116,37)                      -- 2016-02-07 00:00:00.000

 

Ugedag navn og ugenummer, husk sproget:

SELECT @@LANGUAGE

SET LANGUAGE us_english;

SET LANGUAGE danish;

SELECT DATENAME(weekday, GETDATE()) -- ugedag navnet, name of day in week

SELECT @@DATEFIRST

SET DATEFIRST 1

SELECT DATENAME(week, GETDATE()) -- ugenummer, number of week

 

En uge kan starte i det forgangne år, lørdag 2. januar 2016 tilhører uge 53 i 2015 der starter mandag 28. december 2015, det kan udregnes med denne algoritme:

DECLARE @dato date = '2016-01-02'

SET DATEFIRST 1 -- gør mandag til første dag i ugen, SELECT @@DATEFIRST

SELECT DATEADD(day,1-DATEPART(weekday,@dato),@dato) -- 2015-12-28

 

Eller uden brug af datefirst 1 fås mandag i ugen ud fra en datetime:

DECLARE @dato datetime = '2016-01-02 15:36:21.553'

SELECT DATEADD(wk, DATEDIFF(d, 0, @dato) / 7, 0)    -- 2015-12-28

 

Første dato i en måned som date fra en datetime, derfor cast til date:

DECLARE @dato datetime = '2016-02-07 15:36:21.553'

SELECT CAST(@dato-DAY(@dato)+1 AS date)             -- 2016-02-01

 

Reporting Services rapport: Mandag i denne uge, angiver at uge starter mandag:

=DateAdd(Dateinterval.Day,-DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday)+1,Today())

Mandag i forrige uge:

=DateAdd(Dateinterval.Day,-DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday)-6,Today())

Søndag i forrige uge:

=DateAdd(Dateinterval.Day,-DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday),Today())

Aktuelle ugenummer:

=Year(Today())*100+DatePart(DateInterval.WeekOfYear,Today(),FirstDayOfWeek.Monday)

Sidste uges ugenummer:

=Year(Dateadd("d",-7,Today()))*100+DatePart(DateInterval.WeekOfYear,Dateadd("d",

                             -7,Today()),FirstDayOfWeek.Monday)

 

Programmeringen for FraDato default, hvis dagsdato er mandag, så vis mandagen fra forrige uge ellers mandag fra gældende uge:

=IIF(DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday)=1,

DateAdd(Dateinterval.Day,-DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday)-6,Today()),

DateAdd(Dateinterval.Day,-DatePart(DateInterval.Weekday,Today(),FirstDayOfWeek.Monday)+1,Today()))

 

Programmeringen for TilDato default:

=dateadd(Dateinterval.Day,-1,Today())

Dagsdato er onsdag 19-12-2018 bliver FraDato 17-12-2018, TilDato 18-12-2018. Dagsdato er mandag 17-12-2018 bliver FraDato 10-12-2018 og TilDato 16-12-2018 d.v.s. fra mandagen i ugen før til og med i går som er søndag i ugen før.

 

En rapport har flere kolonner men skal kun vise en kolonne hvis der er indhold i den i en af rækkerne. Højreklik på kolonnen øverste grå firkant ovenover kolonne­overskriften, vælg Column Visibility, marker Show or hide based on an expression, klik på Fx symbolet og indtast expression for hidden, d.v.s. er ud­tryk­ket sand så skjules kolonnen. Når max() funktionen finder en værdi så giver isnothing() funk­tionen falsk og kolonnen vises i rapporten, fordi der er indhold i kolonnen i en eller flere rækker i datasættet:

=IsNothing(Max(Fields!Column8.Value, "Dataset"))

Bemærk, at nedenstående kun ser på første række i rapporten:

=IIf(IsNothing(Fields!Column8.Value),true,false)

 

Er det en kolonne der skal skjules ved eksport af rapportens viste data til et Excel regneark fil anvendes dette expression: =Not Globals!RenderFormat.IsInteractive

Reporting Services Reusable Code Blocks

 

En sql sætning kunne også gøre kolonnen null, men er ikke så brugbar for rap:

Column8 = CASE

            WHEN MAX(Column8) OVER(ORDER BY (SELECT 1)) IS NOT NULL

            THEN ISNULL(CAST(Column8 AS varchar),'') ELSE NULL END

 

Første dato og sidste dato i en uge via ugenummer og det årstal ugen er startet i:

DECLARE @uge int = 52

DECLARE @år int = 2016

SET DATEFIRST 1 -- gør mandag til første dag i ugen

SELECT UgeStatdato = DATEADD(WEEK, @uge, CONCAT(@år,'0101')) - 4 –

       DATEPART(WEEKDAY, DATEADD(WEEK, @uge, CONCAT(@år,'0101')) - 4) + 1

SELECT UgeSlutdato = DATEADD(WEEK, @uge, CONCAT(@år,'0101')) - 4 –

       DATEPART(WEEKDAY, DATEADD(WEEK, @uge, CONCAT(@år,'0101')) - 4) + 7

 

Angivelse af SET DATEFIRST har ingen effekt på DATEDIFF. DATEDIFF bruger altid

søndag som den første dag i ugen for at sikre, at funktionen er deterministisk.

 

Udregning af uge nummer og med et årstal for ugen fordi f.eks. søndag den 1. januar 2012 tilhører uge 52 som kommer fra året 2011 så ÅrUge = 201152, hvor der bruges iso_week som svarer til danske uger hvor det er første torsdag i det nye år som bestemmer at den uge bliver til uge 1 i året. (USA anvender første mandag i året til at stemme uge 1, derfor er uge nummer i DK og USA ikke altid samme værdi).

 

DECLARE @dato date = '20120101' -- '20160102' -- GETDATE()

SELECT

Uge = DATEPART(iso_week,@dato),

ÅrUge =

CASE WHEN DATEPART(iso_week,@dato) = 1 AND

          YEAR(@dato) < YEAR(DATEADD(day,7,@dato))

     THEN YEAR(DATEADD(day,7,@dato))*100+DATEPART(iso_week,@dato)

     WHEN DATEPART(iso_week,@dato) >= 52 AND

          YEAR(@dato) > YEAR(DATEADD(day,-7,@dato))

     THEN YEAR(DATEADD(day,-7,@dato))*100+DATEPART(iso_week,@dato)

     ELSE YEAR(@dato)*100+DATEPART(iso_week,@dato)

END

UgeStartÅr = ÅrUge / 100.

 

DECLARE @ÅrUge int = 201807

SELECT  År = @ÅrUge / 100  -- qoutient, kvotient

SELECT Uge = @ÅrUge % 100  -- remainder, resten

 

Ønskes en dato klokkeslæt som et timestamp i en log historik audit tabel bør data­typen datatime2 overvejes fordi den er mere præcis og det er uheldigt hvis to rækker i tabellen får samme dato klokkeslæt, tidspunkt:

DECLARE @d1 DATETIME     = GETDATE()

DECLARE @d2 DATETIME2(7) = SYSDATETIME()

SELECT @d1  -- 1966-02-07 15:36:21.553

SELECT @d2  -- 1966-02-07 15:36:21.5556335

 

Og som default værdi i et felt/kolonne i en tabel:

CREATE TABLE [dbo].[CustomerLog]

(

...

  [CreatedDate] [datetime2](7) NOT NULL

      CONSTRAINT [DF_CustomerLog_CreatedDate] DEFAULT (SYSDATETIME())

...

)

GO

 

Eller udført efter Create Table:

ALTER TABLE [dbo].[CustomerLog]

ADD CONSTRAINT [DF_CustomerLog_CreatedDate] 

  DEFAULT (SYSDATETIME()) FOR [CreatedDate]

GO

 

En periode er i formattet mmyy og jeg ønsker rækkerne sorteret efter år og må­ned, derfor omdanner jeg mmyy til en dato som den første i måneden:

-- Test eksempel:

DECLARE @mmyy nchar(4)

DECLARE @dato date

SET @mmyy = '0315'

SET @dato = Convert(date,Stuff(Stuff('01'+@mmyy,5,0,'.'),3,0,'.'),4)

PRINT @dato

 

SELECT Convert(date, Stuff(Stuff('01'+Periode,5,0,'.'),3,0,'.'), 4)

FROM dbo.BESTILLING

 

Performance problemet er, at nedenstående ikke anvender indeks på Ordredato, og at DateDiff skal udregnes for hver række post i tabellen:

WHERE DATEDIFF(DAY, Ordredato, GETDATE()) < 210

Bedre at gøre dette som anvender indekset og hvor DateAdd udregnes én gang:

WHERE Ordredato > DATEADD(DAY, -210 , GETDATE())

Dateadd fratrækker 210 dage fra dagsdato men er gældende klokkeslæt f.eks. kl. 14:12:39 så vil en ordredato for 210 dage siden med klokkeslæt kl. 10:00:00 ikke medtages, omvendt er feltet Ordredato formentlig af data typen Date i tabellen.

Datediff sammenligner kun på dagen. Alternativt og fortsat brug af indeks:

WHERE Ordredata > DATEADD(DAY, -210 , GETDATE()) AND

           DATEDIFF(DAY, Ordredato, GETDATE()) < 210

 

Eksempel med brug af Year funktionen: WHERE YEAR(Ordredato) = 2015

Hurtigere er: WHERE Ordredato BETWEEN '2015-01-01' AND '2015-12-31'

YEAR funktionen er bedst kun at bruge i SELECT til at vise et årstal.

 

Konvertere en ægte dato og klokkeslæt til en tekst dato dd-mm-yyyy

En ægte datetime Dato felt med værdien 2013-08-06 07:57:27.327 bliver med:

CAST(Dato as varchar)            til "Aug  6 2013  7:57AM"

CONVERT(varchar(10), Dato, 20)   til "2013-08-06"

CONVERT(varchar(10), Dato, 105)  til "06-08-2013"

 

Konvertere en dato som tekst til en ægte dato f.eks. dd-mm-yyyy dmy

Convert har en række formater til parsing af en tekst-dato til en ægte dato f.eks.:

SELECT Convert(datetime, '23/07/2009', 103)

SELECT Convert(datetime, '02-07-2009', 105)

 

SET DATEFORMAT dmy
SELECT Cast('23/07/2009' AS date)

SELECT Cast('02-07-2009' AS date)

 

Mere om Cast og Convert in SQL Server 2012 og frem

Mere om Cast og Convert in SQL Server 2000

SQL Date Format Examples using CONVERT Function

 

En nyttig dato funktion

CREATE function [dbo].[SenesteBankDag] (@Dato date)

RETURNS date

AS

/* SELECT dbo.SenesteBankDag(getdate()) */

/* SELECT dbo.SenesteBankDag('2019-11-25') */

BEGIN

    DECLARE @ForrigeDato date = DATEADD(day, -1, @Dato)

    DECLARE @SenesteBankDag date

 

    SELECT @SenesteBankDag = MAX(Dato)

    FROM   dbo.Kalender

    WHERE  Dato <= @ForrigeDato AND ErBankdag = 1

 

    RETURN @SenesteBankDag

END

 

Noget der skal ske efter kl. 18

IF CAST(SUBSTRING(CAST(CONVERT(time, GETDATE()) AS varchar),1,2) AS int) >= 18

 

Byg tidspunkt op som en streng og så cast til datetime

Tidspunkt data d.v.s. dato og klokkeslæt kan bygges op som en streng, og så ca­stes til datatype datetime: CAST ('20100917 08:40' AS datetime)

d.v.s. ååååmmdd mellemrum tt:mm

Den indbyggede funktion DATETIMEFROMPARTS blev vist tidligere.

 

Konvertere et tal som tekst til et ægte tal

Belob i et tekst felt 4.210.593,79 konverteres til decimaltal ved at fjerne punktum og erstatte det danske komma med amerikansk punktum som decimaltegn:

CAST(REPLACE(REPLACE(Belob,'.',''),',','.') AS DECIMAL(19,4))

 

Procent

SELECT p.Pris * 1.25 lægger 25% oveni, p.Pris * 0.75  fratrækker 25%.

 

Order by med feltnavn eller feltnummer placering

Normalt skriver man de felter der skal sorteres på, f.eks.:

SELECT Kundenummer, Kundenavn, Postnummer, Rabatbeløb * 1.10 AS Ny

FROM dbo.KUNDE                    -- eller Ny = Rabatbeløb * 1.10

ORDER BY Postnummer, Ny, Kundenavn

 

Når der sorteres på felter der er medtaget i SELECT, kan man i stedet anvende de­res feltnummer placering hvor første felt i select er nummer 1:

SELECT Kundenummer, Kundenavn, Postnummer, Rabatbeløb * 1.10 AS Ny

FROM dbo.KUNDE

ORDER BY 3, 4, 2

 

Tilfældig sortering af rækker i en tabel der får et sorteringsnummer

NewId() danner en GUID f.eks.: C820BE93-2AC1-44CA-876F-D51D36777D22

SELECT KundeId, KundeNavn,

       ROW_NUMBER() OVER(ORDER BY NewId()) AS Sorteringsnummer

FROM dbo.Kunde

 

Generering af næste nummer til et Id felt med data fra anden tabel

Tabel Kunde har primærnøglefelt KundeId som ikke er Identity, fordi databasen er en på en distribueret arkitektur (distributed architecture). Derfor skal næste num­mer til KundeId udregnes i en SQL sætning, når der indsættes data fra en anden tabel, her Stage_Customers som har engelske feltnavne. Med Max findes største værdi af KundeId i Kunde tabellen, og er tabellen tom, d.v.s. ingen rækker, så sættes største værdi til default 0. Med Row_Number over et unikt felt i den anden tabel, her feltet CustomerID, fås talrækken 1, 2, 3 som lægges til Max værdien, hvorved der bliver udregnet unikke numre til primærnøglefeltet KundeId.

;WITH maxKundeId AS

(

 SELECT MaxKundeId = ISNULL(MAX(KundeId), 0)

 FROM dbo.Kunde WITH (TABLOCKX)

)

INSERT INTO dbo.Kunde WITH (TABLOCKX) (KundeId, Kundenavn)

SELECT

  MaxKundeId + ROW_NUMBER() OVER(ORDER BY CustomerID) AS KundeId,

  CompanyName AS Kundenavn   

FROM dbo.Stage_Customers

     CROSS JOIN maxKundeId

 

Forløbende nummer tal fra 1 til ca. 14735

SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS Id

FROM MASTER.dbo.syscolumns

 

;WITH TallyTable(n) AS

(

    -- 8000 rows as in max length of the VARCHAR string

    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))

    FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)

    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n)

    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) d(n)

    CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0)) e(n)

)

SELECT *

FROM TallyTable;

 

;WITH TallyTable(n) AS

(

    SELECT TOP 100000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL))

    FROM dbo.syscolumns tb1,dbo.syscolumns tb2

)

SELECT *

FROM TallyTable;

 

WITH E00(N) AS (SELECT 1 UNION ALL SELECT 1)

    ,E02(N) AS (SELECT 1 FROM E00 a, E00 b)

    ,E04(N) AS (SELECT 1 FROM E02 a, E02 b)

    ,E08(N) AS (SELECT 1 FROM E04 a, E04 b)

    ,E16(N) AS (SELECT 1 FROM E08 a, E08 b)

    ,E32(N) AS (SELECT 1 FROM E16 a, E16 b)

    ,TallyTable(N) AS (SELECT ROW_NUMBER() OVER

                       ORDER BY (SELECT NULL)) FROM E32)

SELECT N

FROM TallyTable

WHERE N <= 1000000;

 

Tally Tables in T-SQL

 

Tilfældig tal – randum number

newid() giver for hver række en ny guid 36 tegn kode f.eks.:

EDD3D7A3-A11C-4577-BCBD-5C40FBB88708 som randum funktionen oversætter til et tilfældigt tal mellem 0 og 2:

SELECT table_name,

       floor(3 * rand(convert(varbinary, newid()))) number,

       abs(checksum(newid()) % 3) number

FROM INFORMATION_SCHEMA.TABLES

WHERE TABLE_TYPE = 'BASE TABLE'

ORDER BY 2

 

Single-Row Table / One-Row Table / tabellen kan tømmes til 0 rækker

CREATE TABLE dbo.SingleRow

(

  RestrictToOneRow char(1) NOT NULL

    CONSTRAINT DF_SingleRow_RestrictToOneRow DEFAULT 'X',

  Season varchar(9) NOT NULL DEFAULT '2022/2021',

  ImportFileFolder varchar(100) NULL,

  CONSTRAINT PK_SingleRow PRIMARY KEY (RestrictToOneRow),

  CONSTRAINT CK_SingleRow_Lock CHECK (RestrictToOneRow='X')

)

INSERT INTO dbo.SingleRow (ImportFileFolder)

VALUES ('C:\ImportFile\2022\')

 

What is the difference between Charindex and Patindex?

The charindex and patindex functions return the starting position of a pattern you specify. Both take two arguments, but they work slightly differently, since patindex can use wildcard characters, but charindex cannot.

 

SELECT TOP 1 eller SELECT TOP 10 er langsom

Et view som indeholder flere joins til tabeller kan forårsage, at SELECT TOP(10) har lang svartid, det kan undgås ved at anvende hash join type:

INNER HASH JOIN eller LEFT OUTER HASH JOIN. Examples

Et view kan ikke anvende OPTION (HASH JOIN).

 

Dynamic SQL build on the fly, dynamisk sql sætning, fang fejl og vis sql

Test eksempler:

 

DECLARE @sql nvarchar(max)

SET @sql = N'SELECT A = 1'

EXECUTE sys.sp_executesql @sql

EXECUTE (@sql)

 

Inde fra en stored procedure:

 

CREATE PROCEDURE dbo.CustomerDynamicSort

  @Column    NVARCHAR(128) = N'CustomerId',

  @Direction NVARCHAR(4)   = N'ASC'

AS

BEGIN

  SET NOCOUNT ON;

  DECLARE @sql NVARCHAR(MAX)

  SET @Column = QUOTENAME(@Column); -- set [] around the column

  SET @sql = N'SELECT CustomerName, CustomerAddress

               FROM dbo.Customer

               ORDER BY ' + @Column + N' ' + @Direction

  BEGIN TRY

    EXEC [SalesSystem].sys.sp_executesql @sql --databasename if different from current

  END TRY

  BEGIN CATCH

    SELECT CONVERT(XML, @sql) --click Results, click sql statement, copy-paste to new query

    PRINT CAST(@sql AS NTEXT) --click Messages to see sql at text.

    ;THROW

  END CATCH

END

 

object_id=OBJECT_ID('HumanResources.Employee')

 

Data fra en dynamisk sql udført select from sætning kan indsættes i en tabel:

IF OBJECT_ID('tempdb..#tmp') IS NOT NULL DROP TABLE #tmp

--from sql server 2016 DROP TABLE IF EXISTS #tmp

CREATE TABLE #tmp

(

  Id int NOT NULL, 

  Name nvarchar(256) NOT NULL,

  CreateDate datetime NOT NULL,

  PRIMARY KEY CLUSTERED (Id)     -- nameless primary key

      WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,

      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,

      ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100)

)

INSERT INTO #tmp (Id, Name, CreateDate)

EXEC sys.sp_executesql @sql, @params, @par1, ...

 

Fetch the result of a stored procedure and insert it into a table:

INSERT INTO dbo.Customers

EXEC dbo.Fetch_Customers

 

Dynamisk sql med output parametre

Antal rækker i en tabel:

DECLARE @Schema nvarchar(256) = 'dbo'

DECLARE @Table  nvarchar(256) = 'Employees'

DECLARE @sql as nvarchar(max)

DECLARE @params nvarchar(max)

DECLARE @out_Count bigint

 

SET @sql = 'SELECT @prm_Count = COUNT(1) FROM [<Schema>].[<Table>]'

SET @sql = REPLACE(@sql,'<Table>',@Table)

SET @sql = REPLACE(@sql,'<Schema>',@Schema)

SET @params = N'@prm_Count bigint OUTPUT'

EXEC sys.sp_executesql @sql, @params, @prm_Count = @out_Count OUTPUT

SELECT @out_Count

 

Andet eksempel med where kriterie og værdier i to variable:

DECLARE @LowerPrice INT = 3000, @HigherPrice INT = 6000

DECLARE @sql nvarchar(1000)

DECLARE @params nvarchar(100)

SET @sql = N'SELECT Id, Name, Price

             FROM Book

             WHERE Price > @paramLowerPrice AND Price < @paramHigherPrice'

SET @params = N'@paramLowerPrice INT, @paramHigherPrice INT'

EXECUTE sys.sp_executesql @sql ,@params,

            @paramLowerPrice = @LowerPrice, @paramHigherPrice = @HigherPrice

 

Tredje eksempel med where kriterie og parameter todate med default værdi

DECLARE @tbl    nvarchar(MAX),

        @sql    nvarchar(MAX),

        @params nvarchar(MAX),

        @count  int

 

SELECT @sql =

  N'SELECT @cnt = COUNT(*) FROM dbo.' + quotename(@tbl) + N' ' +

  N'WHERE LastUpdated >= @fromdate AND LastUpdated < coalesce(@todate, ''99991231'')'

 

SET @params = N'@fromdate date, @todate date = NULL, @cnt int OUTPUT'

 

EXEC sys.sp_executesql @sql, @params, '20100101', @cnt = @count OUTPUT

 

PRINT @tbl + ': ' + convert(varchar(10), @count) + ' rows.'

 

Fra SQL 2005 blev TOP gjort dynamisk eller parameter styret / variabel styret:

DECLARE @n int = 5

SELECT TOP(@n) *

FROM dbo.Orders

ORDER BY Ordredato

 

Finde dubletter i en tabel

De varenummer som findes flere gange hos samme kunde i samme leverings­måned, derfor omdannes leveringsdatoen til en måned på formen ååååmm, hvor HAVING COUNT(*) >= 2 er kriteriet, at der skal være mindst to rækker med samme Varenummer, Kundenummer og Leveringsmåned.

SELECT Varenummer

FROM   dbo.ORDRE

GROUP BY Varenummer, Kundenummer,

         YEAR(Leveringsdato) * 100 + MONTH(Leveringsdato)

HAVING COUNT(*) >= 2   -- kan også være HAVING COUNT(*) > 1.

 

Et kunde arkiv har to eller flere ens KundeId’er inden for samme leverance:

SELECT t.*

FROM

(

  SELECT k.*,

         Antal = COUNT(*) OVER (PARTITION BY k.LeveringId, k.KundeId)

  FROM [Arkiv].[Kunde] k

) t

WHERE t.Antal > 1

 

OVER PARTITION clause allows the use of aggregation without using a Group By clause.

 

Frasortering af dubletter i en tabel (Removing duplicates)

Et kunde arkiv indeholder dubletter inden for samme kunde, hvor kolonnen KundeId indeholder samme værdi over flere rækker. For hver kunde (KundeId) opstilles dubletterne sorteret efter seneste Oprettel­sesdato inkl. klokkeslæt hvor første række får RowNumber = 1, anden række får RowNumber = 2 inden for samme værdi i kolonnen KundeId. Det kaldes at danne en partition for hver af værdier i kolonne KundeId. Når der er mange rækker med forskellige værdier i KundeId, fås der mange partitioner, hvor én partition består af en eller flere rækker med samme værdi i kolonnen KundeId d.v.s. består af dubletter. Inden for hver partition d.v.s. inden for hver værdi af KundeId, bliver partitionens dublet­rækker sorteret efter seneste Oprettelsesdato inkl. klokkeslæt og hver række inden for partitionen får hver deres RowNumber fra 1 og fremefter. Det betyder, at hver partition har RowNumber fra 1 og fremefter, d.v.s. inden for hver KundeId hvorved dubletrækkerne markeret med RowNumber 1, 2, 3… Where delen medtager kun første dubletrække inden for hver partition med kriteriet at RowNumber = 1 og derved bliver de andre dubletter frasorteret.

 

SELECT t.*

FROM

(

  SELECT k.*,

         RowNumber = ROW_NUMBER() OVER

           (PARTITION BY k.KundeId,

            CAST(k.Oprettelsesdato AS dato)

           ORDER BY k.Oprettelsesdato DESC)

  FROM [Arkiv].[Kunde] k

) t

WHERE t.RowNumber = 1

 

Sletning af dubletter i en tabel

Sletning af dubletter i felt Id i tabel Friend uden primærnøgle eller Identity felt:

 

CREATE TABLE dbo.Friend

(

 Id INT NOT NULL,

 Name VARCHAR (20) NOT NULL

)

GO

INSERT INTO dbo.Friend(Id, Name) VALUES

 (1, 'Ole'),

 (2, 'Ane'),

 (3, 'Per'),

 (4, 'Ida')

GO 5

 

SELECT *

FROM dbo.Friend

 

;WITH duplicate AS

(

 SELECT *,

        RowNumber = ROW_NUMBER() OVER

                   (PARTITION BY Id ORDER BY (SELECT NULL) ASC)

 FROM dbo.Friend

)

DELETE FROM duplicate WHERE RowNumber > 1

 

SELECT *

FROM dbo.Friend

 

Sletning af dubletter i felt F1 og F2 i tabellen T, som har et RowNumber indsat som et Identity felt:

DELETE dbo.T

FROM   dbo.T AS A

WHERE  RowNumber <> (SELECT MIN(B.RowNumber)

                     FROM   dbo.T AS B

                     WHERE  B.F1 = A.F1 AND B.F2 = A.F2)

 

;WITH dublet AS -- keep the youngest row by SystemDateTime

(

 SELECT *,

        RowNumber = ROW_NUMBER() OVER

                   (PARTITION BY F1, F2 ORDER BY SystemDateTime DESC)

 FROM dbo.T

)

DELETE FROM dublet WHERE RowNumber > 1

--SELECT * FROM dublet WHERE RowNumber > 1

 

Sletning af dubletter i felt LokalId i en staging dimensionstabel i et datavarehus inden den indgår i en Merge med mart dimensionstabellen af type 1.

Først konstaterer vi, at der er dubletter:

SELECT [LokalId], COUNT(*)

FROM [StagingDim].[Virksomhed]

WHERE [LokalId] IS NOT NULL -- flere virksomheder har ingen LokalId.

GROUP BY [LokalId]

HAVING COUNT(*) >= 2

 

Dernæst sletter vi dubletterne ud fra en prioriteringsregel i Row_Number:

;WITH dublet AS

(

 SELECT [VirksomhedId],[LokalId],

  RowNumber = ROW_NUMBER() OVER (PARTITION BY [LokalId] ORDER BY

    CASE WHEN [Status] = 'Ophørt' THEN 1

         WHEN [Status] = 'Aktiv' THEN 2

         WHEN [Status] = 'Brexit' THEN 3

         WHEN [Status] = 'Fusioneret' THEN 4

         ELSE 5 END ASC, -- Ophørt beholdes først.

    ISNULL([Ophørsdato],'1900-01-01') DESC, -- seneste dato beholdes.

    [Tilladelsesdato] DESC) -- datoen skal være udfyldt i tabellen.

FROM [StagingDim].[Virksomhed]

WHERE [LokalId] IS NOT NULL  -- flere virksomheder har ingen LokalId.

)

DELETE FROM dublet WHERE RowNumber > 1

--SELECT * FROM dublet WHERE RowNumber > 1

 

Indsæt data uden dubletter i en tabel

INSERT INTO dbo.DB

SELECT n.F1, n.F2, n.F3 n.F4

FROM dbo.NEW n

     INNER JOIN

     (

      SELECT t.Cprnr, t.Dato, MAX(t.RowNumber) AS RowNumber

      FROM   dbo.NEW t

      GROUP BY t.Cprnr, t.Dato

     ) AS maks ON maks.RowNumber = n.RowNumber

 

Find seneste data registreret på en dato i år

SELECT *

FROM   dbo.T

WHERE  Dato = (SELECT MAX(Dato)

               FROM   dbo.T

               WHERE  YEAR(Dato) = YEAR(GETDATE()))

 

Find seneste dags ordredata

Med brug af indlejret (nested) select sætning som finder max værdi som der joines med til den ydre select sætning:

SELECT Kundenummer, Ordredato, Antal, Beløb

FROM dbo.ORDRE

     INNER JOIN

     (

      SELECT MAX(Ordredato) AS SenesteOrdreDato

      FROM dbo.ORDRE

     ) Seneste ON Seneste.SenesteOrdreDato = dbo.ORDRE.OrdreDato

 

Eller inden for hver vare som derved ingår i indlejret select og i join:

SELECT o.Kundenummer, o.Ordredato, o.Antal, o.Beløb

FROM dbo.ORDRE o

     INNER JOIN

     (

      SELECT VareId, MAX(Ordredato) AS SenesteOrdreDato

      FROM dbo.ORDRE

      WHERE Antal >= 100

      GROUP BY VareId

     ) s ON s.VareId = o.VareId AND s.SenesteOrdreDato = o.OrdreDato

 

Foranstillet nuller - Leading zeros – Left padding zeros

SELECT RIGHT(CONCAT('00000', Kode), 5) AS Kode,   -- null giver 00000.

       REPLICATE('0', 5 - LEN(Kode)) + CAST(Kode AS varchar) AS Kode

                                                  -- null giver null.

       RIGHT(STUFF(Kode, 1, 0, REPLICATE('0', 5)), 5) AS Kode

FROM   dbo.T

Kode kolonnen har datatype int med værdien 8 eller varchar med værdien 8A.

Kode 8 giver 00008 og 8A giver 0008A, mens negativ værdi -8 giver 000-8.

 

Format og Str kræver at det er tal, derfor giver tekstværdien 8A giver fejl:

SELECT FORMAT(Kode, '00000') AS Kode,            -- null giver null.

       FORMAT(Kode, '00000;-00000;00000') AS Kode, -- pos, neg, nul.

       FORMAT(ISNULL(Kode,''), '00000') AS Kode, -- null giver 00000.

       FORMAT(Kode, 'D5') AS Kode,               -- null giver null.

       REPLACE(STR(Kode, 5),' ','0') AS Kode --STR foranstil mellemrum

FROM   dbo.T

Kode 8 giver 00008 og negativ værdi -8 giver -00008 ved Format.

 

Løsning for positiv tal, negativ tal og tekst værdi, undtagen -8A som giver 00-8A:

SELECT CASE WHEN ISNUMERIC(Kode) = 1

            THEN FORMAT(TRY_CAST(Kode AS int), '00000')

            ELSE REPLICATE('0', 5 - LEN(Kode)) + CAST(Kode AS varchar)

       END AS Kode

FROM   dbo.T

 

Leg med James Bond:

DECLARE @Number int = 7

SELECT RIGHT(CONCAT('000', @Number), 3)  -- 007

SELECT FORMAT(@Number, '000')            -- 007

 

DECLARE @NumberN int = -7

SELECT RIGHT(CONCAT('000', @NumberN), 3) -- 0-7

SELECT FORMAT(@NumberN, '000')           -- -007

 

DECLARE @NumberA varchar(3) = '7X'

SELECT RIGHT(CONCAT('000', @NumberA), 3) -- 07X

SELECT FORMAT(@NumberA, '000')           -- fejl / error.

 

PostgreSQL eksempel med en left padding funktion, MySQL har den også

SELECT p.name AS product_name,

       date_part('year', s.date) AS year,

       -- foranstillet 0 så altid på to cifre:

       lpad(date_part('month', s.date)::text, 2, '0') AS month,

       -- konvertering til string med et format på to cifre/tvunget 0:

       to_char(date_part('day', s.date), '00') AS day,

       SUM(p.price * sd.count)::text::money AS totalrevenue,

       1 AS sort

FROM sales AS s

     INNER JOIN sales_details AS sd ON sd.sale_id = s.id

     INNER JOIN products AS p ON p.id = product_id

GROUP BY p.name, date_part('year', s.date),

         date_part('month', s.date), date_part('day', s.date)

 

left pad lpad foranstiller både tal og tekst uden fejl:

lpad(8::text, 5, '0'), lpad('8'::text, 5, '0'), lpad('8A'::text, 5, '0')

mens lpad('-8'::text, 5, '0') giver 000-8.

to_char(8, '00000') giver 00008 og to_char(-8, '00000') giver -00008.

mens to_char('8A', '00000') giver fejl.

 

Beløb til summering i udskrift (to summarize)

SELECT ROUND(Beløb,2) AS Beløb -- til visning og summering i rapport.

FROM   dbo.T

 

Gennemsnit

SELECT AVG(Antal) -– Antal er int, giver AVG også heltal.

FROM   dbo.T

 

SELECT AVG(CAST(Antal AS float)) AS Gennemsnitlig_Antal

FROM   dbo.T

 

Dividere heltal med et kommatal, så resultatet bliver kommatal

SELECT AntalDage / 365.0  -- adder .0 så er nævneren et kommatal.

SELECT AntalDage / CAST(DageIMåned AS float) – nævner er et kommatal.

 

Produkt af rækker, altså værdi ganget sammen fremfor summet sammen

SELECT EXP(SUM(LOG(Værdi)))

FROM   dbo.STAT

 

SumProdukt

Vi kender den fra Excel eksempelvis

=SUMPRODUKT(A1:A32;B1:B32)/SUM(A1:A32)

 

Som svarer til følgende beregninger:

(A1*B1)+(A2*B2)+...+(An*Bn)/SUM(A1:An)

 

SELECT SUM(A*B)/SUM(A)

FROM   dbo.STAT

 

SELECT ISNULL(SUM(A*B)/NULLIF(SUM(A),0),0)

FROM   dbo.STAT

 

select sum(baseSeries.Actual * weightSeries.Actual) /

       sum(weightSeries.Actual)

from (

       select RecordDate, Actual

       from ProductionRecords

       where KPI = 'Weighty'

     ) baseSeries

inner join

     (

       select RecordDate, Actual

       from ProductionRecords

       where KPI = 'Tons Milled'

     ) weightSeries on weightSeries.RecordDate = baseSeries.RecordDate

 

NULL bliver til en værdi

Med funktion ISNULL får man anden parameter, når første parameter IS NULL, man kan sige, at man erstatter en null værdi i en kolonne med en kendt værdi:

ISNULL(dbo.KUNDE.SkyldnerBeløb, 0)

 

Få den værdi som er ikke NULL

Med funktion COALESCE får man den parameter som er IS NOT NULL:

COALESCE(dbo.KUNDE.SkyldnerBeløb, dbo.ORDRE.Beløb, dbo.VARE.StkBeløb)

 

Hvilken SELECT returnerer ikke en error?

DECLARE @i SMALLINT = NULL

SELECT ISNULL (@i, 40000)

SELECT COALESCE (@i, 40000)

SELECT COALESCE (@i, 'abc')

 

ISNULL versus COALESCE

For the single replacements, ISNULL in SQL Server is a bit faster than COALESCE.

With ISNULL, the datatype of the second operand is also auto-magically converted to the first.

With COALESCE, the datatype will be returned as which ever data type of the finally selected operand is. COALESCE use the data type precedents rules.

That can lead to some major surprises:

ISNULL (SmallintValue, 499) – OK.

ISNULL (SmallintValue, 44499) – Error.

COALESCE (SmallintValue, 499) – OK.

COALESCE (SmallintValue, 44499) - OK, return data type is INT.

COALESCE (SmallintValue1, SmallintValue2, 67, 99999) - OK, will return 67 having the data type INT, because 99999 is INT.

ISNULL (VARCHAR_5, 'NULL') - OK, returns the string constant 'NULL'.

ISNULL (VARCHAR_5, 'The column must have a value' ) - OK, returns 'The c'.

COALESCE (VARCHAR_5, 'NULL') - OK, returns 'NULL'.

COALESCE (VARCHAR_5, 'The column must have a value' ) - OK, returns 'The column must have a value'.

 

Få en NULL når feltet har en bestemt værdi

Med funktion NullIf får man NULL værdi når de to parametre har samme værdi ellers får man værdien af første parameter, er sammenlignes med den tomme streng:

 

NULLIF(dbo.KUNDE.Navn, '')

 

Når Navn = 'Jensen' fås 'Jensen'.

Når Navn = '' fås NULL.

Når Navn = NULL fås NULL.

 

SELECT CASE WHEN NULLIF(dbo.KUNDE.Navn, '') IS NULL

            THEN 0

            ELSE 1 END

       AS UdfyldtNavn

 

Det kan ofte være en god idé at undgå at have NULL værdi i et felt i en tabel, og i KUNDE tabellen kunne Navn feltet i stedet have en default værdi på '', fordi på et skærmbilledet i en applikation vises NULL og den tomme streng på samme måde, nemlig som ingenting klar til at udfyldt af brugeren og select bliver lidt kortere:

SELECT CASE WHEN dbo.KUNDE.Navn = '' THEN 0 ELSE 1 END AS UdfyldtNavn

SELECT IIF(dbo.KUNDE.Navn = '', 0 ,1) AS UdfyldtNavn

 

Andre eksempler:

SELECT ISNULL(NULLIF(Navn,''),'¤')

-- Når Navn er den tomme streng, fås Null, der giver ¤ tegnet.

 

SELECT IIF(LEN(Navn)=0,'¤',Navn)

-- Jeg synes brug af Len er mere selvforklarende.

 

SELECT ISNULL(Beløb / NULLIF(Antal,0), 0)

-- Når nævner Antal er 0, fås Null, d.v.s. ingen division, der fås 0.

 

Strenge og null

SELECT 'abc' + NULL  -- giver NULL

 

SET CONCAT_NULL_YIELDS_NULL OFF

SELECT 'abc' + NULL  -- giver abc.

 

CONCAT funktion så concatenate NULL er uden problem i SQL Server 2012

SELECT CONCAT('abc', NULL) -- giver abc.

 

DECLARE @a nvarchar(50), @b nvarchar(50), @c nvarchar(101)

SET @a = 'abc'

SET @b = 'æøå'

SET @c = CONCAT(@a, @b)

PRINT @c

SET @b = NULL

SET @c = CONCAT(@a, @b)

PRINT @c

 

CONCAT konverterer selv parametrene til strenge:

DECLARE @a smallint, @b tinyint, @c nvarchar(50)

SET @a = 12345

SET @b = 123

SET @c = CONCAT(@a, @b)

PRINT @c

SET @b = NULL

SET @c = CONCAT(@a, @b)

PRINT @c

 

SELECT sætning der giver nyt felt ved at sætte felter sammen med mellemrum:

SELECT Result = CONCAT(Region, ' ', Name, ' ', SalesYTD) FROM Customer

 

Brug Concat fremfor SELECT Land + '_F' , fordi den giver NULL når Land is NULL.

 

IIF (Inline IF eller Immediately IF) i SQL Server 2012

Syntaks: IIF( boolean_expression, true_value, false_value )

Før måtte man anvende CASE WHEN, men IIF gør programeringen kønnere:

SELECT CASE WHEN Num1 > Num2 THEN Num1 ELSE Num2 END AS 'Max'

FROM   dbo.Numbers

 

SELECT IIF(Num1 > Num2, Num1, Num2) AS 'Max'

FROM   dbo.Numbers

 

I tilfælde af, at felterne Num1 og Num2 kan indeholde NULL, så gøres NULL til nul:

SELECT IIF(ISNULL(Num1, 0) > ISNULL(Num2, 0), Num1, Num2) AS 'Max'

 

IIF kan også bruges som indlejret (nested):

SELECT IIF(Num1 > Num2, IIF (Num1 > Num3, Num1, Num3), Num2) AS 'Max'

 

SELECT PersonId = IIF(PersonId >= 0, PersonId, 0)

SELECT ErIdag=IIF(CAST(Ordredato AS date)=CAST(GETDATE() AS date),1,0)

 

IIF kan indgå i alle udtryk, her sammen med ISNUMERIC om streng-feltindholdet er en talværdi så kan den konverteres til en ægte talværdi og indgå i en join:

UPDATE new

SET   KommuneId = db.KommuneId

FROM   dbo.EDW_NEW_TILLADELSE new

       INNER JOIN dbo.EDW_DB_Kommune db

         ON db.KommuneKode = IIF(ISNUMERIC(new.KommuneKode) = 1,

                                 CAST(new.KommuneKode AS int), 0)

 

SELECT IIF([Antal Uger] = 0, NULL,

           [Salgsbeløb] / [Antal Uger]) AS Gennemsnitssalgpruge

 

IIF is simular to IF THEN ELSE.

 

Find max værdi ud fra tre felter der omdannes til en on-the-fly tabel med tre rækker ved brug af values kommandoen vi normal ser ved en Insert Into:

SELECT *, MaxBeløb =

 (SELECT MAX(t.Beløb)

  FROM (VALUES (AfgiftBeløb), (ForsendelseBeløb), (MomsBeløb))

  AS t(Beløb))

FROM VareSalg

 

Join to tabeller A og B og udtag den første værdiJOIN TO TABELLER SAMMEN

SELECT A.Id, B.Id, MaxBeløb =

 (SELECT MAX(t.Beløb) FROM (VALUES (A.Beløb), (B.Beløb)) AS t(Beløb))

FROM A INNER JOIN B ON B.Id = A.Id

 

CHOOSE udvælger en værdi via et indeks i SQL Server 2012

Syntaks: CHOOSE ( index, val_1, val_2 [, val_n ] )

Giver værdien på indeks pladsen, nedenfor gives 'Developer':

SELECT Type = CHOOSE(3, 'Manager', 'Director', 'Developer', 'Tester')

 

Til gruppering af værdier, her forudsat at ProductCategoryID er mellem 1 og 5:

SELECT ProductCategoryID,

       CHOOSE(ProductCategoryID, 'A','B','C','D','E') AS Mode

FROM Production.ProductCategory;

 

Måneder til sæson:

SELECT JobTitle, HireDate, CHOOSE(MONTH(HireDate),'Winter','Winter', 'Spring','Spring','Spring','Summer','Summer','Summer','Autumn', 'Autumn','Autumn','Winter') AS Season

FROM HumanResources.Employee

WHERE YEAR(HireDate) > 2005

ORDER BY YEAR(HireDate);

 

Choose is simular to Switch/Case When Then End.

 

Formatteringer

DECLARE @d DATETIME = GETDATE()

SELECT

FORMAT(@d, 'yyyy-MM-dd') AS 'år-måned-dag',        -- 2015-01-10

FORMAT(@d, 'dd-MM-yyyy') AS 'dag-måned-år',        -- 10-01-2015

FORMAT(@d, 'dd/MM/yyyy') AS 'dd/MM/yyyy',          -- 10/01/2015

FORMAT(@d, 'dd/MMM/yyy') AS 'dd/MMM/yy',           -- 10/Jan/2015

FORMAT(@d, 'd', 'da-DK') AS 'Danmark',             -- 10-01-2015

FORMAT(@d, 'dd. MMMM yyyy', 'da-DK') AS 'Danmark', -- 10. januar 2015

FORMAT(@d, 'D', 'da-DK') AS 'Danmark',             -- 10. januar 2015

FORMAT(123456789, 'F0', 'da-DK') AS 'Nummer',      -- 123456789

FORMAT(123456789, 'F1', 'da-DK') AS 'Nummer',      -- 123456789,0

FORMAT(123456789, 'F2', 'da-DK') AS 'Nummer',      -- 123456789,00

FORMAT(123456789, 'F3', 'da-DK') AS 'Nummer',      -- 123456789,000

FORMAT(123456789, 'N0', 'da-DK') AS 'Nummer',      -- 123.456.789

FORMAT(123456789, 'N2', 'da-DK') AS 'Nummer',      -- 123.456.789,00

FORMAT(1.23456789, 'N4', 'da-DK') AS 'Nummer',     -- 1,2346

FORMAT(1.494, 'N2', 'da-DK') AS 'Nummer'           -- 1,49

FORMAT(1.495, 'N2', 'da-DK') AS 'Nummer'           -- 1,50

FORMAT(ROUND(1.495,0), 'N2', 'da-DK') AS 'Nummer'  -- 1,00

FORMAT(ROUND(1.5,0), 'N2', 'da-DK') AS 'Nummer'    -- 2,00

FORMAT(123456789, 'C', 'da-DK') AS 'Beløb',      -- kr. 123.456.789,00

FORMAT(123456789, '###-##-####') AS 'Format',      -- 123-45-6789

FORMAT(0.3333, 'P', 'da-DK') AS 'Procent'          -- 33,33 %

 

Format 'N0' viser ingen decimaler, 'N1' viser en decimal, 'N2' viser 2 decimaler, 'N4' viser 4 decimaler, og der afrundes på normalvis så 1.495, 'N2' giver 1,50. Round 0 afrunder på normalvis til uden decimaler og sammen med 'N2' fås ,00.

Jeg bruger tit Format på tal kolonner, inden jeg copy-paste resultatet til Excel, så bliver det til ægte kommatal og kan summeres i Excel.

 

Kontrol inden der udføres en cast til en bestemt data type

Det kan være, at data ikke kan castes til den ønskede data type, hvorved CAST vil give fejl: Conversion failed when converting the nvarchar value '123a' to data type int, så kan TRY_CAST anvendes fordi den ikke giver fejl men i stedet giver NULL:

DECLARE @s nvarchar(100) = N'123a'

SELECT Tal = CAST(@s AS int)                  -– giver fejl

SELECT Tal = TRY_CAST(@s AS int)              -– giver null

SELECT Tal = ISNULL(TRY_CAST(@s AS int), -1)  -– giver -1

SET @s = N'123'

SELECT Tal = CAST(@s AS int)                  -– giver 123

SELECT Tal = TRY_CAST(@s AS int)              -– giver 123

SELECT Tal = ISNULL(TRY_CONVERT(int, @s), -1) -– giver 123

 

Nedenstående sætter Tal til Null når SET @s = NULL:

SELECT Tal = CASE WHEN @s IS NOT NULL

                  THEN ISNULL(TRY_CAST(@s AS int), -1) END

 

Dato er altid et klassisk tilfælde, hvordan er formatet for en dato:

SET DATEFORMAT mdy

IF TRY_CAST('12/31/2010' AS date) IS NOT NULL

   PRINT 'OK'

ELSE

   PRINT 'ERROR'

 

SET DATEFORMAT dmy

IF TRY_CAST('31/12/2010' AS date) IS NOT NULL

   PRINT 'OK'

ELSE

   PRINT 'ERROR'

 

SET DATEFORMAT dmy

IF TRY_CAST('31-12-2010' AS date) IS NOT NULL

   PRINT 'OK'

ELSE

   PRINT 'ERROR'

 

Det er ikke altid, at et Nummer felt indeholder et tal nummer, så her undersøges om Nummer feltet kan castes til et helttal og går det godt, så castes til en streng ellers giver TRY_CAST Null og så forsøger jeg med feltets indhold for at se om det er lig med Kunde.Kode feltet:


INNER JOIN ISNULL(CAST(

    TRY_CAST(t.Nummer AS int) AS varchar(50)), t.Nummer) = Kunde.Kode

 

En slags konverteringsfunktioner PARSE() and TRY_PARSE() i SQL 2012

SELECT

TRY_PARSE('27 juli 2011' AS DATE) AS 'English', -- giver null

TRY_PARSE('27 july 2011' AS DATE USING 'en-US') AS 'English',

TRY_PARSE('27 juli 2011' AS DATE USING 'da-DK') AS 'Danish',

TRY_PARSE('27 juillet 2011' AS DATE USING 'fr-FR') AS 'French'

 

Videnskabelig notation konverter til kommatal

DECLARE @QuantityScientificNotation varchar(100) = '1.0E7'

-- '7.890123456E8' '5.68579E+17' '7.54001E+006' -- E means exponent

DECLARE @QuantityDecimal decimal(28, 5)

 

SELECT

CASE WHEN (@QuantityScientificNotation IS NULL OR

           (@QuantityScientificNotation <> '' AND

            (TRY_CAST(@QuantityScientificNotation AS decimal(28, 5)) IS NOT NULL) OR

            (TRY_CAST(@QuantityScientificNotation AS float) IS NOT NULL)))

THEN 'Success' ELSE 'Failure' END

 

SET @QuantityDecimal =

    CASE WHEN (TRY_CAST(@QuantityScientificNotation AS decimal(28, 5)) IS NOT NULL)

      THEN CAST(@QuantityScientificNotation AS decimal(28, 5))

    WHEN (TRY_CAST(@QuantityScientificNotation AS float) IS NOT NULL)

      THEN CAST(CAST(@QuantityScientificNotation AS float) AS decimal(28, 5))

    END

 

SELECT @QuantityDecimal

 

3,12977952358254E+16 =

31297795235825400 men var i virkeligheden lidt mere præcist som

31297795235825420 så E+ skal undgås i float værdi.

 

SELECT CONVERT(decimal(28,2), CAST('3.12977952358254E16' AS FLOAT))

SELECT CONVERT(decimal(28,2), CAST('5E16' AS FLOAT))

SELECT CONVERT(decimal(28,16), CAST('5E-16' AS FLOAT))

31297795235825400.00

50000000000000000.00

0.0000000000000005

 

Bedst at finde store værdier ved at udtrykke det i en SQL Where del:

SELECT *

FROM dbo.RapportData

WHERE FloatValue >= 1000000000000000

 

Linieskift og email

DECLARE @tekst nvarchar(1024)

SET @tekst = 'ETL udført.' + CHAR(10) + 'Kuber processeret.'

EXEC msdb.dbo.sp_send_dbmail -- Database Mail opsat smtp ip-adresse

     @profile_name = 'Drift',

     @recipients = 'bla@bla.dk;blu@bla.dk',

     @body = @tekst,

     @subject = 'ETL status'

 

Data fra en tabel bliver konverteret til en html tabel og sendt på email

DECLARE @xml NVARCHAR(MAX)

DECLARE @body VARCHAR(MAX)

SET @xml = CAST((

 SELECT [CustomerID] AS 'td','',[CompanyName] AS 'td','',

        [ContactName] AS 'td','',[ContactTitle] AS 'td'

 FROM Customers

 ORDER BY CustomerID

 FOR XML PATH('tr'), ELEMENTS

 ) AS NVARCHAR(MAX))

SET @body =

'<html><body><table border=1><tr><th>CustomerID</th><th>CompanyName</th>

<th>ContactName</th><th>ContactTitle</th></tr>' +

CAST(CAST(@xml AS XML) AS NVARCHAR(MAX)) +

'</table></body></html>'

--SELECT @body -- output kopieres til noteblok og gem som ansi test.htm fil.

EXEC msdb.dbo.sp_send_dbmail

 @recipients = @recipients, -- variablene er tildelt med ønskede værdier.

 @subject = @subject,

 @body = @body,

 @body_format ='HTML';

 

<html><body><table border=1>

<tr><th>CustomerID</th><th>CompanyName</th><th>ContactName</th>

<th>ContactTitle</th></tr>

<tr><td>ALFKI</td><td>Alfreds Futterkiste</td><td>Maria Anders</td>

<td>Sales Representative</td></tr>

</table></body></html>

 

Username without domain

SELECT SUSER_SNAME() AS [Domain\User],

 STUFF(SUSER_SNAME(), 1, charindex('\', SUSER_SNAME()), '') AS [User]

 

SELECT ORIGINAL_LOGIN(), SUSER_SNAME()

EXECUTE AS LOGIN = 'XXX'

GO

SELECT ORIGINAL_LOGIN(), SUSER_SNAME() -- SUSER_SNAME = 'XXX'.

 

The Current_User will return the name of the user in the database whereas SUSER_NAME() will return the user name on the server.

 

Foreningsmængde

SELECT F1, F2

FROM   dbo.T1

UNION -- dropper dubletter, UNION ALL medtager dubletter fra T1 og T2.

SELECT F1, F2

FROM   dbo.T2

 

Hvilke medarbejdere har enten C++ eller C# som programmeringsfærdigheder:

SELECT e.EmployeeId, e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C++'

UNION

SELECT e.EmployeeId, e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C#'

 

Se senere hvilke medarbejdere har både C++ og C# programmeringsfærdigheder.

 

Fællesmængde (eksempler fra SQL 2005’s AdventureWorks database)

A INTERSECT B. Rækker som er i A er også i B.

A og B har fælles rækker for angivet felter i SELECT og der fås unikke rækker retur (ind­byg­get distinct), d.v.s. der fås de VendorID,ModifiedDate som både er i ta­bel­len VendorContact og i tabellen VendorAddress:

SELECT VendorID,ModifiedDate

FROM Purchasing.VendorContact

INTERSECT

SELECT VendorID,ModifiedDate

FROM Purchasing.VendorAddress

ORDER BY VendorID,ModifiedDate  -- sikring af sortering.

Giver 104 unikke række.

 

Brug af INNER JOIN i stedet for INTERSECT og sortering efter clustered:

SELECT vc.VendorID,vc.ModifiedDate

FROM Purchasing.VendorContact vc

     INNER JOIN Purchasing.VendorAddress va

    ON va.VendorID = vc.VendorID AND va.ModifiedDate = vc.ModifiedDate

Giver 156 rækker med gentagelser (dubletter).

 

Brug af DISTINCT for at undgå gentagelser og sortering efter clustered:

SELECT DISTINCT vc.VendorID,vc.ModifiedDate

FROM Purchasing.VendorContact vc

     INNER JOIN Purchasing.VendorAddress va

    ON va.VendorID = vc.VendorID AND va.ModifiedDate = vc.ModifiedDate

Giver 104 unikke række.

 

Alternativ til INTERSECT og INNER JOIN:

SELECT VendorID,ModifiedDate

FROM (

      SELECT 1 AS dummy,VendorID,ModifiedDate

      FROM Purchasing.VendorContact

      UNION ALL -- medtager dubletter i modsætning til UNION.

      SELECT 2 AS dummy,VendorID,ModifiedDate

      FROM Purchasing.VendorAddress

     )AS T

GROUP BY VendorID,ModifiedDate

HAVING COUNT(*) >= 2

ORDER BY VendorID,ModifiedDate

Giver 104 unikke række.

 

Hvilke medarbejdere har både C++ og C# som programmeringsfærdigheder:

SELECT e.EmployeeId, e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C++'

INTERSECT

SELECT e.EmployeeId, e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C#'

ORDER BY EmployeeName, EmployeeId

 

Alternativ til INTERSECT og INNER JOIN er String_Split og en variabel:

DECLARE @SkillList varchar(1000) = 'C++,C#'

SELECT e.EmployeeId, e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill IN(SELECT value FROM STRING_SPLIT(@SkillList, ','))

GROUP BY e.EmployeeId, e.EmployeeName

HAVING COUNT(*) = (SELECT COUNT(*) FROM STRING_SPLIT(@SkillList, ','))

ORDER BY e.EmployeeName, e.EmployeeId

 

Har en medarbejder kun et af de to programmeringsfærdigheder, så vil COUNT(*) give 1 og det er ikke lig med de to fag som er angivet i variablen. En medarbejder medtages kun, når COUNT(*) giver 2 d.v.s. at begge programmeringsfærdigheder er opfyldt. Hvorvidt medarbejderen har andre programmeringsfærdigheder er ligegyldigt i for denne forespørgsels formål. Brug af GROUP BY betyder, at samme medarbejder ikke medtages to gange, d.v.s. svarende til SELECT DISTINCT.

 

Differensmængde (eksempler fra SQL 2005’s AdventureWorks database)

A MINUS B. Rækker som er i A men ikke i B.

A er fratrukket B for angivet felter i SELECT og der fås unikke rækker retur (ind­byg­get distinct). Kaldes EXCEPT fremfor MINUS:

SELECT VendorID,ModifiedDate

FROM Purchasing.VendorContact

EXCEPT

SELECT VendorID,ModifiedDate

FROM Purchasing.VendorAddress

ORDER BY VendorID,ModifiedDate  -- sikring af sortering.

Giver en række, fordi Vendor 105 er blevet indsat med en kontaktperson uden adresse.

 

Sammenligne indholdet mellem to tabeller for at se, om indholdet er ens.

 

SELECT F1, F2, F3

FROM T1

EXCEPT

SELECT F1, F2, F3

FROM T2

 

Og vice versa.

0 rows is good for a equal comparison test.

 

Brug af NOT EXISTS i stedet for EXCEPT:

SELECT vc.VendorID,vc.ModifiedDate

FROM Purchasing.VendorContact vc

WHERE NOT EXISTS(SELECT TOP(1) 1

                 FROM Purchasing.VendorAddress va

                 WHERE va.VendorID = vc.VendorID AND

                 va.ModifiedDate = vc.ModifiedDate)

Giver en række.

 

A logical left semi join is a query with an EXISTS() clause.

A logical left anti semi join is a query with an NOT EXISTS() clause.

 

Brug af LEFT OUTER JOIN i stedet for NOT EXISTS er ofte hurtigere på store data mængder:

SELECT vc.VendorID,vc.ModifiedDate

FROM Purchasing.VendorContact vc

     LEFT OUTER JOIN Purchasing.VendorAddress va

    ON va.VendorID = vc.VendorID AND va.ModifiedDate = vc.ModifiedDate WHERE va.VendorID IS NULL AND va.ModifiedDate IS NULL

Giver en række.

 

Når left outer join indeholder et kriterie betyder det, at en række fra A medtages når enten dens Id ikke findes i B eller når Id findes i B så skal B rækken også have IsCurrent = 1 for at A rækken medtages:

 

FROM A LEFT OUTER JOIN B ON B.Id = A.Id AND B.IsCurrent = 1

 

Alternativ til EXCEPT og NOT EXISTS og LEFT OUTER JOIN:

SELECT VendorID,ModifiedDate

FROM (

      SELECT 1 AS dummy,VendorID,ModifiedDate

      FROM Purchasing.VendorContact

      UNION ALL

      SELECT 2 AS dummy,VendorID,ModifiedDate

      FROM Purchasing.VendorAddress

     )AS T

GROUP BY VendorID,ModifiedDate

HAVING MAX(dummy) = 1

Giver en række.

 

Hvad tror du disse to sql’er giver:

SELECT *

FROM t INNER JOIN s ON s.pk = t.pk

WHERE NOT EXISTS (SELECT s.* INTERSECT SELECT t.*)

 

SELECT *

FROM t INNER JOIN s ON s.pk = t.pk

WHERE

 COALESCE(s.id, -2147483648) <> COALESCE(t.id, -2147483648) OR

 COALESCE(s.txt, '*') <> COALESCE(t.txt, '*') OR

 COALESCE(s.val, -922337203685477.5808) <>

 COALESCE(t.val, -922337203685477.5808)

 

Hvilke måneder er komplete fra Deltagerlisten, d.v.s. hvor der er data på alle ugerne i måneden:

SELECT DISTINCT a.ÅrMåned         -- Finder alle de måneder der ikke

FROM  dbo.DW_KALENDER_ÅrUge a     -- er i mængden af den indre select,

WHERE NOT EXISTS(                 -- d.v.s. mængde difference.

          SELECT DISTINCT b.ÅrMåned     -- Finder alle de måneder der

          FROM dbo.DW_KALENDER_ÅrUge b  -- ikke er uger på.

          WHERE NOT EXISTS(

                    SELECT TOP(1) 1

                    FROM dbo.Deltagerliste c

                    WHERE c.ÅrUge = b.ÅrUge)

          AND b.ÅrMåned = a.ÅrMåned)

ORDER BY a.ÅrMåned

 

Hvilke medarbejdere har C++ og har ikke C# som programmeringsfærdighed:

SELECT e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C++'

EXCEPT

SELECT e.EmployeeName

FROM   dbo.Employee e

       INNER JOIN dbo.SkillGroup s ON s.SkillGroupId = e.SkillGroupId

WHERE s.Skill = 'C#'

 

Forrige række, næsten, med self referencing join og theta join

Ordre tabellen har dette indhold:

 

Kundenummer

Ordredato

Antal

123

10-12-2014

12

123

03-01-2015

30

123

28-01-2015

8

123

02-02-2015

26

125

03-02-2015

10

125

04-02-2015

8

125

09-03-2015

30

 

Der ønskes de ordrer med antal 20+ og den forrige ordre som har antal 10+:

 

Kundenummer

Ordredato

Forrige_Ordredato

123

03-01-2015

10-12-2014

123

02-02-2015

03-01-2015

125

09-03-2015

03-02-2015

 

Findes med denne sql sætning:

SELECT cur.Kundenummer, cur.Ordredato,          -- current

       MAX(pre.Ordredato) AS Forrige_Ordredato  -- previous

FROM   dbo.ORDRE cur

       INNER JOIN dbo.ORDRE pre

       ON pre.Kundenummer = cur.Kundenummer AND -- selfreferencingjoin

          pre.Ordredato < cur.Ordredato         -- thetajoin

WHERE  cur.Antal >= 20 AND pre.Antal >= 10      -- filtercriteria

GROUP BY cur.Kundenummer, cur.Ordredato

ORDER BY cur.Kundenummer, cur.Ordredato

 

Forventet indlæsningsdatoer og indlæsningsdatoer

SELECT Dato

FROM [Kalender].[dbo].[Kalender]

WHERE DagIUge BETWEEN 1 AND 5 AND

      (Helligdag IS NULL OR

       Helligdag NOT IN('Langfredag','2. Påskedag',

             'Kristi Himmelfartsdag','Juledag','2. Juledag')

       ) AND

      År BETWEEN 2018 AND 2023 AND

      Dato != '2020-12-31' -- undtagelsesvis ikke forventet dato.

 

EXCEPT   

 

SELECT Load_Dato

FROM [Salgsmart].[audit].[Load_Log]

WHERE [Status] = 'Success'

ORDER BY 1

 

Tom tabel eller tabel med mindst en række

Count giver antal rækker i en tabel:

IF (SELECT COUNT(*) FROM dbo.Kunde) = 0

   PRINT 'TOM'

ELSE

   PRINT 'MINDST EN RÆKKE'

 

IF (SELECT COUNT(*) FROM dbo.Kunde) >= 1

   PRINT 'MINDST EN RÆKKE'

ELSE

   PRINT 'TOM'

 

Exists kan bruges til en formulering, hvis der ikke findes… TOP(1) for at begrænse antallet af rækker til kun én række for det er nok med én række til at lave en mængde med værdien 1 d.v.s. det andet 1 i SELECT, og det 1 står for sand):

IF NOT EXISTS (SELECT TOP(1) 1 FROM dbo.Kunde)

   PRINT 'TOM'

ELSE

   PRINT 'MINDST EN RÆKKE'

 

IF EXISTS (SELECT TOP(1) 1 FROM dbo.Kunde)

   PRINT 'MINDST EN RÆKKE'

ELSE

   PRINT 'TOM'

 

Table Value Constructor – lav en tabel med data til en sql sætning

Values kan danne on-the-fly data med flere rækker og kolonner, så det bliver til en tabel med navngivne kolonner, tabellen får navnet t med kolonnerne a og b:

SELECT t.a, t.b

FROM ( VALUES (1, 2), (3, 4), (5, 6), (7, 8), (9, 10) ) AS t(a, b)

 

Eller on-the-fly til en Cross join:

SELECT DISTINCT a.ProductName, t.Name

FROM dbo.Products a

     CROSS JOIN (VALUES ('Blade'), ('Race'), ('Cap')) AS t(Name)

 

Max fun fact

Max() always give a row and it can have NULL in the column, like this:

CREATE TABLE #t(i INT)

SELECT MAX(i) FROM #t              -- return null row

SELECT ISNULL(MAX(i),0) FROM #t    -- convert null to zero

SELECT MAX(i) FROM #t WHERE 0 = 1  -- return null row

SELECT MAX(i) FROM #t HAVING 0 = 1 -- here no row

DROP TABLE #t

 

Return two rows with 1 and 2 values:

SELECT *

FROM (VALUES (1), (2)) AS x(i)

WHERE EXISTS (SELECT MAX(y.i)

FROM (VALUES (1)) AS y(i)

WHERE y.i = x.i)

 

Return one row with 1 value:

SELECT *

FROM (VALUES (1), (2)) AS x(i)

WHERE EXISTS (SELECT MAX(y.i)

FROM (VALUES (1)) AS y(i)

WHERE y.i = x.i

HAVING MAX(y.i)>=1)

 

Statistik med CASE WHEN

Antal gode kunder i de enkelte lande hvorfra kunderne kommer i forhold til det samlet antal kunder per land. Der kan være lande, som ikke har gode kunder, så bliver forholdet 0 for de lande. Gange med 1.0 (kommatal i USA) for at få brøkens resultat som decimaltal:

 

SELECT

  KundeLand,

  ForholdsvisGodeKunder = SUM(CASE WHEN KundeStatus IN('Bedre','God') THEN 1 ELSE 0 END) * 1.0 / COUNT(*)

FROM dbo.Kunde

GROUP BY KundeLand  -- per land, d.v.s. FOR COUNT(*) og SUM.

 

Krydstabulering med CASE WHEN d.v.s uden Pivot

Pivotering af rækker, så kolonnen Træart der indeholder flere forskellige data bliver til flere kolonner navnet efter dataindholdet:

CREATE PROCEDURE [dbo].[EffektPrTræart_Salgsliste]

     @År     smallint,

     @Halvår  smallint

AS

SET NOCOUNT ON

 

SELECT CAST(@Halvår AS nvarchar) + '. halvår ' + CAST(@År AS nvarchar)

       AS Periode,

       EffektNavn,

       SUM(CASE WHEN Træart = 'ALØ' THEN Salgspris END) AS ALØ,

       SUM(CASE WHEN Træart = 'ANÅ' THEN Salgspris END) AS ANÅ,

       SUM(CASE WHEN Træart = 'ASK' THEN Salgspris END) AS ASK,

       SUM(CASE WHEN Træart = 'BØG' THEN Salgspris END) AS BØG,

       SUM(CASE WHEN Træart = 'EG'  THEN Salgspris END) AS EG

FROM

(

 SELECT e.EffektNavn, p.Træart,

        CASE @Halvår WHEN 1 THEN NULLIF(p.Salgspris1halvår, 0)

                     WHEN 2 THEN NULLIF(p.Salgspris2halvår, 0)

        END AS Salgspris

 FROM dbo.EFFEKT e

     INNER JOIN dbo.EFFEKTPRIS p ON p.EffektId = e.EffektId

 WHERE p.År = @År

) t

GROUP BY EffektNavn

ORDER BY EffektNavn

 

Krydstabulering med PIVOT

Kundesalget opgjort pr. kundegruppe og måneder fra tabellen Kundesalg, hvor oplysninger står som rækker, men med pivot vendes månedernes om til kolonner:

 

Kundegruppe

201101

201102

201103

201104

...

201112

 

PIVOT angiver Antal kolonnen summeres og fordeles ud på kolonneoverskrifterne:

SELECT *

FROM

(

 SELECT ÅrMåned, Kundegruppe, COUNT(*) Antal

 FROM dbo.Kundesalg

 GROUP BY ÅrMåned, Kundegruppe

) T

PIVOT (SUM (Antal) FOR ÅrMåned IN ([201101],[201102],[201103],

[201104],[201105],[201106],[201107],[201108],[201109],

[201110],[201111],[201112])) P

 

Medarbejdernes tre fraværstyper med hvert sit Id nummer, ønskes vist som ko­lon­ner med sigende fraværstypenavne. Det gøres med brug af PIVOT:

CREATE VIEW dbo.Oversigt

AS

SELECT TOP 99.99 PERCENT M.Cprnr, M.Navn, -- TOP i View med ORDER BY

       [1] AS [Sygdom], [2] AS [Barsel], [3] AS [Ferie] –-tre kolonner

FROM dbo.EDW_Medarbejder AS M  -- tabel med medarbejder og PK Cprnr

LEFT OUTER JOIN   –- medtager også medarbejder der ikke har fravær

(SELECT *

 FROM dbo.EDW_Medarbejder_Fravær  -- tabel med fravær og FK Cprnr

 PIVOT (SUM (AntalDage) FOR FraværTypeId IN ([1],[2],[3])

) AS PVT) AS T ON T.Cprnr = M.Cprnr

ORDER BY M.Cprnr

 

Cprnr

Navn

Sygdom

Barsel

Ferie

0101010001

Kim Jensen

5

0

15

0202020002

Pia Nielsen

10

55

5

 

Kunder har op til tre telefonnumre, som ønskes vist som kolonner:

 

KundeId

TelefonId

Telefonnummer

123

1

31000000

123

2

31000123

123

3

31001000

124

1

88001021

124

2

88001022

 

SELECT KundeId, Telefonnummer1 = MAX([1]), Telefonnummer2 = MAX([2]),

                Telefonnummer3 = MAX([3])

FROM dbo.KUNDE_TELEFONNUMMER

PIVOT (MAX(Telefonnummer) FOR TelefonId IN ([1],[2],[3])) p

GROUP BY KundeId

 

Eller uden MAX fordi Telefonnummer er af datatypen bigint:

SELECT KundeId, Telefonnummer1 = [1], Telefonnummer2 = [2],

                Telefonnummer3 = [3]

FROM dbo.KUNDE_TELEFONNUMMER

PIVOT (MAX(Telefonnummer) FOR TelefonId IN ([1],[2],[3])) p

ORDER BY KundeId

 

Eller uden PIVOT hvor MAX klapper rækker sammen i én række (smash, collaps):

SELECT KundeId,

       [1] = MAX(CASE WHEN TelefonId = 1 THEN Telefonnummer END),

       [2] = MAX(CASE WHEN TelefonId = 2 THEN Telefonnummer END),

       [3] = MAX(CASE WHEN TelefonId = 3 THEN Telefonnummer END)

FROM dbo.KUNDE_TELEFONNUMMER

GROUP BY KundeId

ORDER BY KundeId

 

Brug af Pivot ved en rapport forespørgsel, men idet der indgår kriterier som Startdato og Slutdato, anvendes funktion (Inline Table-Valued Function TVF) til at udsøge data, fordi Pivot har nogle begrænset i brug af WHERE:

Funktion med parameter som kriterier for rapporten, der returnerer en tabel:

CREATE FUNCTION [dbo].[Rapport_StedStatistikBasis]

  (@StartDato date, @SlutDato date)

RETURNS TABLE AS RETURN

(

SELECT BrugerId, HandlingTypeNavn, HandlingTidspunkt

FROM dbo.BRUGERLOG

WHERE Handlingstidspunkt BETWEEN @StartDato AND @SlutDato

)

GO

Funktionen kaldes eller anvendes som om det var en tabel:

SELECT *

FROM dbo.Rapport_StedStatistikBasis('2004-03-01', '2004-05-31')

 

Udvidet udgave som indeholder tabel definition og en opdatering kaldet

Multi-Statement Table-Valued Function TVF:

CREATE FUNCTION [dbo].[Rapport_StedStatistikBasis]

  (@StartDato date, @SlutDato date)

RETURNS @Resultet TABLE

  (BrugerId int NOT NULL,

   HandlingTypeNavn varchar(50) NOT NULL,

   HandlingTidspunkt date NOT NULL,

   Status varchar(5) NOT NULL,

   PRIMARY KEY CLUSTERED(BrugerId, HandlingTidspunkt)

  ) AS

BEGIN

 INSERT INTO @Resultet

 SELECT BrugerId, HandlingTypeNavn, HandlingTidspunkt, NULL

 FROM dbo.BRUGERLOG

 WHERE Handlingstidspunkt BETWEEN @StartDato AND @SlutDato

 

 UPDATE @Resultet

 SET Status =

     CASE WHEN HandlingTidspunkt = @SlutDato THEN 'End'

          ELSE 'Okay'

     END

       

 RETURN  -- returnerer output tabellen fra variablen @Resultet.

END

GO

 

En anden funktion som danner Pivot på basis af første der bliver kaldt i FROM:

CREATE FUNCTION [dbo].[Rapport_StedStatistikPivot]

  (@StartDato date, @SlutDato date)

RETURNS TABLE AS RETURN

(

SELECT BrugerId, Sted,

       SUM(Login) AS AntalLogin, SUM(Søgning) AS AntalSøg

FROM dbo.Rapport_StedStatistikBasis(@StartDato, @SlutDato) A

     INNER JOIN dbo.BRUGERSTED AS B ON B.BrugerId = A.BrugerId AND

                A.HandlingTidspunkt BETWEEN B.FraDato AND B.TilDato

PIVOT (COUNT(HandlingTidspunkt) FOR HandlingTypeNavn

  IN ([Login],[Søgning])) AS PVT

GROUP BY BrugerId, Sted

)

GO

 

Data skal anvendes i en rapport f.eks. i Access eller Reporting Services, hvor ste­der­ne skal sorteres i faldende orden med antal login. En stored procedure kan kal­des fra rapportens dataset med de kriterier brugerens har udfyldt som parameter. Hvis rap­porten kun kan stigende sortering (og ikke faldende), sikres faldende sortering ved at datasættet gør sorteringsværdien negativ ved at gange med minus 1:

CREATE PROCEDURE [dbo].[Rapport_StedStatistik]

  @StartDato date, @SlutDato date

AS

BEGIN

  SET NOCOUNT ON

  SELECT A.Sted, A.BrugerId, A.AntalLogin, A.AntalSøg,

         C.AntalStedLogin * (-1) AS AntalStedLogin

  FROM dbo.Rapport_StedStatistikPivot(@StartDato, @SlutDato) A

       INNER JOIN –- indlejret forespørgsel giver tabel kaldet B der

       ( --finder antal login pr. sted så der kan sorteres på værdien.

        SELECT T.Sted, T.SUM(AntalLogin) AS AntalStedLogin

        FROM dbo.Rapport_StedStatistikPivot(@StartDato, @SlutDato) T

        GROUP BY T.Sted

       ) B ON B.Sted = A.Sted

  ORDER BY B.AntalStedLogin DESC, A.Sted ASC, -- sortering ikke nødven

           A.AntalLogin DESC, A.BrugerId ASC -- dig fordi rap. gør det

END

GO

 

Modkrydstabulering med UNPIVOT

Tabellen Salgspris har kolonner for hver træart med hver deres salgspris per år:

 

SalgsprisÅrstal

ALØ

ANÅ

ASK

BØG

EG

2020

123

234

345

456

567

2021

987

876

765

654

543

2022

624

742

746

267

754

 

I en database ønskes træarterne som rækker, så der kun er en kolonne at søge i. Ovenstående tabel kan krydstabuleres, så træarterne bliver til rækker, og vær­dier­ne der repræsenterer salgspriser kommer i sin egen kolonne, så der fås neden­stående tabel, som en unpivot af ovenstående tabel:

 

SalgsprisÅrstal

Træart

Salgspris

2020

ALØ

123

2020

ANÅ

234

2020

ASK

345

2020

BØG

456

2020

EG

567

2021

ALØ

987

2021

ANÅ

876

2021

ASK

765

2021

BØG

654

2021

EG

543

2022

ALØ

624

2022

ANÅ

742

2022

ASK

746

2022

BØG

267

2022

EG

754

 

UNPIVOT med faste værdier fra kolonnerne med træarter:

SELECT SalgsprisÅrstal, Træart, Salgspris

FROM dbo.Salgspris

UNPIVOT

   (Salgspris FOR Træart IN

      (ALØ, ANÅ, ASK, BØG, EG)

   ) U

 

Ønsker man ikke de oprindelige kolonner i en Træart kolonne, skriv Nothing eller [Type] i UNPIVOT:

 

SalgsprisÅrstal

Salgspris

2020

123

2020

234

2020

345

 

SELECT SalgsprisÅrstal, Salgspris

FROM dbo.Salgspris

UNPIVOT

   (Salgspris FOR Nothing IN

      (ALØ, ANÅ, ASK, BØG, EG)

   ) U

 

CROSS APPLY med faste værdier fra kolonnerne med træarter:

SELECT s.SalgsprisÅrstal, a.Træart, a.Salgspris

FROM dbo.Salgspris s

     CROSS APPLY (VALUES ('ALØ', ALØ),('ANÅ', ANÅ),('ASK', ASK),

                         ('BØG', BØG),('EG', EG)) a(Træart, Salgspris)

 

CROSS JOIN med faste værdier fra kolonnerne med træarter:

SELECT s.SalgsprisÅrstal, a.Træart,

    CASE a.Træart WHEN 'ALØ' THEN s.ALØ WHEN 'ANÅ' THEN s.ANÅ

                  WHEN 'ASK' THEN s.ASK WHEN 'BØG' THEN s.BØG

                  WHEN 'EG'  THEN s.EG  END AS Salgspris

FROM dbo.Salgspris s

     CROSS JOIN (VALUES ('ALØ'), ('ANÅ'), ('ASK'), ('BØG'), ('EG')) AS

     a(Træart)

 

UNION ALL med faste værdier fra kolonnerne med træarter:

SELECT SalgsprisÅrstal, Træart='ALØ', Salgspris = ALØ

FROM dbo.Salgspris

UNION ALL

SELECT SalgsprisÅrstal, Træart='ANÅ', Salgspris = ANÅ

FROM dbo.Salgspris

UNION ALL

SELECT SalgsprisÅrstal, Træart='ASK', Salgspris = ASK

FROM dbo.Salgspris

UNION ALL

SELECT SalgsprisÅrstal, Træart='BØG', Salgspris = BØG

FROM dbo.Salgspris

UNION ALL

SELECT SalgsprisÅrstal, Træart='EG', Salgspris = EG

FROM dbo.Salgspris

ORDER BY SalgsprisÅrstal, Træart

 

Når brugere indtaster data i et regneark, lader de altid tiden være kolonner:

 

Id

Kundenavn

Januar

Februar

Marts

April

Maj

Juni

1

Danske Bank

21

12

32

34

65

23

2

Nodea Bank

100

102

105

106

109

111

 

I en database ønskes tiden som rækker, så der kun er en kolonne at søge i. Ovenstående tabel kan krydstabuleres, så tiden bliver til rækker, og værdierne der repræsenterer henvendelser til en help desk support funktion kommer i sin egen kolonne, så der fås nedenstående tabel, hvor en ÅrMåned kolonne er medtaget via join til en anden tabel:

 

Id

Kundenavn

ÅrMåned

Måned

Henvendelser

1

Danske Bank

200901

Januar

21

1

Danske Bank

200902

Februar

12

1

Danske Bank

200903

Marts

32

1

Danske Bank

200904

April

34

1

Danske Bank

200905

Maj

65

1

Danske Bank

200906

Juni

23

2

Nodea Bank

200901

Januar

100

2

Nodea Bank

200902

Februar

102

2

Nodea Bank

200903

Marts

105

2

Nodea Bank

200904

April

106

2

Nodea Bank

200905

Maj

109

2

Nodea Bank

200906

Juni

111

 

Månedsnavnene knyttes til en ÅrMåned Kalender tabel:

 

ÅrMåned

Måned

200901

Januar

200902

Februar

200903

Marts

200904

April

200905

Maj

200906

Juni

 

SELECT Id, Kundenavn, K.ÅrMåned, K.Måned, Henvendelser

FROM

   (SELECT Id, Kundenavn, Januar, Februar, Marts, April, Maj, Juni,

           Juli, August, September, Oktober, November, December

    FROM dbo.IMPORT) T

UNPIVOT

   (Henvendelser FOR Måned IN

      (Januar, Februar, Marts, April, Maj, Juni,

       Juli, August, September, Oktober, November, December)

) U

INNER JOIN dbo.KALENDER_MÅNED K ON K.Måned = U.Måned

 

Union all kan også lave unpivot

SELECT Id, Kundenavn, ÅrMåned=200901, Måned='Januar', Henvendelser = Januar

FROM dbo.IMPORT

UNION ALL

SELECT Id, Kundenavn, ÅrMåned=200902, Måned='Februar', Henvendelser = Februar

FROM dbo.IMPORT

UNION ALL

SELECT Id, Kundenavn, ÅrMåned=200903, Måned='Marts', Henvendelser = Marts

FROM dbo.IMPORT

UNION ALL

...

 

Cross apply kan også lave unpivot

 

BookingId

AntalVoksne

AntalBørn

123

4

2

124

2

3

125

1

0

 

SELECT BookingId, AntalPersoner

FROM dbo.Booking

     CROSS APPLY(VALUES (AntalVoksne),(AntalBørn))

     COLUMNNAMES(AntalPersoner)

 

BookingId

AntalPersoner

123

4

123

2

124

2

124

3

125

1

125

0

 

Funktion – Scalar-valued functions, giver en værdi retur

Nyttig funktion der klarer division også når nævneren er 0:

CREATE FUNCTION dbo.Div (@t float, @n float) 

RETURNS float

AS   

BEGIN

DECLARE @div float

IF (@t IS NULL) OR (@n IS NULL)

   SET @div = NULL

ELSE

   IF @n <> 0

      SET @div = (@t / @n)

   ELSE

      SET @div = NULL

RETURN (@div)

END

GO

 

Kald en funktion:

SELECT dbo.Div(25,5) as x, dbo.Div(25,4) as y, dbo.Div(25,0) as z,

 

Funktion  – Table-valued functions, giver en tabel retur

Funktion der beregner og returnerer en tabel tilbage:

CREATE FUNCTION dbo.GetKundeSumMængde(@KundeTypeId int, @Dato date)

RETURNS TABLE

AS

RETURN

(

SELECT dbo.VAREMÆNGDE.VareId,

       SUM(dbo.VAREMÆNGDE.Mængde) AS Mængde,

       SUM(dbo.Div(dbo.Nz(dbo.VAREMÆNGDE.Mængde),

     dbo.GetPræstation(dbo.KUNDE.KundeTypeId, @Dato))) AS Tidsforbrug

FROM dbo.KUNDE

     INNER JOIN dbo.VAREMÆNGDE

       ON dbo.VAREMÆNGDE.KundeId = dbo.KUNDE.KundeId

WHERE dbo.KUNDE.KundeTypeId = @KundeTypeId

GROUP BY dbo.VAREMÆNGDE.VareId

)

GO

En stored procedure der anvender funktionen:

CREATE PROCEDURE dbo.Rapport_KundeMængde @Dato date

AS

BEGIN

  SET NOCOUNT ON

 

  SELECT dbo.VARE.VareNavn, GetKundeSumMængde.*

  FROM dbo.KUNDE

       INNER JOIN dbo.VARE

         ON dbo.VARE.KundeTypeId = dbo.KUNDE.KundeTypeId

       INNER JOIN dbo.GetKundeSumMængde(dbo.KUNDE.KundeTypeId, @Dato)

          GetKundeSumMængde

         ON GetKundeSumMængde.VareId = dbo.VARE.VareId

END

GO

 

STRING_SPLIT funktion returnerer tabel til kriterie for stored procedure

En funktion til at opstille en liste til en tabel med listens elementer som ræk­ker:

CREATE FUNCTION dbo.STRING_SPLIT

   (@string varchar(4000), @delimiter varchar(10))

RETURNS @table TABLE (Value varchar (4000))

AS

BEGIN

     DECLARE @pos int

     DECLARE @piece varchar(4000)

     IF RIGHT(RTRIM(@string),1) <> @delimiter

        SET @string = @string  + @delimiter

     SET @pos =  PATINDEX('%'+@delimiter+'%' , @string)

     WHILE @pos <> 0

     BEGIN

        SET @piece = left(@string, @pos - 1)

        INSERT INTO @table VALUES(@piece)

        SET @string = STUFF(@string, 1, @pos, '')

        SET @pos =  PATINDEX('%'+@delimiter+'%' , @string)

     END   

     RETURN

END

GO

 

Eller med brug af en opbygget XML struktur og uendelig lang streng:

CREATE FUNCTION dbo.STRING_SPLIT

   (@string nvarchar(MAX), @delimiter varchar(10))

RETURNS @table TABLE (Value nvarchar(MAX))

AS

BEGIN

     DECLARE @xml XML

     SET @XML = N'<rt><r>' + REPLACE(@string, @delimiter , '</r><r>')+

                '</r></rt>'

     INSERT INTO @table(Value)

     SELECT n.value('.',' nvarchar(MAX)') AS Value

     FROM @xml.nodes('//rt/r') m(n)  -- tabel m med kolonne n

     RETURN

END  

GO

 

Eller med brug af Cross Apply og en opbygget XML struktur uden tabel variabel:

CREATE FUNCTION dbo.STRING_SPLIT

   (@string varchar(4000), @delimiter varchar(10))

RETURNS TABLE

AS

RETURN

(

 SELECT LTRIM(RTRIM(m.n.value('.[1]','varchar(4000)'))) AS Value

 FROM

  (

   SELECT CAST('<XMLRoot><RowData>' +

          REPLACE(@string,@delimiter,'</RowData><RowData>') +

          '</RowData></XMLRoot>' AS XML) AS x

  ) t                                            -- tabel t kolonne x

  CROSS APPLY t.x.nodes('/XMLRoot/RowData') m(n) -- tabel m kolonne n

)

 

SELECT value FROM dbo.STRING_SPLIT('200905,200906,200907', ',')

giver en tabel med tre rækker, d.v.s. strengen er blevet splittet op i rækker:

 

value

200905

200906

200907

 

Når en parameter til en stored procedure indeholder en tekst streng med værdier ad­skilt med komma, så kan strengen vendes om til en tabel og derved kan der sø­ges på vær­dierne i en anden tabel, her LEVERANCE tabel:

CREATE PROCEDURE dbo.List_Leverance

   @ÅrMåneder varchar(4000) = ''

AS

   -- EXEC dbo.List_Leverance '200905,200906,200907'

BEGIN

     SET NOCOUNT ON

     SELECT *

     FROM dbo.LEVERANCE

     WHERE  ÅrMåned IN (SELECT Value

                        FROM dbo. STRING_SPLIT(@ÅrMåneder, ','))

END

GO

 

DECLARE @list VARCHAR(MAX) = '

8C5A7AEF-03FB-4E42-98CE-EB06672B3967

E847BBDE-5324-42BA-B1C4-866453561A6F

E78BC645-96CD-4DEB-8333-0AA596F383C9

1B0EA60A-3BD3-447A-A2D6-2DF80785FC30

'

 

SELECT o.*

FROM dbo.Orders o

     INNER JOIN dbo. STRING_SPLIT(@list, CHAR(13)+CHAR(10)) l

       ON l.Value = o.Id

ORDER BY o.Id

 

-- Here is a TABs in front of table names:

DECLARE @TableList nvarchar(max) = N'

     customer

     order

     invoice

     '

 

SELECT DISTINCT TABLE_NAME

FROM ERP.INFORMATION_SCHEMA.TABLES t

     INNER JOIN dbo. STRING_SPLIT(@TableList, CHAR(13)+CHAR(10)) l

       ON REPLACE(l.Value, CHAR(9), '') = t.TABLE_NAME

WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'commerce'

 

Andet eksempel på split for heltalsværdier

CREATE FUNCTION Slice_CSV_to_Integers(@csv_list varchar(256))

RETURNS @table TABLE (Slice INT NOT NULL)

AS

BEGIN

  DECLARE @current_pos INT = 0,

          @next_pos INT = 1,

          @list_len INT = 0

  WHILE @next_pos > 0

  BEGIN

    SET @next_pos = CHARINDEX(',', @csv_list , @current_pos + 1)

    SET @list_len = CASE WHEN @next_pos > 0 THEN @next_pos

                         ELSE LEN(@csv_list) + 1 END

    INSERT INTO @table

    VALUES(CAST(SUBSTRING(@csv_list, @current_pos + 1, @list_len)

                AS INT))

    SET @current_pos = @next_pos 

  END

END

 

SELECT * FROM Slice_CSV_to_Integers('123,456,789')

 

An Improved SQL 8K CSV Splitter Function

 

Eksempel med en tabel (dannet on the fly) hvis kolonne vriddes om til rækker

DECLARE @t TABLE

(

Id INT,

Titel VARCHAR(8000)

)

INSERT @t VALUES (1,'IT-Konsulent,Datalog,HD,HH,Gymnasium'),

                 (2,'Direktør,cand.scient')

SELECT Id,

LTRIM(RTRIM(m.n.value('.[1]','varchar(8000)'))) AS Titel

FROM

(

 SELECT Id, CAST('<XMLRoot><RowData>' +

            REPLACE(Titel,',','</RowData><RowData>') +

            '</RowData></XMLRoot>' AS XML) AS x

 FROM @t

 ) t

 CROSS APPLY t.x.nodes('/XMLRoot/RowData') m(n)

 

En kommasepareret værdi splittes via en xml til tre kolonner kaldet

string-split-to-columns:

WITH xmldata AS

(

 SELECT CONVERT(XML,'<Data><Tag>' + REPLACE(v,';', '</Tag><Tag>') +

                    '</Tag></Data>') AS Dataset

 FROM ( VALUES ('123;Godt;2016-02-12') ) AS t(v)

)

SELECT

  Id = Dataset.value('/Data[1]/Tag[1]','int'),

  Status = Dataset.value('/Data[1]/Tag[2]','varchar(10)'),

  Dato = Dataset.value('/Data[1]/Tag[3]','date')

FROM xmldata

 

SQL Server 2016 med STRING_SPLIT

STRING_SPLIT konverterer en string indhold til tabel rækker a la SplitToTable.

DECLARE @tags NVARCHAR(400) = 'clothing,road,touring,bike'

SELECT value 

FROM STRING_SPLIT(@tags, ',') 

WHERE RTRIM(value) <> ''

 

Eller at søge efter produkt-id’er som er enten 1, 2 eller 3:

SELECT p.ProduktId, p.ProduktNavn 

FROM dbo.Produkt p

     INNER JOIN STRING_SPLIT('1,2,3', ',') s ON s.value = p.ProduktId

 

SELECT ProduktId, ProduktNavn 

FROM dbo.Produkt

WHERE ProduktId IN(SELECT CONVERT(int, value)

                   FROM STRING_SPLIT('1,2,3', ','))

 

Der kan opnås forbedret performance ved at lægge søgekriterierne i en temporær tabel og sikre at sql server query optimizer kender antallet af dens rækker:

DROP TABLE IF EXISTS #Liste_ProduktId

CREATE TABLE #Liste_ProduktId (ProduktId int NOT NULL PRIMARY KEY)

INSERT #Liste_ProduktId(ProduktId)

SELECT value FROM STRING_SPLIT('1,2,3', ',')

UPDATE STATISTICS #Liste_ProduktId

 

SELECT ProduktId, ProduktNavn

FROM dbo.Produkt

WHERE  ProduktId IN (SELECT ProduktId FROM #Liste_ProduktId)

 

Ovenstående anvender Clustered Index Seek i Produkt tabel.

 

Sikring af sorteringsorden fra string_split inkl. dubletter:

DECLARE @s nvarchar(max)= N'Mit navn er Anders And, husk mit navn'

;WITH MyCTE AS

(

  SELECT value , CHARINDEX (value,@s,0) MyOrder

  FROM STRING_SPLIT(@s,' ')

)

SELECT value , MyOrder,

  RN = ROW_NUMBER() OVER (PARTITION BY MyOrder ORDER BY MyOrder) - 1,

  FinalOrder = CHARINDEX (value,@s,MyOrder + ROW_NUMBER()

                    OVER (PARTITION BY MyOrder ORDER BY MyOrder) - 1)

FROM MyCTE

ORDER BY FinalOrder

 

SQL Server 2022 med STRING_SPLIT rækkefølgenummer, parameter 1

 

DECLARE @s varchar(50) = 'abe,kat,hund'

 

SELECT *

FROM STRING_SPLIT(@s,',')

 

Value

Abe

Kat

Hund

 

Før SQL Server 2022 måtte man selv udregne et rækkefølgenummer for at sikre sorteringsorden med Order By:

 

SELECT *,

       Number = ROW_NUMBER() OVER(ORDER BY (SELECT 1)),

       TotalNumberOf = COUNT(1) OVER()

FROM STRING_SPLIT(@s,',')

ORDER BY 2

 

value

Number

TotalNumberOf

Abe

1

3

Kat

2

3

hund

3

3

 

Fra SQL Server 2022 angiver en parameter med værdien 1 at der ønskes et rækkefølgenummer kolonne kaldet ordinal, som der kan sorteres efter:

 

SELECT *

FROM STRING_SPLIT(@s,',',1)

ORDER BY ordinal

 

value

ordinal

Abe

1

Kat

2

hund

3

 

Kun det andet element i listen d.v.s. når et rækkefølgenummer er 2. Der fås Kat:

 

SELECT value

FROM STRING_SPLIT(@s,',',1)

WHERE ordinal = 2

 

Multiple-character delimiter erstattes med et tegn så String_Split virker

 

DROP TABLE IF EXISTS dbo.Pets;

CREATE TABLE dbo.Pets

(

  StudentID int PRIMARY KEY,

  PetNames  nvarchar(4000)

);

INSERT dbo.Pets(StudentID, PetNames) VALUES

  (1, N'Furface~|~Snowball~|~Max'),

  (2, N'Kirby~|~Quigley'),

  (3, N'Bond'),

  (4, N'Dax~|~Spike~|~Bowser~|~Rosco');

 

SELECT p.StudentID, s.Value

  FROM dbo.Pets AS p

       OUTER APPLY STRING_SPLIT(REPLACE(p.PetNames, N'~|~', NCHAR(9999)), NCHAR(9999)) AS s;

 

StudentID

Value

1

Furface

1

Snowball

1

Max

2

Kirby

2

Quigley

3

Bond

4

Dax

4

Spike

4

Bowser

4

Rosco

 

Fjernelse af mellemrum char(32) i to løsningsforslag

 

DECLARE @text VARCHAR(100) =  '  Hello   World  '

 

SELECT TRIM(REPLACE(REPLACE(REPLACE(@text, CHAR(32), '()'), ')(', ''), '()',

            CHAR(32))) AS CleanedText

 

SELECT STRING_AGG(value, CHAR(32)) WITHIN GROUP

                 (ORDER BY CHARINDEX(value, @text)) AS CleanedText

FROM STRING_SPLIT(TRIM(@text), CHAR(32)) -- gets two lines Hello and World

WHERE value <> ''                        -- and skip emtpy rows

 

 -- @text becomes 'Hello World'

 

Udsøgning af produkter fra en liste af produktnavne til en rapport

STRING_SPLIT for at søge efter udvalgte produkter fra en rapport i Reporting Services, hvor STRING_SPLIT laver strengen af produkter om til en tabel med produkter der indgår i en INNER JOIN:

 

CREATE PROCEDURE dbo.ProduktListePrUge

  @ÅrUge int, -- brugeren indtaster 201740 for uge 40 i 2017.

  @Produktnavne varchar(max) -- hvor flere produkter er valgt i en dropdown

AS

BEGIN

 DECLARE @Uge int = @ÅrUge - ((@ÅrUge / 100) * 100)

 DECLARE @År int = (@ÅrUge / 100)

 DECLARE @UgeStartdato datetime  -- mandag kl. 00:00:00.000 som default.

 DECLARE @UgeSlutdato datetime   -- søndag kl. 23:59:59.997 sidste sekund.

 SET DATEFIRST 1 -- gør mandag til første dag i ugen

 SET @UgeStartdato = DATEADD(WEEK, @Uge, CONCAT(@År,'0101')) - 4 –

       DATEPART(WEEKDAY, DATEADD(WEEK, @Uge, CONCAT(@År,'0101')) - 4) + 1

 SET @UgeSlutdato = DATEADD(MILLISECOND, -3, DATEADD(WEEK, @Uge, CONCAT(@År,'0101'))

     - 4 - DATEPART(WEEKDAY, DATEADD(WEEK, @Uge, CONCAT(@År,'0101')) - 4) + 8)

 SET NOCOUNT ON

 SELECT p.ProduktId, p.ProduktNavn, ÅrUge = @ÅrUge

 FROM dbo.Produkt p

      INNER JOIN STRING_SPLIT(@Produktnavne, ',') s ON s.value = p.ProduktNavn

 WHERE p.IsCurrent = 1 AND

       p.ProduktTidspunkt BETWEEN @UgeStartdato AND @UgeSlutdato

END -- ProduktTidspunkt er en datetime med forskellige klokkeslæt.

 

Søge i et Kimball star schema i en stored procedure med parametre, hvor en multivalue drop down i en Reporting Services rapport giver en streng som inde­holder de valgte kriterier, her er de i parameteren @KontotypeKodeList og STRING_SPLIT laver strengen om til en tabel med kriterierne som rækker til IN:

 

CREATE PROCEDURE [dbo].[Bankpostering]

 -- Default value indicates a parameter must not be null in the call.

 @FraDato     date = '2018-01-01',

 @TilDato     date = '2018-12-31',

 @Kundenummer int, -- Kundenummer er business key.

 @KontotypeKodeList nvarchar(MAX) = N'-1,STANDARD,PENSION,HUSLÅN,BILLÅN,INDLÅN'

AS

BEGIN

 SET NOCOUNT ON

 DECLARE @Kunde_dkey int -- durable key for current value to search fast in fact.

 SELECT  @Kunde_dkey = Kunde_dkey

 FROM Datamart.dbo.Dim_Kunde_Current

 WHERE Kundenummer_bkey = @Kundenummer

 

 SELECT

   Kundenavn = k.Kundenavn,

   Dato = fact.fact.Bankdato,

   Kontonr = fact.Kontonummer,

   Kontonavn = p.Produktnavn

 FROM Datamart.dbo.Fact_Bankpostering fact

  INNER JOIN Datamart.dbo.Dim_Kunde_Current k

     ON k.Kunde_dkey = fact.Kunde_dkey

  INNER JOIN Datamart.dbo.Dim_Produkt_Registered p

     ON p.Produkt_key = fact.Produkt_key

 WHERE fact.Bankdato BETWEEN @FraDato AND @TilDato AND

       ( (@Kundenummer IS NULL) OR (fact.Kunde_dkey = @Kunde_dkey) ) AND

       (fact.Kontotype_dkey IN (SELECT Kontotype_dkey

             FROM Datamart.dbo.Dim_Kontotype_Current

             WHERE KontotypeKode IN(SELECT value

                                    FROM STRING_SPLIT(@KontotypeKodeList, ','))))

 ORDER BY 1,2,3

 OPTION(RECOMPILE) -- bygger query execution plan på aktuelle parameter værdier.

END

 

OPTION (RECOMPILE) for a new plan for every execution.

OPTION (RECOMPILE) can kill your instance immediately. That's fine for an analy­tic-like query that is executed occasionally. Still, if your most important query runs several times a minute or even a second, the CPU overload from RECOMPILE will bring the system to a halt.

OPTIMIZE FOR UNKNOWN for a optimizer uses average distribution.

 

Flere String split eksempler

Produkt tabel kolonne Tags indeholder en streng 'clothing,road,touring,bike' som bliver splittet til rækker, så samme produkt gentager for hvert tag i Tags:

SELECT ProduktId, ProduktNavn, value as Tag

FROM dbo.Produkt

     CROSS APPLY STRING_SPLIT(Tags, ',')

 

CROSS APPLY kan også kalde din egen kodet funktion:

SELECT *

FROM dbo.Kunde

     CROSS APPLY dbo. Scalar_Valued_Function(KundeNavn);

 

Et k-nummer med bindestreg opdeles i enkeltstående talværdier som rækker:

DECLARE @k varchar(50) = '546-13090-074-3595-88505122'

SELECT value as Delnummer

FROM STRING_SPLIT(@k, '-') 

WHERE TRIM(value) <> ''

 

Måske kan Pivot lave de fem rækker om til kolonner, ellers kan det gøres sådan,

a string-split-to-columns giver disse fem kolonner:

D1 = 546

D2 = 13090

D3 = 074

D4 = 3595

D5 = 88505122

 

/* Udregn start position for hvert tegn efter bindestreg for hvert af de fem delnumre:

select 1

select charindex('-',@k)+1

select charindex('-',@k,charindex('-',@k)+1)+1

select charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1

select charindex('-',@k,charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1)+1

 

Udregning af længden (antal tegn) for hvert delnummer så de kan udtages af strengen:

(næste delnummers start position) minus (gældende delnummers start position) minus 1.

*/

 

select -- parse value of @k

Count = coalesce(len(@k) - len(replace(@k,'-','')), len(@k), 0),

Valid = case when @k like '[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' then 1 else 0 end,

D1 = substring(@k, 1, charindex('-',@k)-1),

D2 = substring(@k, charindex('-',@k)+1,

     (charindex('-',@k,charindex('-',@k)+1)+1)-(charindex('-',@k)+1)-1),

D3 = substring(@k, charindex('-',@k,charindex('-',@k)+1)+1,

     (charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1)-

       (charindex('-',@k,charindex('-',@k)+1)+1)-1),

D4 = substring(@k, charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1,

     (charindex('-',@k,charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1)+1)-

       (charindex('-',@k,charindex('-',@k,charindex('-',@k)+1)+1)+1)-1),

D5 = substring(@k, charindex('-',@k,charindex('-',@k,charindex('-',@k,

                   charindex('-',@k)+1)+1)+1)+1, len(@k)) -– len tager resten.

 

LIKE kan bruges i join til udsøgning af data rækker, eksempelvis en tabel Navn indeholder en masse navne, og tabel Del indeholder navne f.eks. »Mette« og »Lotte« som kan være en del af et længere navn f.eks. »Mette Louise« eller »Charlotte« og dette sql udtryk finder de navne som indeholder delnavnene (substring):

SELECT n.*

FROM Navn n

     INNER JOIN Del d ON n.Navn Like '%' + d.Delnavn + '%'

 

Alternativ med kendte værdier som ikke er i en tabel:

WHERE Navn LIKE '%Mette%' OR Navn LIKE '%Lotte%'

 

SELECT n.*

FROM Navn n

     JOIN (values ('Mette'),('Lotte')) d(Delnavn)

           ON n.Navn Like '%' + d.Delnavn + '%'

 

Søge efter email adresser: values ('hotmail'),('gmail'),('yahoo'),('Outlook.com')).

 

Men nedenstående sql udtryk lader sig ikke gøre:

WHERE Navn LIKE ('%Mette%','%Lotte%')

WHERE Navn LIKE IN ('%Mette%','%Lotte%')

WHERE Navn LIKE_IN ('%Mette%','%Lotte%') som en ny kommando eller INLIKE.

 

Hvis du anvender full-text search on full-text indexed column, så prøv dette:

WHERE CONTAINS(Navn, '"*Mette*" OR "*Lotte*"')

 

Oracle har en kommando REGEXP_LIKE:

WHERE REGEXP_LIKE (Navn,'^Mette^|^Lotte^')

 

Postgresql har en kommando LIKE ANY:

SELECT *

FROM database.Navn

WHERE LOWER(Navn) LIKE ANY ('{%mette%,%lotte%}')

 

Feltet TransactionIdentification starter med et ReceiptNumber, find rækkerne:

fact.TransactionIdentification LIKE CONCAT(@ReceiptNumber,'%')

 

UTC til Dansk tid fra SQL Server 2016

Datatypen er datetime eller datetime2 og indholdet af feltet OrderDateTime_UTC er i tiden UTC Universal Time Coordinated d.v.s. uden sommertid Daylight Savings Time DST.

Gældende dato og klokkeslæt fås med GETUTCDATE() eller SYSUTCDATETIME().

 

UTC tid til Dansk tid ved at fortælle, at felt-indholdet er UTC tid og dernæst at konvertere det til centraleuropæisk tid CET som Danmark DK hører ind under:

SELECT GETDATE(), GETUTCDATE(),

       GETCETDATE = GETUTCDATE() AT TIME ZONE 'UTC' AT TIME ZONE 'Central European Standard Time'

 

SELECT OrderDateTime_UTC,

       OrderDateTime_DK = OrderDateTime_UTC AT TIME ZONE 'UTC'

                        AT TIME ZONE 'Central European Standard Time'

FROM dbo.Order

 

Når datatypen er datetimeoffset, ved indholdet, at det er i UTC tid:

SELECT OrderDateTime_UTC,

       OrderDateTime_DK = OrderDateTime_UTC AT TIME ZONE

                                     'Central European Standard Time'

FROM dbo.Order

 

SELECT CONVERT(datetime2(0), '2015-03-29T01:01:00', 126) AT TIME ZONE 'Central European Standard Time'

 

Når datatypen er streng med T og Z hvor T adskiller dato og klokkeslæt og Z angiver Zero timezone d.v.s. at klokkeslættet er i UTC tid, så kan der udregnes en ægte UTC dato og UTC klokkeslæt og en ægte CET dato og CET klokkeslæt som time(6) fordi der seks sekund-cifre i strengen i viste sql @variabel:

 

DECLARE @dateandtime varchar(100) = '2022-01-03T23:06:56.090591Z'

SELECT

DateOfOrder_UTC = CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','') AS date),

TimeOfOrder_UTC = CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','') AS time(6)),

DatetimeOfOrder_UTC = CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','')

     AS datetime2(6)),

DateOfOrder_CET = CAST(CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','')

     AS datetime2(6)) AT TIME ZONE 'UTC' AT TIME ZONE 'Central European Standard Time'

     AS date),

TimeOfOrder_CET = CAST(CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','')

     AS datetime2(6)) AT TIME ZONE 'UTC' AT TIME ZONE 'Central European Standard Time'

     AS time(6)),

DatetimeOfOrder_CET = CAST(CAST(REPLACE(REPLACE(@dateandtime,'T',' '),'Z','')

     AS datetime2(6)) AT TIME ZONE 'UTC' AT TIME ZONE 'Central European Standard Time'

     AS datetime2(6))

 

@dateandtime

2022-01-03T23:06:56.090591Z

DateOfOrder_UTC

2022-01-03

TimeOfOrder_UTC

23:06:56.090591

DateAndTimeOfOrder_UTC

2022-01-03 23:06:56.090591

DateOfOrder_CET

2022-01-04

TimeOfOrder_CET

00:06:56.090591

DateAndTimeOfOrder_CET

2022-01-04 00:06:56.090591

 

Dansk tid til UTC tid, ved at fortælle, at felt-indholdet er centraleuropæisk tid og dernæst at konvertere det til UTC tid:

SELECT OrderDateTime_UTC = OrderDateTime_DK AT TIME ZONE

                  'Central European Standard Time' AT TIME ZONE 'UTC'

FROM dbo.Order

 

WITH PeopleAndTZs AS

(

  SELECT * FROM

  (VALUES

    ('Rob',   'Cen. Australia Standard Time'),

    ('Paul',  'New Zealand Standard Time'),

    ('Phia',  'US Eastern Standard Time')

  ) t (person, tz)

)

SELECT ptz.person, o.SalesOrderID, o.OrderDate AT TIME ZONE 'UTC'

                                               AT TIME ZONE ptz.tz

FROM PeopleAndTZs ptz

CROSS JOIN Sales.SalesOrderHeader o

WHERE o.SalesOrderID BETWEEN 44001 AND 44010

 

Rækker til en streng med Id adskilt med komma, en kommalist (STRING_AGG)

»Horizontal join or Grouped concatenation is when you want to take multiple rows of data and compress them into a single string is called string aggregate«

 

Løbende tildeling af felter til en streng variabel kaldet aggregeret streng:

DECLARE @KundeList varchar(8000)

SELECT @KundeList = CONCAT(ISNULL(@KundeList + ', ', ''), KundeId)

FROM dbo.KUNDE

WHERE KundeId IS NOT NULL

ORDER BY KundeId

PRINT @KundeList   -- 123, 124, 125, 126

 

Aggregeret streng eller string aggregation via xml til en streng variabel:

DECLARE @KundeList varchar(8000)

SET @KundeList = SUBSTRING(

    (SELECT CONCAT(', ', KundeId)

     FROM dbo.KUNDE

     WHERE KundeId IS NOT NULL

     ORDER BY KundeId

     FOR XML PATH('')

    ), 3, 99999999)

PRINT @KundeList   -- 123, 124, 125, 126

 

Aggregeret streng eller string aggregation via json til en streng variabel:

DECLARE @KundeList varchar(8000)

SET @KundeList = JSON_VALUE(REPLACE(

    (SELECT _ = KundeId

     FROM dbo.KUNDE

     WHERE KundeId IS NOT NULL

     ORDER BY KundeId

     FOR JSON PATH),'"},{"_":"',', '),'$[0]._')

PRINT @KundeList   -- 123, 124, 125, 126

 

Aggregeret streng eller string aggregation via string aggregate funktion:

DECLARE @KundeList varchar(8000)

SELECT @KundeList = STRING_AGG(KundeId, ', ')

FROM dbo.KUNDE

WHERE KundeId IS NOT NULL

ORDER BY KundeId

PRINT @KundeList   -- 123, 124, 125, 126

 

Se også afsnittet om: Felter i en tabel eller kolonner i en tabel.

 

En kunde med KundeId har flere rækker i tabellen KUNDE_TELEFONNUMMER, fordi en kunde har flere telefonnumre, og for hver KundeId ønskes kun én række som har smeltet telefonnumrene sammen til en sorteret string adskilt med komma, kaldet en aggregeret streng eller string aggregate:

 

SELECT KundeId,

       TelefonnummerListe = STRING_AGG(Telefonnummer, ', ')

                            WITHIN GROUP (ORDER BY Telefonnummer ASC)

FROM dbo.KUNDE_TELEFONNUMMER T2

GROUP BY KundeId

 

WITHIN GROUP clause fordi der ønskes en sorteret liste med ORDER BY og det har ikke noget at gøre med GROUP BY, fordi den er for at feltet KundeId kan gruppere de enkelte værdier i hver sin række:

 

KundeId

TelefonnummerListe

123

31000000, 31000123, 31001000

124

88001020, 88001021, 88001022

125

10000000, 20000000, 30000000, 40000000, 50000000

126

22222222

 

Før SQL Server 2017:

SELECT DISTINCT T2.KundeId, TelefonnummerListe =

  SUBSTRING(

  (

   SELECT ', ' + CAST(T1.Telefonnummer AS varchar) AS [text()]

   FROM dbo.KUNDE_TELEFONNUMMER T1

   WHERE T1.KundeId = T2.KundeId

   ORDER BY T1.KundeId

   FOR XML PATH ('')

  ), 3, 99999999)

FROM dbo.KUNDE_TELEFONNUMMER T2

 

Aggregeret streng ud fra flere rækker i en en-til-mange relation (STRING_AGG)

SELECT dc.ClientKey,

       dc.ClientName,

       ClientRoleNameList = cr.ClientRoleNameList

FROM dim.Client dc

     OUTER APPLY

     (SELECT(SUBSTRING((SELECT ', ' + dcr.ClientRoleName

               FROM fact.ClientRole fcr WITH(NOLOCK, INDEX(IX_Fact_ClientRole_Keys))

                    INNER JOIN dim.ClientRole dcr WITH(NOLOCK)

                      ON dcr.ClientRoleKey = fcr.ClientRoleKey

               WHERE fcr.ClientKey = dc.ClientKey

               ORDER BY dcr.ClientRoleName

               FOR XML PATH('')), 3, 99999999)

     ) AS ClientRoleNameList) cr

-- eller bruge Json fra version 2016

     OUTER APPLY

     (SELECT(JSON_VALUE(REPLACE((SELECT _ = dcr.ClientRoleName

               FROM fact.ClientRole fcr WITH(NOLOCK, INDEX(IX_Fact_ClientRole_Keys))

                    INNER JOIN dim.ClientRole dcr WITH(NOLOCK)

                      ON dcr.ClientRoleKey = fcr.ClientRoleKey

               WHERE fcr.ClientKey = dc.ClientKey

               ORDER BY dcr.ClientRoleName

               FOR JSON PATH),'"},{"_":"',', '),'$[0]._')

     ) AS ClientRoleNameList) cr

 

SQL Server 2017 aggregeret streng

En aggregeret streng hvor data rækker lægges sammen til én række i output til en csv liste hvor person navne er blevet adskilt med komma:

SELECT STRING_AGG(ISNULL(Navn,'N/A'), ',') AS csv

FROM dbo.Person

Giver csv: Peter,N/A,Yan,Michael,Pia,Jens

 

Sikring af sortering i listen og mellemrum mellem navnene:

SELECT STRING_AGG(ISNULL(Navn,'N/A'), ', ') WITHIN GROUP (ORDER BY Navn ASC) AS csv

FROM dbo.Person

Giver csv: N/A, Jens, Michael, Peter, Pia, Yan

 

Med gruppering og to kolonner, hvor anden kolonne er en aggregeret streng:

SELECT Bynavn, STRING_AGG(Navn, ',') WITHIN GROUP (ORDER BY Navn ASC) AS Beboer

FROM dbo.Person

WHERE Bynavn Is Not Null AND Navn Is Not Null

GROUP BY Bynavn

 

WITHIN GROUP clause fordi der ønskes en sorteret liste med ORDER BY og det har ikke noget at gøre med GROUP BY, fordi den er for at feltet Bynavn kan gruppere de enkelte værdier i hver sin række:

 

Bynavn

Beboer

Gentofte

Michael,Pia

Søborg

Jens,Peter,Yan

 

Trimming af mellemrum både før og efter eller left and right i en samlet funktion:

SELECT TRIM(CustomerName) FROM dbo.Customer

 

SELECT CONCAT(',', '1 Microsoft Way', NULL, NULL, 'Redmond', 'WA', 98052) AS Add

Giver: ,1 Microsoft WayRedmondWA98052

 

Concat with separator: CONCAT_WS ( separator, argument1, argument1 [, argumentN]… )

SELECT CONCAT_WS(',', '1 Microsoft Way', NULL, NULL, 'Redmond', 'WA', 98052) AS Add

Giver: 1 Microsoft Way,Redmond,WA,98052

 

SELECT STRING_AGG(CONCAT_WS( ',', database_id, recovery_model_desc,

                                  ISNULL(containment_desc,'N/A')), char(13)) AS Info

FROM sys.databases

Giver:

1,SIMPLE,NONE

2,SIMPLE,NONE

3,FULL,NONE

4,SIMPLE,NONE

 

SELECT CONCAT_WS(',', CustomerId, CustomerName, CustomerStatus) FROM dbo.Customer

 

SQL Server 2022 aggregeret streng

Fjernelse af dubletter i en streng (Removing duplicates in a string) ved først at opdele en streng til rækker, og dernæst aggregere rækkerne tilbage til en streng, fordi Group By er det samme som Distinct i en Select, hvor ordinal er den nye funktion (new feature) i 2022.

 

DECLARE @List nvarchar(max), @Delim nchar(1) = N'/'

SET @List = N'Bravo/Alpha/Bravo/Tango/Delta/Bravo/Alpha/Delta'

 

SELECT STRING_AGG(t.value, @Delim) WITHIN GROUP (ORDER BY t.ordinal)

FROM

   (

    SELECT value, ordinal = MIN(ordinal)

    FROM STRING_SPLIT(@List, @Delim, 1)

    GROUP BY value

   ) t

 

Giver strengen: Bravo/Alpha/Tango/Delta.

 

Ovenstående GROUP BY value fjerner dubletter a la SELECT DISTINCT.

 

mySQL kalder det GROUP_CONCAT.

 

En SQLCLR routine giver group_concat til tidligere sql server versioner:

http://groupconcat.codeplex.com

 

Drop de mange Replace inde i hinanden når flere tegn skal erstattet en for en:

SELECT TRANSLATE('2*[3+4]/{7-2}', '[]{}', '()()') giver 2*(3+4)/(7-2)

 

SELECT TRANSLATE([DataField], '#[]{}~~', '1111111')

FROM [TableName]

The second and third arguments must contain an equal number of characters.

Value {~~[##Dalby##]~~} becomes: 111111Dalby111111.

 

Tabel type som variabel og parameter men ikke en tabel i databasen

CREATE TYPE customer_table AS TABLE

  (CustomerId INT NOT NULL PRIMARY KEY, Name NVARCHAR(50) NULL)

 

DECLARE @t customer_table

INSERT INTO @t(CustomerId, Name)

VALUES (1,'Jensen'), (2,'Nielsen')

 

CREATE PROCEDURE DoCustomer @p customer_table READONLY

AS

BEGIN

  SELECT *

  FROM dbo.Loans

  WHERE CustomerId IN (SELECT CustomerId FROM @p)

END

 

EXEC DoCustomer @p = @t

 

Kaldes Table-valued parameter men kun som read-only og er ikke optional.

 

Findes en værdi i en tabel

CREATE PROCEDURE dbo.Exist_ÅrDato

   @ÅrDato int

AS

BEGIN

  SET NOCOUNT ON

  SET ROWCOUNT 1

  SELECT 1

  FROM dbo.KALENDER WITH (NOLOCK)

  WHERE (ÅrDato = @ÅrDato)

  RETURN @@ROWCOUNT   -- ROWCOUNT_BIG() kæmpe antal rækker.

END

GO

 

Få fat i en værdi i en tabel

CREATE PROCEDURE dbo.Get_Bruger_Email

   @BrugerLogin nvarchar(20),

   @BrugerEmail nvarchar(50) OUTPUT

AS

BEGIN

  SET NOCOUNT ON

  SELECT @BrugerEmail = ISNULL(BrugerEmail, 'Mangler')

  FROM  dbo.BRUGER WITH (NOLOCK)

  WHERE BrugerLogin = @BrugerLogin

END

GO

 

Få vist en nyttig kolonne

Hvilke kunder har i år afgivet en eller flere ordrer:

SELECT k.Kundenummer, k.Kundenavn,

       CASE WHEN o.Ordrenummer IS NOT NULL

            THEN 1

            ELSE 0

       END AS HarOrdrer

FROM dbo.KUNDE k

     LEFT OUTER JOIN dbo.ORDRE o

          ON o.Kundenummer = k.Kundenummer AND

             YEAR(o.Ordredato) = YEAR(Getdate())

 

Kundenummer

Kundenavn

HarOrdrer

1

Hansen

1

2

Jensen

0

3

Nielsen

1

4

Andersen

0

Jensen har ordrer, men det var sidste år. Andersen har endnu ikke afgivet ordre.

 

Opdatering af rækker med største Id

Find seneste ordre som har det største OrderID nummer og giv en rabat på 40%:

DECLARE @MaxOrderID int

SELECT @MaxOrderID = MAX(OrderID) FROM dbo.[Order Details]

 

UPDATE OD

SET Discount = 0.4

FROM dbo.[Order Details] OD

WHERE OrderID = @MaxOrderID

 

Men mellem de to sql sætninger kunne en anden proces indsætte en ny ordre, der så går glip af 40% rabatten, derfor skal select og update udføres i én sql sætning og output husker på hvordan rækkerne så ud inden opdateringen, så jeg fortsat kan få fat i det OrderID der får rabatten. Der anvendes indlejret forespørgsel (ne­sted query) select i en join:

DECLARE @MaxOrderID int

DECLARE @UpdatedRows TABLE (OrderID int)

 

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] OD

     INNER JOIN

     (

      SELECT MAX(OrderID) AS OrderID

      FROM dbo.[Order Details]

     ) MX ON MX.OrderID = OD.OrderID

 

SELECT DISTINCT @MaxOrderID = OrderID FROM @UpdatedRows

 

OUTPUT inserted indeholder ny værdi og de kolonner der ikke er blev ændret.

OUTPUT deleted  indeholder gamle værdi d.v.s. før opdateringen fandt sted:

DECLARE @ChangedRows TABLE (Id int NOT NULL PRIMARY KEY CLUSTERED,

                            Navn nvarchar(50) NOT NULL)

UPDATE dbo.Kunde

SET Navn = 'Jensen'

OUTPUT deleted.Id, deleted.Navn INTO @ChangedRows

WHERE Id = 876

 

SELECT * FROM @ChangedRows -- viser at kunde 876 før hed Nielsen.

 

Mange kan ikke lide indlejret select fordi det kan gøre programmeringen over­skue­lig, derfor man man i stedet lave et view og anvende det:

CREATE VIEW [dbo].[Max_OrderID]

AS

SELECT MAX(OrderID) AS OrderID

FROM   dbo.[Order Details]

GO

 

DECLARE @MaxOrderID int

DECLARE @UpdatedRows TABLE (OrderID int)

 

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] OD

     INNER JOIN dbo.Max_OrderID MX ON MX.OrderID = OD.OrderID

 

SELECT DISTINCT @MaxOrderID = OrderID FROM @UpdatedRows

 

Eller man kan i stedet lave en table function og anvende den, to slags funktioner.

Scalar-valued function der returnerer en enkelt værdi:

CREATE FUNCTION [dbo].[Max_OrderID]()

RETURNS int

AS

BEGIN

  DECLARE @OrderID int

  SELECT @OrderID = MAX(OrderID)

  FROM dbo.[Order Details]

  RETURN @OrderID

END

 

Funktion indgår i Where fordi der kun gives en værdi:

DECLARE @MaxOrderID int

DECLARE @UpdatedRows TABLE (OrderID int)

 

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] OD

WHERE OrderID = dbo.Max_OrderID()

 

SELECT DISTINCT @MaxOrderID = OrderID FROM @UpdatedRows

 

Table-valued function der returnerer en eller flere rækker og kolonner som der joines til:

CREATE FUNCTION [dbo].[Max_OrderID]()

RETURNS TABLE

AS

RETURN

(

  SELECT MAX(OrderID) AS OrderID

  FROM dbo.[Order Details]

)

 

DECLARE @MaxOrderID int

DECLARE @UpdatedRows TABLE (OrderID int)

 

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] OD

     INNER JOIN dbo.Max_OrderID() MX ON MX.OrderID = OD.OrderID

 

SELECT DISTINCT @MaxOrderID = OrderID FROM @UpdatedRows

 

Table-valued function kan også anvendes i en Where sammen med In:

IF (SELECT COUNT(1) FROM dbo.Max_OrderID()) = 1

 

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] OD

WHERE OrderID IN(SELECT OrderID FROM dbo.Max_OrderID())

 

Mange kan ikke lide indlejret select eller brug af view eller function fordi det kan gøre programmeringen uoverskuelig, derfor man kan i stedet opstille et common table expression CTE med brug af With, så al kodningen fortsat er samlet og nedenstående bliver udført som en transaktion:

DECLARE @MaxOrderID int

DECLARE @UpdatedRows TABLE (OrderID int)

;WITH max_OrderID (OrderID) AS

(

  SELECT MAX(OrderID) AS OrderID

  FROM dbo.[Order Details]

)

UPDATE OD

SET Discount = 0.4

OUTPUT inserted.OrderID INTO @UpdatedRows

FROM dbo.[Order Details] od

     INNER JOIN max_OrderID mx ON mx.OrderID = od.OrderID

 

SELECT DISTINCT @MaxOrderID = OrderID FROM @UpdatedRows

 

With er et nyt reserveret ord og kræver enten semikolon i Declare sætningen eller som vist her i starten af With, men det er ikke altid at ; er nødvendigt, men SQL Server læg­ger op til at alle sætninger afsluttes med ; så det ligner almindelig pro­gram­me­ring. With er ikke en #temporær tabel, fordi With sætningen bliver først udført sam­men med den Select, Insert, Update eller Delete den er knyttet til eller har virkefelt i (execution scope), derved minder With meget om et view men står nu samlet med Update, men With kan ikke genbruges i andre sql sætninger.

 

Flere With kan kombineres som en slags sql-udtryk-mellemberegninger selvom alle sql sætningerne bliver udført i samme transaktion så kan denne opstilling give et bedre overblik end at have indlejret (nested) sql sætninger inde i hinanden eller view der i From anvender et andet view:

;WITH senesteOrdrer

AS

(

 SELECT VareId, MAX(Ordredato) AS SenesteOrdreDato

 FROM dbo.ORDRE

 WHERE Antal >= 100

 GROUP BY VareId

),

senesteKunder

AS

(

 SELECT Kundenummer

 FROM dbo.ORDRE o

      INNER JOIN senesteOrdrer s ON

          s.VareId = o.VareId AND s.SenesteOrdreDato = o.OrdreDato

)

SELECT k.Kundenummer, k.Kundenavn

FROM dbo.KUNDE k

     INNER JOIN senesteKunder s ON s.Kundenummer = k.Kundenummer

 

Læs om forskellige anvendelser

 

Recursive update with et common table expression CTE a kind of iteration

CTE giver mulighed for at formulere et rekursivt udtryk, der minder om at gen­nem­løbe eller at iterere mange rækker som om det var en loop (løkke, typisk med while og en cursor), men med with og union all opstilles blot et udtryk.

 

Fibonaccis talrække:

; WITH rcte (c, n1, n2) AS

(

 -- Starting values

 SELECT 1 AS c,

        CAST(0 AS numeric(38, 0)),

        CAST(1 AS numeric(38, 0))

 UNION ALL

 -- The recursion

 SELECT c+1,

        n2,

        n1+n2

 FROM rcte

 WHERE c < 183

)

SELECT n2 FROM rcte

OPTION(MAXRECURSION 0);

 

En kunde tabel mangler brancherkoder hist og her og derfor ønskes disse null værdier udfyldt efter to principper i prioriteret rækkefølge:

 

Kundenr

År

Branchekode

1

2000

NULL

1

2001

1543

1

2002

NULL

1

2003

NULL

1

2004

NULL

1

2005

1566

1

2006

NULL

1

2007

1588

2

2004

NULL

2

2005

3213

2

2006

NULL

2

2007

4577

3

2010

4432

3

2011

NULL

3

2012

NULL

 

1.  Tilbageføring af branchekoder til kundens forrige år f.eks. branchekode 1566 kopieres til år 2004, 2003 og 2002, og 1543 kopieres til 2000.

2.  Fremføring af branchekoder til kundens næste år f.eks. branchekode 4432 kopieres til år 2011 og 2012.

 

Når samme branchekode skal kopieres til flere år kan det opfattes som en loop eller som rekursion i den forstand at der kopieres en værdi ad gangen og at det faktisk er 1566 fra 2005 til 2004 og derefter er det kopiering fra 2004 til 2003 o.s.v. De to føringer udtrykkes i hver sin CTE med en efterfølgende UPDATE:

;WITH KUNDE_BRANCHE_TILBAGEFØRT AS

(

SELECT a.Kundenr, a.År, b.Branchekode

FROM dbo.KUNDE a

     INNER JOIN KUNDE b ON b.Kundenr = a.Kundenr

WHERE b.Branchekode IS NOT NULL AND a.Branchekode IS NULL

      AND (b.År - a.År) = 1  -- d.v.s forrige år.

 

UNION ALL

 

SELECT a.Kundenr, a.År, b.Branchekode -- I join indgår CTE udtrykket

FROM dbo.KUNDE a

     INNER JOIN KUNDE_BRANCHE_TILBAGEFØRT b ON b.Kundenr = a.Kundenr

WHERE b.Branchekode IS NOT NULL AND a.Branchekode IS NULL

      AND (b.År - a.År) = 1  -- d.v.s forrige år.

)

UPDATE a

SET  Branchekode = b.Branchekode

FROM dbo.KUNDE a

     INNER JOIN KUNDE_BRANCHE_TILBAGEFØRT b ON b.Kundenr = a.Kundenr

                                               AND b.År = a.År

WHERE a.Branchekode IS NULL

 

/* I tilfælde af at man vil se data før man udfører UPDATE.

SELECT Kundenr, År, Branchekode

FROM KUNDE_BRANCHE_TILBAGEFØRT

UNION

SELECT Kundenr, År, Branchekode

FROM dbo.KUNDE

WHERE Branchekode IS NOT NULL

ORDER BY Kundenr, År

*/

 

Kunde 1 og 2 fik deres branchekoder tilbageført til forrige år:

 

Kundenr

År

Branchekode

1

2000

1543

1

2001

1543

1

2002

1566

1

2003

1566

1

2004

1566

1

2005

1566

1

2006

1588

1

2007

1588

2

2004

3213

2

2005

3213

2

2006

4577

2

2007

4577

 

;WITH KUNDE_BRANCHE_FREMFØRT AS

(

SELECT a.Kundenr, a.År, b.Branchekode -- I join indgår CTE udtrykket

FROM dbo.KUNDE a

     INNER JOIN KUNDE b ON b.Kundenr = a.Kundenr

WHERE b.Branchekode IS NOT NULL AND a.Branchekode IS NULL

      AND (b.År - a.År) = -1  -- d.v.s næste år.

 

UNION ALL

 

SELECT a.Kundenr, a.År, b.Branchekode

FROM dbo.KUNDE a

     INNER JOIN KUNDE_BRANCHE_FREMFØRT b ON b.Kundenr = a.Kundenr

WHERE b.Branchekode IS NOT NULL AND a.Branchekode IS NULL

      AND (b.År - a.År) = -1  -- d.v.s næste år.

)

UPDATE a

SET  Branchekode = b.Branchekode

FROM dbo.KUNDE_ a

     INNER JOIN KUNDE_BRANCHE_FREMFØRT b ON b.Kundenr = a.Kundenr

                                            AND b.År = a.År

WHERE a.Branchekode IS NULL

 

Kunde 3 fik sin branchekode fremført til næste år:

 

Kundenr

År

Branchekode

3

2010

4432

3

2011

4432

3

2012

4432

 

Indsættelser til to tabeller, går det galt, så drop alle indsættelser

Der anvendes try-catch til at fange når indsættelserne fejler og transaction til at rullle data tilbage f.eks. når anden insert into fejler, så vil første insert blive til­bageført og derved ikke udført.

CREATE PROCEDURE [dbo].[Depot_Insert]

AS

BEGIN

 SET NOCOUNT ON

 BEGIN TRY

   BEGIN TRANSACTION

 

   TRUNCATE TABLE dbo.DepotPrivat

   INSERT INTO dbo.DepotPrivat (DepotKode, DepotNavn, InsertTime_meta)

   SELECT DepotKode = DCode, DepotNavn = DText,

          InsertTime_meta = GETDATE()

   FROM   DSA.DP.Depot

   WHERE  DType = 'Private'

   ORDER BY DCode

 

   TRUNCATE TABLE dbo.DepotErhverv

   INSERT INTO dbo.DepotErhverv (DepotKode, DepotNavn,InsertTime_meta)

   SELECT DepotKode = DCode, DepotNavn = DText,

          InsertTime_meta = GETDATE()

   FROM   DSA.DP.Depot

   WHERE  DType = 'Business'

   ORDER BY DCode

 

   IF @@TRANCOUNT > 0 COMMIT TRANSACTION

 END TRY

 BEGIN CATCH

   IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION

   ;THROW  -- fejlmelding med præcis kodelinienummer end raiseerror

 END CATCH

END

 

Læs mere

 

Eksempel på fejlmelding som også siver op til den SSIS pakke som kalder sp.:

 

Msg 2627, Level 14, State 1, Procedure Depot_Insert, Line 18

Violation of PRIMARY KEY constraint 'PK_DepotErhverv'. Cannot insert duplicate key in object 'dbo.DepotErhverv'. The duplicate key value is (3).

 

Ønskes navn på stored procedure in Try delen og i Catch delen:

 

PRINT CONCAT('COMMIT AT SP: ',

          OBJECT_SCHEMA_NAME(@@PROCID), '.', OBJECT_NAME(@@PROCID))

 

PRINT CONCAT('ROLLBACK AT SP: ', ERROR_PROCEDURE(), '.

          ERRORMESSAGE: ', ERROR_MESSAGE())

 

Indsæt ny post i en tabel og få Id retur

(no deadlock, blocking, disruption or duplicate ID)

Tabel KUBE primærnøgle er fortløbende nummer KubeId INT IDENTITY (1, 1).

CREATE PROCEDURE dbo.New_Kube

   @KubeNavn nvarchar(50),

   @KubeForklaring nvarchar(255) = NULL,

   @TemaId int -- fremmednøgle.

AS

BEGIN

DECLARE @KubeId int = 0 -- Returneres der 0 er rækken ikke indsat.

SET NOCOUNT ON

BEGIN TRY

  DECLARE @TranCounter INT = @@TRANCOUNT, -- Antal gældende trans.

          @SavePoint NVARCHAR(32) = CAST(@@PROCID AS NVARCHAR(20)) +

          N'_' + CAST(@@NESTLEVEL AS NVARCHAR(2))

  -- Join eksisterende transaktion eller start en ny transaktion.

  IF @TranCounter > 0

     SAVE TRANSACTION @SavePoint

  ELSE

     BEGIN TRANSACTION

 

  INSERT INTO dbo.KUBE WITH (ROWLOCK)

         (KubeNavn,  KubeForklaring,  TemaId)

  VALUES (@KubeNavn, @KubeForklaring, @TemaId)

  SELECT @KubeId = SCOPE_IDENTITY()  -- Får nye dannet KubeId nummer.

 

  IF @TranCounter = 0 -- Transaktion er startet i proceduren.

     COMMIT TRANSACTION

END TRY

BEGIN CATCH 

  IF @TranCounter = 0 -- Transaktion er startet i proceduren.

     ROLLBACK TRANSACTION

  ELSE IF XACT_STATE() = 1 -- Transaktion er startet før proceduren.

     ROLLBACK TRANSACTION @SavePoint

  ELSE IF XACT_STATE() = -1

     ROLLBACK TRANSACTION

  THROW

END CATCH

RETURN @KubeId

END

GO

 

Learn Microsoft

 

Få fat i sidste anvendte Id nummer, selvom dens række måtte være blevet slettet i tabellen, fordi et Identity nummer må ikke blive genbrugt af senere indsatte rækker:

 

SELECT IDENT_CURRENT('dbo.KUBE')

 

Men ovenstående giver ikke altid det korrekte svar f.eks. efter nedbrud.

Nedenstående giver aktuelle maksimum og minimum værdier der findes som rækker i tabellen, d.v.s. slettede rækkers Identity nummer medtages ikke:

 

SELECT MAX($IDENTITY), MIN($IDENTITY)

FROM dbo.KUBE

 

Bedst i kombination for at finde den største værdi der ikke bliver genbrugt:

 

SELECT GREATEST(ISNULL(IDENT_CURRENT('dbo.KUBE'), 0),

                ISNULL(MAX($IDENTITY), 0))

FROM dbo.KUBE

 

Fra SQL Server 2005 har INSERT fået en OUTPUT mulighed, hvor Id kan aflæses:

CREATE PROCEDURE dbo.New_Kube

   @KubeNavn nvarchar(50),

   @KubeForklaring nvarchar(255) = NULL,

   @TemaId int -- fremmednøgle.

AS

BEGIN

DECLARE @KubeId int

SET NOCOUNT ON

DECLARE @InsertedRow TABLE (KubeId int) -– en #TEMP tabel som variabel

INSERT INTO dbo.KUBE WITH (ROWLOCK) (KubeNavn, KubeForklaring, TemaId)

OUTPUT inserted.KubeId INTO @InsertedRow -- får fat i dannet KubeId

VALUES (@KubeNavn, @KubeForklaring, @TemaId)

SELECT @KubeId = KubeId FROM @InsertedRow

RETURN @KubeId

END

GO

Fra SQL Server 2012 fået et Sequence object som en global nummertæller for hele databasen, læs mere om den i indlægget om SQL Server 2014.

 

OUTPUT er praktisk i et ETL forløb, hvor en række kildedata skal indsættes i et datawarehouse tabel hvor de nye rækker også får et Id nummer, der ønskes tildelt de nye data fra NEW tabel til DIM tabellen, eksempelvis:

CREATE PROCEDURE [dbo].[DW_DIM_KUNDE_New]

AS

BEGIN

SET NOCOUNT ON -- KundeId er PK for DW og KundeNummer er kildens PR.

DECLARE @InsertedRows TABLE (KundeId int, KundeNummer int)

INSERT INTO dbo.DW_DIM_KUNDE WITH(TABLOCK) (KundeNummer, KundeNavn)

OUTPUT inserted.KundeId, inserted.KundeNummer INTO @InsertedRows

SELECT KundeNummer, KundeNavn

FROM DW_NEW_KUNDE new WITH(NOLOCK)

WHERE NOT EXISTS (SELECT 1 FROM dbo.DW_DIM_KUNDE dim WITH(NOLOCK)

                  WHERE dim.KundeNummer = new.KundeNummer);

-- Evt. opdatere NEW tabellen med de nyoprettede Id numre.

UPDATE dbo.DW_NEW_KUNDE WITH(TABLOCK)

SET    KundeId = r.KundeId

FROM   dbo.DW_NEW_KUNDE n

       INNER JOIN @InsertedRows AS r ON r.KundeNummer = n.KundeNummer;

-- Opdatere eksisterende kunder med nyt navn fra NEW tabellen.

UPDATE dbo.DW_DIM_KUNDE WITH(TABLOCK)

SET    KundeNavn = new.KundeNavn

FROM   dbo.DW_DIM_KUNDE dim

       INNER JOIN dbo.DW_NEW_KUNDE new WITH(NOLOCK)

         ON new.KundeNummer = dim.KundeNummer

WHERE  dim.KundeNavn <> new.KundeNavn;

-- Forberedelse af nye segmenter til nye kunders KundeId

WITH CTE(KundeId, KundeNummer, Segment)

AS

(

 SELECT r.KundeId, new.KundeNummer, new.Segment

 FROM DW_NEW_KUNDE_SEGMENT new WITH(NOLOCK)

      INNER JOIN @InsertedRows r ON r.KundeNummer = new.KundeNummer

)

-- Indsæt nye segmenter for de nye kunder

INSERT INTO dbo.DW_DIM_KUNDE_SEGMENT WITH(TABLOCK) (KundeId, Segment)

SELECT new.KundeId, new.Segment

FROM   CTE new   

WHERE NOT EXISTS (SELECT 1 FROM dbo.DW_DIM_KUNDE_SEGMENT dim

       WHERE dim.KundeId = new.KundeId AND dim.Segment = new.Segment);

END

Har en eksisterende kunde fået nyt segment kommer det ikke med i ovenstående.

 

Nedenstående viser indsættelse af fire kundenumre og hvordan man får fat i hver deres Globally unique identifier GUID værdier via en temporær tabel:

CREATE TABLE dbo.Customer

(

 CustomerGuid uniqueidentifier NOT NULL DEFAULT NewID(),

                                              --NewSequentialID()

 CustomerNumber int            NOT NULL

)

GO

DECLARE @Guid TABLE (Guid uniqueidentifier) -- temporær tabel

INSERT INTO dbo.Customer(CustomerNumber)    

OUTPUT inserted.CustomerGuid INTO @Guid

VALUES (1), (2), (3), (4)                   -- indsætter fire rækker.

 

SELECT * FROM @Guid

SELECT * FROM dbo.Customer

 

NewID() giver:

CustomerGuid

CustomerNumber

9FC9E5CC-CBCC-438F-AC86-B7E1FEA26069

1

42F8CE54-D2C9-40FB-9D23-3B09873086B7

2

B17E2285-94EB-49F6-900B-A9705B2B4055

3

668C630F-742F-44FF-9AF9-67B506E02406

4

 

NewSequentialID() giver og er derved god for et clustered index på Guid felt:

CustomerGuid

CustomerNumber

2AA18D12-7704-EA11-8BD9-BCEE7BDF1324

1

2BA18D12-7704-EA11-8BD9-BCEE7BDF1324

2

2CA18D12-7704-EA11-8BD9-BCEE7BDF1324

3

2DA18D12-7704-EA11-8BD9-BCEE7BDF1324

4

 

Opdater en post i en tabel via primærnøglen

CREATE PROCEDURE dbo.Update_Kube

   @KubeId int,

   @KubeNavn nvarchar(50),

   @KubeForklaring nvarchar(255),

   @TemaId int

AS

BEGIN

SET NOCOUNT ON

UPDATE dbo.KUBE WITH (ROWLOCK)

SET   KubeNavn       = @KubeNavn,

      KubeForklaring = @KubeForklaring,

      TemaId         = @TemaId

FROM  dbo.KUBE

WHERE KubeId = @KubeId

END

GO

 

Slet en post i en tabel via primærnøglen

CREATE PROCEDURE dbo.Delete_Kube

   @KubeId int

AS

BEGIN

SET NOCOUNT ON

DELETE dbo.KUBE WITH (ROWLOCK)

FROM  dbo.KUBE

WHERE KubeId = @KubeId

END

GO

 

Indsættelse af ny værdi husk at undersøge om den findes i forvejen

I stedet for VALUES anvendes SELECT sammen med NOT EXISTS så bliver un­der­sø­gelse/tjek og indsættelse samtidig i en transaktion:

CREATE PROCEDURE dbo.Ny_Kunde

   @KundeNavn nvarchar(50), @KundeAdresse nvarchar(100)

AS

BEGIN

  INSERT INTO dbo.KUNDE (KundeNavn, KundeAdresse)

  SELECT @KundeNavn, @KundeAdresse

  WHERE NOT EXISTS

        (

         SELECT TOP(1) 1

         FROM dbo.KUNDE

         WHERE KundeNavn = @KundeNavn

        )

 

  DECLARE @KundeId INT;

  SET @KundeId = SCOPE_IDENTITY();

  IF @KundeId > 0

  BEGIN

     ...

  END

  RETURN @KundeId

END

GO

 

Kopiering af en-til-mange forekomst i to tabeller

CREATE PROCEDURE dbo.KundeOrdre_Kopiering

   @KundeId int,

   @NytKundeNavn nvarchar(50)

AS

BEGIN

DECLARE @NytKundeId int

SET NOCOUNT ON

 

INSERT INTO dbo.KUNDE WITH (ROWLOCK) (KundeNavn, Adresse, Postnummer)

SELECT @NytKundeNavn, Adresse, Postnummer

FROM   dbo.KUNDE WITH (NOLOCK)

WHERE  KundeId = @KundeId

 

SELECT @NytKundeId = SCOPE_IDENTITY()

 

INSERT INTO dbo.KUNDEORDRE WITH (ROWLOCK) (KundeId, Ordrenummer, Pris)

SELECT @NytKundeId, Ordrenummer, Pris

FROM   dbo.KUNDEORDRE

WHERE  KundeId = @KundeId

 

RETURN @NytKundeId  -- retur til applikation der viser den nye kunde.

END

GO

 

Opdatering og indlejret select – kan du gætte hvad der ser her

UPDATE dbo.EDW_NEW_BerørteBorger WITH (TABLOCK)

SET    FørsteFalddato = kal1.SidsteArbejdsdato

FROM   dbo.EDW_NEW_BerørteBorger new

       INNER JOIN dbo.EDW_RULE_KALENDER kal WITH (NOLOCK)

             ON kal.Dato = DateAdd(day,56,new.FørsteFraværsdato)

       INNER JOIN

       (

        SELECT ÅrUge, MAX(Dato) AS SidsteArbejdsdato

        FROM dbo.EDW_RULE_KALENDER WITH (NOLOCK)

        WHERE Arbejdsdag = 1

        GROUP BY ÅrUge

       ) kal1 ON kal1.ÅrUge = kal.ÅrUge

 

Fordel dage i uger

1-5 dage placeres i uge 1, 6-10 dage i uge 2, 11-15 i 3, 16-20 i 4 o.s.v.:

SELECT CASE WHEN (Dage % 5) = 0

       THEN (Dage / 5)

       ELSE ((Dage + (5 - (Dage % 5))) / 5) END AS AntalUger

 

Reset et Identity felt, typisk efter en Delete og før en Insert til fact tabel

DELETE dbo.FACT -- slet rækker med ÅrDato'er som findes i STAGING.

FROM   dbo.FACT

WHERE  ÅrDato IN (SELECT DISTINCT ÅrDato FROM dbo.STAGING)

 

DECLARE @maxId int

SELECT @maxId = ISNULL(MAX(Id),0) -- tom tabel giver null derfor 0.

FROM   dbo.FACT WITH(TABLOCK)

DBCC CHECKIDENT ('dbo.FACT', RESEED, @maxId) -- reset for genbrug Id.

 

INSERT INTO dbo.FACT(ÅrDato, StedId, ArtId, Beløb) -- -1=Mangler værdi

SELECT stg.ÅrDato, ISNULL(st.StedId,-1), ISNULL(ar.ArtId,-1),

       ISNULL(SUM(Beløb),0)

FROM   dbo.STAGING stg

       LEFT OUTER JOIN dbo.DIM_STED st ON st.Stedkode = stg.Stedkode

       LEFT OUTER JOIN dbo.DIM_ART  ar ON ar.Artkode = stg.Artkode

GROUP BY stg.ÅrDato, ISNULL(st.StedId,-1), ISNULL(ar.ArtId,-1)

 

-- left outer join og isnull sikrer at indeholder staging en kode som

-- ikke findes i dimensionen så medtages staging rækken med -1 i id.

 

SET IDENTITY_INSERT dbo.FACT ON  -- tilsidesætter Identity.

INSERT INTO dbo.FACT(Id, ÅrDato, StedId, ArtId, Beløb)

SELECT Id+@maxId, ÅrDato, StedId, ArtId, Beløb

FROM   dbo.TMP

SET IDENTITY_INSERT dbo.FACT OFF -- genetablerer Identity.

 

TRUNCATE TABLE dbo.FACT

tømmer en tabel og sætter Identity til 0, men tabellen må ikke indgå i nogen relation (reference integrity) til andre tabeller.

 

Select Max kan tage performance for en tabel med mange millioner rækker, ligeså kan Select Top 1 Id From dbo.Fact Order By Id Desc, mens nedenstående giver et hurtigt svar:

 

DECLARE @maxId bigint

SET NOCOUNT ON

--SELECT @maxId = ISNULL(MAX(Orders_Id), 0) + 1

--FROM base.Orders

--SELECT TOP 1 @maxId = ISNULL(Orders_Id), 0) + 1

--FROM base.Orders

--ORDER BY Orders_Id DESC

SELECT @maxId = GREATEST(ISNULL(IDENT_CURRENT('base.Orders'), 0) + 1,

                         ISNULL(MAX($IDENTITY), 0) + 1)

FROM base.Orders

DBCC CHECKIDENT ('base.Orders', RESEED, @maxId)

 

IDENT_CURRENT kan give forkert resultat ved nedbrud, derfor er MAX($IDENTITY) også anvendt, hvor den største værdi bliver valgt.

 

Index på et view til hurtigere svartid (performance)

View oprettes med et skemabinding, eksempelvis:

CREATE VIEW Dim.Customer_CityState

WITH SCHEMABINDING

AS

SELECT City, State, COUNT_BIG(*) AS NumRows -- stort antal rækker

FROM Dim.Customer

GROUP BY City, State

GO

CREATE UNIQUE CLUSTERED INDEX IX_Customer_CityState

  ON Dim.Customer_CityState (City, State)

GO

 

Schemabinding forhindrer også ændring (alter) af tabellen.

 

Få vist logical reads and physical reads som helst skal være så lille som muligt, og det kan et indeks (index IX) hjælpe med til og give en bedre performance:

 

SET STATISTICS IO ON;

GO

SET STATISTICS TIME ON;

GO

 

SELECT

FROM

WHERE

 

SET STATISTICS TIME OFF;

GO

SET STATISTICS IO OFF;

GO

 

Kik på:

Logical reads 2930, physical reads 2, read-ahead reads 2910, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times: CPU time = 26498 ms, elapsed time = 3520 ms.

 

Tilføj et felt til tabel

IF NOT EXISTS (select 1 from syscolumns where id=object_id('KUNDE')

               and name = 'Aktiv')

BEGIN

   ALTER TABLE dbo.KUNDE

   ADD Aktiv bit NOT NULL

   CONSTRAINT DF_KUNDE_Aktiv DEFAULT 0

END

GO

 

Ændre felt fra tillad null til ikke tillad null

IF EXISTS (select 1 from syscolumns where id=object_id('KUNDE')

           and name = 'Antal')

BEGIN

   ALTER TABLE dbo.KUNDE

   ALTER COLUMN Antal smallint NOT NULL

END

 

Slet felt i en tabel

ALTER TABLE dbo.KUNDE

DROP CONSTRAINT DF_KUNDE_Aktiv

GO

ALTER TABLE dbo.KUNDE

DROP COLUMN Aktiv

GO

 

Lidt mere DDL

Tilføj en kolonne eller et felt til en tabel.

IF NOT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS

   WHERE TABLE_NAME = 'Orders' AND COLUMN_NAME = 'Pickup')

BEGIN

  ALTER TABLE dbo.Orders

  ADD Pickup bit NOT NULL

      CONSTRAINT DF_Orders_Pickup DEFAULT 0

END

GO

 

Kan ikke summere på en bit data type, derfor ændres den til int.

ALTER TABLE dbo.Orders

ALTER COLUMN Pickup int NOT NULL

GO

 

Tjek om et felt findes i en tabel.

DECLARE @Schema nvarchar(256) = 'dbo'

DECLARE @Table  nvarchar(256) = 'Employees'

DECLARE @Column nvarchar(256) = 'City'

 

IF EXISTS(SELECT TOP(1) 1 FROM INFORMATION_SCHEMA.COLUMNS

   WHERE TABLE_SCHEMA = @Schema AND

         TABLE_NAME   = @Table AND

         COLUMN_NAME  = @Column)

BEGIN

  PRINT 'FINDES'

END

ELSE

BEGIN

  PRINT 'FINDES IKKE'

END

 

Gør en kolonne not null og tildel en primærnøgle med GO som batch separator ellers fås fejlmeldinger:

Cannot define PRIMARY KEY constraint on nullable column in table 'Depot'.

Could not create constraint or index. See previous errors.

 

IF (SELECT IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS

    WHERE TABLE_SCHEMA = 'dp' AND

          TABLE_NAME = 'Depot' AND

          COLUMN_NAME = 'DType') = 'YES'

BEGIN

  ALTER TABLE dp.Depot

  ALTER COLUMN [DType] nvarchar(50) NOT NULL

END

GO

IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS

               WHERE TABLE_SCHEMA ='dp' AND

                     TABLE_NAME = 'Depot' AND

                     CONSTRAINT_TYPE = 'PRIMARY KEY')

BEGIN

ALTER TABLE dp.Depot

ADD CONSTRAINT PK_Depot PRIMARY KEY CLUSTERED (DCode, DText, DType) ON [PRIMARY]

END

GO

 

T-SQL har ikke batch separator, fordi det er en applikation der separerer eller ad­skiller batches og sender dem enkeltvis fra applikation til databasen.

Vi kan emulate eller efterligne GO operator ved at pakke hver batch ind i en Execute kommando som en streng, så bliver de udført

 

IF (SELECT IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS

    WHERE TABLE_SCHEMA = 'dp' AND

          TABLE_NAME = 'Depot' AND

          COLUMN_NAME = 'DType') = 'YES'

EXECUTE ('

ALTER TABLE dp.Depot

ALTER COLUMN [DType] nvarchar(50) NOT NULL

')

 

IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS

               WHERE TABLE_SCHEMA ='dp' AND

                     TABLE_NAME = 'Depot' AND

                     CONSTRAINT_TYPE = 'PRIMARY KEY')

EXECUTE ('

ALTER TABLE dp.Depot

ADD CONSTRAINT PK_Depot PRIMARY KEY CLUSTERED (DCode, DText, DType) ON [PRIMARY]

')

 

Shrink database

CREATE PROCEDURE dbo.Shrink2000_2005

AS

BEGIN

  BACKUP LOG <databasenavn> WITH TRUNCATE_ONLY

  DBCC SHRINKDATABASE (<databasenavn>, 0)

  DBCC UPDATEUSAGE (<databasenavn>)

END

GO

 

CREATE PROCEDURE dbo.Shrink2008

AS

BEGIN

  BEGIN TRY

    DBCC SHRINKFILE (<databasenavn>_LOG, 1)

    DBCC SHRINKDATABASE (<databasenavn>, 0)

    DBCC UPDATEUSAGE (<databasenavn>)

  END TRY

  BEGIN CATCH

    SELECT ERROR_NUMBER() AS ErrorNumber

  END CATCH

END

GO

 

Genereret sql sætning fra en Select sætning

En tabel UDSKIFT indeholder gamle værdier fra 2018 og nye værdier for 2019 som skal bruges til at udskifter værdier med en OPGAVE tabel på den måde, at der genereres mange UPDATE sql sætninger.

 

CREATE TABLE [dbo].[UDSKIFT](

 [SKOVNUMMER 2018] [float] NOT NULL,

 [LITRA 2018] [nvarchar](255) NOT NULL,

 [SKOVNUMMER 2019] [float] NOT NULL,

 [LITRA 2019] [nvarchar](255) NOT NULL

) ON [PRIMARY]

 

Select sætning danner UPDATE sætninger en for en for hver række fra UDSKIFT:

 

SELECT

'UPDATE dbo.OPGAVE SET [Skovnummer] = ' + CAST([SKOVNUMMER 2019] AS nvarchar) + ', [Litra] = ''' + CAST([LITRA 2019] AS nvarchar) + ''' WHERE [Skovnummer] = ' + CAST([SKOVNUMMER 2018] AS nvarchar) + ' AND [Litra] = ''' + CAST([Litra 2018] AS nvarchar) + ''''

FROM dbo.[UDSKIFT]

 

Herefter kan update sql sætningerne udføres i SQL Server Management Studio.

 

Udførsel af genereret sql sætning

CREATE PROCEDURE dbo.EXEC_SQL

   @Statement nvarchar(MAX)  -- ntext in SQL Server 7.0 and 2000.

AS

BEGIN

  EXEC sys.sp_executesql @Statement

END

GO

 

Ovenstående giver mulighed for SQL Injection, lad os tage et konkret eksempel på en stored procdure, der kan tømme en tabel uanset hvilken rettighed brugeren måtte have:

 

CREATE PROCEDURE [dbo].[TruncateTable]

  @SchemaTablename varchar(200)

  WITH EXECUTE AS OWNER

AS

BEGIN

  DECLARE @sqlCommand nvarchar(220) = '';

  SET @sqlCommand = N'TRUNCATE TABLE ' + @SchemaTablename + ';'

  EXECUTE sp_executesql @sqlCommand;

END

 

Kald af stored procedure for at tømme en tabel:

 

EXEC [dbo].[TruncateTable] 'Dim.Customer';

 

Hvad med at slette tabellen, klassisk eksempel på SQL Injection:

 

EXEC [dbo].[TruncateTable] 'Dim.Customer; DROP TABLE Dim.Customer;';

 

Måske kunne databasen også droppes?

 

Læs mere om sql injection

 

En forbedret udgave af stored proceduren, der tjekker om tabellen findes.

 

CREATE PROCEDURE [dbo].[TruncateTable]

  @SchemaTablename varchar(200)

  WITH EXECUTE AS OWNER

AS

BEGIN

  DECLARE @sqlCommand nvarchar(220) = '';

  IF EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES

            WHERE CONCAT(TABLE_SCHEMA,'.',TABLE_NAME) = @SchemaTablename)

  BEGIN

    SET @sqlCommand = N'TRUNCATE TABLE ' + @SchemaTablename + ';'

    EXECUTE sp_executesql @sqlCommand;

  END

END

 

Adgang og rettigheder

Giver execute kørsel rettighed til en stored procedure enten til en Windows AD gruppe, en AD bruger eller en SQL login, eksempelvis til en login med public adgang:

USE [KUNA]  -- Kunde administration databasen

GO

GRANT EXECUTE ON [dbo].[EXEC_SQL]

      TO [NT AUTHORITY\authenticated users]

GO

Adgang til en stored procedure til en bestemt bruger:

GRANT EXECUTE ON  [dbo].[GetFinance] TO [username]

GO

Til alle stored procedures til en bestemt bruger:

GRANT EXECUTE TO [username]  

GO

Til alle stored procedures til alle brugere:

GRANT EXECUTE TO public

GO

 

Et schema knyttes til en AD-gruppe LAN\AnalyseBrugere der er tilføjet i Security med user mapping til databasen og schema Analyse er tilføjet til databasen, og der tildeles læseadgang (kaldes Select) til objekter f.eks. views i skemaet:

GRANT CONNECT TO [LAN\AnalyseBrugere]

GRANT SELECT ON SCHEMA::Analyse TO [LAN\AnalyseBrugere]

 

PostDeployment.sql:

DECLARE @dbName nvarchar(255)

SELECT @dbName = DB_NAME()

IF @dbName = 'ERP'

BEGIN

 RAISERROR ('Adding access rights to ERP.', 0, 1) WITH NOWAIT

END

 

Automatisk genereret view fra et arkiv til opdagelse hos brugere

CREATE PROCEDURE [dbo].[GenerateViewsToDataDiscovery]

AS

BEGIN

  SET NOCOUNT ON

  DECLARE @schema nvarchar(50) = N'customer'

  DECLARE @table nvarchar(255)

  DECLARE @sqldrop nvarchar(300)

  DECLARE @sqlview nvarchar(MAX)

  DECLARE @Scan CURSOR

 

  SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

      SELECT TABLE_NAME

      FROM Archive.INFORMATION_SCHEMA.TABLES

      WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = @schema

 

  OPEN @Scan

  FETCH NEXT FROM @Scan INTO @table

  WHILE @@FETCH_STATUS = 0

  BEGIN

    SELECT @sqlview =

     SUBSTRING(

     (

      SELECT ',' + QUOTENAME(COLUMN_NAME)

      FROM Archive.INFORMATION_SCHEMA.COLUMNS

      WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND

            COLUMN_NAME != 'LogId' AND COLUMN_NAME NOT LIKE '%_meta'

      FOR XML PATH ('')

      ), 2, 99999999)

 

    SET @sqlview = N'CREATE VIEW ' + @schema + '.' + @table +

     N' AS SELECT ' + @sqlview + N' FROM Archive.' + @schema + N'.' +

     @table

 

    SET @sqldrop = 'IF OBJECT_ID(''' + @schema + '.' +

     @table + ''') IS NOT NULL DROP VIEW ' + @schema + '.' + @table

 

    --PRINT @sqldrop

    --PRINT @sqlview

    EXECUTE sp_executesql @sqldrop

    EXECUTE sp_executesql @sqlview   

    FETCH NEXT FROM @Scan INTO @table

  END

  CLOSE @Scan

  DEALLOCATE @Scan

END

GO

 

CREATE PROCEDURE [dbo].[MaterializeViewsToTables]

AS

BEGIN

  SET NOCOUNT ON

  DECLARE @schemaview nvarchar(50) = N'customer_vw'

  DECLARE @schematable nvarchar(50) = N'customer'

  DECLARE @view nvarchar(255)

  DECLARE @sqldrop nvarchar(300)

  DECLARE @sqltable nvarchar(300)

  DECLARE @Scan CURSOR

 

  SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

      SELECT DISTINCT TABLE_NAME

      FROM DataDiscovery.INFORMATION_SCHEMA.VIEWS

      WHERE TABLE_SCHEMA = @schemaview

 

  OPEN @Scan

  FETCH NEXT FROM @Scan INTO @view

  WHILE @@FETCH_STATUS = 0

  BEGIN

    SET @sqldrop = 'IF OBJECT_ID(''DataDiscovery.' + @schematable +

      '.' + @view + ''') IS NOT NULL DROP TABLE DataDiscovery.' +

      @schematable + '.' + @view

    SET @sqltable = 'SELECT * INTO DataDiscovery.' + @schematable +

      '.' + @view + ' FROM DataDiscovery.' + @schemaview + '.' + @view

              

    --PRINT @sqldrop

    --PRINT @sqltable

    EXECUTE sp_executesql @sqldrop

    EXECUTE sp_executesql @sqltable

    FETCH NEXT FROM @Scan INTO @view

  END

  CLOSE @Scan

  DEALLOCATE @Scan

END

GO

 

Undgå subquery i Select for at op bedre performance, forespørgselstid

KundeOrdre tabellen har felterne Kundenr int , Ordredato datetime, Beløb money.

Det er langsomt at indsætte en subquery select i hoved select delen:

SELECT kun.Kundenr,

       (SELECT ISNULL(SUM(ko.Beløb), 0)

        FROM dbo.KundeOrdre ko WITH (NOLOCK)

        WHERE ko.Kundenr = kun.Kundenr AND Ordredato >= '20090101'

       ) AS SamletBeløb

FROM   dbo.Kunde kun WITH (NOLOCK)

 

Det er hurtigere at have subquery i from delen via en left outer join:

SELECT kun.Kundenr, SamletBeløb = ISNULL(T.SamletBeløb,0)

FROM   dbo.Kunde kun WITH (NOLOCK)

       LEFT OUTER JOIN      -- fælles mængde med INNER JOIN

       (SELECT ko.Kundenr, SamletBeløb = SUM(ko.Beløb)

        FROM dbo.KundeOrdre ko WITH (NOLOCK)

        WHERE Ordredato >= '20090101'

        GROUP BY ko.Kundenr

       ) AS T ON T.Kundenr = kun.Kundenr  -- alias kun er udenfor.

 

Outer apply er en indlejret sql sætning, der angiver join-delen i Where-delen ligesom i første eksempel ovenfor d.v.s. inklusiv left outer join:

SELECT kun.Kundenr, SamletBeløb = ISNULL(T.SamletBeløb,0)

FROM   dbo.Kunde kun WITH (NOLOCK)

       OUTER APPLY

       (SELECT SamletBeløb = SUM(ko.Beløb)

        FROM dbo.KundeOrdre ko WITH (NOLOCK)

        WHERE ko.Kundenr = kun.Kundenr AND Ordredato >= '20090101'

       ) AS T              -- alias kun er indenfor.

 

UPDATE kun

SET  kun.SamletBeløb = ISNULL(T.SamletBeløb,0)

FROM dbo.Kunde kun

       OUTER APPLY

       (SELECT SamletBeløb = SUM(ko.Beløb)

        FROM dbo.KundeOrdre ko WITH (NOLOCK)

        WHERE ko.Kundenr = kun.Kundenr AND Ordredato >= '20090101'

       ) AS T              -- alias kun er indenfor.

 

Cross apply er en indlejret sql sætning, der angiver join-delen i Where-delen inklusiv inner join (at en kunde har mindst en ordre for at kunden bliver medtaget), men kræver, at primærnøglen indgår i select delen, derfor medtages her ko.Kundenr. Kaldes også en »lateral join using a query's output as input to another query in the same Select statement.«:

SELECT kun.Kundenr, SamletBeløb = ISNULL(T.SamletBeløb,0)

FROM   dbo.Kunde kun WITH (NOLOCK)

       CROSS APPLY

       (SELECT ko.Kundenr, SamletBeløb = SUM(ko.Beløb)

        FROM dbo.KundeOrdre ko WITH (NOLOCK)

        WHERE ko.Kundenr = kun.Kundenr AND Ordredato >= '20090101'

        GROUP BY ko.Kundenr

       ) AS T

 

Læs mere 1    Læs mere 2

 

Kan også løses med Partition i Windowed function:

SELECT DISTINCT Kundenr,

                SamletBeløb = SUM(Beløb) OVER(PARTITION BY Kundenr)

FROM dbo.KundeOrdre WITH (NOLOCK)

WHERE Ordredato >= '20090101'

Her sikrer DISTINCT at kunden kun vises en gang selvom samme Kundenr står i fle­re rækker, det samme gør GROUP BY ovenfor også som her er erstattet af PARTITION BY, men vi opnår at få samme resultat uden join hvilket er godt.

 

Inderste From-Where finder største ordredato per kunde fra den ydre From:

SELECT k.Kundenavn, o.Ordredato, o.Beløb

FROM dbo.KUNDE k INNER JOIN dbo.ORDRE o ON o.KundeId = k.KundeId

WHERE o.Ordredato =

(SELECT MAX(oo.Ordredato) -– finder største ordredato i Ordre tabel.

 FROM dbo.ORDRE oo

 WHERE oo.KundeId = k.KundeId

)

Ulempen er, at Ordre tabellen indgår to gange fordi ordrens beløb ønskes vist. Det kan undgås med Cross apply og jeg prøver her TOP(1) med DESC som finder én ordre-række i tabellen med største ordredato per kunde, og fra den ordre-række kan dato og beløb udtages og vises (overvej selv hvordan med WITH TIES):

SELECT k.Kundenavn, o.Ordredato, o.Beløb

FROM dbo.KUNDE k

CROSS APPLY

(

 SELECT TOP(1) oo.Ordredato, oo.Beløb -– finder én række i Ordre tbl.

 FROM dbo.ORDRE oo

 WHERE oo.KundeId = k.KundeId  -- for den k.KundeId der søges på.  

 ORDER BY oo.Ordredato DESC

) o

 

Brug af OR logisk udtryk kan nogen gange drille forespørgselsplanen når der ind­går mange tabeller, derfor kan en OR med fordel opdeles. Fra:

SELECT T1.FF

FROM T1

     LEFT OUTER JOIN T2 ON T2.F1 = T1.F1

     LEFT OUTER JOIN T3 ON T3.F1 = T2.F1

WHERE T1.F3 = 'OK' AND (T1.FINISHDATE >= DATEADD(month, -3, GETDATE())

                        OR T3.F2 = 'Finish')

 

Til dette med brug af common table expression cte:

;WITH T1_OK_WITHIN_3_MONTHS AS

(SELECT T1.FF

 FROM T1

 WHERE T1.F3 = 'OK' AND T1.FINISHDATE >= DATEADD(month, -3, GETDATE())

),

T1_OK_WITH_FINISH AS

(

 SELECT T1.FF

 FROM T1

     INNER JOIN T2 ON T2.F1 = T1.F1

     INNER JOIN T3 ON T3.F1 = T2.F1

 WHERE T1.F3 = 'OK' AND T3.F2 = 'Finish'

),

T1_OK_TOTAL AS

(SELECT *

 FROM T1_OK_WITHIN_3_MONTHS

 UNION

 SELECT *

 FROM T1_OK_WITH_FINISH

),

.... many more cte...der slutter af med en række joins mellem cte’s:

SELECT

FROM T1_OK_TOTAL

     INNER JOIN......

 

Procentvis andel af beløb ud af det samlede beløb og løsning af division med 0:

SELECT Kundenr, Beløb,

  ProcentAndel = ISNULL(Beløb / NULLIF(SUM(Beløb)

                 OVER(PARTITION BY Kundenr),0),0) * 100,

  TotalAntalRækker    = COUNT(1) OVER(),

  KundevisAntalRækker = COUNT(1) OVER(PARTITION BY Kundenr),

  ProcentAndelRækker  = COUNT(1) OVER(PARTITION BY Kundenr) /

                        1.0 * COUNT(1) OVER()

FROM dbo.KundeOrdre WITH (NOLOCK)

WHERE Ordredato >= '20090101'

 

For divide-by-zero, how to avoid Divide by zero error encountered jeg konverterer sum(beløb) fra 0 til null med NULLIF funktionen og hvis jeg ikke ønsker null værdi i resultatet så kan jeg konvertere null til 0 med ISNULL funktionen. Ved at gange med 1.0 får jeg brøkens tæller som kommatal, kunne også gøres med en cast as float.

 

I tilfælde af opdatering af andele felter i tabellen:

;WITH beregn AS

(

 SELECT Kundenr,

  ProcentAndel = ISNULL(Beløb / NULLIF(SUM(Beløb)

                 OVER(PARTITION BY Kundenr),0),0) * 100,

  TotalAntalRækker    = COUNT(1) OVER(),

  KundevisAntalRækker = COUNT(1) OVER(PARTITION BY Kundenr),

  ProcentAndelRækker  = COUNT(1) OVER(PARTITION BY Kundenr) /

                        1.0 * COUNT(1) OVER()

 FROM dbo.KundeOrdre WITH (NOLOCK)

 WHERE Ordredato >= '20090101'

)

UPDATE k WITH (TABLOCK)

SET ProcentAndel        = Beregn.ProcentAndel,

    TotalAntalRækker    = Beregn.TotalAntalRækker,

    KundevisAntalRækker = Beregn.KundevisAntalRækker,

    ProcentAndelRækker  = Beregn.ProcentAndelRækker

FROM  dbo.KundeOrdre k

      INNER JOIN beregn ON Beregn.Kundenr = k.Kundenr

WHERE k.Ordredato >= '20090101'

 

Løbende sum eller løbende total, running sum or running total:

SELECT

  Ordredato,

  Beløb,

  LøbendeSamletBeløb = SUM(Beløb) OVER (ORDER BY Ordredato)

FROM dbo.KundeOrdre WITH (NOLOCK)

ORDER BY Ordredato

 

Løbende sum pr. kunde:

 

SELECT

  Kundenr,

  Ordredato,

  Beløb,

  LøbendeSamletBeløb = SUM(Beløb) OVER (PARTITION BY Kundenr

                                        ORDER BY Ordredato)

FROM dbo.KundeOrdre WITH (NOLOCK)

ORDER BY Ordredato

 

Tilføjelse af Window frame or range efter Ordredato ROWS UNBOUNDED PRECEDING giver ingen forskel her da det definerer vinduet, hvorpå resultatet beregnes. Hvordan beregnes Resterende Sum Remaining Sum?

ROWS uses an in-memory spool and RANGE uses an on-disk spool.

 

Window function

Partition By og Group By minder en del om hinanden, nedenfor sker summeringen over hver kunde, d.v.s. sum pr. kunde, men samme sum vises i alle rækkerne fra tabellen. Uden brug af Partition fås en Total sum eller en I alt sum:

SELECT Kundenr,

       SumPrKunde = SUM(Beløb) OVER(PARTITION BY Kundenr),

       SumIalt    = SUM(Beløb) OVER()

FROM dbo.KundeOrdre WITH (NOLOCK)

 

Group By viser hver kunde én gang som en sammentræk af rækkerne fra tabellen og ved at pakke SUM(Beløb) ind i en endnu en SUM, så er Group By uændret, d.v.s. det er en sammenlægning af de enkelte kunders SumPrKunde beregning:

SELECT Kundenr,

       SumPrKunde = SUM(Beløb),

       SumIalt    = (SUM(SUM(Beløb)) OVER())

FROM dbo.KundeOrdre WITH (NOLOCK)

GROUP BY KundeId

 

Group By collapses rows into summaries.

Window Function preserves all rows and enrich them with contextual calculations.

 

Et klassisk eksempel på at finde den største værdi inden for en kolonne:

SELECT Kundenr, MAX(Beløb) AS StørsteBeløb

FROM dbo.KundeOrdre WITH (NOLOCK)

GROUP BY Kundenr

 

Som Window function hvor Distinct sikrer, at samme kunde ikke gentages:

SELECT DISTINCT Kundenr,

       MAX(Beløb) OVER(PARTITION BY Kundenr) AS StørsteBeløb

FROM dbo.KundeOrdre WITH (NOLOCK)

 

Kriterie på det største beløb:

SELECT Kundenr, MAX(Beløb) AS StørsteBeløb

FROM dbo.KundeOrdre

GROUP BY Kundenr WITH (NOLOCK)

HAVING MAX(Beløb) >= 100

 

Som Window function:

SELECT DISTINCT ko.Kundenr, ko.StørsteBeløb

FROM   (

        SELECT Kundenr,

               MAX(Beløb) OVER(PARTITION BY Kundenr) AS StørsteBeløb

        FROM dbo.KundeOrdre WITH (NOLOCK)

       ) AS ko

WHERE ko.StørsteBeløb >= 100

 

Den klassiske self-join når en største værdi skal findes og den har et kriterie til­knyt­tet, her større end eller lig med 100 men en join koster altid performance:

SELECT DISTINCT ko.Kundenr, ko.Ordredato, ko.Beløb

FROM   dbo.KundeOrdre ko WITH (NOLOCK)

       INNER JOIN

       (SELECT Kundenr, MAX(Beløb) AS StørsteBeløb

        FROM dbo.KundeOrdre

        GROUP BY Kundenr

        HAVING MAX(Beløb) >= 100

       ) AS T ON T.Kundenr = ko.Kundenr AND T.StørsteBeløb = ko.Beløb

 

Et klassisk eksempel på at finde antal poster inden for en kolonne:

SELECT Kundenr, COUNT(*) AS AntalOrdre

FROM dbo.KundeOrdre WITH (NOLOCK)

GROUP BY Kundenr

 

Som Window function:

SELECT DISTINCT Kundenr,

       COUNT(*) OVER(PARTITION BY Kundenr) AS AntalOrdre

FROM dbo.KundeOrdre WITH (NOLOCK)

 

Antal unikke datoer ud fra tidsstemplet Ordredato:

SELECT Kundenr, COUNT(DISTINCT CAST(Ordredato AS date)) AS AntalOrdre

FROM dbo.KundeOrdre WITH (NOLOCK)

GROUP BY Kundenr

 

Windows function tillader ikke COUNT(DISTINCT) så må CROSS APPLY anvendes:

COUNT(DISTINCT CAST(Ordredato AS date)) OVER(PARTITION BY Kundenr)

 

SELECT a.Kundenr, t.AntalOrdre

FROM dbo.KundeOrdre a WITH (NOLOCK)

     CROSS APPLY

     (

      SELECT COUNT(DISTINCT CAST(Ordredato AS date)) AS AntalOrdre

      FROM dbo.KundeOrdre b WITH (NOLOCK)

      WHERE b.Kundenr = a.Kundenr

     ) t

 

How to find the second largest value or second highest or second best or rank #2:

Using max:

select max(col)

from [table]

where col < (select max(col) from [table])

 

Using sort top two and then bottom one because of top(1) with order by desc:

select top(1) col

from (

      select top(2) col

      from [table]

      order by col

     ) topTwo

order by col desc

 

Using row number and fetch the second row:

select col

from (

      select row_number() over (order by col asc) as rowNum, col

      from [table]

     ) withRowNum

where rowNum = 2

 

Using offset and fetch:

select top(2) col

from [table]

group by col

order by col desc

offset 1 rows

fetch next 1 rows only

 

Or with mysql:

select col

from [table]

group by col

order by col desc

limit 1,1

 

De 10 første rækker fra en sorteret liste af rækker:

SET ROWCOUNT 10

SELECT p.ProductName AS TenMostExpensiveProducts, p.UnitPrice

FROM Products p

ORDER BY p.UnitPrice DESC

 

Find data rækker ud fra en on-the-fly defineret prioritetsliste, eksempelvis en UserSystem tabel hvor der for hvert UserId ønskes den række hvis SystemId har højeste prioritet, vist med fed tekst:

 

RowId

UserId

SystemId

1

1

37

2

1

84

3

2

37

4

2

27

5

3

10

6

3

27

7

3

37

8

3

84

9

4

10

10

5

NULL

 

;WITH systemPriority AS  -- On-the-fly defined priority list of systems

(

SELECT sp.*

FROM (VALUES (42, 1), (57, 2), (19, 3), (28, 4), (NULL, 5) )

     sp(SystemId, Priority)

)

SELECT DISTINCT

     us.UserId,

     SystemId = FIRST_VALUE(us.SystemId) OVER(PARTITION BY us.UserId

                ORDER BY sp.Priority ROWS

                BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)

FROM dbo.UserSystem us

     LEFT OUTER JOIN systemPriority sp ON sp.SystemId = us.SystemId

WHERE us.SystemId Is Not Null

 

FIRST_VALUE udsøger den første række inden for hver partition/gruppe af UserId hvor rækkerne er blevet sorteret efter Priority.

 

Starting and ending rows might be fixed or relative to the current row based on the following keywords:

·    CURRENT ROW, the current row

·    UNBOUNDED PRECEDING, all rows before the current row -> fixed

·    UNBOUNDED FOLLOWING, all rows after the current row -> fixed

·    x PRECEDING, x rows before the current row -> relative

·    y FOLLOWING, y rows after the current row -> relative

Window functions rows clause   SELECT - OVER Clause

 

STRING_AGG enten i en subquery eller i en join:

 

SELECT p.ProjectName,

       (SELECT Customers = STRING_AGG(c.CustomerName, ', ')

               WITHIN GROUP (ORDER BY i.InvoiceAmount DESC, c.CustomerName ASC)

        FROM dbo.Invoice i

             INNER JOIN dbo.Customer c ON c.CustomerId = i.CustomerId

        WHERE i.ProjectId = p.ProductId

       ) AS Customers

FROM dbo.Project p

 

SELECT p.ProjectName, l.Customers

FROM dbo.Project p

OUTER APPLY

       (SELECT Customers = STRING_AGG(c.CustomerName, ', ')

               WITHIN GROUP (ORDER BY i.InvoiceAmount DESC, c.CustomerName ASC)

        FROM dbo.Invoice i

             INNER JOIN dbo.Customer c ON c.CustomerId = i.CustomerId

        WHERE i.ProjectId = p.ProductId

       ) l

 

I en liste ønskes udvalgte kæder vist først, dog skal »Alle« vises allerførst og der­næst skal alle de andre kæder vises i alfabetisk orden derfor anvender »jeg full outer join« for både at få medtaget »Alle« som ikke er en kæde og alle de andre kæder som ikke er angivet i den udvalge og prioriteret sorteret liste:

 

CREATE VIEW [report].[Chain]

AS

WITH Priority AS

(

  SELECT t.*

  FROM

     ( VALUES

       (0,'Alle'),

       (1,'Circle K'),

       (2,'COOP Brugsen'),

       (3,'Dagli'' Brugsen'),

       (4,'Irma'),

       (5,'Kvickly'),

       (6,'LokalBrugsen'),

       (7,'SuperBrugsen')

     ) AS t(Sortorder, ChainName)

),

Chain AS

(

SELECT ChainName

FROM Datamart.SalesForce.Dim_Chain

WHERE ChainName NOT IN('Mangler', 'Uoplyst')

)

SELECT TOP 99.99 PERCENT ISNULL(c.ChainName, p.ChainName) AS ChainName

FROM Chain c

FULL OUTER JOIN Priority p ON p.ChainName = c.ChainName

ORDER BY ISNULL(p.Sortorder,99), ISNULL(c.ChainName, p.ChainName)

GO

 

Nedenfor skal organisationsnavn prioriteres før gruppens navn som igen skal an­ven­des før brugerens navn, d.v.s. bruger navn vises kun hvis ejeren hverken er or­ga­ni­sa­tion eller group:

 

SELECT e.Name, Name = Coalesce(o.Name, g.Name, u.Name)

FROM dbo.Event e

     LEFT OUTER JOIN dbo.Organisation o ON o.Id = e.OwnerId

     LEFT OUTER JOIN dbo.Group g ON g.Id = e.OwnerId

     LEFT OUTER JOIN dbo.User u ON u.Id = e.OwnerId

 

År til dato ÅTD (Year-to-date YTD) er løbende sum reset pr. år. Her ønskes det fordelt pr. måned repræsenteret som 1. dato i hver måned, d.v.s. ordredato f.eks. 21-02-2015 omdannes til 01-02-2015 og ordrer pr. måned summeres sammen pr. kunde så rækkerne sammentrækkes til en række pr. kunde pr. måned ved brug af Group By og Partition By pr. kundenr og år fra ordredato, derfor skal Year(Ordre­dato) indgå i Group By, men årstallet skader ikke fordi der allerede grupperes på månedsniveau. Ingen self-join så performance er meget hurtig:

 

SELECT

 Kundenr,

 MånedsvisDato = CAST(Ordredato - DAY(Ordredato) + 1 AS date),

 Beløb         = SUM(Beløb),

 Beløb_ÅTD     = SUM(SUM(Beløb)) OVER (

                   PARTITION BY Kundenr, YEAR(Ordredato)

                   ORDER BY CAST(Ordredato - DAY(Ordredato) + 1 AS date)),

 Rækker        = COUNT(*),

 Rækker_ÅTD    = SUM(COUNT(*)) OVER (

                   PARTITION BY Kundenr, YEAR(Ordredato)

                   ORDER BY CAST(Ordredato - DAY(Ordredato) + 1 AS date))

FROM dbo.KundeOrdre WITH(NOLOCK)

GROUP BY

     Kundenr,

     YEAR(Ordredato),

     CAST(Ordredato - DAY(Ordredato) + 1 AS date)

 

Kundenr

MånedsvisDato

Beløb

Beløb_ÅTD

Rækker

Rækker_ÅTD

123

01-01-2015

1

1

1

1

123

01-02-2015

1

2

1

2

123

01-03-2015

1

3

1

3

123

01-01-2016

1

1

1

1

123

01-02-2016

1

2

1

2

124

01-02-2015

3

3

2

2

124

01-10-2015

6

9

3

5

124

01-02-2016

4

4

1

1

124

01-05-2016

4

8

4

5

124

01-10-2016

10

18

5

10

 

Fordelt pr. uge repræsenteret som mandag datoen, d.v.s. ordredato f.eks. 21-02-2015 omdannes til 16-02-2015, så forespørgslen ligner næsten den månedsvise:

 

SET DATEFIRST 1 -- sætter Mandag som første dag i ugen.

SELECT

 Kundenr,

 UgevisDato    = CAST(Ordredato - DATEPART(weekday,Ordredato) + 1 AS date),

 Beløb         = SUM(Beløb),

 Beløb_ÅTD     = SUM(SUM(Beløb)) OVER (

     PARTITION BY Kundenr, YEAR(Ordredato - DATEPART(weekday,Ordredato) + 1)

     ORDER BY CAST(Ordredato - DATEPART(weekday,Ordredato) + 1 AS date)),

 Rækker        = COUNT(*),

 Rækker_ÅTD    = SUM(COUNT(*)) OVER (

     PARTITION BY Kundenr, YEAR(Ordredato - DATEPART(weekday,Ordredato) + 1)

     ORDER BY CAST(Ordredato - DATEPART(weekday,Ordredato) + 1 AS date))

FROM dbo.KundeOrdre WITH(NOLOCK)

GROUP BY

     Kundenr,

     YEAR(Ordredato - DATEPART(weekday,Ordredato) + 1),

     CAST(Ordredato - DATEPART(weekday,Ordredato) + 1 AS date)

 

ÅTD kan også udføres frem til en bestemt dato, her repræsenteret som en måned hvor der både ønskes vist beløbet for oktober 2016 pr. kunde og beløbets ÅTD d.v.s. fra januar 2016 til og med oktober 2016:

 

DECLARE @dato date = '2016-10-01'

SELECT

  Kundenr,

  Beløb = SUM(CASE WHEN CAST(Ordredato - DAY(Ordredato) + 1 AS date) = @dato

                   THEN Beløb ELSE 0 END),

  Beløb_ÅTD = SUM(Beløb)

FROM dbo.KundeOrdre WITH(NOLOCK)

WHERE CAST(Ordredato - DAY(Ordredato) + 1 AS date)

      BETWEEN DATEADD(year,DATEDIFF(year,0,@dato),0) AND @dato

GROUP BY Kundenr

 

Perioden fra januar til og med oktober er angivet i WHERE betingelsen. Den al­min­de­lige SUM(Beløb) er summen for hele perioden d.v.s. netop år-til-dato grupperet pr. kunde. For at få beløbet kun for den udsøgte oktober måned anvendes en CASE WHEN betingelse og for at Ordredato ikke skal indgå i Group By så sættes en SUM udenom (gør ingen skade, men her er der faktisk flere ordredatoer inden for en måned hvis beløb naturligvis skal summeres sammen). Der fås:

 

Kundenr

Beløb

Beløb_ÅTD

123

0

3

124

10

18

 

For oktober 2015 fås:

 

Kundenr

Beløb

Beløb_ÅTD

123

1

10

124

6

9

 

Indeks i forhold til sidste år som har basisværdien 100 (procentvise stigning) for en givet måned og tilsvarende måned sidste år. Ordredato omdannes igen til må­ned og det ville være oplagt med en self-join på samme Kundenr og måned = må­ned -1 men husk at alle join koster performance på mange rækker, derfor an­ven­des en Union All mellem gældende måned og sidste års måned som sam­men­træk­kes via en SUM, så begge datasæt fås i sammen række pr. kunde:

 

DECLARE @dato date = '2016-02-01'

 

SELECT Kundenr, Beløb = SUM(Beløb), SidsteÅr_Beløb = SUM(SidsteÅr_Beløb),

       Indeks = 100.0 * SUM(Beløb) / NULLIF(SUM(SidsteÅr_Beløb),0)

FROM

(

 SELECT Kundenr, Beløb = SUM(Beløb), SidsteÅr_Beløb = NULL

 FROM dbo.KundeOrdre

 WHERE CAST(Ordredato - DAY(Ordredato) + 1 AS date) = @dato

 GROUP BY Kundenr

 

 UNION ALL

 

 SELECT Kundenr, Beløb = NULL, SidsteÅr_Beløb = SUM(Beløb)

 FROM dbo.KundeOrdre

 WHERE CAST(Ordredato - DAY(Ordredato) + 1 AS date) = DATEADD(year,-1,@dato)

 GROUP BY Kundenr

) t

GROUP BY Kundenr

 

Foreningsmængde af disse to mængder for gældende måned og sidste års måned:

 

Kundenr

Beløb

SidsteÅr_Beløb

123

1

NULL

124

4

NULL

 

Kundenr

Beløb

SidsteÅr_Beløb

123

NULL

1

124

NULL

3

 

Bliver sammentrukket via SUM til to rækker og Indeks kan udregnes:

 

Kundenr

Beløb

SidsteÅr_Beløb

Indeks

123

1

1

100.00

124

4

3

133.33

 

Performance ved hjælp af et indeks

Store data mængder kræver gode indeks, et eksempel på et indeks som query planen vil anvende og få svartiden ned på få sekunder fremfor minutter fordi indekset er skræddersyet til OUTER APPLY WHERE sætningen:

 

CREATE NONCLUSTERED INDEX [IX_Fact_Sale_Concat] ON [dbo].[Fact_Sale]

(

 [DimTransactionType] ASC,

 [DimProduct] ASC,

 [SpecialNumber] ASC,

 [DimTransactionDate] ASC

)

INCLUDE ([DimDebitor])

WHERE ([IsDeleted]=(0))

WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION = PAGE) ON [Primary]

 

SELECT

 TransactionDate = fact.DimTransactionDate,

 DepositDebitor = ISNULL(rd.Debitor, r.Debitor)

FROM [dbo].[Fact_Sale] fact

     INNER JOIN [dbo].[DimDebitor] r

           ON r.DimDebitor = fact.DimDebitor

     OUTER APPLY

     (SELECT TOP 1 DimDebitor

      FROM [dbo].[Fact_Sale] factd

      WHERE factd.DimTransactionType IN

             (SELECT DimTransactionType

              FROM [dbo].[DimTransactionType]

              WHERE Code ='DEPOSIT') AND

            factd.DimProduct = fact.DimProduct AND

            factd.SpecialNumber = fact.SpecialNumber AND

            factd.DimTransactionDate <= fact.DimTransactionDate

      ORDER BY factd.DimTransactionDate

     ) factd

     LEFT OUTER JOIN [dbo].[DimDebitor] rd

          ON rd.DimDebitor = factd.DimDebitor

WHERE IsDeleted=0

 

Where betingelsen er vigtig for at anvende indekset IX_Fact_Sale_Concat, fordi indekset har en tilsvarende where betingelse ([IsDeleted]=(0)).

 

Include er et covered index der indeholder alle nødvendige felter/kolonner til at løse forespørgslen, hvorved det clustered index ikke tages i anvendelse.

 

A covered index is an index which »cover« all columns needed from a specific table, removing the need to access the physical table at all for a given query/ operation. While covering indexes can often provide good benefit for retrieval, they do add somewhat to insert/ update overhead; due to the need to write extra or larger index rows on every update.

 

Performance ved hjælp af option

SELECT t.Field1, x.Field2

FROM   dbo.T t

       INNER JOIN dbo.X x ON x.Id = t.Id

WHERE  t.Field3 = 'Maling'

ORDER BY x.SortOrder ASC

OPTION(FORCE ORDER) eller OPTION(MAXDOP 3)

 

OPTION(FORCE ORDER) bruges til at fortælle SQL Server at den skal joine tabeller i den rækkefølge der er angivet i sql sætningen og derved kan SQL Serveren hurtigere lave en query plan, især ved rigtig mange tabeller der skal joines.

Læs mere om Option force order

 

OPTION(MAXDOP 3) eller OPTION(MAXDOP 1) max degree of parallelism angiver antallet af kerner som sql sætningen må blive udført på eller med, fordi nogen gange kan for mange kerne ende i deadlock.

Læs mere om MAXDOP   Læs mere om table hints.

 

SELECT name, value_in_use

FROM sys.configurations

WHERE description LIKE '%max%%parallelism%'

 

Har du en forespørgel med mange joins, kan dannelsen af query plan give en fejlmelding a la:

Query processor could not produce a query plan because of the hints defined in this query. Resubmit the query without specifying any hints and without using SET FORCEPLAN.

The query processor ran out of internal resources and could not produce a query plan. This is a rare event and only expected for extremely complex queries or queries that reference a very large number of tables or partitions. Please simplify the query. If you believe you have received this message in error, contact Customer Support Services for more information.

 

Løsningen er:

OPTION (HASH JOIN), OPTION (LOOP JOIN) eller OPTION (MERGE JOIN).

OPTION (FORCE ORDER, HASH JOIN) ved rigtig mange joins.

 

Læs mere derom    Query hints    Joins

 

En table-valued function kaldes oftest fra enten et view med felt som parameter værdi eller fra en stored procedure med declared @variabel som parameter, og sidst nævnte kan give en ekstra lang udførselstid som kan undgås ved brug af:

INSERT INTO #temp

SELECT 'CRM' AS Source, t.*

FROM dbo.CRMdatafn(

    DATETIMEFROMPARTS(YEAR(@DateTimeFrom), MONTH(@DateTimeFrom),

                  DAY(@DateTimeFrom), 0, 0, 0, 000),

    DATETIMEFROMPARTS(YEAR(@DateTimeTo), MONTH(@DateTimeTo),

                  DAY(@DateTimeTo), 23, 59, 59, 999),

    @KundeId, @BrandId, @Weekno, @Amount) AS t

OPTION(RECOMPILE)

 

Recompile tager nogle millisekunder for at lave en ny query plan, men der undgås de problemer som kaldes parameter sniffing og om ulemper. Når en stored proce­dure har en parameter, kan det nogen gange være en fordel at lave en lokal varia­bel i stored proceduren og udføre SET lokalvariabel = parametervariabel og anvende den lokale variabel i where delen af forespørgslen.

 

En tabel har over 1 milliard rækker eller flere milliarder rækker og der kun søges på primærnøgle clustered indeks og der ikke er andre indeks i tabellen. Sql server opsamler optimizer statistikker og det koster når der løbende indsættes nye millio­ner rækker i tabellen, fordi Trace Flag 2371 er default fra SQL Server 2016, og når der udføres en select from where på primærnøglen er det en del af parsingen at trigge en ajourføring af statistikker, som kan tage mange minutter, hvorved den samlede svartid bliver lang (update statistics is triggered). Det kan undgås ved at bruge: OPTION(KEEPFIXED PLAN).

En anden løsning er at enable, at ajourføringen sker asynkront, læs mere:

ALTER DATABASE [databasename] SET AUTO_UPDATE_STATISTICS_ASYNC ON;

Kan også sættes per tabel, læs mere.

 

Måske kan en genkompilering af sp give en hurtigere svartid:

CREATE PROCEDURE dbo.MySP @FilterExpr varchar(100)

WITH RECOMPILE

AS

 

Denne anvendes også for at speede læsning op og se bort fra låsning

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

 

At studere query plan og se efter task med stort antal rækker, memory forbrug eller høj estimated time kan give inspiration til omskrivning af sql sætningen, f.eks. for at undgå lazy table spool.

 

Flere gode råd på https://sqlperformance.com f.eks. om table expressions.

 

Slet index før stor indsættelse og opret index bagefter for hurtig søgning

IF EXISTS (SELECT 1 FROM sysindexes

           WHERE name = 'IX_KUNDE_Postnummer')

BEGIN

   DROP INDEX dbo.KUNDE.IX_KUNDE_Postnummer

END

 

DROP INDEX IF EXISTS dbo.KUNDE.IX_KUNDE_Postnummer

 

IF NOT EXISTS (SELECT 1 FROM sysindexes

               WHERE name = 'IX_KUNDE_Postnummer')

BEGIN

   CREATE NONCLUSTERED INDEX IX_KUNDE_Postnummer

     ON dbo.KUNDE(Postnummer) WITH FILLFACTOR = 100 ON [PRIMARY]

END

 

forudsætning, at der er et clustered index typisk primærnøglen.

 

Sæt et index midlertidig ud af kraft

ALTER INDEX IX_KUNDE_Postnummer ON dbo.KUNDE DISABLE -- indeks i hvile

... indsæt data ...

ALTER INDEX IX_KUNDE_Postnummer ON dbo.KUNDE REBUILD -- indeks enabled

 

Sæt alle index midlertidig ud af kraft

TRUNCATE TABLE dbo.KUNDE                   -- når indsæt er full load.

ALTER INDEX ALL ON dbo.KUNDE DISABLE       -- tabel kan ikke åbnes.

ALTER INDEX PK_KUNDE ON dbo.KUNDE REBUILD  -- tabel kan åbnes igen.

.. indsæt data .. -- ALTER INDEX IX_KUNDE_Postnummer ON dbo.KUNDE DISABLE

ALTER INDEX ALL ON dbo.KUNDE REBUILD       -- indeks er enabled igen.

 

ALTER INDEX ALL ON dbo.KUNDE REBUILD

  WITH (ONLINE = ON, RESUMABLE = ON, MAX_DURATION = 10)

 

Se hvilke index er sat midlertidig ud af kraft

Select sys.objects.name as table_name,

       sys.indexes.name as index_name

From sys.indexes inner join sys.objects

     on sys.objects.object_id = sys.indexes.object_id

Where sys.objects.name = 'KUNDE' and

      sys.indexes.is_disabled = 1

Order by sys.objects.name, sys.indexes.name

 

Create Index  Alter Index  Transaction log space  Read more  Management views

 

Online Rebuild functionality only supported in Enterprise edition of SQL Server. MAX_DURATION are in minutes.

Rebuild Indexes (online) - An online index rebuild does not require object-level locks until the end of the operation, when a lock must be held for a short duration to complete the rebuild.

 

Single transaction vil kræve meget plads i tempdb i modsætning til enkelvis rebuild af indekserne.

 

DECLARE @TableName VARCHAR(255)

DECLARE @sql NVARCHAR(500)

DECLARE @fillfactor INT

SET @fillfactor = 80

DECLARE TableCursor CURSOR FOR

  SELECT QUOTENAME(OBJECT_SCHEMA_NAME([object_id]))+'.' +

         QUOTENAME(name) AS TableName

FROM sys.tables

OPEN TableCursor

FETCH NEXT FROM TableCursor INTO @TableName

WHILE @@FETCH_STATUS = 0

BEGIN

SET @sql = 'ALTER INDEX ALL ON ' + @TableName + ' REBUILD WITH (FILLFACTOR = ' + CONVERT(VARCHAR(3),@fillfactor) + ')'

EXEC (@sql)

FETCH NEXT FROM TableCursor INTO @TableName

END

CLOSE TableCursor

DEALLOCATE TableCursor

GO

 

Læs mere om index.

 

Dan ikke index på en kolonne med få forskellige værdier som Frøken, Frue, Herre. Det gælder også en bit kolonne.

 

SELECT dbschemas.[name] as 'Schema',

dbtables.[name] as 'Table',

dbindexes.[name] as 'Index',

indexstats.avg_fragmentation_in_percent,

indexstats.page_count

FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, NULL) AS indexstats

INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id]

INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id]

INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id]

AND indexstats.index_id = dbindexes.index_id

WHERE indexstats.database_id = DB_ID() AND dbtables.[name] like '%%'

ORDER BY indexstats.avg_fragmentation_in_percent desc

 

Slet alle index undtagen primærnøglen

DECLARE @qry nvarchar(4000)

IF EXISTS(select 1 from sys.indexes i join sys.objects o on i.object_id=o.object_id join sys.schemas s on o.schema_id=s.schema_id where o.name = 'KUNDE' and o.type<>'S' and is_primary_key<>1 and index_id>0 and s.name<>'sys')

BEGIN

select @qry = (select 'drop index ['+s.name+'].['+o.name+'].['+i.name+'];'

from sys.indexes i join sys.objects o on i.object_id=o.object_id join sys.schemas s on o.schema_id=s.schema_id

where o.name = 'KUNDE' and o.type<>'S' and is_primary_key<>1 and index_id>0 and s.name<>'sys'

for xml path(''))

select @qry

EXEC sp_executesql @qry

END

GO

 

Påtving brug af indeks (force index)

Her dannes et komprimeret indeks:

CREATE NONCLUSTERED INDEX [IX_Order_InsertDate]

ON [dbo].[Order]

(

  [InsertedDate] ASC   

)WITH (DATA_COMPRESSION = PAGE)

GO

Det er ikke altid, at SQL Serveren kan finde ud af anvende et indeks, hvilket be­ty­der, at der bliver udført en Clustered Index Scan som medfører en Table Scan der tager mange minutter på den store data mængde. Men i sql sætningen kan vi tvin­ge den til at bruge indekset, så der bliver udført en Index Seek og Key Lookup som er meget hurtig, f.eks. ved en inkremental udlæsning (delta data):

DECLARE @d as datetime2(7) = '2017-12-13 02:36:08.6333333'

SELECT *

FROM dbo.Order WITH(nolock, index(IX_Order_InsertDate))

WHERE InsertedDate > @d

 

Key Lookup er langsom når sql sætningen returnerer mange rækker.

 

Pas på med forceseek    Query hints

 

NOLOCK - KAN VÆRE FARLIG VED ROLLBACK. This table hint, also known as READUNCOMMITTED, is applicable to SELECT statements only. NOLOCK indicates that no shared locks are issued against the table that would prohibit other transactions from modifying the data in the table. The benefit of the statement is that it allows you to keep the database engine from issuing locks against the tables in your queries; this increases concurrency and performance because the database engine does not have to maintain the shared locks involved. The downside is that, because the statement does not issue any locks against the tables being read, some "dirty," uncommitted data could potentially be read. A "dirty" read is one in which the data being read is involved in a transaction from another connection. If that transaction rolls back its work, the data read from the connection using NOLOCK will have read uncommitted data. This type of read makes processing inconsistent and can lead to problems.

 

Tilføjelse af et fortløbende nummer med indeks

ALTER TABLE [archive].[Order]

ADD ArcRecordId bigint NOT NULL IDENTITY (1, 1)

GO

 

CREATE NONCLUSTERED INDEX [IX_Order_ArcRecordId]

ON [archive].[Order]

(

  [ArcRecordId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

GO

 

Slet eller opret fremmednøgle (Foreign Key Constraint)

IF EXISTS (SELECT 1

           FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE

           WHERE TABLE_NAME = 'KUNDE' AND

                 CONSTRAINT_NAME = 'FK_KUNDE_POSTNUMMER')

BEGIN

   ALTER TABLE dbo.KUNDE

   DROP CONSTRAINT FK_KUNDE_POSTNUMMER

END

 

IF NOT EXISTS (SELECT 1

               FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE

               WHERE TABLE_NAME = 'KUNDE' AND

                     CONSTRAINT_NAME = 'FK_KUNDE_POSTNUMMER')

BEGIN

   ALTER TABLE dbo.KUNDE

   ADD CONSTRAINT FK_KUNDE_POSTNUMMER

     FOREIGN KEY (Postnummer) REFERENCES dbo.POSTNUMMER (Postnummer)

END

 

giver fejl når data i fremmednøglen (barn-tabel) ikke findes i primærnøglen (mor). De data findes via en NOT EXISTS:

SELECT *

FROM dbo.KUNDE k

WHERE NOT EXISTS(SELECT TOP(1) 1 FROM dbo.POSTNUMMER p

                 WHERE p.Postnummer = k.Postnummer)

 

Midlertidig slå alle fremmednøgle check fra før stor indsættelse

ALTER TABLE dbo.KUNDE NOCHECK CONSTRAINT ALL

Men når den slås til igen fås der ikke fejlmelding ved forkerte data i fremmednøgle

ALTER TABLE dbo.KUNDE CHECK CONSTRAINT ALL

 

-- Disable single constraint

ALTER TABLE MyTable NOCHECK CONSTRAINT MyConstraint

-- Enable single constraint

ALTER TABLE MyTable CHECK CONSTRAINT MyConstraint

 

Udfør check af fremmednøgler efter stor indsættelse

Via ad hoc query som returnerer tabel der viser fejlene:

DBCC CHECKCONSTRAINTS ('[TEST]')

Men upraktisk i et ETL job, derfor noget a la:

TRUNCATE TABLE dbo.TEST_ERROR

INSERT INTO dbo.TEST_ERROR(Nr) WITH (TABLOCK)

SELECT Nr

FROM dbo.TEST WITH(NOLOCK)

     LEFT OUTER JOIN dbo.KALENDER_MÅNED WITH(NOLOCK)

          ON dbo.KALENDER_MÅNED.ÅrMåned = dbo.TEST.ÅrMåned

     LEFT OUTER JOIN dbo.ARBEJDE WITH(NOLOCK)

          ON dbo.ARBEJDE.ArbejdeId = dbo.TEST.ArbejdeId

WHERE dbo.KALENDER_MÅNED.ÅrMåned IS NULL OR

      dbo.ARBEJDE.ArbejdeId IS NULL

 

Der kan så udføres en test af om tabellen ikke er tom og sendes en email til driften.

 

Vis alle constraints for en tabel

EXEC sp_helpconstraint @objname = 'dbo.KUNDE', @nomsg = NULL;

 

Full dump

Indlæsning til et enterprise datawarehouse EDW for alle data først slettes og genindlæses fra kildedatasystemet og er behandlet i en NEW tabel via DSA. BorgerId er et Identity (auto incremental) fortløbende nummer.

CREATE PROCEDURE [dbo].[EDW_DB_Borger_Insert]

AS

BEGIN

  SET NOCOUNT ON

  DECLARE @Today datetime = GETDATE()

  TRUNCATE TABLE dbo.EDW_DB_Borger

  SET IDENTITY_INSERT dbo.EDW_DB_Borger ON

  INSERT INTO dbo.EDW_DB_Borger

  (

   BorgerId,

   BorgerKode,

   BorgertypeId,

   Fornavn,

   Efternavn,

   Fødselsdato,

   KønId,

   CivilstatusId,

   DataKilde,

   BorgerOprettet

  )

  VALUES(-1, NULL, -1, 'Mangler', 'Mangler', NULL,-1,-1,'EDW',@Today)

  SET IDENTITY_INSERT dbo.EDW_DB_Borger OFF

 

  INSERT INTO dbo.EDW_DB_Borger

  (

   BorgerKode,

   BorgertypeId,

   Fornavn,

   Efternavn,

   Fødselsdato,

   KønId,

   CivilstatusId,

   DataKilde,

   BorgerOprettet

  )

  SELECT

   BorgerKode,

   BorgertypeId,

   Fornavn,

   Efternavn,

   Fødselsdato,

   KønId,

   CivilstatusId,

   DataKilde,

   BorgerOprettet = @Today

  FROM dbo.EDW_NEW_Borger

  ORDER BY BorgerKode

END

 

Incremental update

Indlæsning til et enterprise datawarehouse EDW hvor CivilstatusId = -1 står for en Mangler værdi der ikke findes i kildedatasystemet og i DSA databasen og CivilstatusKode som er primærnøglen i kilden får værdien 0 der en repræ­sen­tation for null. CivilstatusId er et Identity (auto incremental, surrogate) fort­løbende nummer.

CREATE PROCEDURE [dbo].[EDW_DB_Civilstatus_InsertUpdate]

AS

BEGIN

  SET NOCOUNT ON

  DECLARE @Today datetime = GETDATE()

  IF NOT EXISTS(SELECT TOP(1) 1 FROM dbo.EDW_DB_Civilstatus

                WHERE CivilstatusId = -1)

  BEGIN

    SET IDENTITY_INSERT dbo.EDW_DB_Civilstatus ON

    INSERT INTO dbo.EDW_DB_Civilstatus

    (CivilstatusId, CivilstatusKode, CivilstatusNavn,

     CivilstatusType, CivilstatusOprettet, CivilstatusOpdateret)

    VALUES(-1, 0, 'Mangler', NULL, @Today, @Today)

    SET IDENTITY_INSERT dbo.EDW_DB_Civilstatus OFF

  END

 

  INSERT INTO dbo.EDW_DB_Civilstatus

  (CivilstatusKode, CivilstatusNavn,

   CivilstatusType, CivilstatusOprettet, CivilstatusOpdateret)

  SELECT dsa.CivilstatusKode, dsa.CivilstatusNavn,

         dsa.CivilstatusType, @Today, @Today

  FROM dbo.DSA_BASISDATA_CIVILSTATUS dsa

  WHERE NOT EXISTS(SELECT TOP(1) 1 FROM dbo.EDW_DB_Civilstatus db

                   WHERE db.CivilstatusKode = dsa.CivilstatusKode)

  ORDER BY dsa.CivilstatusKode

 

  UPDATE db

  SET   CivilstatusNavn = dsa.CivilstatusNavn,

        CivilstatusType = dsa.CivilstatusType,

        CivilstatusOpdateret = @Today

  FROM  dbo.EDW_DB_Civilstatus db

        INNER JOIN dbo.DSA_BASISDATA_Civilstatus dsa

           ON dsa.CivilstatusKode = db.CivilstatusKode

  WHERE db.CivilstatusNavn <> dsa.CivilstatusNavn OR

    ISNULL(db.CivilstatusType,'*') <> ISNULL(dsa.CivilstatusType,'*')

END

 

MySql anvender en anden UPDATE syntaks/notation:

DECLARE varToday date;

SET varToday := CURDATE();      

 

UPDATE EDW_DB_Civilstatus db

INNER JOIN DSA_BASISDATA_CIVILSTATUS dsa

      ON dsa.CivilstatusKode = db.CivilstatusKode

SET   db.CivilstatusNavn = dsa.CivilstatusNavn,

      db.CivilstatusType = dsa.CivilstatusType,

      db.CivilstatusOpdateret = varToday

WHERE db.CivilstatusNavn <> dsa.CivilstatusNavn OR

      IFNULL(db.CivilstatusType,'*') <>

      IFNULL(dsa.CivilstatusType,'*');

 

Merge kommando slår insert og update sammen, men mangler order by

MERGE dbo.EDW_DB_Civilstatus AS db          -- target

USING dbo.DSA_BASISDATA_CIVILSTATUS AS dsa  -- source

ON (dsa.CivilstatusKode = db.CivilstatusKode)

WHEN NOT MATCHED THEN

INSERT (CivilstatusKode, CivilstatusNavn,

        CivilstatusType, CivilstatusOprettet, CivilstatusOpdateret)

VALUES (dsa.CivilstatusKode, dsa.CivilstatusNavn,

        dsa.CivilstatusType, @Today, @Today)

WHEN MATCHED AND db.CivilstatusNavn <> dsa.CivilstatusNavn OR

    ISNULL(db.CivilstatusType,'*') <> ISNULL(dsa.CivilstatusType,'*')

THEN UPDATE SET db.CivilstatusNavn = dsa.CivilstatusNavn,

                db.CivilstatusType = dsa.CivilstatusType,

                db.CivilstatusOpdateret = @Today;

 

Alternativ:

WHEN MATCHED AND

(ISNULL(NULLIF(db.CivilstatusNavn, dsa.CivilstatusNavn),

        NULLIF(dsa.CivilstatusNavn, db.CivilstatusNavn)) IS NULL

 

Optimizing MERGE Statement Performance with OPTION (LOOP JOIN) må ikke bruges sammen med String_Agg, fordi det ender i en freezing kørsel som ikke bliver færdig. Alt i alt må brug af query hint testes ekstra gange og kun anvendes, hvor det er nødvendigt for at en SQL sætning kan køres eller med ekstra hurtig kørselstid.

 

Med flere millioner rækker kan Merge fylde transactionsloggen (Merge is filling up the transaction log). Der er heller ikke nogen order by og dette link skriver om fejl: Use Caution with SQL Server’s MERGE Statement    Merge issues

 

Merge hvor en række ikke findes mere og bliver markeret som slettet og med et overblik af antallet af ændrede rækker. Kimball type 1 dimension

 

CREATE TABLE [dim].[ClientState](

[ClientState_key] [int] IDENTITY(1,1) NOT NULL,

[ClientState_bkey] [varchar](100) NOT NULL,

[ClientStateName] [varchar](100) NOT NULL,

[ClientStateDescription] [varchar](255) NOT NULL,

[Comparison_bkey_meta] [binary](32) NOT NULL,

[Comparison_data_meta] [binary](32) NOT NULL,

[IsDeleted_meta] [bit] NOT NULL CONSTRAINT [DF_dim_ClientState_IsDeleted_meta]  DEFAULT ((0)),

[IsInferred_meta] [bit] NOT NULL CONSTRAINT [DF_dim_ClientState_IsInferred_meta]  DEFAULT ((0)),

[InsertTime_meta] [datetime2](3) NOT NULL CONSTRAINT [DF_dim_ClientState_InsertTime_meta]

   DEFAULT (sysdatetime()),

[UpdateTime_meta] [datetime2](3) NOT NULL CONSTRAINT [DF_dim_ClientState_UpdateTime_meta] 

  DEFAULT (sysdatetime()),

[RecordSource_meta] [varchar](100) NOT NULL,

 CONSTRAINT [PK_dim_ClientState] PRIMARY KEY CLUSTERED

(

 [ClientStateKey] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY]

GO

CREATE UNIQUE NONCLUSTERED INDEX [IX_dim_ClientState_ClientState_bkey] ON [dim].[ClientState]

(

 [ClientState_bkey] ASC -- unique key (UK).

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

GO

CREATE UNIQUE NONCLUSTERED INDEX [IX_dim_ClientState_Comparison_bkey_meta] ON [dim].[ClientState]

(

 [Comparison_bkey_meta] ASC -- unique key (UK).

)

INCLUDE ([Comparison_data_meta], [IsDeleted_meta])

WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

GO

 

DECLARE @Inserted int = 0, @Updated int = 0, @Deleted int = 0;

DECLARE @NumberOfChanges TABLE(Change nvarchar(10));

 

MERGE dim.ClientState AS tgt

USING --USING staging.ClientState AS src

 (SELECT TOP 99.99 PERCENT *

  FROM staging.ClientState

  ORDER BY ClientState_bkey) src

  ON tgt.Comparison_bkey_meta = src.Comparison_bkey_meta

WHEN NOT MATCHED BY TARGET THEN

  INSERT (ClientState_bkey, ClientStateName, ClientStateDescription,

          Comparison_bkey_meta, Comparison_data_meta, IsDeleted_meta,IsInferred_meta)

  VALUES (src.ClientState_bkey, src.ClientStateName, src.ClientStateDescription,

          src.Comparison_bkey_meta, src.Comparison_data_meta, 0, 0)

WHEN NOT MATCHED BY SOURCE AND tgt.IsDeleted_meta = 0 AND tgt.IsInferred_meta =0 THEN

  UPDATE SET tgt.IsDeleted_meta = 1,

             tgt.UpdateTime_meta = sysdatetime()

WHEN MATCHED AND (tgt.Comparison_data_meta <> src.Comparison_data_meta OR

                  tgt.IsDeleted_meta = 1) THEN --or is deleted before and now is back

  UPDATE SET tgt.ClientStateName = src.ClientStateName,

             tgt.ClientStateDescription = src.ClientStateDescription

             tgt.IsDeleted_meta = 0,

             tgt.Comparison_data_meta = src.Comparison_data_meta,

             tgt.UpdateTime_meta = sysdatetime();

 

SELECT @Inserted = COUNT(*) FROM @NumberOfChanges WHERE Change = 'INSERT';

SELECT @Updated  = COUNT(*) FROM @NumberOfChanges WHERE Change = 'UPDATE';

SELECT @Deleted  = COUNT(*) FROM @NumberOfChanges WHERE Change = 'DELETE';

 

Incremental update med nye data repræsenteret i ny AnsættelsesMånedDato. I tilfælde af omkørsel af ETL, indledes der med en sletning af tidligere leveret data med samme AnsættelsesMånedDato som findes i NEW tabellen inden indsættelse i DB tabellen. Men der sker ikke nogen tømning af DB tabellen, for så mistes alle data der hidtidig er leveret fra kilesystemet.

DELETE

FROM  dbo.EDW_DB_MedarbejderAnsættelse WITH(TABLOCK)

WHERE AnsættelsesMånedDato IN (SELECT DISTINCT AnsættelsesMånedDato

               FROM dbo.EDW_NEW_MedarbejderAnsættelse WITH (NOLOCK))

 

INSERT INTO dbo.EDW_DB_MedarbejderAnsættelse WITH (TABLOCK)

SELECT *

FROM dbo.EDW_NEW_MedarbejderAnsættelse WITH (NOLOCK)

 

Læs mere

 

Nedenstående stored procedure anvender Merge til enten at indsætte ny værdi eller slette eksisterende værdi baseret på en operation indikator bit 1 eller 0.

 

Indsæt nyt telefonnummer til bestemt kunde hvis nummeret ikke findes i forvejen:

EXEC dbo.Kunde_Telefonnummer_New_Delete 124, 88001022, 1

 

Slet eksisterende telefonnummer til bestemt kunde

EXEC dbo.Kunde_Telefonnummer_New_Delete 124, 88001021, 0

 

CREATE PROCEDURE dbo.Kunde_Telefonnummer_New_Delete

  @KundeId int, @Telefonnummer as bigint,

  @Operation bit -- 1 = Indsæt, 0 = Slet.

AS

BEGIN

 SET NOCOUNT ON;

 MERGE dbo.KUNDE_TELEFONNUMMER AS t

 USING (VALUES(@KundeId, @Telefonnummer)) s(KundeId, Telefonnummer)

   ON s.KundeId = t.KundeId AND s.Telefonnummer = t.Telefonnummer

 WHEN MATCHED AND @Operation = 0 THEN

   DELETE

 WHEN NOT MATCHED BY TARGET AND @Operation = 1 THEN

   INSERT(KundeId, Telefonnummer)

   VALUES(@KundeId, @Telefonnummer);

END

GO

 

(VALUES(@KundeId, @Telefonnummer)) er en Table Value Constructor der danner source table til Merge.

Findes kundens telefonnummer allerede i tabellen og operator er 1, så er de to When betingelser falske og der sker hverken sletning eller indsættelse.

 

Primærnøglen i tabellen består af KundeId og TelefonId hvor TelefonId er et fort­lø­ben­de nummer inden for samme KundeId, eksempelvis:

 

KundeId

TelefonId

Telefonnummer

123

1

31000000

123

2

31000123

123

3

31001000

124

1

88001021

124

2

88001022

 

TelefonId styres af trigger på tabellen der tilsidesætter den normale indsættelse:

CREATE TRIGGER [dbo].[KUNDE_TELEFONNUMMER_Trigger_InsteadOfInsert]

               ON [dbo].[KUNDE_TELEFONNUMMER]

INSTEAD OF INSERT

AS

BEGIN

  SET NOCOUNT ON

  INSERT INTO dbo.KUNDE_TELEFONNUMMER

    (KundeId, Telefonnummer, TelefonId)

  SELECT KundeId, Telefonnummer,

         TelefonId = (SELECT ISNULL(MAX(TelefonId),0) + 1

                      FROM dbo.KUNDE_TELEFONNUMMER

                      WHERE KundeId = INSERTED.KundeId)

  FROM INSERTED

END

GO

 

Merge kræver at der også er en trigger for DELETE operationen:

CREATE TRIGGER [dbo].[KUNDE_TELEFONNUMMER_Trigger_InsteadOfDelete]

               ON [dbo].[KUNDE_TELEFONNUMMER]

INSTEAD OF DELETE

AS

BEGIN

  SET NOCOUNT ON

  DELETE t

  FROM dbo.KUNDE_TELEFONNUMMER t

       INNER JOIN DELETED d ON d.KundeId = t.KundeId AND

                               d.TelefonId = t.TelefonId -- PK

END

GO

 

Trigger for insert kan ikke håndtere bulk insert af flere rækker samtidig fordi de alle får samme TelefonId nummer og så bliver der primærnøgle brud, a la:

INSERT INTO dbo.KUNDE_TELEFONNUMMER (KundeId, Telefonnummer)

VALUES (125,50100000), (125, 50100001), (125, 50100002)

Eller:

INSERT INTO dbo.KUNDE_TELEFONNUMMER (KundeId, Telefonnummer)

SELECT KundeId, Telefonnummer

FROM dbo.IMPORT_TELEFONNUMMER

 

Men det kan denne trigger fordi den først udregner de nye TelefonId numre:

CREATE TRIGGER [dbo].[KUNDE_TELEFONNUMMER_Trigger_InsteadOfInsert]

               ON [dbo].[KUNDE_TELEFONNUMMER]

INSTEAD OF INSERT

AS

BEGIN

  SET NOCOUNT ON

  INSERT INTO dbo.KUNDE_TELEFONNUMMER

    (KundeId, Telefonnummer, TelefonId)

  SELECT i.KundeId, i.Telefonnummer,

         TelefonId = ISNULL(t.TelefonId,0) + ROW_NUMBER()

         OVER (PARTITION BY i.KundeId ORDER BY i.Telefonnummer)

  FROM INSERTED i

       OUTER APPLY

       (SELECT t.KundeId, TelefonId = MAX(t.TelefonId)

        FROM dbo.KUNDE_TELEFONNUMMER t

        WHERE t.KundeId = i.KundeId

        GROUP BY t.KundeId

       ) AS t

END

GO

 

Når en række er blevet opdateret, angiv et tidspunkt (mark a row)

Kolonnen UpdateTime får et nyt tidspunkt, timestamp, når en anden kolonne bliver opdateret. Den har allerede en default værdi:

[UpdateTime] [datetime2](0) NOT NULL

CONSTRAINT [DF_Department_UpdateTime] DEFAULT (sysdatetime())

Det løses med en trigger after update.

CREATE TRIGGER [dbo].[trgAfterUpdate] ON [dbo].[Department]

AFTER UPDATE

AS

  UPDATE t

  SET UpdateTime = sysdatetime()

  FROM [dbo].[Department] t

       INNER JOIN inserted i ON i.DepartmentId = t.DepartmentId

GO

 

Undgå at slette en række. To prevent deletion of the records in a table

CREATE TRIGGER [dbo].[Customer_Delete]

ON [dbo].[Customer] INSTEAD OF DELETE AS

DELETE [dbo].[Customer] WHERE 1=2;

 

Trigger til at lave en Change Data Capture CDC log på en Kunde tabel

Implementeret som after-insert/update/delete triggers på Customer tabel og gem ændringer i data i en CustomerLog tabel med metadata kolonner. Jeg har her indskrænket opdateringsdata til kun at logge når feltet CustomerType ændres.

 

CREATE TRIGGER [dbo].[Customer_Delete]

ON [dbo].[Customer] AFTER DELETE

AS

SET NOCOUNT ON

INSERT INTO dbo.CustomerLog

(

LogEntryAction_metadata,

LogEntryDateTime_metadata,

LogEntryUser_metadata,

LogEntryHost_metadata,

CustomerId,

CustomerName,

CustomerType

)

SELECT

'Deleted',

SYSDATETIME(),

STUFF(SUSER_SNAME(), 1, charindex('\', SUSER_SNAME()), ''),

HOST_NAME(),

CustomerId,

CustomerName,

CustomerType

FROM [Deleted]

GO

 

CREATE TRIGGER [dbo].[Customer_Insert]

ON [dbo].[Customer] AFTER INSERT

AS

SET NOCOUNT ON

INSERT INTO dbo.CustomerLog

(

LogEntryAction_metadata,

LogEntryDateTime_metadata,

LogEntryUser_metadata,

LogEntryHost_metadata,

CustomerId,

CustomerName,

CustomerType

)

SELECT

'Inserted',

SYSDATETIME(),

STUFF(SUSER_SNAME(), 1, charindex('\', SUSER_SNAME()), ''),

HOST_NAME(),

CustomerId,

CustomerName,

CustomerType

FROM [Inserted]

GO

 

CREATE TRIGGER [dbo].[Customer_Update]

ON [dbo].[Customer] AFTER UPDATE

AS

SET NOCOUNT ON

IF UPDATE (CustomerType) -- only when CustomerType is updated by a user

BEGIN

INSERT INTO dbo.CustomerLog

(

LogEntryAction_metadata,

LogEntryDateTime_metadata,

LogEntryUser_metadata,

LogEntryHost_metadata,

CustomerId,

CustomerName,

CustomerType,

[CustomerTypePrevious]

)

SELECT

'Updated',

SYSDATETIME(),

STUFF(SUSER_SNAME(), 1, charindex('\', SUSER_SNAME()), ''),

HOST_NAME(),

i.CustomerId,

i.CustomerName,

i.CustomerType,

d.CustomerType

FROM [Inserted] i INNER JOIN [Deleted] d ON i.CustomerId = d.CustomerId AND i.CustomerType <> d.CustomerType

-- Deleted table contains old values and Inserted contains updated values.

END

GO

 

Når Customer tabellen udvides med nye kolonner eller ændring af data type på eksisterende kolonner, skal tilsvarende gøres i tabellen CustomerLog og de tre triggers skal ligeså udvides. Lad det ske samtidig i en idriftsættelse (deployment).

 

MySql trick

Not exists til subquery giver problem.

Virker med insert:

INSERT INTO DimKunde(KundeKode, KundeNavn)

SELECT c.uid, c.name

FROM source.Customer c

WHERE NOT EXISTS(SELECT 1 FROM DimKunde k WHERE k.Kundekode = c.uid);

 

Virker ikke med update:

UPDATE DataSumBeløb Cur

SET   Cur.Beløb = Cur.HensatBeløb

WHERE NOT EXISTS(SELECT 1

                 FROM DataSumBeløb Pre

                 WHERE Pre.Månedsdato = Cur.ForrigeMånedsdato AND

                       Pre.Kode = Cur.Kode);

 

Giver fejlmelding: ERROR 1093 [HY000]: You can't specify target table 'Table' for update in FROM clause. I Mysql kan man ikke opdatere en tabel når samme tabel indgår i en subquery, men så kan tabellen i subquery erstattes af en select, der returnerer et dataset og så kan tabellen blive opdateret, man siger på engelsk, at MySql materializes subqueries in the FROM clause (»derived tables«):

WHERE NOT EXISTS(SELECT 1

                 FROM (SELECT * FROM DataSumBeløb) Pre

 

En alternativ løsning er som er meget hurtig når der er index på join felterne:

UPDATE DataSumBeløb Cur

LEFT OUTER JOIN DataSumBeløb Pre

      ON Pre.Månedsdato = Cur.ForrigeMånedsdato AND

         Pre.Kode = Cur.Kode

SET   Cur.Beløb = Cur.HensatBeløb

WHERE Pre.Månedsdato IS NULL;

 

Endnu et par mysql 5.7 eksempler, der viser ValidTo og opdaterer ValidTo:

CREATE VIEW DimCustomer_ValidTo AS

SELECT Customer_key, CustomerName, ValidFrom,

  (SELECT IFNULL(MIN(b.ValidFrom), '9999-12-31 00:00:00') AS ValidFrom

   FROM DimCustomer b

   WHERE b.Business_key = a.Business_key AND b.Customer_key > a.Customer_key

  ) AS ValidTo

FROM DimCustomer a

 

UPDATE DimCustomer a

SET a.ValidTo =

  (SELECT IFNULL(MIN(b.ValidFrom), '9999-12-31 00:00:00') AS ValidFrom

   FROM (SELECT * FROM DimCustomer) b

   WHERE b.Business_key  = a.Business_key AND b.Customer_key > a.Customer_key

  )

 

Et nummer pr. række

Med ROW_NUMBER og en sorteringsorden får man et fortløbende rækkenummer pr. række i udtrækket med SELECT eksempelvis fra AdventureWorks hvor der fås en liste i faldende orden af salget år-til-dato med største salg som rækkenr 1:

SELECT ROW_NUMBER() OVER(ORDER BY SalesYTD DESC) AS Row,

       FirstName, LastName, ROUND(SalesYTD,2,1) AS [Sales YTD]

FROM Sales.vSalesPerson

WHERE SalesYTD <> 0

 

Row

FirstName

LastName

Sales YTD

1

Linda

Mitchell

4251368,54

2

Jae

Pak

4116871,22

3

Michael

Blythe

3763178,17

4

Jillian

Carson

3189418,36

 

Med brug af Partition (slags Group By opdeling) får man opdelt rækkenumrene, så de kan starte forfra igen med nummer 1 som her pr. år dannet via Year():

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

       SalesQuota AS CurrentQuota,

       ROW_NUMBER() OVER (PARTITION BY YEAR(QuotaDate)

                          ORDER BY SalesQuota DESC) AS Row

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

Row

279

2011

917000

1

277

2011

872000

2

277

2011

846000

3

276

2011

781000

4

289

2012

1898000

1

277

2012

1600000

2

275

2012

1429000

3

276

2012

1355000

4

275

2013

1575000

1

276

2013

1525000

2

289

2013

1506000

3

289

2013

1419000

4

 

En rangering betyder, at ens data (her samme SalesQuota) får samme nummer:

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

       SalesQuota AS CurrentQuota,

       RANK() OVER (PARTITION BY YEAR(QuotaDate)

                    ORDER BY SalesQuota DESC) AS Row

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

Row

289

2014

1329000

1

276

2014

1124000

2

277

2014

947000

3

290

2014

908000

4

290

2014

908000

4

290

2014

908000

4

275

2014

869000

7

282

2014

830000

8

 

Rangering uden at næste nummer springer som ovenfor fra 4 til 7:

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

       SalesQuota AS CurrentQuota,

       DENSE_RANK() OVER (PARTITION BY YEAR(QuotaDate)

                          ORDER BY SalesQuota DESC) AS Row

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

Row

289

2014

1329000

1

276

2014

1124000

2

277

2014

947000

3

290

2014

908000

4

290

2014

908000

4

290

2014

908000

4

275

2014

869000

5

282

2014

830000

6

 

Inddeling i grupper med hvert sit nummer, her ønskes 8 grupper pr. år:

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

       SalesQuota AS CurrentQuota,

       NTILE(8) OVER (PARTITION BY YEAR(QuotaDate)

                      ORDER BY SalesQuota DESC) AS Row

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

Row

289

2014

1329000

1

276

2014

1124000

1

277

2014

947000

1

290

2014

908000

2

290

2014

908000

2

290

2014

908000

2

275

2014

869000

3

282

2014

830000

3

281

2014

724000

3

279

2014

686000

4

288

2014

627000

4

284

2014

497000

5

286

2014

421000

5

283

2014

403000

6

280

2014

390000

6

278

2014

390000

7

274

2014

187000

7

285

2014

26000

8

287

2014

1000

8

 

Forrige rækkes værdi

Med LAG() funktion får man forrige række værdi uden brug af self-join:

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

SalesQuota AS CurrentQuota,

LAG(SalesQuota,1,0) OVER (ORDER BY YEAR(QuotaDate)) AS PreviousQuota

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

PreviousQuota

274

2011

28000

0

274

2011

7000

28000

274

2011

91000

7000

275

2011

367000

91000

 

Næste rækkes værdi

Med LEAD() funktion får man næste række værdi uden brug af self-join:

SELECT BusinessEntityID, YEAR(QuotaDate) AS SalesYear,

SalesQuota AS CurrentQuota,

LEAD(SalesQuota,1,0) OVER (ORDER BY YEAR(QuotaDate)) AS NextQuota

FROM Sales.SalesPersonQuotaHistory

 

BusinessEntityID

SalesYear

CurrentQuota

NextQuota

274

2011

28000

7000

274

2011

7000

91000

274

2011

91000

367000

275

2011

367000

556000

 

I nedenstående tabel indsættes en ny række hver gang en sats ændrer beløb:

 

Sats

Dato

Beløb

A

07-02-2008

125

A

31-07-2010

135

A

01-01-2011

150

B

01-01-2008

80

B

01-01-2009

85

 

Men hvordan findes beløbet for sats A den 10-10-2010:

SELECT s.Sats, s.Beløb

FROM dbo.SATS s

     INNER JOIN

     (

      SELECT Sats, Dato = MAX(Dato)

      FROM dbo.SATS

      WHERE Sats = 'A' AND Dato <= '20101010'

      GROUP BY Sats

     ) t ON t.Sats = s.Sats AND t.Dato = s.Dato

 

Men skal der laves mange opslag, så kan det betale sig at have en ekstra udgave af tabellen med FraDato og TilDato interval perioder, som LEAD kan hjælpe med, fordi den går til næste række (1) og fra den rækkes dato fratrækkes 1 dag (-1) og findes næste række ikke anvendes en default dato som 9999-12-31:

SELECT Sats, FraDato = Dato,

             TilDato = LEAD(DATEADD(DAY, -1, Dato), 1, '9999-12-31')

                       OVER(PARTITION BY Sats ORDER BY Dato),

       Beløb

FROM dbo.SATS

 

Sats

FraDato

TilDato

Beløb

A

07-02-2008

30-07-2010

125

A

31-07-2010

31-12-2010

135

A

01-01-2011

31-12-9999

150

B

01-01-2008

31-12-2008

80

B

01-01-2009

31-12-9999

85

 

Og så kan der udføres en Where betingelse:

FraDato <= '2010-10-10' AND TilDato >= '2010-10-10'   eller

'2010-10-10' >= FraDato AND '2010-10-10' <= TilDato   eller

'2010-10-10' BETWEEN FraDato AND TilDato

 

Overskuelig sql kode med brug af tabel variabel

I en sql sætning ser man tit CASE WHEN der forestår oversættelser/mapninger:

SELECT Bynr,

       CASE

         WHEN Bynr = 1 THEN 'København'

         WHEN Bynr = 2 THEN 'Aarhus'

         WHEN Bynr = 3 THEN 'Odense'

         ELSE 'Øvrige byer'

       END AS Bynavn

FROM Byliste

 

Oversættelserne kan i stedet placeres i en tabel variabel som der joines til, når man er i en stored procedure:

DECLARE @bynavn TABLE (Bynr int, Bynavn varchar(50))

INSERT INTO @bynavn

VALUES (1,'København'),(2,'Aarhus'),(3,'Odense')

 

SELECT b.Bynr, ISNULL(bn.Bynavn,'Øvrige byer') AS Bynavn

FROM Bylist b LEFT OUTER JOIN @bynavn bn ON bn.Bynr = b.Bynr

 

Tilsvarende kan et kriterie: WHERE Bynr IN (2,3,8) laves om til en tabel variabel:

DECLARE @bynr TABLE (Bynr int)

INSERT INTO @bynr

VALUES (2),(3),(8)

 

SELECT Bynavn

FROM Bylist

WHERE Bynr IN (SELECT Bynr FROM @bynr)

 

Udsøgning og undgå dubletter

Udsøgning af historikdata baseret på en regel og brug af ROW_NUMBER til at fjer­ne dubletter. Hvis samme kombination af KundeId + ProduktId + Oprettelsesdato (inkl. klokkeslæt) + Ændringsdato (uden klokkeslæt) findes flere gange, så udsøges denne kombination:

KundeId + ProduktId + Oprettelsesdato (inkl. klokkeslæt) + Ændringsdato (seneste klokkeslæt).

SELECT DISTINCT

  o.KundeId,

  o.ProduktId,

  o.Oprettelsesdato,

  o.Ændringsdato

FROM

 (

  SELECT his.*,

     ROW_NUMBER() OVER (PARTITION BY his.KundeId, his.ProduktId,

     his.Oprettelsesdato, CAST(his.Ændringsdato AS date)

     ORDER BY his.Ændringsdato DESC) AS RowNumber

  FROM dbo.KundeOrdre his

 ) o

WHERE o.RowNumber = 1

 

Sletning af dubletter:

;WITH dublet AS

(

SELECT

     ROW_NUMBER() OVER (PARTITION BY his.KundeId, his.ProduktId,

     his.Oprettelsesdato, CAST(his.Ændringsdato AS date)

     ORDER BY his.Ændringsdato DESC) AS RowNumber

  FROM dbo.KundeOrdre

)

DELETE FROM dublet WHERE RowNumber > 1

 

Eller i små bidder:

DECLARE @Antal int

DECLARE @Fornavn nvarchar(50)

DECLARE @Efternavn nvarchar(50)

DECLARE @Scan CURSOR FAST_FORWARD FOR

  SELECT Fornavn, Efternavn, Count(*) – 1 AS Antal

  FROM Kunde

  GROUP BY Fornavn, Efternavn

  HAVING Count(*) > 1

 

OPEN @Scan

FETCH NEXT FROM @Scan INTO @Fornavn, @Efternavn, @Antal

WHILE @@FETCH_STATUS = 0

BEGIN

  DELETE TOP(@Antal)

  FROM Kunde

  WHERE Fornavn = @Fornavn AND Efternavn = @Efternavn

  FETCH NEXT FROM @Scan INTO @Fornavn, @Efternavn, @Antal

END

CLOSE @Scan

DEALLOCATE @Scan

 

Andet eksempel på sletning i små bidder (chunking the deletes further):

SET NOCOUNT ON;

DECLARE @r INT;

SET @r = 1;

WHILE @r > 0

BEGIN

    BEGIN TRANSACTION;

      DELETE TOP (100000)

      FROM dbo.SalesOrderDetail

      WHERE ProductID IN (712, 870, 873);

 

      SET @r = @@ROWCOUNT;

    COMMIT TRANSACTION;

END

 

DECLARE @done bit = 0;

WHILE (@done = 0)

BEGIN

    DELETE TOP(1000)

    FROM LogMessages WHERE LogDate < '20020102';

    IF @@rowcount < 1000 SET @done = 1;

END;

 

Read more.

 

I hvor mange rækker optræder dublet

SELECT his.*,

   COUNT(*) OVER (PARTITION BY his.KundeId, his.ProduktId,

   his.Oprettelsesdato, CAST(his.Ændringsdato AS date)

   ORDER BY his.Ændringsdato DESC) AS Antal

FROM dbo.KundeOrdre his

 

Mere udsøgning

 

Person

Afdeling

Sagsnr

Sagstrin

Behandlingsdato

Status

22

Samtale

2017003

1

17-01-2017

Afsluttet

22

Tests

2017003

2

17-01-2017

Afsluttet

22

Analyse

2017003

1

03-02-2017

Løbende

22

Røntgen

2017003

2

03-02-2017

Afsluttet

22

Undersøgelse

2017003

3

03-02-2017

Løbende

30

Samtale

2017010

1

01-03-2017

Afsluttet

32

Samtale

2017011

1

01-03-2017

Løbende

32

Røntgen

2017011

2

01-03-2017

Afsluttet

 

Jeg ønsker for hvert sagsnr at få seneste behandlingsdato og dens seneste sagstrin og de andre tilhørende oplysninger:

 

Person

Afdeling

Sagsnr

Sagstrin

Behandlingsdato

Status

22

Undersøgelse

2017003

3

03-02-2017

Løbende

30

Samtale

2017010

1

01-03-2017

Afsluttet

32

Røntgen

2017011

2

01-03-2017

Afsluttet

 

Den klassiske metode med indlejret sql sætninger og self-joins:

SELECT s1.*

FROM Sagsbehandling s1

     INNER JOIN

     (

      SELECT s2.Sagsnr, s2.Behandlingsdato, MAX(s2.Sagstrin) AS Sagstrin

      FROM Sagsbehandling s2

           INNER JOIN

          (

            SELECT s3.Sagsnr, MAX(s3.Behandlingsdato) AS Behandlingsdato

            FROM Sagsbehandling s3

            GROUP BY s3.Person, s3.Sagsnr

           ) m2 ON m2.Sagsnr = s2.Sagsnr AND m2.Behandlingsdato = s2.Behandlingsdato

      GROUP BY s2.Sagsnr, s2.Behandlingsdato

     ) m1 ON m1.Sagsnr = s1.Sagsnr AND m1.Behandlingsdato = s1.Behandlingsdato AND

             m1.Sagstrin = s1.Sagstrin

 

Med ROW_NUMBER og en faldende sorteringsorden får man et fortløbende rækkenummer pr. række i udtrækket med SELECT og udtager den første række 1:

SELECT s.*

FROM (

      SELECT *,

           ROW_NUMBER() OVER (PARTITION BY Sagsnr

           ORDER BY Behandlingsdato DESC, Sagstrin DESC) AS RowNumber

      FROM Sagsbehandling

      ) s

WHERE s.RowNumber = 1

 

Streng gymnastik

Et Navn angivet som "Efternavn, Fornavn" ændres til "Fornavn Efternavn":

SELECT CASE WHEN Navn = '-' OR Navn = '- -' OR Navn = ',' THEN '-'

            WHEN CHARINDEX(',', Navn) + 1 > 1

            THEN CAST(LTRIM(SUBSTRING(Navn, CHARINDEX(',', Navn) + 1,

        LEN(Navn))) + ' ' + SUBSTRING(Navn,1,CHARINDEX(',', Navn) - 1)

        AS nvarchar(100))

            ELSE Navn

       END AS Navn,

FROM dbo.PERSON

CAST til nvarchar(100) er vigtig for at sikre at den samlede string er på 100 tegn.

 

Fjernelse af ekstra 0 i en adresse, så

Plantevej 001, 03 tv bliver til Plantevej 1, 3 tv

Plantevej 031, 03 tv bliver til Plantevej 31, 3 tv

SELECT REPLACE(REPLACE(REPLACE([Adresse], ' 0', ' '), ' 0', ' '),

                                          ' 0', ' ') AS Adresse

FROM dbo.PERSON

 

En Litra værdi består af en række bogstaver og nogen gange efterfulgt at et tal eksempelvis a, abc, a1, abc18, hvor talværdien kaldes et løbenummer. Litraens bogstaver og talværdi ønskes skilt ad så der kan bestemmes det største løbe­num­mer inden for en litra eksempelvis for tre litraer: a1, a2, a3 bliver det største løbe­num­mer 3 inden for litra a:

CREATE VIEW [dbo].[Tilmelding_Løbenummer]

AS

WITH SkovpartAfdelingLitraLøbenummer (Skovpart, Afdeling, Litra, Løbenummer)

AS

(

SELECT Skovpart, Afdeling, Litra,

   Løbenummer = CAST(ISNULL(NULLIF(RIGHT([Litra]+' ',PATINDEX('%[^0-9]%',

   REVERSE([Litra]))),''),0) AS int) -– eksempel: a1 = 1, abc18 = 18, a = 0.

FROM dbo.OPGAVE WITH(NOLOCK)

),

SkovpartAfdelingLitraLøbenummer_LitraUdenLøbenummer

  (Skovpart, Afdeling, Litra, Løbenummer, LitraUdenLøbenummer)

AS

(

SELECT Skovpart, Afdeling, Litra, Løbenummer,

   CASE WHEN Løbenummer >= 1

        THEN LEFT(Litra, LEN(Litra)-LEN(Løbenummer))

        ELSE Litra

   END AS LitraUdenLøbenummer   -- eksempel: a1 = a, abc18 = abc, a = a

FROM SkovpartAfdelingLitraLøbenummer

)

SELECT Skovpart, Afdeling, LitraUdenLøbenummer, MAX(Løbenummer) AS Løbenummer

FROM SkovpartAfdelingLitraLøbenummer_LitraUdenLøbenummer

GROUP BY Skovpart, Afdeling, LitraUdenLøbenummer

 

Kriteriet: WHERE LEFT(TransactionIdentification,17) = @ReceiptNo

er langsom fordi funktionen LEFT kaldes for hver række i tabellen.

WHERE TransactionIdentification LIKE @ReceiptNo + '%'

er hurtig. Men er indholdet af @ReceiptNo ikke 17 cifre men kun to cifre '26%' fås der rigtig mange rækker retur, derfor bedst at kombinere dem for at undgå et slags sql injection agtig problem:

 

WHERE LEFT(TransactionIdentification,17) = @ReceiptNo

           AND TransactionIdentification LIKE @ReceiptNo + '%'

 

Left funktionen bliver kun kaldt af query planen, når Like giver et sandt resultat, derved begrænses kaldet af Left funktionen til de rækker sql sætningen returnerer.

 

Udtage en bestemt værdi mellem to -> og ikke medtage | tegnet:

 

DECLARE @s as varchar(100) =

'2018-03-06 20:45:00 : |Paris Saint-Germain| |v| |Real Madrid| -> |Match Winner| -> |Paris Saint-Germain|'

SELECT REPLACE(SUBSTRING(@s, CHARINDEX('->', @s)+3, CHARINDEX('->', @s, CHARINDEX('->', @s)+1)-CHARINDEX('->', @s)-4),'|','')

 

Funktion som udtager første bogstav per ord i en streng, d.v.s. laver en forkortelse

 

CREATE FUNCTION [dbo].[FørsteBogstavOgHeleTal] (@streng varchar(8000))

RETURNS varchar(8000)

AS

BEGIN

DECLARE @anvendTegn varchar(100) = 'A-Å0-9' -- de tegn der skal håndteres

DECLARE @resultat varchar(8000) = ''

DECLARE @i int = 0

-- Når @streng indeholder cifre indsættes et mellemrum foran hvert ciffer.

SET @i = 48 -- hvor ciffer 0 starter fra.

WHILE @i <= 57 -- til og med 9.

BEGIN

  SET @streng = REPLACE(@streng COLLATE Danish_Norwegian_CS_AS, CHAR(@i),

                CONCAT(' ',CHAR(@i))) 

  SET @i += 1

END

-- Når @streng anvender CamelCase indsættes et mellemrum foran hvert store bogstav.

SET @i = 65 -- hvor bogstav A starter fra.

WHILE @i <= 90 -- til og med Z.

BEGIN

  SET @streng = REPLACE(@streng COLLATE Danish_Norwegian_CS_AS, CHAR(@i),

                CONCAT(' ',CHAR(@i)))

  SET @i += 1

END

-- Håndtering af Æ Ø Å bogstaverne, så de også får et mellemrum foran sig.

SET @i = 198 -- Æ ASCII ('Æ') giver 198

SET @streng = REPLACE(@streng COLLATE Danish_Norwegian_CS_AS, CHAR(@i),

              CONCAT(' ',CHAR(@i)))

SET @i = 216 -- Ø

SET @streng = REPLACE(@streng COLLATE Danish_Norwegian_CS_AS, CHAR(@i),

              CONCAT(' ',CHAR(@i)))

SET @i = 197 -- Å

SET @streng = REPLACE(@streng COLLATE Danish_Norwegian_CS_AS, CHAR(@i),

              CONCAT(' ',CHAR(@i)))

-- Lave slut resultatet hvor bogstaver efter et mellemrum udtages og hele tallet.

SET @i = 0

WHILE @i IS NOT NULL

BEGIN

   SET @resultat += ISNULL(SUBSTRING(@streng,@i+1,1),'')

   SET @i += NULLIF(PATINDEX('%[^('+@anvendTegn+')]['+@anvendTegn+']%',

             SUBSTRING(@streng,@i+1,8000)),0)

END

RETURN RTRIM(LTRIM(UPPER(@resultat)))

/*

SELECT dbo.FørsteBogstavOgHeleTal(null);

SELECT dbo.FørsteBogstavOgHeleTal('');

SELECT dbo.FørsteBogstavOgHeleTal('J');

SELECT dbo.FørsteBogstavOgHeleTal('Joakim');

SELECT dbo.FørsteBogstavOgHeleTal('Joakim Dalby');

SELECT dbo.FørsteBogstavOgHeleTal('Joakim von and Dalby');

SELECT dbo.FørsteBogstavOgHeleTal('JoakimVonAndDalby');

SELECT dbo.FørsteBogstavOgHeleTal('   Ingen    ko på isen 1966 æ    ø  å  ');

SELECT dbo.FørsteBogstavOgHeleTal('   Ingen    ko på isen 1 9 6 6 æ    ø  å  ');

*/

END

GO

 

COLLATE Danish_Norwegian_CS_AS kan også være COLLATE DATABASE_DEFAULT hvis SQL Server instansen er opsat til at være Danish_Norwegian_CS_AS.

 

White space trailing space tegn

Mystiske tegn kan komme i slutningen af tekst strenge og gemme sig i slutningen af et varchar eller nvarchar tekst felt eksempelvis et Adresse felt med tegn som: CHAR(0), CHAR(254), Horizontal Tab CHAR(9), Line Feed CHAR(10), Vertical Tab CHAR(11), Form Feed CHAR(12), Carriage Return CHAR(13), Column Break CHAR(14), Undo Ctrl+Z CHAR(26), Escape CHAR(27) eller Non-breaking space CHAR(160). Carriage Return (\r) and Line Feed (\n): Char(13)+Char(10).

 

Alt+0160 at numeric keyboard giver et Non-breaking space (No-Break Space) som kan erstattes til en tom streng: Replace(<StringColumn>,CHAR(160),'').

 

De mystiske tegn tæller ikke med Len funktionen på et felt men er med i DATALENGTH funktionen der tæller antal byte og kan ses med Cast til varbinary(max) og data vaskes væk med Substring, eksempelvis:

 

0x4D0065007200630075007200760065006A002000200020002000

    M      e       r       c       u       r       v       e       j        White spaces starter

 

4D i hex = 77 i dec = M i ascii tabellen.

 

TRIM([Adresse]) fjerner white space i nye sql server versioner ligesom C# Trim. Tidligere i Sql kunne det gøres:

SELECT CAST(Adresse AS varbinary(max)) AS Indhold,

       SUBSTRING(Adresse,1,LEN(Adresse)) AS Adresse_Renset

FROM dbo.Kunde

WHERE (LEN(Adresse) * 2) <> DATALENGTH(Adresse)

 

Trim til den tomme streng (empty string) og lav den om til null:

nullif(rtrim(ltrim(ADDRESS_LINE)),'') as ADDRESS_LINE

og brug det afledte felt efter dens data rens og data vask på normal måde f.eks.:

where ADDRESS_LINE is not null.

TRIM(REPLACE(Adresse, CHAR(0x00), ''))

WHERE column LIKE CONCAT("%", CHAR(0x00 using utf8), "%");

 

Brugerdefineret valideringskrav

Constraints eller valideringskrav for felter i en tabel i databasen, eksempelvis at når SlutDato feltet er udfyldt, så skal StartDato feltet også være udfyldt:

[SlutDato] IS NULL OR ([SlutDato] IS NOT NULL AND [StartDato] IS NOT NULL)

Slutdatoen skal være større end eller lig med statdatoen:

[SlutDato] IS NULL OR ([SlutDato] IS NOT NULL AND [StartDato]<=[SlutDato])

 

Er email adressen gyldig eller ugyldig syntaks mæssigt

declare @email nvarchar(255) = 'hej.med@dig.dk'

select case when @email like '%_@_%.__%'

                 then cast(1 as bit)

                 else cast(0 as bit)

       end as ValidEmail

 

Cast er nødvendig for ellers er data typen int og går data gennem en SSIS pipeline til en destination tabel med en bit kolonne, så kan SSIS vise gul advarselstrekant om at truncate værdien. Det gælder især for en streng efter en concat m.fl.

 

Udtrække et c/o navn fra tre adresse felter med flere WHEN sætninger

I en af tre adresse felter kan der stå 'c/o Jens Jensen' og navnet ønskes ved at erstatte c/o med tom string og så efterfølgende trimme værdien:

select

case when left(adresse1,3) = 'c/o' then rtrim(ltrim(replace(adresse1,'c/o','')))

     when left(adresse2,3) = 'c/o' then rtrim(ltrim(replace(adresse2,'c/o','')))

     when left(adresse3,3) = 'c/o' then rtrim(ltrim(replace(adresse3,'c/o','')))

end as CoNavn

 

Når en when betingelse er opfyldt, så udføres then delen og derefter hoppes ud

(break) af case sætningen, d.v.s. de andre when betingelser springes over.

 

Cannot resolve collation conflict for equal to operation (Temporary Table)

Når tempdb og en normal database ikke anvender samme sprog collation, normalt i Danmark: Danish Norwegian CI AS og en stored procedure opretter en temporær # tabel og anvender tabellen sammen med en tabel i den normale database, så får man fejlmelding, hvis man glemmer at angive Danish_Norwegian_CI_AS ud for et tekst felt:

CREATE TABLE #TEMP (

  [KundeNavn] [nvarchar](50) COLLATE Danish_Norwegian_CI_AS NOT NULL ,

  [Projektnummer] [int] NOT NULL

) ON [PRIMARY]

UPDATE dbo.KUNDE

SET    Projektnummer = #TEMP.Projektnummer

FROM   dbo.KUNDE

       INNER JOIN #TEMP ON #TEMP.KundeNavn = dbo.KUNDE.KundeNavn

 

Normalt sættes der ikke dbo. foran # tabeller, og de behøver heller ikke at blive slet­tet før oprettelsen på denne måde men det kan være en god idé:

 

IF OBJECT_ID('tempdb..#TEMP') IS NOT NULL DROP TABLE #TEMP

 

I SQL Server 2016 brug DIE: DROP TABLE IF EXISTS #TEMP

 

SQL Server sætter en masse understreger og nummer efter #TEMP så den er unik per session d.v.s. to forespørgsler laver deres egne temp tabeller, ses således:

SELECT * FROM tempdb..sysobjects

 

Tabeller med hver sin collation

Når man ønsker at joine to tabeller sammen fra hver sin database med hver sin col­la­ti­on, kan man i ON delen tvinge en collation igennem således:

ON A.Navn = B.Navn COLLATE Danish_Norwegian_CI_AS

ON A.Navn = B.Navn COLLATE DATABASE_DEFAULT

 

COLLATE Danish_Norwegian_CS_AS kan også være COLLATE DATABASE_DEFAULT hvis SQL Server instansen er opsat til at være Danish_Norwegian_CS_AS.

 

Husk, at man godt kan anvende funktioner i en ON del af en INNER JOIN:

ON A.Navn = CAST(SUBSTRING(B.Navn, 5, 8) AS nvarchar(8)) COLLATE Danish_Norwegian_CI_AS

 

Anvend samme collation fra sql server default på begge felter fra to databaser:

ON IMP.dbo.KUNDE.Status COLLATE DATABASE_DEFAULT =

   ERP.dbo.KUNDE.Status COLLATE DATABASE_DEFAULT

 

Collations anvendes til sortering (ordering) og sammenligning (comparing) af strenge (strings), tekster. Tal er ikke omfattet en collation.

 

Datarække sortering via et clustered indeks som er non-unique

Et clustered indeks bestemmer lagringen af data rækkerne i en tabel, det kan være praktisk når en tabel har ikke unikke rækker og derfor ikke en primærnøgle, men for at sikre en sorteringsorden af rækkerne, får tabellen et clustered index som er non-unique:

create clustered index [CIX_Fil_Nummer_Navn] on [dbo].[Fil]

([Nummer] asc,

 [Navn] asc

)with (pad_index = off, statistics_norecompute = off, sort_in_tempdb = off, drop_existing = off, online = off, allow_row_locks = on, allow_page_locks = on) on [PRIMARY]

 

Reindeksering løste en gang et problem

DBCC DBREINDEX ('dbo.DIMENSIONDATA')

 

Harddisk plads forbrug af tempdb

SELECT [Source] = 'database_files',

    [TEMPDB_max_size_MB] = SUM(max_size) * 8 / 1027.0,

    [TEMPDB_current_size_MB] = SUM(size) * 8 / 1027.0,

    [FileCount] = COUNT(FILE_ID)

FROM tempdb.sys.database_files

WHERE type = 0

 

SELECT

 [FileSizeMB]    = convert(numeric(10,2),round(a.size/128.,2)),

 [UsedSpaceMB]   = convert(numeric(10,2),round(fileproperty( a.name,'SpaceUsed')/128.,2)) ,

 [UnusedSpaceMB] = convert(numeric(10,2),round((a.size-fileproperty( a.name,'SpaceUsed'))/128.,2)) ,

 [DBFileName] = a.name

FROM tempdb..sysfiles a

 

SELECT  *

FROM tempdb.sys.tables t

 

Flytning af tempdb

USE master

GO

ALTER DATABASE tempdb MODIFY FILE (NAME = tempdev,

      FILENAME = 'D:\SQLData\tempdb.mdf')

GO

ALTER DATABASE tempdb MODIFY FILE (NAME = templog,

      FILENAME = 'E:\SQLLogs\tempdb.ldf')

GO

 

Der er en række tiltag man bør tage for at optimere anvendelsen af tempdb af­hæn­gig af antal CPU:

http://msdn.microsoft.com/en-us/library/ms175527.aspx

http://searchsqlserver.techtarget.com/tip/0,289483,sid87_gci1307255,00.html

 

Tøm tempdb

DBCC FREEPROCCACHE

DBCC Shinkfile (tempdb)

DBCC UPDATEUSAGE (tempdb)

 

Oprettelse af schema

IF NOT EXISTS(

   SELECT  schema_name

   FROM    INFORMATION_SCHEMA.SCHEMATA

   WHERE   schema_name = 'MySchema')

BEGIN

  CREATE SCHEMA MySchema AUTHORIZATION dbo

END

 

Grouping eksempler

GROUPING SETS TO MAKE GRAND TOTAL

 

SELECT c.CustomerName, CONVERT(CHAR(7), l.Loan_date, 126) AS YM,

       SUM(l.Loan_amount) AS Loans

FROM Customers AS c

     INNER JOIN Loans AS l ON l.CustomerId = c.CustomerId

GROUP BY

GROUPING SETS

((c.CustomerName, CONVERT(CHAR(7), l.Loan_date, 126)),(c.CustomerName),(CONVERT(CHAR(7), l.Loan_date, 126)),())

 

Laver sum per customerId og en grand total:

SELECT COALESCHE(CAST(CustomerId AS varchar(10)), 'Total'),

       COALESCHE(CAST(YEAR(loan_date) AS varchar(10)), 'Subtotal'),

       SUM(Loan_amount)

FROM Loans

GROUP BY CustomerId, YEAR(loan_date)

WITH ROLLUP

 

Men ovenstående problem hvis der optræder NULL i data, derfor:

SELECT CASE WHEN GROUPING(CustomerId) = 1 THEN 'Total' ELSE CAST(CustomerId AS varchar(10)) END AS Customer

       CASE WHEN GROUPING(YEAR(loan_date)) = 1 THEN 'Subtotal' ELSE CAST(YEAR(loan_date) AS varchar(10)) END AS Loan_year

       SUM(Loan_amount) as total_loans

FROM Loans

GROUP BY CustomerId, YEAR(loan_date)

WITH ROLLUP

 

Sum per CustomerId eller sum per Year:

SELECT CustomerId, YEAR(loan_date), SUM(Loan_amount)

FROM Loans

GROUP BY CustomerId, YEAR(loan_date)

WITH CUBE

 

SELECT CustomerId, Year(loan_ate), sum(loan_amount)

FROM loans

GROUP BY ROLLUP(CustomerId, year(loan_date))

 

SELECT CustomerId, Year(loan_ate), sum(loan_amount)

FROM loans

GROUP BY CUBE(CustomerId, year(loan_date))

 

SELECT CustomerId, Year(loan_ate), sum(loan_amount)

FROM loans

GROUP BY GROUPING SETS((CustomerId, year(loan_date)), (CustomerId),())

 

SELECT CustomerId, Year(loan_ate), sum(loan_amount)

FROM loans

GROUP BY GROUPING SETS((CustomerId, year(loan_date)), ROLLUP(YEAR(loan_date))))

 

SELECT CustomerId,

CASE GROUPING_ID(CustomerId, year(loan_amount))

  WHEN 0 THEN CAST(YEAR(Loan_date) as varchar(4)) -- binary 00

  WHEN 1 THEN 'Subtotal'                          --        01

  WHEN 3 THEN 'Grand total'                       --        11

END AS Loan_year, SUM(Loan_amount) as total_loan

GROUP BY ROLLUP(CustomerId, year(loan_date))

 

Kontrol af modtaget fil, her en backup database fil fra en anden sql server:

CREATE PROCEDURE [dbo].[Backup_Fil]

AS

BEGIN

 BEGIN TRY

 SET NOCOUNT ON

 DECLARE @Now datetime = GETDATE()

 IF EXISTS (SELECT 1 FROM tempdb.INFORMATION_SCHEMA.TABLES

            WHERE CHARINDEX ('#DirFileInfo', TABLE_NAME) = 1)

    DROP TABLE #DirFileInfo

 CREATE TABLE #DirFileInfo (DirInfo NVARCHAR(256))

 

 INSERT INTO #DirFileInfo

 EXEC master.dbo.xp_cmdshell 'dir Z:\Backup\*.bak  | findstr .bak'

 

 INSERT INTO dbo.LOG_BACKUP_FIL(DatabaseId, Modtagelsestidspunkt, Filnavn,

                                Filtidspunkt, Filstørrelse)

 SELECT DatabaseId, Modtagelsestidspunkt = @Now,

Filnavn = REVERSE(SUBSTRING(REVERSE(DirInfo), 0, CHARINDEX(' ', REVERSE(DirInfo)))),

Filtidspunkt = CAST(LEFT(DirInfo,20) AS datetime),

Filstørrelse = CAST(REPLACE(REVERSE(SUBSTRING(SUBSTRING(REVERSE(DirInfo), CHARINDEX(' ', REVERSE(DirInfo))+1, LEN(DirInfo)),1,CHARINDEX(' ', SUBSTRING(REVERSE(DirInfo), CHARINDEX(' ', REVERSE(DirInfo))+1, LEN(DirInfo))))),',','') AS bigint)

 FROM #DirFileInfo

      INNER JOIN dbo.[DATABASE] db ON db.Filnavn =

        REVERSE(SUBSTRING(REVERSE(DirInfo), 0, CHARINDEX(' ', REVERSE(DirInfo))))

 WHERE DirInfo IS NOT NULL AND db.BackupIndlæs = 1

 END TRY

 BEGIN CATCH

  EXEC dbo.Status_Fejl 'Registrering af dagens backup filer fejlede (sp Backup_Fil).'

  RAISERROR('Registrering af dagens backup filer fejlede (sp Backup_Fil).', 2, 1)

 END CATCH

END

 

Efter restore af backup database filen hentes data om databasen:

CREATE PROCEDURE [dbo].[DatabaseLastBackup]

AS

BEGIN

 SET NOCOUNT ON

 SELECT bs.database_name AS DatabaseName, MAX(bms.physical_device_name)

        AS FullBackupName, bs.backup_finish_date as BackupTime

 FROM msdb.dbo.backupset bs

     INNER JOIN msdb.dbo.backupmediafamily bms ON bs.media_set_id = bms.media_set_id

     INNER JOIN master.dbo.sysdatabases s ON bs.database_name = s.name

 WHERE CONVERT(VARCHAR(20), bs.backup_finish_date, 101) = CONVERT(VARCHAR(20),

 GETDATE(), 101) AND s.name NOT IN ('master', 'msdb', 'model', 'pubs', 'Northwind') AND

 bs.type = 'D'

 GROUP BY bs.database_name, bs.backup_finish_date

END

 

Linked server

Smart til at overføre data fra andre databasesystemer, men kræver en del i op­sæt­nin­gen, hvor Microsoft bl.a. skriver, når forbindelsen ikke virker fra klient PC via SQL Server Management Studio:

When connecting to another data source, SQL Server impersonates the login ap­pro­pria­tely for Windows authenticated logins; however, SQL Server cannot im­per­so­na­te SQL Server authenticated logins. Therefore, for SQL Server authenticated logins, SQL Server can access another data source, such as files, nonrelational data sources like Active Directory, by using the security context of the Windows account under which the SQL Server service is running. Doing this can potentially give such logins access to another data source for which they do not have per­mis­sions, but the account under which the SQL Server service is running does have permissions. This possibility should be considered when you are using SQL Server authenticated logins. Instead of having to use sp_addlinkedsrvlogin to create a predetermined login mapping, SQL Server can automatically use the Windows se­cu­rity credentials (Windows login name and password) of a user issuing the query to connect to a linked server when all the following conditions exist:

• A user is connected to SQL Server by using Windows Authentication Mode.

• Security account delegation is available on the client and sending server.

• The provider supports Windows Authentication Mode; for example, SQL Server running on Windows.

Delegation does not have to be enabled for single-hop scenarios, but it is required for multiple-hop scenarios.

Ovenstående kræver en del for at få fjernet eksempelvis følgende fejlmelding fra linked server til en IBM DB2 database:

Msg 7399, Level 16, State 1, Line 2 The OLE DB provider "IBMDADB2" for linked ser­ver "<linked server name>" reported an error. Access denied.

Msg 7350, Level 16, State 2, Line 2 Cannot get the column information from OLE DB provider "IBMDADB2" for linked server "<linked server name>".

En metode er at anvende Local System som bruger af servicen SQL Server (MSSQLSERVER) (Database Engine service) og kun have en ægte service account på servicen for SQL Agent Jobs, hvilket er nødvendigt, hvis indlæsningen af data sker på en DSA server og senere skal overføres til en EDW server, hvis overførslen ikke skal ske via en SQL User ac­count i IS-pakken, men det anbefales normalt ikke at anvende fordi kontoens password skal indtastes og gemmes.

 

Eksempel på en database server kaldet smartserver med en database kaldet Kalender hvori der er en scalar-valued function kaldet AntalHverdage som har to dato indparametre der bliver kaldt via linked servers på en anden database server:

 

IF OBJECT_ID('tempdb..#output') IS NOT NULL DROP TABLE #output

DECLARE @Fradato nvarchar(10) = '20170207'

DECLARE @Tildato nvarchar(10) = '20170816'

DECLARE @AntalHverdage as int

DECLARE @sql as nvarchar(255)

CREATE TABLE #output (Værdi nvarchar(50) NULL)

SET @sql = 'SELECT Kalender.dbo.AntalHverdage('''''+ @Fradato +''''', '''''+ @Tildato +''''')'

SET @sql = 'SELECT * FROM OPENQUERY([SMARTSERVER], '''+ @sql +''')'

INSERT INTO #output

EXECUTE (@sql)

SELECT @AntalHverdage = Værdi FROM #output

SELECT @AntalHverdage

 

Overførsel af data fra views på en linked server til en modtager server data­base kaldet AnalysisArea der automatisk opretter en tabel med primærnøgle for hvert et view.

 

-- =============================================

-- Author:      Joakim Dalby

-- Create date: 2022-10-19 in the database AnalysisArea

-- SQL job:     Transfer_CustomerMart

-- Description: Transfer all dimension and fact data from views in a schema in parameter

-- @SourceSchema into several on-the-fly created tables in schemas dim and fact.

-- Changelog:

--   yyyy-mm-dd Developer name..... when there is a change.

-- =============================================

CREATE PROCEDURE [etl].[Views_to_Tables]

   @SourceSchema nvarchar(255) = 'CustomerMart',

   @PrimaryKeySuffix nvarchar(255) = '_key'

AS

BEGIN

 SET NOCOUNT ON;

 

 DECLARE @Scan CURSOR

 DECLARE @SourceTable nvarchar(255)

 DECLARE @TargetSchema nvarchar(255)

 DECLARE @TargetTable nvarchar(255)

 DECLARE @TargetTablePrimaryKey nvarchar(255)

 

 DECLARE @sqlTempleteTable nvarchar(MAX)

 DECLARE @sqlTempletePrimaryKey nvarchar(MAX)

 DECLARE @sqlExecute nvarchar(MAX)

 

 BEGIN TRY

 

  SET @sqlTempleteTable = N'

    DROP TABLE IF EXISTS [<TargetSchema>].[<TargetTable>]

    SELECT *, InsertTime_UTC_meta = sysutcdatetime()

    INTO [<TargetSchema>].[<TargetTable>]

    FROM [<linkedserver>].[SalesDatawarehouse].[<SourceSchema>].[<SourceTable>]'

 

  SET @sqlTempletePrimaryKey = N'

    ALTER TABLE [<TargetSchema>].[<TargetTable>]

    ADD CONSTRAINT PK_<TargetSchema>_<TargetTable> PRIMARY KEY CLUSTERED

                                                                    (<TargetTablePrimaryKey>)'

 

  SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

      --SELECT SourceSchema = QUOTENAME(TABLE_SCHEMA), SourceTable = QUOTENAME(TABLE_NAME)

      -- because there are [ ] in the template.

      SELECT SourceSchema = TABLE_SCHEMA, SourceTable = TABLE_NAME

      FROM [<linkedserver>].[SalesDatawarehouse].INFORMATION_SCHEMA.VIEWS

      WHERE TABLE_SCHEMA = @SourceSchema

      ORDER BY TABLE_NAME

  OPEN @Scan

  FETCH NEXT FROM @Scan INTO @SourceSchema, @SourceTable

  WHILE @@FETCH_STATUS = 0

  BEGIN

   IF CHARINDEX('v_Dim_', @SourceTable) !=-- example of @SourceTable: [v_Dim_Branche]

   BEGIN

    SET @TargetSchema = 'dim'

    SET @TargetTable  =  REPLACE(@SourceTable, 'v_Dim_', '')

   END

   IF CHARINDEX('v_Fact_', @SourceTable) !=-- example of @SourceTable: [v_Fact_Sales]

   BEGIN

    SET @TargetSchema = 'fact'

    SET @TargetTable  =  REPLACE(@SourceTable, 'v_Fact_', '')

   END

 

   SET @sqlExecute = @sqlTempleteTable

   SET @sqlExecute = REPLACE(@sqlExecute, '<TargetSchema>', @TargetSchema)

   SET @sqlExecute = REPLACE(@sqlExecute, '<TargetTable>' , @TargetTable)

   SET @sqlExecute = REPLACE(@sqlExecute, '<SourceSchema>', @SourceSchema)

   SET @sqlExecute = REPLACE(@sqlExecute, '<SourceTable>' , @SourceTable)

   --PRINT @sqlExecute

   EXECUTE (@sqlExecute)

   --EXECUTE sp_executesql @sqlExecute

 

   SET @TargetTablePrimaryKey = NULL

   SELECT @TargetTablePrimaryKey = STRING_AGG(QUOTENAME(COLUMN_NAME), ',')

                                   WITHIN GROUP (ORDER BY QUOTENAME(COLUMN_NAME) ASC)

   FROM INFORMATION_SCHEMA.COLUMNS

   WHERE TABLE_SCHEMA = @TargetSchema AND TABLE_NAME = @TargetTable AND

         RIGHT(COLUMN_NAME, LEN(@PrimaryKeySuffix)) = @PrimaryKeySuffix

 

   IF @TargetTablePrimaryKey IS NOT NULL

   BEGIN

    SET @sqlExecute = @sqlTempletePrimaryKey

    SET @sqlExecute = REPLACE(@sqlExecute, '<TargetSchema>', @TargetSchema)

    SET @sqlExecute = REPLACE(@sqlExecute, '<TargetTable>' , @TargetTable)

    SET @sqlExecute = REPLACE(@sqlExecute, '<TargetTablePrimaryKey>', @TargetTablePrimaryKey)

    --PRINT @sqlExecute

    EXECUTE (@sqlExecute)

    --EXECUTE sp_executesql @sqlExecute

   END

 

   FETCH NEXT FROM @Scan INTO @SourceSchema, @SourceTable

  END

  CLOSE @Scan

  DEALLOCATE @Scan

 

 END TRY

 BEGIN CATCH

  DECLARE @ErrorMessage nvarchar(4000),  @ErrorSeverity int, @ErrorState int

  SELECT @ErrorMessage = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(),

         @ErrorState = ERROR_STATE()

  RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState) WITH NOWAIT

 END CATCH

END

 

Stored proceduren etl.Views_to_Tables ønskes kaldt fra en SQL Server Agent job Transfer_CustomerMart og når der anvendes en linked server skal det opsættes på følgende måde:

1.  Etablering af AD servicekonto der får adgang til at læse data fra views på linked server databasen og skrive data på modtager server databasen AnalysisArea.

2.  På modtager serveren under Security oprettes en Credential for servicekontoen.

3.  Under SQL Server Agent under Proxies oprettes en Proxy som Operating System (CmdExec) der anvender den oprettede Credential.

4.  Der oprettes et SQL Server Agent job med et step med Type Operating System (CmdExec) og Run as den oprettede Proxy og i Command angives:

SQLCMD -I -b -S $(ESCAPE_DQUOTE(SRVR)) -d AnalysisArea -Q"EXEC etl.Views_to_Tables"

5.  En Schedules oprettes til at køre jobbet en gang om dagen.

 

Overførsel af mange millioner rækker ved at tage en dato ad gangen

CREATE TABLE [audit].[CCI_Log](

   [CCI_Log_Id] [int] IDENTITY(1,1) NOT NULL,

   [DateOfOrder_UTC] [date] NOT NULL,

   [InsertTime_UTC] [datetime2](3) NOT NULL,

   [UpdateTime_UTC] [datetime2](3) NULL,

   [Duration]  AS (CONVERT([time](0),dateadd(millisecond,

                   datediff(millisecond,[InsertTime_UTC],

                   [UpdateTime_UTC]),(0)))) PERSISTED,

   [Status] [varchar](10) NULL,

 CONSTRAINT [PK_audit_CCI_Log] PRIMARY KEY CLUSTERED

 (

  [CCI_Log_Id] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [AUDIT]

) ON [AUDIT]

GO

 

CREATE UNIQUE NONCLUSTERED INDEX [UIX_audit_CCI_Log_DateOfOrder_UTC] ON [audit].[CCI_Log]

(

 [DateOfOrder_UTC] ASC -- unique key (UK).

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF, DATA_COMPRESSION = PAGE) ON [AUDIT]

GO

 

ALTER TABLE [audit].[CCI_Log] ADD  CONSTRAINT [DF_audit_CCI_Log_InsertTime_UTC]  DEFAULT (sysutcdatetime()) FOR [InsertTime_UTC]

GO

 

CREATE PROCEDURE [audit].[CCI_Insert]

AS

BEGIN

  DECLARE @CCI_Log_Id int

  DECLARE @DateOfOrder_UTC date

  DECLARE @Scan CURSOR

  DECLARE @TranCounter int

  DECLARE @SavePoint NVARCHAR(32) = CAST(@@PROCID AS NVARCHAR(20)) +

                                    N'_' + CAST(@@NESTLEVEL AS NVARCHAR(2))

  SET NOCOUNT ON

  BEGIN TRY

    INSERT INTO [audit].[CCI_Log] WITH(TABLOCK) ([DateOfOrder_UTC])

    SELECT DISTINCT DateOfOrder_UTC

    FROM base.Orders b WITH(NOLOCK)

    WHERE NOT EXISTS(SELECT 1 FROM [audit].[CCI_Log] a

                              WHERE a.DateOfOrder_UTC = b.DateOfOrder_UTC)

    ORDER BY DateOfOrder_UTC

                        

    SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

        SELECT CCI_Log_Id, DateOfOrder_UTC

        FROM [audit].[CCI_Log] WITH(NOLOCK)

        WHERE [Status] IS NULL

        ORDER BY DateOfOrder_UTC

    OPEN @Scan

    FETCH NEXT FROM @Scan INTO @CCI_Log_Id, @DateOfOrder_UTC

    WHILE @@FETCH_STATUS = 0

    BEGIN

      SET @TranCounter = @@TRANCOUNT

      SET @SavePoint = CAST(@@PROCID AS NVARCHAR(20)) +

                       N'_' + CAST(@@NESTLEVEL AS NVARCHAR(2))

      IF @TranCounter > 0

         SAVE TRANSACTION @SavePoint

      ELSE

         BEGIN TRANSACTION

 

      SET IDENTITY_INSERT [base].[OrdersCCI] ON

 

      INSERT INTO [base].[OrdersCCI] WITH(TABLOCK)

          ([Orders_Id]

          ,[InsertTime_UTC_meta]

          ,[RecordSource_meta_SourceFile_Log_Id]

          ,[Area_meta]

          ,[DateOfBusiness_meta]

          ,[DateOfOrder_UTC]

          ,[SubmittingEntity]

          ,[Client]

          ,[SequenceNumber]

          ,[FinancialInstrument]

          ,[OrderEvent]

          ,[TransactionPrice]

          ,[TransactionPrice_NOAP])

      SELECT

           [Orders_Id]

          ,[InsertTime_UTC_meta]

          ,[RecordSource_meta_SourceFile_Log_Id]

          ,[Area_meta]

          ,[DateOfBusiness_meta]

          ,[DateOfOrder_UTC]

          ,[SubmittingEntity]

          ,[Client]

          ,[SequenceNumber]

          ,[FinancialInstrument]

          ,[OrderEvent]

          ,[TransactionPrice]

          ,[TransactionPrice_NOAP]

      FROM base.Orders WITH(NOLOCK)

      WHERE DateOfOrder_UTC = @DateOfOrder_UTC

        

      SET IDENTITY_INSERT [base].[OrdersCCI] OFF

 

      UPDATE [audit].[CCI_Log]

      SET [Status] = 'Success',

          [UpdateTime_UTC] = sysutcdatetime()

      WHERE CCI_Log_Id = @CCI_Log_Id

 

      IF @TranCounter = 0

         COMMIT TRANSACTION

 

      FETCH NEXT FROM @Scan INTO @CCI_Log_Id, @DateOfOrder_UTC

    END

    CLOSE @Scan

    DEALLOCATE @Scan

  END TRY

  BEGIN CATCH

    IF @TranCounter = 0

        ROLLBACK TRANSACTION

    ELSE IF XACT_STATE() = 1

        ROLLBACK TRANSACTION @SavePoint

    ELSE IF XACT_STATE() = -1

        ROLLBACK TRANSACTION

    ;THROW

  END CATCH

END

GO

 

Sammenligne tabeller fra to databaser til afstemning – Compare tables

De to databaser kan repræsenterer gammel produktion og ny produktion for at afstemme, om data er ens. I hver database for dens alle tabellens rækker sum­merer jeg alle dens tal og bit kolonner sammen, så hver tabel får en sum værdi, som jeg gemmer i en tabel, og de to tabeller fra gammel og ny produktion kan jeg sammenligne i Excel for at finde ud, om en eller flere tabeller har forskellige summer. Hver tal og bit kolonne bliver konverteret til data type numeric(38,10) og er summen null laves værdien om til 0, så kolonnerne kan lægges sammen + :

 

CREATE PROCEDURE [dbo].[Reconciliation_Make]

AS

BEGIN

  SET NOCOUNT ON

 

  DECLARE @TableSchema nvarchar(255) = 'dbo'

  DECLARE @TableName nvarchar(255)

  DECLARE @ColumnList nvarchar(max)

  DECLARE @sqlTempleteTable nvarchar(MAX)

  DECLARE @sqlExecute nvarchar(MAX)

  DECLARE @Scan CURSOR

 

  TRUNCATE TABLE [dbo].[ReconciliationCheck]

 

  SET @sqlTempleteTable = N'

  INSERT INTO [dbo].[ReconciliationCheck] (TableName, Sum)

  SELECT TableName = ''<Table>'', <ColumnList>

  FROM [ERP].[<Schema>].[<Table>]'

 

  SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

      SELECT TABLE_NAME

      FROM ERP.INFORMATION_SCHEMA.TABLES

      WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = @TableSchema

  OPEN @Scan

  FETCH NEXT FROM @Scan INTO @TableName

  WHILE @@FETCH_STATUS = 0

  BEGIN

       SET @ColumnList = NULL

       SET @ColumnList =

              SUBSTRING(

              (SELECT CONCAT('+','ISNULL(SUM(','CONVERT(numeric(38,10),',

                             QUOTENAME(COLUMN_NAME),')'

                     ,'),0)')

              FROM ERP.INFORMATION_SCHEMA.COLUMNS

              WHERE DATA_TYPE IN('decimal','numeric','float',

                                 'real','smallmoney','money',

                                 'tinyint','smallint','int','bigint','bit')

              AND TABLE_SCHEMA = @TableSchema AND TABLE_NAME = @TableName

              ORDER BY ORDINAL_POSITION

              FOR XML PATH('')

              ), 2, 99999999)

 

              -- STRING_AGG came in SQL SERVER 2017   

              SELECT @ColumnList = STRING_AGG(CONVERT(varchar(max),

                     CONCAT('ISNULL(SUM(',

                     CASE WHEN DATA_TYPE = 'bit'

                     THEN CONCAT('CONVERT(numeric(38,10),',

                             QUOTENAME(COLUMN_NAME),')')

                     ELSE CONCAT('CONVERT(numeric(38,10),',

                             QUOTENAME(COLUMN_NAME),')')

                     END

                     ,'),0)')),'+')

                     WITHIN GROUP (ORDER BY ORDINAL_POSITION ASC)

              FROM ERP.INFORMATION_SCHEMA.COLUMNS

              WHERE DATA_TYPE IN('decimal','numeric','float',

                                 'real','smallmoney','money',

                                 'tinyint','smallint','int','bigint','bit')

              AND TABLE_SCHEMA = @TableSchema AND TABLE_NAME = @TableName

                    

       SET @sqlExecute = @sqlTempleteTable

       SET @sqlExecute = REPLACE(@sqlExecute, '<Schema>'     , @TableSchema)

       SET @sqlExecute = REPLACE(@sqlExecute, '<Table>'      , @TableName)

       SET @sqlExecute = REPLACE(@sqlExecute, '<ColumnList>' , @ColumnList)

 

       --PRINT @sqlExecute

       EXECUTE (@sqlExecute)

       FETCH NEXT FROM @Scan INTO @TableName

  END

  CLOSE @Scan

  DEALLOCATE @Scan

END

 

Tabel ReconciliationCheck eksempelvis:

 

TableName

Sum

Accounts

146752743508921.7578063801

Booking

31393397404593.0520989344

Customer

1813095968775.0000000000

 

Transactions and Concurrency nogle spændende links

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/transactions-and-concurrency

https://docs.microsoft.com/en-us/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-2017

https://sqlperformance.com/2014/04/t-sql-queries/the-serializable-isolation-level

https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-table?view=sql-server-2017

 

Parallel sql nogle spændende links

https://stackoverflow.com/questions/4571823/sql-server-tsql-is-it-possible-to-exec-statements-in-parallel

http://rusanu.com/2009/08/05/asynchronous-procedure-execution/

https://www.sqlservercentral.com/Forums/Topic1809449-392-2.aspx

http://www.sqlnotes.info/2015/12/03/run-t-sql-in-parallel/

 

SQL Server 2022 nyheder

Microsoft nyheder

STRING_SPLIT rækkefølgenummer med parameter 1, se eksempler ovenfor.

 

IS [NOT] DISTINCT FROM

Kunde tabel hvor kategori kan være NULL eller tom d.v.s. ikke udfyldt grundet ukendt kategori, eksempelvis:

 

KundeId

KundeNavn

KundeKategori

1

Andersen

God

2

Bertelsen

Middel

4

Carlsen

God

5

Dalby

NULL

6

Eriksen

NULL

 

Udsøgning via en variabel med en tildelt værdi:

DECLARE @Kategori varchar(50) = 'God'

SELECT *

FROM dbo.Kunde

WHERE KundeKategori = @Kategori

finder de Gode.

 

Er variablens værdi NULL eller tom lader ovenstående sig ikke gøre:

DECLARE @Kategori varchar(50) = NULL

fordi det kræver en anden sætningsopbygning for at finde de tomme:

WHERE KundeKategori IS NULL

eller en tredje sætningsopbygning for at finde de udfyldte, d.v.s. ikke tom:

WHERE KundeKategori IS NOT NULL

I kombination d.v.s. null eller en søgeværdi:

DECLARE @Kategori varchar(50) = NULL --'God'

SELECT *

FROM dbo.Kunde

WHERE KundeKategori = @Kategori OR (@Kategori IS NULL AND KundeKategori IS NULL)

 

Med et nyt udtryk opnår vi samme sætningsopbygning uanset variablens værdi, Two Value Logic IS NOT DISTINCT, er ikke forskellig fra som håndterer NULL.

 

DECLARE @Kategori varchar(50) = 'God'

SELECT *

FROM dbo.Kunde

WHERE KundeKategori IS NOT DISTINCT FROM @Kategori -- NOT DISTINCT er lig med God

finder de Gode.

 

DECLARE @Kategori varchar(50) = NULL

SELECT *

FROM dbo.Kunde

WHERE KundeKategori IS NOT DISTINCT FROM @Kategori -- NOT DISTINCT er lig med NULL

og finder de tomme/NULL fordi NULL behandles som lig med.

 

DECLARE @Kategori varchar(50) = 'God'

SELECT *

FROM dbo.Kunde

WHERE KundeKategori IS DISTINCT FROM @Kategori -- DISTINCT er forskellig fra God

finder også de tomme/NULL.

 

DECLARE @Kategori varchar(50) = NULL

SELECT *

FROM dbo.Kunde

WHERE KundeKategori IS DISTINCT FROM @Kategori -- DISTINCT er forskellig fra NULL

finder de udfyldte (not null).

 

I kombination d.v.s. NULL eller en søgeværdi:

DECLARE @Kategori varchar(50) = NULL --'God'

SELECT *

FROM dbo.Kunde k

WHERE KundeKategori IS NOT DISTINCT FROM @Kategori

 

Ved sammenligning af to kolonner uden at tænke på om der er NULL værdier:

SELECT *

FROM dbo.Kunde k

WHERE KundeKategori IS NOT DISTINCT FROM KundeType

 

Left join giver alle rækker fra Kunde tabel selvom KundeKategori er NULL i Kunde og der ikke er en tilsvarende række i Ordre tabel:

SELECT k.Kundenavn, o.Ordretekst

FROM dbo.Kunde k

     LEFT OUTER JOIN dbo.Ordre o

        ON k.KundeKategori IS NOT DISTINCT FROM o.KundeKategori

 

DATE_BUCKET funktion

Finder første (1) dato i ugen, måned, år og tilsvarende for klokkeslæt:

 

DECLARE @date datetime                    = '2023-05-10 16:06:50.989'

SELECT DATE_BUCKET(day, 1, @date)         -- 2023-05-10 00:00:00.000

SELECT DATE_BUCKET(week, 1, @date)        -- 2023-05-08 00:00:00.000

SELECT DATE_BUCKET(month, 1, @date)       -- 2023-05-01 00:00:00.000

SELECT DATE_BUCKET(quarter, 1, @date)     -- 2023-04-01 00:00:00.000

SELECT DATE_BUCKET(year, 1, @date)        -- 2023-01-01 00:00:00.000

SELECT DATE_BUCKET(millisecond, 1, @date) -- 2023-05-10 16:06:50.990

SELECT DATE_BUCKET(second, 1, @date)      -- 2023-05-10 16:06:50.000

SELECT DATE_BUCKET(minute, 1, @date)      -- 2023-05-10 16:06:00.000

SELECT DATE_BUCKET(hour, 1, @date)        -- 2023-05-10 16:00:00.000

 

Bucket size er 1, hvorved funktionen returnerer startdatoen for angivet periode f.eks. week giver ugens første dato.

 

Næste eksempel vil vise: Group dates into buckets called Date_Bucket by identify the first day of the week (or month, quarter,  year) that a given date occurred in.

 

Fordeling af fakturaer ud fra deres fakturadato:

 

FakturaDato

KundeId

FakturaBeløb

01-03-2023

2

120

06-03-2023

2

90

07-03-2023

2

40

08-03-2023

2

200

26-04-2023

1

150

05-05-2023

1

130

05-05-2023

2

30

 

Funktionen tager forskellige fakturadatoer inden for 1 uge og samler dem i en spand med en bucket size på 1 week, d.v.s. fakturadatoerne bliver omdannet til datoen for mandagen i pågældende uge, her kaldet FakturaUge og der tælles antal fakturaer og unikke kunder inden for hver FakturaUge, hvorved der fås en statistik per uge:

 

SELECT FakturaUge = DATE_BUCKET(week, 1, FakturaDato),

       AntalUgerFrem = 1,

       AntalFaktura = COUNT(*),

       AntalKunder = COUNT(DISTINCT KundeId)

FROM dbo.Faktura

GROUP BY DATE_BUCKET(week, 1, FakturaDato)

      -- alle datoer i en uge bliver grupperet sammen.

ORDER BY FakturaUge

 

FakturaUge

AntalUgerFrem

AntalFaktura

AntalKunder

27-02-2023

1

1

1

06-03-2023

1

3

1

24-04-2023

1

1

1

01-05-2023

1

2

2

 

Funktionen tager forskellige fakturadatoer inden for 2 uger og samler dem i en spand med en bucket size på 2 week, d.v.s. en større spand af datoer som giver en længere periode og derved flere antal fakturarækker at lave statistik på:

 

SELECT FakturaUge = DATE_BUCKET(week, 2, FakturaDato),

       AntalUgerFrem = 2,

       AntalFaktura = COUNT(*),

       AntalKunder = COUNT(DISTINCT KundeId)

FROM dbo.Faktura

GROUP BY DATE_BUCKET(week, 1, FakturaDato)

      -- alle datoer i to uger bliver grupperet sammen.

ORDER BY FakturaUge

 

FakturaUge

AntalUgerFrem

AntalFaktura

AntalKunder

27-02-2023

2

4

1

24-04-2023

2

3

2

 

Window function OVER clause angivet kun en gang

Summering af fakturabeløb per uge og mange andre beregninger:

 

SELECT DISTINCT

       FakturaUge = DATE_BUCKET(week, 1, FakturaDato),

       AntalUgerFrem = 1,

       UgentligtFakturaBeløb = SUM(FakturaBeløb) OVER win,

       SamletFakturaBeløb = SUM(FakturaBeløb) OVER (),

       ProcentAndel = ISNULL(SUM(FakturaBeløb) OVER win /

                      NULLIF(SUM(FakturaBeløb) OVER (),0),0) * 100,

       UgentligtAntalFaktura = COUNT(1) OVER win,

       SamletAntalFaktura = COUNT(1) OVER()

FROM dbo.Faktura

WINDOW win AS (PARTITION BY DATE_BUCKET(week, 1, FakturaDato))

ORDER BY FakturaUge

 

Faktura

Uge

Antal

Uger

Frem

Ugentligt

Faktura

Beløb

Samlet

Faktura

Beløb

Procent

Andel

Ugentligt

Antal

Faktura

Samlet

Antal

Faktura

27-02-2023

1

120

760

15,78

1

7

06-03-2023

1

330

760

43,42

3

7

24-04-2023

1

150

760

19,73

1

7

01-05-2023

1

160

760

21,05

2

7

 

Forskellige OVER clauses kan angives:

WINDOW winasc  AS (PARTITION BY KundeSegment ORDER BY KundeNavn ASC),

       windesc AS (PARTITION BY KundeSegment ORDER BY KundeNavn DESC)

 

Windows Referencing Other Windows:

WINDOW base  AS (PARTITION BY KundeSegment),

       was AS (base ORDER BY KundeNavn ASC),

       windesc AS (base ORDER BY KundeNavn DESC)

 

Læs mere.

 

Eksempel på en stored procedure med samme sætningsopbygning uanset parameterens værdi og brug en fælles window function Over clause:

CREATE PROCEDURE dbo.KundeStatistik

(@Kategori varchar(50) = NULL)

AS

BEGIN

  SELECT DISTINCT

    FakturaUge = DATE_BUCKET(week, 1, FakturaDato),

    AntalUgerFrem = 1,

    UgentligtFakturaBeløb = SUM(FakturaBeløb) OVER win,

    SamletFakturaBeløb = SUM(FakturaBeløb) OVER (),

    ProcentAndel = ISNULL(SUM(FakturaBeløb) OVER win /

                   NULLIF(SUM(FakturaBeløb) OVER (),0),0) * 100,

    UgentligtAntalFaktura = COUNT(1) OVER win,

    SamletAntalFaktura = COUNT(1) OVER(),

    GennemsnitligFakturaBeløb = AVG(FakturaBeløb) OVER win,

    MindsteFakturaBeløb = MIN(FakturaBeløb) OVER win,

    StørsteFakturaBeløb = MAX(FakturaBeløb) OVER win

  FROM dbo.Faktura f

       INNER JOIN dbo.Kunde k ON k.KundeId = f.KundeId

  WHERE k.KundeKategori IS NOT DISTINCT FROM @Kategori

  WINDOW win AS (PARTITION BY DATE_BUCKET(week, 1, FakturaDato))

  ORDER BY FakturaUge

END

 

EXEC dbo.KundeStatistik -- ingen parameterværdi, så anvendes default som er NULL

EXEC dbo.KundeStatistik 'God'

 

Lave numre

SELECT value

FROM GENERATE_SERIES(1, 5)     -- START = 1, STOP = 5

 

SELECT value

FROM GENERATE_SERIES(1, 32, 7) -- START = 1, STOP = 32, STEP = 7

ORDER BY value DESC

 

DECLARE @Start datetime = '20220501 09:00',

        @End   datetime = '20220501 17:00'

SELECT [Hour] = DATE_BUCKET(HOUR, 1, DATEADD(HOUR, gs.value, @Start)),

       Number = gs.value

FROM GENERATE_SERIES(0, DATEDIFF(HOUR, @Start, @End) - 1) AS gs

 

value

 

value

 

Hour

Number

1

 

29

 

2022-05-01 09:00:00.000

0

2

 

22

 

2022-05-01 10:00:00.000

1

3

 

15

 

2022-05-01 11:00:00.000

2

4

 

8

 

2022-05-01 12:00:00.000

3

 

1

 

2022-05-01 13:00:00.000

4

 

 

2022-05-01 14:00:00.000

5

 

 

2022-05-01 15:00:00.000

6

 

 

2022-05-01 16:00:00.000

7

 

Minutterne inden for et klokkelæt timetal:

SELECT DATEADD(minute, value, '2023-04-01 10:00') AS [Interval]

FROM GENERATE_SERIES(0, 59, 1)

 

SELECT value

FROM GENERATE_SERIES(0.0, 1.0, 0.01)

 

Største og Mindst værdi

SELECT

  CASE WHEN 1 > 5 THEN 1 ELSE 5 END,

  GREATEST(1, 5),

  GREATEST(6, 2),

  GREATEST(NULL, 2),

  LEAST   (1, 5),

  LEAST   (6, 2),

  LEAST   (6, NULL),

  CASE WHEN 1 > 5

       THEN

         CASE WHEN 1 > 3 THEN 1 ELSE 3 END

       ELSE

         CASE WHEN 5 > 3 THEN 5 ELSE 3 END

  END,

  GREATEST(1, 5, 3),

  LEAST   (6, 2, 1, 3)

 

5

5

6

2

1

2

6

5

5

1

 

SELECT GREATEST('B','A','C','N','M')

SELECT GREATEST('2001-09-11', '2001-11-09')

SELECT GREATEST('Michael', 'Mikael')

SELECT GREATEST('a', 'å')

 

Efter en pivotering, hvor første kvartals måneder er blevet til kolonner:

SELECT Year,

  BestMonth  = GREATEST([Jan],[Feb],[Mar]),

  WorstMonth = LEAST   ([Jan],[Feb],[Mar])

FROM dbo.SummarizedSales

 

DATETRUNC funktion – truncate/fetch from datetime into a datetime format

DECLARE @d datetime2(3) = Sysdatetime()

SELECT DATETRUNC(year, @d),     DATEPART(year, @d)     -- yyyy-01-01 00:00:00.000 eller yyyy

SELECT DATETRUNC(month, @d),    DATEPART(month, @d)    -- yyyy-mm-01 00:00:00.000 eller mm

SELECT DATETRUNC(iso_week, @d), DATEPART(iso_week, @d) -- yyyy-mm-dd 00:00:00.000 eller week

SELECT DATETRUNC(day, @d),      DATEPART(day, @d)      -- yyyy-mm-dd 00:00:00.000 eller dd

SELECT DATETRUNC(hour, @d),     DATEPART(hour, @d)     -- yyyy-mm-dd hh:00:00.000 eller hh

 

Jeg synes en trunkering til en dag/day ikke burde inde et klokkeslæt for midnat, men det er nok for at bevare datatype datetime fremfor nedenfor at lave det om til en ægte dato:

DECLARE @d datetime2(3) = Sysdatetime()

SELECT CAST(@d AS date)

 

FIRST_VALUE, LAST_VALUE, LAG, LEAD med RESPECT NULLS og IGNORE NULLS

A NULL treatment clause.

·         RESPECT NULLS er default og funktionen kan returnere NULL.

·         IGNORE NULLS får funktionen til at springe NULL over og altid returnere non-NULL, men kan der ikke findes en non-NULL returneres en NULL.

SELECT bucket, bucketorder, bucketvalue,

firstvalueX = FIRST_VALUE(bucketvalue) RESPECT NULLS OVER (PARTITION BY T.bucket

   ORDER BY T.bucketorder ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),

lastvalueX = LAST_VALUE(bucketvalue) RESPECT NULLS OVER (PARTITION BY T.bucket

   ORDER BY T.bucketorder ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),

firstvalueY  = FIRST_VALUE(bucketvalue) IGNORE NULLS OVER (PARTITION BY T.bucket

   ORDER BY T.bucketorder ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),

lastvalueY = LAST_VALUE(bucketvalue) IGNORE NULLS OVER (PARTITION BY T.bucket

  ORDER BY T.bucketorder ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)

 FROM (VALUES

         ('A', 1,  'first'),

         ('A', 2,  'middle'),

         ('A', 3,  'last'),

         ('B', 1,  NULL),

         ('B', 2,  'middle'),

         ('B', 3,  'last'),

         ('C', 1,  'first'),

         ('C', 2,  'middle'),

         ('C', 3,  NULL),

         ('D', 1,  NULL),

         ('D', 2,  '2'),

         ('D', 3,  '1'),

         ('D', 4,  '3'),

         ('D', 5,  NULL)

      ) T(bucket, bucketorder, bucketvalue)

 

bucket

bucketorder

bucketvalue

firstvalueX

lastvalueX

firstvalueY

lastvalueY

A

1

first

first

last

first

last

A

2

middle

first

last

first

last

A

3

last

first

last

first

last

B

1

NULL

NULL

last

middle

last

B

2

middle

NULL

last

middle

last

B

3

last

NULL

last

middle

last

C

1

first

first

NULL

first

middle

C

2

middle

first

NULL

first

middle

C

3

NULL

first

NULL

first

middle

D

1

NULL

NULL

NULL

2

3

D

2

2

NULL

NULL

2

3

D

3

1

NULL

NULL

2

3

D

4

3

NULL

NULL

2

3

D

5

NULL

NULL

NULL

2

3

 

TRIM udvidelse sammen med Left og Right

TRIM ( [ LEADING | TRAILING | BOTH ] [characters FROM ] string )

Angivelse af en eller flere tegn/karakterer der skal fjernes/trimmes væk.

SELECT

  A = LTRIM('The Boat', 'The'),

  B = LTRIM('TheBoat', 'The'),

  C = LTRIM('Thethe Boat', 'The'),

  D = LTRIM('Thy Boat', 'The'),

  E = RTRIM('Boat The', 'The'),

  F = RTRIM('BoatThe', 'The'),

  G = TRIM(BOTH 'The' FROM 'The Edge'),

  H = TRIM(BOTH 'The' FROM 'TheEdge'),

  I = LTRIM('The Boat', 'The '),

  J = LTRIM('Mr. Michael', 'Mr. ')

 

A

B

C

D

E

F

G

H

I

J

 Boat

Boat

 Boat

y Boat

Boat

Boa

 Edg

dg

Boat

ichael

 

A, C, E, G bibeholder mellemrum, fordi det tegn var ikke angivet, men det er i I.

H går helt galt, fordi fjernelse af hver tre tegn t h e også gør det i Edge så det bliver til dg. Det kunne være smart at fjerne Mr. men går galt i Michael i J.

 

Statistical Window Functions

PERCENTILE_CONT og APPROX_COUNT_DISTINCT.

 

Bit Operations

LEFT_SHIFT (), RIGHT_SHIFT (), BIT_COUNT (), GET_BIT (), SET_BIT ().

 

JSON Features

Læs mere og her.

 

SQL Server 2025 nyheder

Microsoft nyheder

STRING_SPLIT udvidelse med REGEXP more

 

SELECT *

FROM REGEXP_SPLIT_TO_TABLE ('the quick brown fox', '\s+');

 

Value  Ordinal

the       1

quick    2

brown   3

fox       4

 

---------------------------

 

Tabelstruktur fra landingzone til datavarehus med metadata felter

Et kildesystem kan indeholde mange tabeller f.eks. et kildesystem som allerede har udført en Dimensionel modellering og opstillet data i dim og fact tabeller i et dbo schema. I et datavarehus ønskes tabeller i et staging lag/område og i et mart lag/område fordelt på forskellige schemas og tilføjelse af metadata felter.

Her beskrives en metode til at få kreeret tabellerne i datavarehuset.

Jeg har oprettet et Utility schema til at indeholde tabeller og stored procedures.

De to tabeller er metadata om tabeller og kolonner fra en landingzone database.

CREATE TABLE [Utility].[LandingZone_INFORMATION_SCHEMA_COLUMNS](

       [TABLE_CATALOG] [nvarchar](128) NULL,

       [TABLE_SCHEMA] [nvarchar](128) NULL,

       [TABLE_NAME] [nvarchar](128) NULL,

       [COLUMN_NAME] [nvarchar](128) NULL,

       [ORDINAL_POSITION] [int] NULL,

       [COLUMN_DEFAULT] [nvarchar](4000) NULL,

       [IS_NULLABLE] [varchar](3) NULL,

       [DATA_TYPE] [nvarchar](128) NULL,

       [CHARACTER_MAXIMUM_LENGTH] [int] NULL,

       [CHARACTER_OCTET_LENGTH] [int] NULL,

       [NUMERIC_PRECISION] [tinyint] NULL,

       [NUMERIC_PRECISION_RADIX] [smallint] NULL,

       [NUMERIC_SCALE] [int] NULL,

       [DATETIME_PRECISION] [smallint] NULL,

       [CHARACTER_SET_CATALOG] [nvarchar](128) NULL,

       [CHARACTER_SET_SCHEMA] [nvarchar](128) NULL,

       [CHARACTER_SET_NAME] [nvarchar](128) NULL,

       [COLLATION_CATALOG] [nvarchar](128) NULL,

       [COLLATION_SCHEMA] [nvarchar](128) NULL,

       [COLLATION_NAME] [nvarchar](128) NULL,

       [DOMAIN_CATALOG] [nvarchar](128) NULL,

       [DOMAIN_SCHEMA] [nvarchar](128) NULL,

       [DOMAIN_NAME] [nvarchar](128) NULL

) ON [PRIMARY]

GO

CREATE TABLE [Utility].[LandingZone_INFORMATION_SCHEMA_TABLES](

       [TABLE_CATALOG] [nvarchar](128) NULL,

       [TABLE_SCHEMA] [nvarchar](128) NULL,

       [TABLE_NAME] [nvarchar](128) NULL,

       [TABLE_TYPE] [varchar](10) NULL

) ON [PRIMARY]

GO

Jeg opretter en SSIS pakke til at udlæse fra landingzone kildedatabasens meta­data fra INFORMATION_SCHEMA og indlæse til de to ovenstående tabeller i data­vare­huset.

Jeg programmerer en stored procedure i datavarehuset, der opstiller en tabel­struktur kolonneliste baseret på metadata fra tabellen LandingZone_INFORMA­TION_SCHEMA_COLUMNS. Kolonnelisten sendes ud af stored proceduren og vil indgå i en Create Table opstilling i næste stored procedure:

CREATE PROCEDURE [Utility].[Table_ColumnList_Build]

       @TableSchema nvarchar(255),

       @TableName nvarchar(255),

       @ColumnList nvarchar(MAX) OUTPUT

AS

BEGIN

 SET NOCOUNT ON

 ;WITH TableColumnList AS

 (

  SELECT

  TableSchema = t.TABLE_SCHEMA, TableName = t.TABLE_NAME, ColumnName = c.COLUMN_NAME,

  ColumnDataType = QUOTENAME(c.COLUMN_NAME) +

   CASE

    WHEN c.DATA_TYPE IN ('binary','varbinary','char','nchar','varchar','nvarchar')

    THEN CONCAT(QUOTENAME(c.DATA_TYPE),'(',IIF(c.CHARACTER_MAXIMUM_LENGTH = -1,

         'MAX',CAST(c.CHARACTER_MAXIMUM_LENGTH AS varchar)),')')

    WHEN c.DATA_TYPE IN ('datetime2','time','datetimeoffset')

    THEN CONCAT(QUOTENAME(c.DATA_TYPE),'(',CAST(c.DATETIME_PRECISION AS varchar),')')

    WHEN c.DATA_TYPE IN ('decimal','numeric')

    THEN CONCAT(QUOTENAME(c.DATA_TYPE),'(',CAST(c.NUMERIC_PRECISION AS varchar),',',

         CAST(c.NUMERIC_SCALE AS varchar),')')

    ELSE QUOTENAME(c.DATA_TYPE)

   END + IIF(c.IS_NULLABLE = 'YES', 'NULL', 'NOT NULL') + ',',

  ColumnOrder = ORDINAL_POSITION

  FROM Utility.LandingZone_INFORMATION_SCHEMA_TABLES t

       INNER JOIN Utility.LandingZone_INFORMATION_SCHEMA_COLUMNS c

       ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME

  WHERE t.TABLE_TYPE = 'BASE TABLE' AND

        t.TABLE_SCHEMA = @TableSchema AND t.TABLE_NAME = @TableName

 )

 SELECT @ColumnList = STRING_AGG(ColumnDataType, '') WITHIN GROUP (ORDER BY

                                                                   ColumnOrder ASC)

 FROM TableColumnList

END

Jeg programmerer en stored procedure i datavarehuset, der for hver fact tabel i landingzone opstiller en Create Table sql sætning for to schemas StagingFact og Fact via dynamisk SQL. StagingFact og Fact tabel vil anvende clustered column­store index i filegroup MART, og Fact tabel vil anvende en partition. Jeg opstiller en skabelon sql sætningen med < > der løbende bliver erstattet af ægte sql delsætninger, f.eks. <ColumnList> bliver erstattet med output fra ovenstående stored procedure:

CREATE PROCEDURE [Utility].[Staging_Mart_Fact_Build]

AS

BEGIN

 SET NOCOUNT ON

 

 DECLARE @sqlStagingFact_Template nvarchar(MAX)

 DECLARE @sqlExecute nvarchar(MAX)

 DECLARE @ColumnList nvarchar(MAX)

 DECLARE @Filegroup nvarchar(500)

 

 DECLARE @SourceSchema nvarchar(255)

 DECLARE @SourceTable nvarchar(255)

 DECLARE @TargetSchema nvarchar(255)

 DECLARE @TargetTable nvarchar(255)

 DECLARE @Scan CURSOR

 

 SET @sqlStagingFact_Template = N'

  DROP TABLE IF EXISTS [<TargetSchema>].[<TargetTable>]

  CREATE TABLE [<TargetSchema>].[<TargetTable>](

  InsertTime_UTC_meta datetime2(3) NOT NULL CONSTRAINT

    DF_<TargetSchema>_<TargetTable>_InsertTime_UTC_meta DEFAULT (sysutcdatetime()),

  InsertExecutionLogId_meta int NOT NULL,

  Referenceperiode_meta int NOT NULL,

  <ColumnList>

  ) ON <Filegroup>

  CREATE CLUSTERED COLUMNSTORE INDEX CCI_<TargetSchema>_<TargetTable>

    ON [<TargetSchema>].[<TargetTable>]

    WITH (DROP_EXISTING = OFF, COMPRESSION_DELAY = 0, DATA_COMPRESSION = COLUMNSTORE)

    ON <Filegroup>

  '

-- Udsøger fact tabeller fra landingzone opstillet to gange med hvert sit schema.

 SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

     SELECT TABLE_SCHEMA, TABLE_NAME, t.TargetSchema

     FROM [Utility].[LandingZone_INFORMATION_SCHEMA_TABLES]

          CROSS JOIN (VALUES ('StagingFact'), ('Fact') ) t(TargetSchema)

     WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'dbo' AND

           LEFT([TABLE_NAME],5) = 'Fact_'

 OPEN @Scan

 FETCH NEXT FROM @Scan INTO @SourceSchema, @SourceTable, @TargetSchema

 WHILE @@FETCH_STATUS = 0

 BEGIN

  SET @ColumnList = NULL

  EXEC Utility.Table_ColumnList_Build @SourceSchema, @SourceTable, @ColumnList OUTPUT

 

  SET @TargetTable  = REPLACE(@SourceTable,'Fact_','')

  -- Fact tabel skal have partition. StagingFact tabel skal ikke have partition.

  IF @TargetSchema = 'Fact'

  BEGIN

   SET @Filegroup = '[PartitionScheme_Referenceperiode]([Referenceperiode_meta])'

  END

  ELSE -- @TargetSchema = 'StagingFact'

  BEGIN

   SET @Filegroup = '[MART]'

  END

  -- De fleste tekstkolonne er nvarchar, derfor ændrer jeg de få tekstkolonner som er

  -- varchar til nvarchar, så er datatypen ens i datavarehuset.

  SET @ColumnList = REPLACE(@ColumnList, '[varchar]', '[nvarchar]')

  SET @ColumnList = REPLACE(@ColumnList, '[char]', '[nchar]')

 

  SET @sqlExecute = @sqlStagingFact_Template

  SET @sqlExecute = REPLACE(@sqlExecute, '<TargetSchema>', @TargetSchema)

  SET @sqlExecute = REPLACE(@sqlExecute, '<TargetTable>' , @TargetTable)

  SET @sqlExecute = REPLACE(@sqlExecute, '<ColumnList>', @ColumnList)

  SET @sqlExecute = REPLACE(@sqlExecute, '<Filegroup>', @Filegroup)

  --PRINT @sqlExecute

  EXECUTE (@sqlExecute)

  --EXECUTE sp_executesql @sqlExecute

 

  FETCH NEXT FROM @Scan INTO @SourceSchema, @SourceTable, @TargetSchema

 END

 CLOSE @Scan

 DEALLOCATE @Scan

END

Eksempel på en automatisk dannet Periodic snapshot fact tabel via feltet Referen­ce­periode fra ovenstående stored procedure:

CREATE TABLE [Fact].[Instrument](

 [InsertTime_UTC_meta] [datetime2](3) NOT NULL

   CONSTRAINT DF_Fact_Instrument_InsertTime_UTC_meta DEFAULT (sysutcdatetime()),

 [InsertExecutionLogId_meta] [int] NOT NULL,

 [Referenceperiode_meta] [int] NOT NULL,

 [DW_SK_Indberetter] [int] NOT NULL,

 [DW_SK_Instrument] [bigint] NOT NULL,

 [DW_SK_Valuta] [int] NOT NULL,

 [DW_SK_JaNej_VariabelLøbetid] [int] NOT NULL,

 [DW_SK_Dato_Optagelse] [int] NOT NULL,

 [Bruttoudlån] [decimal](28, 10) NULL,

 [Renteloft] [decimal](28, 10) NULL,

 [Rentegulv] [decimal](28, 10) NULL,

 [Restance] [decimal](28, 10) NULL,

) ON [PartitionScheme_Referenceperiode]([Referenceperiode_meta])

GO

CREATE CLUSTERED COLUMNSTORE INDEX [CCI_Fact_Instrument] ON [Fact].[Instrument] WITH (DROP_EXISTING = OFF, COMPRESSION_DELAY = 0, DATA_COMPRESSION = COLUMNSTORE) ON [PartitionScheme_Referenceperiode]([Referenceperiode_meta])

GO

Tilsvarende stored procedure kan programmeres for en dimension tabel.

 

Fjernskrivebord adgang til SQL server

Når de to standard brugere er optaget, så skriv i Start, Kør:

mstsc /admin

 

SQL Server harddisk ledig plads

EXEC master..xp_fixeddrives

 

SQL Server harddisk liste over filer i en mappe

EXEC master.dbo.xp_DirTree 'F:\Input\Csv', 1, 1

 

SQL Server restore a backupfile

--Set Singleuserrmode for SalesSystem

ALTER DATABASE [SalesSystem] SET SINGLE_USER

WITH ROLLBACK IMMEDIATE

ALTER DATABASE [SalesSystem]

SET MULTI_USER

-- Restore Newest Database

DECLARE @FileList TABLE ([FileName] varchar(300), [Depth] int, [IsFile] int)

DECLARE @BackupFile varchar(300)

INSERT INTO @FileList

EXEC Master.dbo.xp_DirTree 'F:\Landingzone\BackupFolder',1,1

SELECT TOP 1 @BackupFile = [FileName]

FROM @FileList

WHERE [FileName] like'X_Full_20%' 

ORDER BY [FileName] DESC

SET @BackupFile = 'F:\Landingzone\BackupFolder\' + @BackupFile

RESTORE DATABASE [SalesSystem]

FROM DISK = @BackupFile

WITH

MOVE 'SalesSystem'     TO 'D:\MSSQL\Data\SalesSystem.mdf',

MOVE 'SalesSystem_log' TO 'L:\MSSQL\Log\SalesSystem_log.ldf', REPLACE

--Add Permissions for SalesSystem

CREATE USER [LAN\<servicekonto>] FOR LOGIN [LAN\<servicekonto>]

GO

ALTER ROLE [db_owner] ADD MEMBER [LAN\<servicekonto>]

GO

 

Aflæse SSISDB SSIS project environment variable med T-SQL

CREATE FUNCTION [dbo].[LandingzoneServername]()

RETURNS nvarchar(50)

AS

BEGIN

  DECLARE @Landingzone nvarchar(50)

 

  SELECT @Landingzone = CAST(ev.[value] AS nvarchar(50))

  FROM [SSISDB].[internal].[environment_variables] ev

       INNER JOIN [SSISDB].[internal].[environments] e

         ON e.environment_id = ev.environment_id

  WHERE e.environment_name = 'SalesSystem' AND

        ev.[name] = 'LandingZone_ServerName'

 

  RETURN @Landingzone

END

GO

 

Databasereferencen SSISDB giver ikke build problem i et Visual Studio DB Project, fordi SSISDB er angivet i en funktion fremfor i et view eller i en stored procedure.

 

Eksempel på kald af funktionen f.eks. inde fra en stored procedure:

DECLARE @LandingzoneServername AS nvarchar(50);

SELECT @LandingzoneServername = dbo.LandingzoneServername()

SELECT @LandingzoneServername

 

SQL Server versionsnummer som indirekte fortæller om service pack

SELECT @@VERSION

 

Database server version og service pack

SELECT ServerProperty('Edition') AS SQLEdition,

       ServerProperty('ProductVersion') AS Version,

       Serverproperty('ProductLevel') AS ServicePack

 

Server oplysninger bl.a. konto som services kører under

SELECT  *

FROM    sys.dm_server_services

 

SELECT  *

FROM    sys.database_files

 

SELECT  *

FROM    sys.dm_server_registry

 

Tabeller i en database

SELECT TABLE_NAME

FROM INFORMATION_SCHEMA.TABLES

WHERE TABLE_TYPE = 'BASE TABLE'

 

Antal rækker i tabeller og views

SET NOCOUNT ON

IF EXISTS (SELECT 1 FROM tempdb.INFORMATION_SCHEMA.TABLES

           WHERE CHARINDEX ('#TableViewRows', TABLE_NAME) = 1)

   DROP TABLE #TableViewRows

SELECT IDENTITY(int, 1,1) AS Id, TABLE_CATALOG AS [Database],

   TABLE_TYPE AS [Type], TABLE_SCHEMA AS [Schema],

   TABLE_NAME AS [Name], 0 AS [Rows] INTO #TableViewRows

FROM INFORMATION_SCHEMA.TABLES

WHERE TABLE_NAME NOT IN('dtproperties', 'sysdiagrams')

 

DECLARE @id int

DECLARE @object nvarchar(512)

DECLARE @sql nvarchar(255)

DECLARE @rows int

DECLARE @Scan CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

SELECT Id, '[' + [Database] + '].[' + [Schema] + '].[' + [Name] + ']'

       AS [Object] FROM #TableViewRows

OPEN @Scan

FETCH NEXT FROM @Scan INTO @id, @object

WHILE @@FETCH_STATUS = 0

BEGIN  -- Dynamic SQL

  SET @sql = 'SELECT @prmrows = COUNT(*) FROM '+ @object

  BEGIN TRY

    EXECUTE sp_executesql @sql, N'@prmrows int OUTPUT',

                                  @prmrows = @rows OUTPUT

    UPDATE #TableViewRows

    SET [Rows] = @rows

    WHERE Id = @id

  END TRY

  BEGIN CATCH

  END CATCH

  FETCH NEXT FROM @Scan INTO @id, @object

END

CLOSE @Scan

DEALLOCATE @Scan

SELECT [Database], [Type], [Schema], [Name], [Rows]

FROM #TableViewRows

ORDER BY [Database], [Type], [Schema], [Name]

 

Eller via rekursion danner en samlet dynamisk sql og udfører den til sidst

DECLARE @sql nvarchar(MAX)

SET NOCOUNT ON

SELECT

  @sql = COALESCE(@sql + ' UNION ALL ', '') +

    'SELECT

    ''' + t.type + ''' AS ''Type'',

    ''' + s.name + ''' AS ''Schema'',

    ''' + t.name + ''' AS ''Name'',

    COUNT(*) AS Rows

    FROM ' + QUOTENAME(s.name) + '.' + QUOTENAME(t.name)

FROM sys.schemas s

     INNER JOIN

       (

        SELECT 'Table' AS type, name, schema_id

        FROM sys.tables

        WHERE name NOT IN('dtproperties', 'sysdiagrams')

        UNION

        SELECT 'View' AS type, name, schema_id

        FROM sys.views

       ) t ON t.schema_id = s.schema_id  

ORDER BY t.type, s.name, t.name

EXECUTE (@sql)

 

Antal rækker i tabeller med indlejret sql

SELECT st.name AS TableName,

       (SELECT SUM(max_length) FROM sys.columns

      WHERE object_id=st.object_id) AS RowLength,

       si.rowcnt AS [RowCount],

       CAST(si.rowcnt*(SELECT SUM(max_length) FROM sys.columns

      WHERE object_id=st.object_id)/1024.0/1024.0 AS INT)

     AS MaxTableSizeMB

FROM sys.tables as st

LEFT JOIN sysindexes si on st.object_id=si.id and si.indid < 2

ORDER BY st.name

 

Antal rækker i schema og tabeller – bedst at bruge

SELECT

  TableSchemaName = SCHEMA_NAME(t.schema_id),

  TableName       = t.name,

  TableRowCount   = SUM(p.rows)

FROM sys.tables t

     INNER JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN(0, 1)

WHERE SCHEMA_Name(t.schema_id) = 'base' AND t.name = 'Transaction'

GROUP BY t.schema_id, t.name

 

Eller på en anden måde med samme resultat:

SELECT

  TableSchemaName = SCHEMA_NAME(o.schema_id),

  TableName       = o.name,

  TableRowCount   = SUM(p.row_count)

FROM sys.objects o

     INNER JOIN sys.dm_db_partition_stats p ON o.object_id = p.object_id

WHERE o.type = 'U' AND o.is_ms_shipped = 0x0 AND p.index_id IN(0, 1) AND

      SCHEMA_Name(o.schema_id) = 'base' AND o.name = 'Transaction'

GROUP BY o.schema_id, o.name

 

Ikke behov for kald af DBCC UPDATEUSAGE.

Har man lige indsat flere millioner rækker i en clustered columnstore index tabel, så kan ovenstående finde på at vise enten 1.048.576 eller 2.097.152 rækker for meget (kaldes en bucket count). Man kan enten vente WAITFOR 00:10 minutter og prøve igen, eller udføre en SELECT COUNT_BIG(1) som ofte er hurtig på en clustered columnstore index tabel. A rowgroup has exactly 1.048.576 rows.

 

Et kald af en system stored procedure som bl.a. returnerer Rows:

EXEC sp_spaceused N'base.Tranction'

 

Oversigt over hvilke staging tabeller der har 0 rækker:

DECLARE @NumberOfTableRowCount0 int = 0

DECLARE @StagingTableList varchar(1024)

;WITH dataset AS

(

 SELECT

  TableSchemaName = SCHEMA_NAME(o.schema_id),

  TableName       = o.name,

  TableRowCount   = SUM(p.row_count)

 FROM sys.objects o

      INNER JOIN sys.dm_db_partition_stats p ON o.object_id = p.object_id

 WHERE o.type = 'U' AND o.is_ms_shipped = 0x0 AND p.index_id IN(0, 1) AND

       SCHEMA_Name(o.schema_id) IN('StagingDim','StagingFact')

 GROUP BY o.schema_id, o.name

)

SELECT @NumberOfTableRowCount0 = COUNT(*),

     @StagingTableList = STRING_AGG(CONCAT(TableSchemaName,'.',TableName), ', ')

FROM dataset

WHERE TableRowCount = 0

 

SELECT @NumberOfTableRowCount0, @StagingTableList

IF @NumberOfTableRowCount0 >= 1

Uventet, at mindst en staging tabel med 0 rækker, skriv det i en Log tabel for ETL forløbet, anden Log tabel eller send en email til DataDevops, så de kan se på data leverancen fra et kildesystem.

 

Tabellers total space og used space på harddisk

SELECT

t.NAME AS TableName,

s.Name AS SchemaName,

p.rows AS RowCounts,

SUM(a.total_pages) * 8 AS TotalSpaceKB,

CAST(ROUND(((SUM(a.total_pages) * 8) / 1024.00), 2) AS NUMERIC(36, 2)) AS TotalSpaceMB,

SUM(a.used_pages) * 8 AS UsedSpaceKB,

CAST(ROUND(((SUM(a.used_pages) * 8) / 1024.00), 2) AS NUMERIC(36, 2)) AS UsedSpaceMB,

(SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB,

  CAST(ROUND(((SUM(a.total_pages) - SUM(a.used_pages)) * 8) / 1024.00, 2)

  AS NUMERIC(36, 2)) AS UnusedSpaceMB

FROM

 sys.tables t

INNER JOIN

 sys.indexes i ON t.OBJECT_ID = i.object_id

INNER JOIN

 sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id

INNER JOIN

 sys.allocation_units a ON p.partition_id = a.container_id

LEFT OUTER JOIN

 sys.schemas s ON t.schema_id = s.schema_id

WHERE

 t.NAME NOT LIKE 'dt%'

 AND t.is_ms_shipped = 0

 AND i.OBJECT_ID > 255

GROUP BY

 t.Name, s.Name, p.Rows

ORDER BY

 t.Name

 

Hukommelse, momery, RAM

SELECT object_name, counter_name, cntr_value

FROM sys.dm_os_performance_counters

WHERE counter_name IN ('Total Server Memory (KB)', 'Target Server Memory (KB)')

 

SELECT physical_memory_kb FROM sys.dm_os_sys_info

 

SELECT

(physical_memory_in_use_kb/1024) AS Memory_usedby_Sqlserver_MB,

(locked_page_allocations_kb/1024) AS Locked_pages_used_Sqlserver_MB,

(total_virtual_address_space_kb/1024) AS Total_VAS_in_MB,

process_physical_memory_low,

process_virtual_memory_low

FROM sys.dm_os_process_memory

 

SELECT object_name,counter_name,cntr_value / 1024 "MB"

FROM sys.dm_os_performance_counters

WHERE counter_name IN ('Free Memory (KB)','Total Server Memory (KB)','Target Server Memory (KB)')

 

SQL koden bag et view

SELECT VIEW_DEFINITION

FROM INFORMATION_SCHEMA.VIEWS

WHERE TABLE_NAME = 'Customer_List'

-- Ovenstående medtager kun de første 4000 tegn af viewets indhold.

 

SELECT MODS.DEFINITION

FROM SYS.OBJECTS OBJ INNER JOIN SYS.SQL_MODULES MODS

       ON OBJ.OBJECT_ID = MODS.OBJECT_ID

WHERE OBJ.TYPE = 'V' AND OBJ.NAME = 'Customer_List'

 

SELECT MODS.DEFINITION

FROM SYS.OBJECTS OBJ INNER JOIN SYS.SQL_MODULES MODS

       ON OBJ.OBJECT_ID = MODS.OBJECT_ID

     INNER JOIN SYS.SCHEMAS SCHS ON OBJ.SCHEMA_ID = SCHS.SCHEMA_ID

WHERE OBJ.TYPE = 'V' AND SCHS.NAME = 'dbo' AND

      OBJ.NAME = 'Customer_List'

 

Opdatere et view efter en tabel ændring – refresh view

Der anvendes EXECUTE sp_refreshview vist på tre måder.

Den lange version med en cursor:

CREATE PROCEDURE [dbo].[Refresh_Views]

  @param_Schemaname nvarchar(128) = NULL

 

WITH EXEC AS CALLER

AS

/* To refresh all the views on the current database e.g.:

   EXEC dbo.Refresh_Views <refresh all views>

   EXEC dbo.Refresh_Views 'dbo'

   EXEC dbo.Refresh_Views 'customer'

*/

BEGIN

     SET NOCOUNT ON

 

     DECLARE @Scan CURSOR

     DECLARE @Schemaname nvarchar(128)

     DECLARE @Viewname nvarchar(128)

     DECLARE @SchemaViewname nvarchar(257)

     DECLARE @Number int

 

     SET @Scan = CURSOR LOCAL FORWARD_ONLY DYNAMIC FOR

       SELECT QUOTENAME(TABLE_SCHEMA) AS Schemaname, QUOTENAME(TABLE_NAME) AS Viewname,

               ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS Number

       FROM INFORMATION_SCHEMA.VIEWS

       WHERE @param_Schemaname IS NULL OR TABLE_SCHEMA = @param_Schemaname

       ORDER BY TABLE_NAME, TABLE_SCHEMA

       /*

       SELECT QUOTENAME(s.name) AS Schemaname, QUOTENAME(v.name) AS Viewname,

               ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS Number

       FROM sys.views v       

            INNER JOIN sys.schemas s ON s.schema_id = v.schema_id

       WHERE @param_Schemaname IS NULL OR s.name = @param_Schemaname

       ORDER BY v.name, s.name

       */

     OPEN @Scan FETCH NEXT FROM @Scan INTO @Schemaname, @Viewname, @Number

     WHILE @@FETCH_STATUS = 0

     BEGIN  

       SET @SchemaViewname = @Schemaname + '.' + @Viewname

       BEGIN TRY

         EXECUTE sp_refreshview @SchemaViewname

         PRINT CONCAT('Refreshed: ', FORMAT(@Number, '00000'), '  ',

                       @SchemaViewname)

       END TRY    

       BEGIN CATCH

         PRINT CONCAT('Failed: ', FORMAT(@Number, '00000'), '  ',

                       @SchemaViewname, ', Error: ', ERROR_MESSAGE())

       END CATCH

       FETCH NEXT FROM @Scan INTO @Schemaname, @Viewname, @Number

     END

     CLOSE @Scan

     DEALLOCATE @Scan

     SELECT @Number AS 'Number of views, see them at tab Messages'

END

Den korte version:

DECLARE @sqlcmd NVARCHAR(MAX) = ''

SELECT @sqlcmd = @sqlcmd +  'EXEC sp_refreshview ''' +

                 CONCAT(QUOTENAME(TABLE_SCHEMA),'.',QUOTENAME(TABLE_NAME)) + ''';'

FROM INFORMATION_SCHEMA.VIEWS

ORDER BY TABLE_NAME, TABLE_SCHEMA

EXEC(@sqlcmd)

--SELECT @sqlcmd

Eller genere sql sætninger og manuelt afvikle dem:

SELECT DISTINCT 'EXEC sp_refreshview ''' + name + ''''

FROM sys.objects AS so

INNER JOIN sys.sql_expression_dependencies AS sed

ON so.object_id = sed.referencing_id

WHERE so.type = 'V' AND sed.referenced_id = OBJECT_ID('Person.Person');

 

Værdier fra en række i en tabel

En række udvalgt med primærnøgleværdi kriterie og dens felter/kolonner og deres værdier vises som rækker:

select T2.N.value('local-name(.)', 'nvarchar(128)')+': '+  

       T2.N.value('.', 'nvarchar(max)')

from (select *

      from Customers

      Where CustomerID='ALFKI'

      for xml path(''), type) as T1(X)

  cross apply T1.X.nodes('/*') as T2(N)

 

Hvis der er flere collation i samme database

SELECT

TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLLATION_NAME,

CAST(DATABASEPROPERTYEX(TABLE_CATALOG, 'Collation') AS NVARCHAR(MAX)) AS DatabaseCollation

FROM INFORMATION_SCHEMA.COLUMNS C

WHERE EXISTS

(SELECT *

 FROM INFORMATION_SCHEMA.TABLES T

 WHERE T.TABLE_TYPE = 'BASE TABLE' AND T.TABLE_NAME = C.TABLE_NAME)

AND COLLATION_NAME <> CAST(DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS NVARCHAR(128))

 

Udlæs datapunkter fra en XML fil

En XML fil indeholder tre namespaces (ns) som identificeres i en SQL sætning og i en sti (path) til et datapunkt (data point). ESMAsample.xml. Over tid har name­space versionsnummeret ændret sig, derfor foretages et tjek for at bestemme rette versionsnummer, så der ikke fås fejl ved udlæsning af datapunkterne.

 

DECLARE @Xml xml

DECLARE @Check bit

DECLARE @Doc int

DECLARE @RootXmlns varchar(255)

 

SELECT @Xml = BulkColumn

FROM OPENROWSET(BULK 'M:\XML\ESMAsample.xml', SINGLE_BLOB) AS XMLDATA

 

-- Version from 2010:

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.017.001.00' AS dns) -- dns = document namespace

-- BizData element root with own namespace has two children: a header (Hdr) element with own namespace

-- and a payload (Pyld) element with root namespace.

-- The Pyld element has a child called Document with own namespace.

SELECT @Check = @Xml.exist('rns:BizData/rns:Pyld/dns:Document')

IF @Check = 1

   SET @RootXmlns = '<root xmlns:rns="urn:iso:std:iso:20022:tech:xsd:head.003.001.01"

                           xmlns:hns="urn:iso:std:iso:20022:tech:xsd:head.001.001.01"

                           xmlns:dns="urn:iso:std:iso:20022:tech:xsd:auth.017.001.00"/>'

 

-- Version from 2012:

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.017.001.01' AS dns) -- dns = document namespace

SELECT @Check = @Xml.exist('rns:BizData/rns:Pyld/dns:Document')

IF @Check = 1

   SET @RootXmlns = '<root xmlns:rns="urn:iso:std:iso:20022:tech:xsd:head.003.001.01"

                           xmlns:hns="urn:iso:std:iso:20022:tech:xsd:head.001.001.01"

                           xmlns:dns="urn:iso:std:iso:20022:tech:xsd:auth.017.001.01"/>'

 

-- Version from 2015:

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.017.001.02' AS dns) -- dns = document namespace

SELECT @Check = @Xml.exist('rns:BizData/rns:Pyld/dns:Document')

IF @Check = 1

   SET @RootXmlns = '<root xmlns:rns="urn:iso:std:iso:20022:tech:xsd:head.003.001.01"

                           xmlns:hns="urn:iso:std:iso:20022:tech:xsd:head.001.001.01"

                           xmlns:dns="urn:iso:std:iso:20022:tech:xsd:auth.017.001.02"/>'

 

EXEC sp_xml_preparedocument @Doc OUTPUT, @Xml, @RootXmlns

 

SELECT *

FROM OPENXML(@Doc, '//rns:BizData/rns:Hdr/hns:AppHdr') -- root namespace and header namespace

WITH

(

  MsgDefIdr varchar(50) 'hns:MsgDefIdr' -- fetch an element from header namespace

)

 

SELECT InstrumentId, ShortName, Amount, Currency, Issr, TradeId,

       FirstTradeDatetime = CAST(FirstTradeDatetime AS datetime2(0)), RefData_XML

FROM OPENXML(@Doc, '//rns:BizData/rns:Pyld/dns:Document/dns:FinInstrmRptgRefDataRpt/dns:RefData')

WITH                                               -- RefData represents a record out of many records.

(

  InstrumentId       varchar(12)  'dns:FinInstrmGnlAttrbts/dns:Id', -- fetch an element.

  ShortName          varchar(50)  'dns:FinInstrmGnlAttrbts/dns:ShrtNm',

  Amount             decimal(19,4)'dns:FinInstrmGnlAttrbts/dns:NtnlCcy',

  Currency           varchar(3)   'dns:FinInstrmGnlAttrbts/dns:NtnlCcy/@ccy', -- fetch an attribute.

  Issr               varchar(20)  'dns:Issr',

  TradeId            varchar(50)  'dns:TradgVnRltdAttrbts/dns:Id',

  FirstTradeDatetime varchar(50)  'dns:TradgVnRltdAttrbts/dns:FrstTradDt',

  RefData_XML        xml          '.'  -- fetch a xml structure for the RefData sub xml.

)

EXEC sp_xml_removedocument @Doc

 

Når der er flere elementer for RefData, så fås der flere rækker lige til at indsætte i en tabel med INSERT INTO foran eller INTO før FROM.

 

MsgDefIdr

auth.017.001.02

InstrumentId

ShortName

Amount

Currency

Issr

TradeId

FirstTradeDatetime

AB1234567890

HZL CMB 4,45/10

7,4500

EUR

EQYXK86SF381Q21S3020

XDUB

23-02-2015 00:00

AB1234567890

HZL CMB 4,45/10

7,4400

EUR

EQYXK86SF381Q21S3020

XHEL

23-02-2015 00:00

AB1234567890

DAX9908TLOPENZ

6,6600

US

885100KZRBC1KP0WDC71

XLIS

23-02-2015 11:22

 

OPENROWSET and OPENXML   OPENXML   OPENXML examples

WITH XMLNAMESPACES   Add namespaces

 

Siden 2008 har man kunne anvende xml Data Type Methods og xquery, men koden bliver ikke pæn for at håndtere flere name­space versionsnumre, så det und­lades her. Jeg anvender termen List for at angive, at der kan være mellem 0 og mange elementer som jeg termer Element og sætter xml-elementnavnet RefData foran.

 

DECLARE @XmlTable TABLE

  (Id int identity primary key,

   XmlData xml,

   LoadedDateTime datetime default getdate());

INSERT INTO @XmlTable (XmlData)

SELECT CAST(BulkColumn AS XML)

FROM OPENROWSET(BULK 'M:\XML\ESMAsample.xml', SINGLE_BLOB) AS XMLDATA

 

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.017.001.02' AS dns) -- dns = document namespace

-- BizData element root with own namespace has two children: a header (Hdr) element with own namespace

-- and a payload (Pyld) element with root namespace.

-- The Pyld element has a child called Document with own namespace.

SELECT

  InstrumentId = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:Id/text())[1]','varchar(12)'),-- an element

  ShortName    = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:ShrtNm/text())[1]','varchar(50)'),

  Amount       = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:NtnlCcy/text())[1]','decimal(19,4)'),

  Currency     = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:NtnlCcy/@ccy)[1]','varchar(3)'),-- an attribute

  Issr         = RefDataList.RefDataElement.value('(dns:Issr/text())[1]','varchar(20)'),

  TradeId      = RefDataList.RefDataElement.value('(dns:TradgVnRltdAttrbts/dns:Id/text())[1]','varchar(50)'),

  FirstTradeDatetime = RefDataList.RefDataElement.value('(dns:TradgVnRltdAttrbts/dns:FrstTradDt/text())[1]','varchar(50)')

FROM @XmlTable AS t

    OUTER APPLY t.XmlData.nodes('//rns:BizData/rns:Pyld/dns:Document/dns:FinInstrmRptgRefDataRpt/dns:RefData') AS

                RefDataList(RefDataElement)

 

Common Table Expression CTE i stedet for en tabel variabel.

 

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.017.001.02' AS dns) -- dns = document namespace

, xmlrs (XmlData) AS

(

    SELECT CAST(BulkColumn AS XML)

    FROM OPENROWSET(BULK 'M:\XML\ESMAsample.xml', SINGLE_BLOB) AS XMLDATA

)

SELECT

  InstrumentId = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:Id/text())[1]','varchar(12)'),

  ShortName    = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:ShrtNm/text())[1]','varchar(50)'),

  Amount       = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:NtnlCcy/text())[1]','decimal(19,4)'),

  Currency     = RefDataList.RefDataElement.value('(dns:FinInstrmGnlAttrbts/dns:NtnlCcy/@ccy)[1]','varchar(3)'),

  Issr         = RefDataList.RefDataElement.value('(dns:Issr/text())[1]','varchar(20)'),

  TradeId      = RefDataList.RefDataElement.value('(dns:TradgVnRltdAttrbts/dns:Id/text())[1]','varchar(50)'),

  FirstTradeDatetime = RefDataList.RefDataElement.value('(dns:TradgVnRltdAttrbts/dns:FrstTradDt/text())[1]','varchar(50)')

FROM xmlrs AS t

    OUTER APPLY t.Xmldata.nodes('//rns:BizData/rns:Pyld/dns:Document/dns:FinInstrmRptgRefDataRpt/dns:RefData') AS

                RefDataList(RefDataElement)

 

Andet eksempel med en #tabel og XML direkte i koden og uden namespaces og ingen root tag og uden /text() selvom det er standard XPath:

 

CREATE TABLE #XmlTable(XmlData Xml);

INSERT INTO #XmlTable(XmlData)

SELECT '<Info>Validated order</Info>

        <Data>1234567890</Data>

       'As XmlData;

 

SELECT tbl.XmlData.value('Info[1]','varchar(20)') AS Info,

       tbl.XmlData.value('Data[1]','bigint') AS Data

FROM #XmlTable AS tbl

 

DROP TABLE #XmlTable;

 

Info

Data

Validated order

1234567890

 

Tredje eksempel med en #tabel og XML direkte i koden og uden namespaces og med en 1-til-mange-til-mange relation, fordi der er flere Test som indeholder en Subject med flere Cap elementer og flere fix elementer.

Jeg ønsker at danne one big table (obt) tabel.

Jeg anvender termen List for at angive, at der kan være mellem 0 og mange elementer som jeg termer Element og sætter xml-elementnavnene foran for at få sigende referencer:

TestList.TestElement, SubjectCapList.CapElement, SubjectFitList.FitElement og SupportCapList.CapElement.

 

CREATE TABLE #XmlTable(XmlData Xml);

INSERT INTO #XmlTable(XmlData)

SELECT '<First>

          <Test id="83847">

            <Identifier>123456789</Identifier>

            <ProductItemCode>8888</ProductItemCode>

            <subject>

              <cap>15</cap>

              <cap>25</cap>

              <cap>100</cap>

              <fit>6</fit>

              <fit>7</fit>

            </subject>

            <support type = "ABC">

              <cap>9</cap>

            </support>

          </Test>

          <Test id="83848">

            <Identifier>987654321</Identifier>

            <ProductItemCode>9999</ProductItemCode>

            <subject>

              <cap>150</cap>

              <cap>2</cap>

              <cap>10</cap>

            </subject>

            <support type = "XYZ">

              <cap>19</cap>

            </support>

          </Test>

        </First>'As XmlData;

 

SELECT Id              = TestList.TestElement.value('@id', 'int'), -- an attribute from Test tag

       Identifier      = TestList.TestElement.value('(Identifier/text())[1]','varchar(10)'),

       ProductItemCode = TestList.TestElement.value('(ProductItemCode/text())[1]','int'),

       SubjectCap      = SubjectCapList.CapElement.value('text()[1]', 'int'),

       SubjectFit      = SubjectFitList.FitElement.value('text()[1]', 'int'),

       SupportType     = TestList.TestElement.value('(support/@type)[1]','varchar(max)'),--attribute

       SupportCap      = SupportCapList.CapElement.value('text()[1]', 'int'),

       SupportCapx     = SupportCapList.CapElement.value('(.)[1]', 'int')

FROM #XmlTable AS tbl

     OUTER APPLY tbl.XmlData.nodes('/First/Test') AS TestList(TestElement)

     OUTER APPLY TestList.TestElement.nodes('subject/cap') AS SubjectCapList(CapElement)

     OUTER APPLY TestList.TestElement.nodes('subject/fit') AS SubjectFitList(FitElement)

     OUTER APPLY TestList.TestElement.nodes('support/cap') AS SupportCapList(CapElement)

 

DROP TABLE #XmlTable;

 

Brug af OUTER APPLY giver en slags Left Outer Join, fordi fit element ikke findes under Test id="83848" og brug af CROSS APPLY havde ikke medtaget den.

 

Slutter med endnu en XML fil fundet på internettet drive.google med en-til-mange relation i xml datastrukturen, derfor anvendes to OUTER APPLY for at udlæse data­punkter fra både »en« og »mange« delene sant absolutte og relative stier med XPath:

 

DECLARE @XmlTable TABLE

  (XmlData xml);

INSERT INTO @XmlTable (XmlData)

SELECT CAST(BulkColumn AS XML)

FROM OPENROWSET(BULK 'M:\XML\EMIRsample.xml', SINGLE_BLOB) AS XMLDATA

 

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.030.001.01' AS dns) -- dns = document namespace

-- BizData element root with own namespace has two children: a header (Hdr) element with own namespace

-- and a payload (Pyld) element with root namespace.

-- The Pyld element has a child called Document with own namespace.

SELECT

            -- hele stien, den d.v.s. den absolutte sti (fuldstændige sti) giver mindre query plan og bedre svartid.

 FromIdx  = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:Fr/hns:OrgId/hns:Id/hns:OrgId/hns:Othr/hns:Id/text())[1]','varchar(5)'),

            -- næsten hele stien fordi // springer nogle tags, d.v.s den relative sti.

 FromId   = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:Fr/hns:OrgId/hns:Id/hns:OrgId//hns:Id/text())[1]','varchar(5)'),

 ToId     = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:To/hns:OrgId/hns:Id/hns:OrgId//hns:Id/text())[1]','varchar(5)'),

 NbRcrds  = BizData.e.value('(rns:Pyld/dns:Document/dns:DerivsTradRpt/dns:RptHdr/dns:NbRcrds/text())[1]','int'),

            -- hele stien, den d.v.s. den absolutte sti (fuldstændige sti) giver mindre query plan og bedre svartid.

 RCPLEIx  = RptList.e.value('(dns:Tx/dns:ValtnUpd/dns:CtrPtySpcfcData/dns:CtrPty/dns:RptgCtrPty/dns:Id/dns:LEI/text())[1]',

            'varchar(20)'),

  -- // springer nogle tags over for at nå Id og LEI d.v.s den relative sti. Starter med punktum lig med start fra Rpt,

  -- fordi Tx kan også være Pos, og ValtnUpd kan også være New, Mod, PosCmpnt m.fl. så undgås at angive flere

  -- XPath stier, men det koster med en større query plan og en langsommere svartid.

 RCPLEI   = RptList.e.value('(.//dns:RptgCtrPty/dns:Id/dns:LEI/text())[1]','varchar(20)'),

 OCPLEI   = RptList.e.value('(.//dns:OthrCtrPty/dns:Id/dns:LEI/text())[1]','varchar(20)'),

 SALEI    = RptList.e.value('(.//dns:SubmitgAgt/dns:LEI/text())[1]','varchar(20)'),

 Amount   = RptList.e.value('(.//dns:Valtn/dns:CtrctVal/dns:Amt/text())[1]','decimal(19,4)'),

 Currency = RptList.e.value('(.//dns:Valtn/dns:CtrctVal/dns:Amt/@Ccy)[1]','varchar(3)'),      -- an attribute

 TimeStmp = CAST(RptList.e.value('(.//dns:TmStmp/text())[1]','varchar(20)') AS datetime2(0)),

 Tp       = RptList.e.value('(.//dns:Tp/text())[1]','varchar(10)')

FROM @XmlTable AS t

    OUTER APPLY t.XmlData.nodes('//rns:BizData') AS BizData(e)

    OUTER APPLY t.XmlData.nodes('//rns:BizData/rns:Pyld/dns:Document/dns:DerivsTradRpt/dns:TradData/dns:Rpt') AS RptList(e)

 

Eller:

 

DECLARE @XmlTable xml

SELECT @XmlTable = BulkColumn

FROM OPENROWSET(BULK 'M:\XML\EMIRsample.xml', SINGLE_BLOB) AS XMLDATA

 

;WITH XMLNAMESPACES ('urn:iso:std:iso:20022:tech:xsd:head.003.001.01' AS rns, -- rns = root namespace

                     'urn:iso:std:iso:20022:tech:xsd:head.001.001.01' AS hns, -- hns = header namespace

                     'urn:iso:std:iso:20022:tech:xsd:auth.030.001.01' AS dns) -- dns = document namespace

-- BizData element root with own namespace has two children: a header (Hdr) element with own namespace

-- and a payload (Pyld) element with root namespace.

-- The Pyld element has a child called Document with own namespace.

SELECT

            -- hele stien, den d.v.s. den absolutte sti (fuldstændige sti) giver mindre query plan og bedre svartid.

 FromIdx  = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:Fr/hns:OrgId/hns:Id/hns:OrgId/hns:Othr/hns:Id/text())[1]','varchar(5)'),

            -- næsten hele stien fordi // springer nogle tags, d.v.s den relative sti.

 FromId   = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:Fr/hns:OrgId/hns:Id/hns:OrgId//hns:Id/text())[1]','varchar(5)'),

 ToId     = BizData.e.value('(rns:Hdr/hns:AppHdr/hns:To/hns:OrgId/hns:Id/hns:OrgId//hns:Id/text())[1]','varchar(5)'),

 NbRcrds  = BizData.e.value('(rns:Pyld/dns:Document/dns:DerivsTradRpt/dns:RptHdr/dns:NbRcrds/text())[1]','int'),

            -- hele stien, den d.v.s. den absolutte sti (fuldstændige sti) giver mindre query plan og bedre svartid.

 RCPLEIx  = RptList.e.value('(dns:Tx/dns:ValtnUpd/dns:CtrPtySpcfcData/dns:CtrPty/dns:RptgCtrPty/dns:Id/dns:LEI/text())[1]',

            'varchar(20)'),

  -- // springer nogle tags over for at nå Id og LEI d.v.s den relative sti. Starter med punktum lig med start fra Rpt,

  -- fordi Tx kan også være Pos, og ValtnUpd kan også være New, Mod, PosCmpnt m.fl. så undgås at angive flere

  -- XPath stier, men det koster med en større query plan og en langsommere svartid.

 RCPLEI   = RptList.e.value('(.//dns:RptgCtrPty/dns:Id/dns:LEI/text())[1]','varchar(20)'),

 OCPLEI   = RptList.e.value('(.//dns:OthrCtrPty/dns:Id/dns:LEI/text())[1]','varchar(20)'),

 SALEI    = RptList.e.value('(.//dns:SubmitgAgt/dns:LEI/text())[1]','varchar(20)'),

 Amount   = RptList.e.value('(.//dns:Valtn/dns:CtrctVal/dns:Amt/text())[1]','decimal(19,4)'),

 Currency = RptList.e.value('(.//dns:Valtn/dns:CtrctVal/dns:Amt/@Ccy)[1]','varchar(3)'),      -- an attribute

 TimeStmp = CAST(RptList.e.value('(.//dns:TmStmp/text())[1]','varchar(20)') AS datetime2(0)),

 Tp       = RptList.e.value('(.//dns:Tp/text())[1]','varchar(10)')

FROM @XmlTable.nodes('//rns:BizData') AS BizData(e)

    OUTER APPLY @XmlTable.nodes('//rns:BizData/rns:Pyld/dns:Document/dns:DerivsTradRpt/dns:TradData/dns:Rpt') AS RptList(e)

 

FromId

ToId

NbRcrds

RCPLEI

OCPLEI

SALEI

Amount

Currency

TimeStmp

Tp

TRDTI

ESMAS

3477

549300E5ENQVY2IBLF67

529900BJGD0X650NVB68

549300E5ENQVY2IBLF67

1122771.2600

EUR

23-01-2023 01:50

MTMA

TRDTI

ESMAS

3477

R0MUWSFPU8MPRO8K5P83

5493001GWWCBB16Z7U26

R0MUWSFPU8MPRO8K5P83

NULL

NULL

NULL

NULL

TRDTI

ESMAS

3477

549300E5ENQVY2IBLF67

315700NQ3QCBRX5PN732

549300E5ENQVY2IBLF67

NULL

NULL

NULL

NCNF

TRDTI

ESMAS

3477

549300U97W9KLUG0V074

529900M6JY6PUZ9NTA71

549300U97W9KLUG0V074

NULL

NULL

NULL

NCNF

TRDTI

ESMAS

3477

549300E5ENQVY2IBLF67

529900BJGD0X650NVB68

549300E5ENQVY2IBLF67

113477.2000

EUR

23-01-2023 01:50

MTMA

TRDTI

ESMAS

3477

315700BW3K8BT08B9523

549300E5ENQVY2IBLF67

549300E5ENQVY2IBLF67

274270.3400

EUR

24-01-2023 23:59

CCPV

 

Omdøbe skema navn på et tabel eller view eller stored procedure

fra [Crm].[Customer] til [Shared].[Customer]:

ALTER SCHEMA [Shared] TRANSFER [Crm].[Customer]

 

Omdøbe et feltnavn

Kontrollen med IF sikrer, at koden kan udføres flere gange uden fejl.

Sammenligning af små bogstaver og store bogstaver sker med Case-Sensitive.

Her omdøbes feltet standard til Standard.

IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS

           WHERE TABLE_SCHEMA = 'Shared' AND

                 TABLE_NAME = 'Customer' AND

                 COLUMN_NAME COLLATE Danish_Norwegian_CS_AS =

                   'standard' COLLATE Danish_Norwegian_CS_AS)

EXEC sp_rename 'Shared.Customer.standard', 'Standard', 'COLUMN';

 

Omdøbe et tabelnavn

Her omdøbes tabellen customer til Customer.

IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES

           WHERE TABLE_SCHEMA = 'Shared' AND

                 TABLE_NAME COLLATE Danish_Norwegian_CS_AS =

                    'customer' COLLATE Danish_Norwegian_CS_AS)

EXEC sp_rename 'Shared.customer', 'Customer';

 

Brugere på en SQL Server

SELECT sp.name AS LoginName ,sp.type_desc AS LoginType,

IIF(slog.sysadmin = 1 OR slog.securityadmin = 1 OR slog.serveradmin = 1 OR slog.setupadmin = 1 OR slog.processadmin = 1 OR slog.dbcreator = 1 OR slog.bulkadmin = 1,1,0) AS AdminDbRole

FROM sys.server_principals sp 

     INNER JOIN master..syslogins slog ON sp.sid = slog.sid

WHERE sp.type  <> 'R' AND sp.name NOT LIKE '##%' AND sp.name NOT LIKE 'NT SERVICE%' AND sp.name NOT LIKE 'NT AUTHORITY%'and sp.is_disabled = 0

 

Vis kørende bruger-processer – en overvågning, se deres SQL sætninger

Kræver adgang for almindelige brugere:

GRANT VIEW SERVER STATE TO [LAN\ad-gruppe];

 

SELECT login_name ,COUNT(session_id) AS session_count

FROM sys.dm_exec_sessions

GROUP BY login_name;

 

Kræver en indstilling i SSMS, hvor der under Tools og Option søges på xml:

 

 

 

SET DEADLOCK_PRIORITY -10;

SELECT

  DB_NAME(S.database_id) AS DB, T.text Query

, SUBSTRING(T.text, (R.statement_start_offset/2)+1, ((CASE R.statement_end_offset WHEN -1 THEN

  DATALENGTH(T.text) ELSE R.statement_end_offset END - R.statement_start_offset)/2) + 1) AS Statement

, P.query_plan

, R.status, S.status, R.command

, S.session_id, R.blocking_session_id, S.host_name, S.login_name, S.program_name

, S.login_time, S.last_request_start_time, S.last_request_end_time

, S.memory_usage, S.cpu_time s_cpu_time, S.total_scheduled_time, S.total_elapsed_time s_total_elapsed_time

, R.cpu_time r_cpu_time, R.total_elapsed_time r_total_elapsed_time

, S.reads s_reads, S.writes s_writes, S.logical_reads s_logical_reads, S.row_count s_row_count

, R.reads r_reads, R.writes r_writes, R.logical_reads s_Logical_reads, R.row_count r_row_count

, S.deadlock_priority, R.wait_time, R.wait_type, R.last_wait_type, R.wait_resource, R.open_transaction_count

, R.open_resultset_count, R.transaction_id, R.percent_complete, R.estimated_completion_time

FROM sys.dm_exec_sessions AS S WITH (NOLOCK)

     INNER JOIN sys.dm_exec_requests AS R WITH (NOLOCK) ON S.session_id = R.session_id

     OUTER APPLY sys.dm_exec_sql_text(R.sql_handle) AS T    -- function call

     OUTER APPLY sys.dm_exec_query_plan(R.plan_handle) AS P -- function call

WHERE ISNULL(T.text,'') <> 'sp_server_diagnostics' AND S.session_id <> @@SPID AND S.session_id >= 50 AND

      R.status <> 'background'

ORDER BY 1, S.session_id;

 

Wait type beskrivelser    Mere om PAGEIOLATCH    Endnu mere om PAGEIOLATCH

 

Flere eksempler på overvågninger

 

Vis kørende tabeller i TEMPDB og deres antal rækker her og nu

SELECT

  TableSchemaName = SCHEMA_NAME(t.schema_id),

  TableName       = t.name,

  TableRowCount   = FORMAT(SUM(p.rows) , 'N0', 'da-DK')

FROM tempdb.sys.tables t

     INNER JOIN tempdb.sys.partitions p ON t.object_id = p.object_id

                AND p.index_id IN(0, 1)

GROUP BY t.schema_id, t.name

 

Vis indholdet af en tabel i TEMPDB her og nu, f.eks. en global ## tabel

SELECT *

FROM tempdb.INFORMATION_SCHEMA.TABLES

WHERE TABLE_NAME = '##T_5361_b6ae9a336bbe43fea487f0f336fd3d9c'

 

Stoppe en forespørgsel på en SQL Server

SELECT OBJECT_NAME(P.object_id) AS TableName, Resource_type,

       Session_ID = request_session_id

FROM   sys.dm_tran_locks L

       INNER JOIN sys.partitions P

       ON L.resource_associated_entity_id = p.hobt_id

WHERE  OBJECT_NAME(P.object_id) = 'Archive_Customer'

 

KILL   angiv talværdien fra ovenstående Session_ID

 

Slet en bruger på en SQL Server

SELECT session_id

FROM sys.dm_exec_sessions

WHERE login_name = 'bruger'

 

KILL   angiv talværdien fra ovenstående Session_ID

DROP LOGIN 'bruger'

 

Kopiering af en tabel struktur

SELECT TOP(0) * INTO Archive_Customer

FROM dbo.Customer

 

Top(0) giver 0 rækker men vi får tabel strukturen til at danne den nye kopi tabel.

 

Felter i en tabel eller kolonner i en tabel (STRING_AGG)

SELECT COLUMN_NAME + ','

FROM INFORMATION_SCHEMA.COLUMNS

WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

 

Løbende tildeling af felter til en streng variabel kaldet aggregeret streng:

DECLARE @ColumnList varchar(max) = ''

SELECT @ColumnList = @ColumnList + ', ' + t.COLUMN_NAME

FROM

   (

    SELECT COLUMN_NAME

    FROM INFORMATION_SCHEMA.COLUMNS

    WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

          AND COLUMN_NAME NOT LIKE '%_meta'

   ) t

SET @ColumnList = SUBSTRING(@ColumnList, 3, LEN(@ColumnList))

SELECT @ColumnList

 

Eller ved at udbytte at @ColumnList starter som NULL som default værdi:

DECLARE @ColumnList varchar(max)

SELECT @ColumnList = CONCAT(@ColumnList + ', ', COLUMN_NAME)

FROM INFORMATION_SCHEMA.COLUMNS

WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

      AND COLUMN_NAME NOT LIKE '%_meta'

SELECT @ColumnList

 

Aggregeret streng eller string aggregation via xml til en streng variabel:

DECLARE @ColumnList varchar(max)

SET @ColumnList = SUBSTRING(

      (SELECT CONCAT(', ', QUOTENAME(COLUMN_NAME))

       FROM INFORMATION_SCHEMA.COLUMNS

       WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

             AND COLUMN_NAME NOT LIKE '%_meta'

       FOR XML PATH('')

      ), 3, 99999999)

SELECT @ColumnList

 

STUFF 1,2 erstatter strengens første to tegn ', ' med '' den tomme streng:

DECLARE @ColumnList varchar(max)

SET @ColumnList = STUFF(

      (SELECT CONCAT(', ', QUOTENAME(COLUMN_NAME))

       FROM INFORMATION_SCHEMA.COLUMNS

       WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

             AND COLUMN_NAME NOT LIKE '%_meta'

       FOR XML PATH('')

      ),1,2,'')

SELECT @ColumnList

 

STUFF er sjov:

SELECT STUFF('SQL Tutorial', 1, 3, 'Python')

becomes Python Tutorial

 

Aggregeret streng eller string aggregation via json til en streng variabel,

JSON fra version 2016:

DECLARE @ColumnList varchar(max)

SET @ColumnList = JSON_VALUE(REPLACE(

      (SELECT _ = QUOTENAME(COLUMN_NAME)

       FROM INFORMATION_SCHEMA.COLUMNS

       WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

             AND COLUMN_NAME NOT LIKE '%_meta'

       FOR JSON PATH),'"},{"_":"',', '),'$[0]._')

 

Aggregeret streng eller string aggregation via string aggregate funktion,

STRING_AGG fra version 2017:

DECLARE @ColumnList varchar(max)

SELECT @ColumnList = STRING_AGG(CONVERT(varchar(max),

                                QUOTENAME(COLUMN_NAME)), ', ')

                     WITHIN GROUP (ORDER BY ORDINAL_POSITION ASC)

FROM INFORMATION_SCHEMA.COLUMNS

WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'DimCustomer'

      AND COLUMN_NAME NOT LIKE '%_meta'

SELECT @ColumnList

 

Aggregeret streng med XML eller String_Agg:

 

CREATE TABLE #TestData (GroupID INT, Value NVARCHAR(50));

 

INSERT INTO #TestData

SELECT TOP 50000 GroupID = ABS(CHECKSUM(NEWID())) % 10000, Value = 'Item' + CAST(ROW_NUMBER() OVER (ORDER BY NEWID()) AS VARCHAR(10));

 

SELECT GroupID, STUFF((SELECT ',' + Value FROM #TestData t2 WHERE t1.GroupID = t2.GroupID FOR XML PATH('')), 1, 1, '') AS Aggregated

FROM #TestData t1 GROUP BY GroupID;

 

SELECT GroupID, STRING_AGG(Value, ',') AS Aggregated

FROM #TestData GROUP BY GroupID;

 

CONVERT(varchar(max) er en angivelse af input datatype som også bliver til out­put datatype der tilsidesætter default output datatype på 8000 tegn for varchar: »STRING_AGG aggregation result exceeded the limit of 8000 bytes. Use LOB types to avoid result truncation.«

 

Ønskes @ColumnList begrænset til 8000 tegn, så LEFT til at udtage de første 8000 tegn. ColumnList kan være et felt i en tabel med data typen varchar(8000):

 

DECLARE @ColumnList varchar(8000)

SELECT @ColumnList = LEFT(STRING_AGG(CONVERT(varchar(max),

                                QUOTENAME(COLUMN_NAME)), ', ')

                   WITHIN GROUP (ORDER BY ORDINAL_POSITION ASC), 8000)

 

Når et tekst felt er defineret således: [Tekst] [nvarchar](max) NULL

og ønskes afkortet til 4000 tegn eller 8000 tegn som varchar, må der castes:

 

[Tekst_4000] = CAST([Tekst] AS nvarchar(4000)),

[Tekst_8000] = CAST([Tekst] AS varchar(8000))

 

Med en linked server ønskes data overført dynamisk som en ny tabel hver gang, men tekst-kolonnerne har en anden collation som medtages i INTO men der ønskes anvendt modtager databasens collation som angives i SQL sætningen.

Vist med en kolonne:

 

DROP TABLE IF EXISTS [order].[Transaction]

SELECT [Reporter] = [ReporterName] COLLATE Danish_Norwegian_CI_AS

INTO [order].[Transaction]

FROM [<linkedserver>].[<database>].[order].[Transaction]

 

Kan ikke anvende COLLATE DATABASE_DEFAULT, fordi SELECT sætningen sendes over til den anden server via linked server, og de returnerede data ønskes at være med COLLATE Danish_Norwegian_CI_AS.

 

Vist med alle kolonner genereret med dynamisk sql, hvor der er taget højde for, at datatypen decimal bliver til numeric via linked server overførsel til ny tabel og der­for skal konverteres tilbage til decimal, selvom der gælder: »Decimal and Numeric are synonyms and can be used interchangeably.«

Måtte man have en xml kolonne, så må man i stedet anvende OpenQuery, hvor jeg har opfundet en dummy datatype xmlnvarchar til at indgå i Replace senere hen til oprindelige xml data type:

 

DECLARE @ColumnList varchar(max)

SELECT @ColumnList = STRING_AGG(CONVERT(varchar(max),

 CASE WHEN COLLATION_NAME IS NOT NULL

      THEN CONCAT(QUOTENAME(COLUMN_NAME),'=',QUOTENAME(COLUMN_NAME),

                                         ' COLLATE Danish_Norwegian_CI_AS')

      ELSE CASE WHEN DATA_TYPE = 'decimal'

                THEN CONCAT(QUOTENAME(COLUMN_NAME),'=',

                  'CONVERT(decimal(',NUMERIC_PRECISION,',',NUMERIC_SCALE,'),',

                  QUOTENAME(COLUMN_NAME),')')

                WHEN DATA_TYPE = 'xml'

                THEN CONCAT(QUOTENAME(COLUMN_NAME),'=',

                  'CONVERT(xmlnvarchar(max),',QUOTENAME(COLUMN_NAME),')')

                ELSE QUOTENAME(COLUMN_NAME)

           END

 END),',') WITHIN GROUP (ORDER BY ORDINAL_POSITION ASC)

FROM [<linkedserver>].[<database>].INFORMATION_SCHEMA.COLUMNS

WHERE TABLE_SCHEMA = 'order' AND TABLE_NAME = 'Transaction'

 

--SELECT @ColumnList

 

DECLARE @sql nvarchar(max)

SET @sql = CONCAT(

'DROP TABLE IF EXISTS [order].[Transaction]

SELECT ', @ColumnList, '

INTO [order].[Transaction]

FROM [<linkedserver>].[<database>].[order].[Transaction]')

--PRINT @sql

EXECUTE (@sql)

 

--When the table has a column of data type xml must use OpenQuery.

DECLARE @sql nvarchar(max)

SET @sql = CONCAT(

'DROP TABLE IF EXISTS [order].[Transaction]

SELECT ', REPLACE(@ColumnList,'xmlnvarchar(max)','xml'), '

INTO [order].[Transaction]

FROM OPENQUERY([<linkedserver>], ''SELECT ',

  REPLACE(@ColumnList,'xmlnvarchar(max)','nvarchar(max)'),

  ' FROM [<database>].[order].[Transaction]'')')

--PRINT @sql

EXECUTE (@sql)

 

Slet alle tabeller, views, stored procedures i en database

SET NOCOUNT ON

 

DECLARE @SQL AS NVARCHAR(MAX)

DECLARE @SchemaName SYSNAME

DECLARE @TableName SYSNAME

DECLARE @TableType SYSNAME

 

DECLARE @Scan CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR

  SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE

  FROM DM_PROD.INFORMATION_SCHEMA.TABLES  --Include tables and views.

  WHERE TABLE_SCHEMA != 'dbo'

  UNION ALL

  SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE

  FROM DM_PROD.INFORMATION_SCHEMA.ROUTINES –Include stored procedures.

  WHERE ROUTINE_SCHEMA != 'dbo'

  ORDER BY TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE

 

OPEN @Scan

FETCH NEXT FROM CursorName INTO @SchemaName, @TableName, @TableType

WHILE @@FETCH_STATUS = 0

BEGIN

  IF @TableType = 'BASE TABLE'

  BEGIN

    SET @SQL = N'DROP TABLE IF EXISTS ['+ @SchemaName+'].['+ @TableName+ ']'

    --PRINT @SQL

    EXEC [OrdreMart].sys.sp_executesql @SQL

  END

  IF @TableType = 'VIEW'

  BEGIN

    SET @SQL = N'DROP VIEW IF EXISTS ['+ @SchemaName+'].['+ @TableName+ ']'

    --PRINT @SQL

    EXEC [OrdreMart].sys.sp_executesql @SQL

  END

  IF @TableType = 'PROCEDURE'

  BEGIN

    SET @SQL = N'DROP PROCEDURE IF EXISTS ['+ @SchemaName+'].['+ @TableName+ ']'

    --PRINT @SQL

    EXEC [OrdreMart].sys.sp_executesql @SQL

   END

   FETCH NEXT FROM CursorName INTO  @SchemaName, @TableName, @TableType

END

CLOSE @Scan

DEALLOCATE @Scan

 

Bemærk

DROP PROCEDURE IF EXISTS [base].[Customer_Rep]

Msg 3705, Level 16, State 1, Line 3

Cannot use DROP PROCEDURE with 'base.Customer_Rep' because 'base.Customer_Rep' is a table. Use DROP TABLE.

Tjek inden drop sætningen om det er en stored procedure:

IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.ROUTINES

           WHERE ROUTINE_SCHEMA = 'base' AND ROUTINE_NAME = 'Customer_Rep')

   DROP PROCEDURE [base].[Customer_Rep];

 

Seneste Identity værdi for alle tabeller i en database

WITH cteColumns AS (

    SELECT OBJECT_SCHEMA_NAME(c.object_id) AS [SchemaName],

           OBJECT_NAME(c.object_id) AS [TableName]

    FROM sys.all_columns c

         INNER JOIN sys.types t ON c.system_type_id = t.system_type_id

    WHERE c.is_identity = 1

    AND t.name IN ('int', 'bigint', 'smallint', 'tinyint')

)

SELECT [SchemaName], [TableName],

       IDENT_CURRENT([SchemaName] + '.' + [TableName]) AS [IdentityValue]

FROM cteColumns

ORDER BY [IdentityValue] DESC

 

Felter i et view eller en tabel

SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME,

       COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE

FROM INFORMATION_SCHEMA.COLUMNS

WHERE TABLE_SCHEMA = 'schema' AND TABLE_NAME = 'view/table'

ORDER BY ORDINAL_POSITION

 

SELECT OBJECT_SCHEMA_NAME(c.object_id) AS 'Schema',

       OBJECT_NAME(c.object_id) AS 'View', c.name AS 'Column'

FROM sys.columns AS c

     INNER JOIN sys.views AS v ON c.object_id = v.object_id

WHERE v.name = 'navn på et view'