SQL-fråga med CROSS APPLY

APPLY (msdn) är ett nytt SQL-kommando i SQL Server från och med version 2005 och finns i två olika varianter, CROSS APPLY och OUTER APPLY.

Kan inte säga att jag förstått dessa funktionerna helt, men jag har hittat ett bra användningsområde för CROSS APPLY. Nämligen att kunna bygga upp VIEWs med data från en relationstabell och sammanfoga dessa i en ny kolumn separerade med valfritt tecken. Detta kallas tydligen "aggregate concatenation".

Läste även att denna metod går emot huvudideén för hur en relationsdatabas är tänkt att fungera, så har du ett behov att göra såna här SQL-frågor kan det vara snart att tänka om ifall det går att göra på ett bättre sätt. Eller kanske hämta ut datan separat och bygga ihop vyn i buisness-lagret.  

Kommer här visa hur det kan fungera genom ett exempel, här först databasdiagrammet.

 

Sen ett skript (skapat med SubSonic) som bygger upp hela databasen:

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Products]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Products](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [ProductName] [nvarchar](150) COLLATE Finnish_Swedish_CI_AS NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
END
/****** Object:  Table [dbo].[Colors]    Script Date: 07/07/2008 16:58:21 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Colors]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Colors](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [ColorName] [nvarchar](150) COLLATE Finnish_Swedish_CI_AS NULL,
 CONSTRAINT [PK_Colors] PRIMARY KEY CLUSTERED
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
END
/****** Object:  Table [dbo].[ProductColor]    Script Date: 07/07/2008 16:58:21 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[ProductColor]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[ProductColor](
    [ProductId] [int] NOT NULL,
    [ColorId] [int] NOT NULL,
 CONSTRAINT [PK_ProductColor] PRIMARY KEY CLUSTERED
(
    [ProductId] ASC,
    [ColorId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
END
/****** Object:  View [dbo].[ListOfColorsView]    Script Date: 07/07/2008 16:58:21 ******/
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
IF NOT EXISTS (SELECT * FROM sys.views WHERE object_id = OBJECT_ID(N'[dbo].[ListOfColorsView]'))
EXEC dbo.sp_executesql @statement = N'CREATE VIEW dbo.ListOfColorsView
AS
SELECT DISTINCT ProductName, ListOfColors = LEFT(o.list, LEN(o.list) - 1)
FROM         Products CROSS APPLY
                          (SELECT     CONVERT(VARCHAR(12), ColorName) + '','' AS [text()]
                            FROM          dbo.Colors c INNER JOIN
                                                   dbo.ProductColor ON c.Id = dbo.ProductColor.ColorId
                            WHERE      (dbo.ProductColor.ProductId = dbo.Products.Id)
                            ORDER BY c.ColorName FOR XML PATH('''')) o(list)
'
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_ProductColor_Colors]') AND parent_object_id = OBJECT_ID(N'[dbo].[ProductColor]'))
ALTER TABLE [dbo].[ProductColor]  WITH CHECK ADD  CONSTRAINT [FK_ProductColor_Colors] FOREIGN KEY([ColorId])
REFERENCES [Colors] ([Id])
ALTER TABLE [dbo].[ProductColor] CHECK CONSTRAINT [FK_ProductColor_Colors]
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_ProductColor_Products]') AND parent_object_id = OBJECT_ID(N'[dbo].[ProductColor]'))
ALTER TABLE [dbo].[ProductColor]  WITH CHECK ADD  CONSTRAINT [FK_ProductColor_Products] FOREIGN KEY([ProductId])
REFERENCES [Products] ([Id])
ALTER TABLE [dbo].[ProductColor] CHECK CONSTRAINT [FK_ProductColor_Products]

Och lite testdata:

ALTER TABLE [Colors] NOCHECK CONSTRAINT ALL
GO
ALTER TABLE [Colors] DISABLE TRIGGER ALL
GO

SET IDENTITY_INSERT [Colors] ON
PRINT 'Begin inserting data in Colors'
INSERT INTO [Colors] ([Id], [ColorName])
VALUES(1, 'Red')
INSERT INTO [Colors] ([Id], [ColorName])
VALUES(2, 'Blue')
INSERT INTO [Colors] ([Id], [ColorName])
VALUES(3, 'Green')
SET IDENTITY_INSERT [Colors] OFF
ALTER TABLE [Colors] CHECK CONSTRAINT ALL
GO

ALTER TABLE [Colors] ENABLE TRIGGER ALL
GO



ALTER TABLE [Products] NOCHECK CONSTRAINT ALL
GO
ALTER TABLE [Products] DISABLE TRIGGER ALL
GO

SET IDENTITY_INSERT [Products] ON
PRINT 'Begin inserting data in Products'
INSERT INTO [Products] ([Id], [ProductName])
VALUES(1, 'Test product 1')
INSERT INTO [Products] ([Id], [ProductName])
VALUES(2, 'Test product 2 - No Colors')
SET IDENTITY_INSERT [Products] OFF
ALTER TABLE [Products] CHECK CONSTRAINT ALL
GO

ALTER TABLE [Products] ENABLE TRIGGER ALL
GO



ALTER TABLE [ProductColor] NOCHECK CONSTRAINT ALL
GO
ALTER TABLE [ProductColor] DISABLE TRIGGER ALL
GO

PRINT 'Begin inserting data in ProductColor'
INSERT INTO [ProductColor] ([ProductId], [ColorId])
VALUES(1, 1)
INSERT INTO [ProductColor] ([ProductId], [ColorId])
VALUES(1, 2)
INSERT INTO [ProductColor] ([ProductId], [ColorId])
VALUES(1, 3)
ALTER TABLE [ProductColor] CHECK CONSTRAINT ALL
GO

ALTER TABLE [ProductColor] ENABLE TRIGGER ALL
GO

Det mest intressanta är ju själva vyn som separerar datan från relationstabellen med ett kommatecken mellan varje värde.

SELECT DISTINCT ProductName, ListOfColors = LEFT(o.list, LEN(o.list) - 1)
FROM         Products CROSS APPLY
                          (SELECT     CONVERT(VARCHAR(12), ColorName) + ',' AS [text()]
                            FROM          dbo.Colors c INNER JOIN
                                                   dbo.ProductColor ON c.Id = dbo.ProductColor.ColorId
                            WHERE      (dbo.ProductColor.ProductId = dbo.Products.Id)
                            ORDER BY c.ColorName FOR XML PATH('')) o(list)

Svaret från denna vyn ser ut så här:

Test product 1    Blue,Green,Red
Test product 2 - No Colors    NULL

Mer läsning:

http://zulfiqar.typepad.com/zulfiqars_web/2005/04/tsql_concatenat.html

http://blog.sqlauthority.com/2008/01/04/sql-server-2005-cross-apply/

By Jesper Lind

For-loop till SQL Server

Har funderat på ibland hur man kan göra foor-loop i SQL Server. Efter en sökning hittade jag svaret hos SQLAuthority som också visar upp exempel på break och continue.

I mitt fall ville jag köra en insert för varje loopning och mitt skript såg ut ungefär som följer. Variablen intFlag används också för att sätta in ett stigande heltal i tabellen.

DECLARE @intFlag INT
SET @intFlag = 1
WHILE (@intFlag <=100)
  BEGIN
        PRINT @intFlag

        INSERT INTO TheTable (IntValue,StringValue,CounterValue) VALUES (1,'MyDefaultString,@intFlag))
  SET @intFlag = @intFlag + 1
  END
GO

By Jesper Lind

Kopiera innehåll i tabeller mellan databaser

Vi har tidigare visat hur man kan göra för att kopiera ett enskild fält i SQL Server mellan två databaser. Idag är det dags för att visa hur man kopierar all data i en tabell. Det man ska tänka på är att tillfälligt tillåta insättning av primärnycklar, för att dessa ska gå att kopiera.

SET IDENTITY_INSERT [TargetDB].[dbo].[TableToCopy] ON
INSERT INTO [TargetDB].[dbo].[TableToCopy] (Id,Field1,AnotherField,ABoolField)
    SELECT Id,Field1,AnotherField,ABoolField FROM [SourceDB].[dbo].[TableToCopy]
SET IDENTITY_INSERT [TargetDB].[dbo].[TableToCopy] OFF
By Jesper Lind

Övervaka databaskopplingar med sp_who2

Att vara slarvig med att stänga databaskopplingar i en webb-app kan ha förödande effekt på prestanda. Att spåra buggarna kan vara svårt ifall man inte är säker på hur man gör.

Ett bra tips kan vara att använda något av följande kommandon i SQL-managern.

EXEC sp_who

EXEC sp_who 'active'

Det finns även en odokumenterad version som heter sp_who2 och som visar några fler parametrar som CPUTime, DiskIO, LastBatch och ProgramName. Samma som innan alltså fast med en 2:a på slutet.

EXEC sp_who2

Med dessa kommandon får man upp en lista över de kopplingar som är igång och dess parametrar. Ifall det är fler än 100-200 stycken samtidigt så kan det betyda att man har problem. En normal sajt bör endast ha några få kopplingar öppna åt gången.

När det börjar gå riktigt åt skoken så kommer följande felmeddelande dyka upp i event-loggarna.

The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.

Mer läsning om sp_who finns hos dotnetyuppie eller om hur man bör stänga kopplingar i sin kod på 15seconds

By Jesper Lind

SQL-skriptet som räddade våra data

Att förlora stora mängder av data är aldrig roligt. Vi hade just ett sådant upplevelse där ett felkonfigurerat skript uppdaterade ALLA rader i databastabellen istället för den tänkta enradsuppdateringen. En riktig klassiker med andra ord.

Som tur var hade vi en relativt färsk backup, och Glenn kunde förda över datan från backup-databasen, med ett ihopknåpat superskript. Så här ser det ut för den som är intresserad.

UPDATE [RealDB].[dbo].[TheTable] SET LostDataColumn = (SELECT LostDataColumn FROM [BackupDB].[dbo].[TheTable] WHERE [BackupDB].[dbo].[TheTable].Id = [RealDB].[dbo].[LostDataColumn].Id)

By Jesper Lind

SubSonics skapare anställs av Microsoft

Rob Conery, skaparen av det automagiska databaslagret SubSonic, tackar ja till en anställning av Microsoft och kommer börja redan om några veckor.

Här på Code Odyssey har vi börjat använda SubSonic i våra projekt och det är verkligen en fantastisk hjälp när man jobbar med SQL-Server. Men eftersom verktyget är öppen källkod och inte har varit sammanknutet med Microsoft så har vi varit lite nervösa inför framtiden.

Vi har funderat på hur det kommer se ut när LINQ är redo att använda. Kommer det vara ännu bättre än SubSonic, och kommer vi då lockas att byta DAL-lösning en gång till. Har vi satsat på rätt teknik och hur framtidssäker är den?

Men med skaparen på plats i Redmond känns det inte osäkert längre. Rob kommer få i uppgift att bygga ihop SubSonic med det nya MVC-ramverket som är under utveckling. Själva SubSonic kommer även fortsättningsvis vara öppen källkod under MPL 1.1-licens.

Om du inte har hört något om Microsofts planerade Model-view-controller (MVC) arkiterktur, så rekommenderar jag läsa hos Scott Guttrie och Scott Hanselman. Hos den senare finns video-presentationerna från ALT.NET-konferensen i Austin där MVC-ramverket presenterades publikt för första gången.

By Jesper Lind

Få insyn i databasen med SQL Server 2005 Express Profiler

På de dyrare licenserna av MS SQL Server så får man med Profiler som är ett bra verktyg för att se vilka querys som körs. Denna funktion finns tyvärr inte på SQL Server Express.

Det finns dock ett open source-alternativ som heter SQL Server 2005 Express Profiler och finns att ladda hem på Google Code.

Programmet bjuder inte på några fantastiska finesser men klarar av det grundligaste. Förhoppningsvis blir det bättre i framtida versioner. Tack till vår Glenn för fyndet. 

By Jesper Lind

Krymp stora loggfiler på SQL Server

Har man ingen strategi för att ta backup av loggfilerna för transaktionerna kan dessa växa något enormt. Då kan det bli läge att krympa ner dem och jobba fram en struktur så att de ska hålla sig på en rimlig nivå.

En del tips på nätet går ut på att man ska ta bort transaktions-loggen helt. Då återskapas en ny som startar om från början. Även om detta kan vara ett effektivt sätt, så är det inget som rekommenderas av Microsoft.

Istället bör man se till att göra backup på loggen, då frigörs det utrymme som inte behövs. Här följer några kommandon som fungerade bra för mig. Obs var försiktigt när du jobbar med filerna och läs på ordentligt innan precis vad kommandona gör. Jag använde SQL 2005, med detta fungerar säkert på SQL 2000 också.

1. Först skapade jag en Checkpoint

Use MyDatabaseName
go
Checkpoint

2. Sen gjorde jag en "backup" av loggfilen med alternativet "TRUNCATE_ONLY". Det innebär att ingen backupfil skapas, den frigör bara det utrymme på redan avklarade transaktioner som är onödiga att spara. Med kommandot "SHRINKFILE" förminskas loggfilen, i detta fall till 30MB

BACKUP LOG MyDatabaseName WITH TRUNCATE_ONLY DBCC SHRINKFILE('DatabasNamn_Log', 30)

The Code Project visar hur man kan göra detta genom programmering. Kolla även Webbproffs där jag gjort lite fler anteckningar. 

By Jesper Lind

Felsök: Error 15023: User, group, or role already exists in the current database

Nu är det dags för oss att börja att brottas ordentligt med SQL Server 2005, och vi håller på att flytta en del databaser från 2000.

Något som kan ställa till med problem är att de gamla användarna följer med när man importerar databaserna. Vi har tidigare visat hur man kan ändra ägare på tabeller. Enligt min erfarenhet är det bäst att alltid köra med 'dbo' som ägare.

Vid import kan det bli en del konstigheter som resulterar att man inte kan ändra rättigheter alls en användare, och man får följande felmeddelande.

User, group, or role already exists in the current database. (Microsoft SQL Server, Error: 15023)

Detta beror på att Security identification numbers (SID) förväxlade eller 'föräldrarlösa' i sysusers-tabellen, som Falafel Software förklarar.

För att se en lista på övergivna inloggningar kan man göra följade stored procedure:

exec sp_change_users_login Report

SQLAuthority har exempel på skript som kan reparera felaktigheter på databas-användare.

By Jesper Lind