luglio 2008 - Posts

SQL Server Blog
31 luglio 08 03.18 | abenedetti | with no comments

A futura memoria personale... e come semplice suggerimento... :-)

Filed under: ,
Cluster Index, Datapages, BTree, DBCC Page, DBCC Ind... un'occhiata dietro le quinte
31 luglio 08 03.07 | abenedetti | with no comments

Spesso mi rendo conto che capire come SQL Server gestisca le sue strutture ad albero, le datapages, le IAM, gli extent, ... non sia così semplice.

Mi piacerebbe allora, con questo post, costruire un esempio che possa raccontare in maniera semplice il meccanismo con cui il database engine mantiene e gestisce le informazioni delle nostre tabelle.

Per farlo costruiremo un database di test che andrà a contenere una sola tabella con una struttura molto semplice.

set nocount on USE master GO IF EXISTS (SELECT name FROM sys.databases WHERE name = N'testRootPage') DROP DATABASE testRootPage GO CREATE DATABASE testRootPage GO USE testRootPage GO

Definito il database andiamo a costruire la tabella che utilizzeremo nel nostro esempio.

Prima di proseguire ricordiamo tre concetti:

  1. SQL Server memorizza i dati in pagine, unità elementari ognuna di 8 Kb
  2. SQL Server mantiene i dati memorizzato in una struttura ad albero bilanciato (BTree+)
  3. Parlando di indici cluster (per ridurre il campo di attenzione): al livello foglia (leaf level) vengono mantenute la pagine contenenti i dati (le righe della tabella). Nei livelli superiori solo la chiave dell'indice (quindi il valore stesso della chiave che è stata definita, che sia composta da una o più colonne)

La nostra tabella la disegniamo in modo tale da poter contenere una singola riga per ogni pagina dati.

Inoltre, sempre per semplicità, definiamo una colonna idRecord, che utilizzeremo come chiave, con una dimensione di 93 bytes in modo che abbia, con i 7 bytes di overhead necessari alla gestione dei metadati, una dimensione di riga sulle pagine "non leaf-level" di 100 bytes (concetto numero 3 poco sopra - ovvero: in ogni pagina indice, quindi in tutte i livelli tranne quello foglia, le pagine potranno contenere tante righe quante ne stanno in 8060 bytes. Quindi 8060 bytes disponibili / 100 bytes per la dimensione della chiave = 80 righe per pagina.).

create table test ( idRecord char(93) not null, note char (7500) ) GO -- definisco la PK cluster sulla colonna idRecord alter table test add constraint tkTest primary key clustered (idRecord) go

Inserisco cento righe così da ottenere 100 data pages a livello foglia.

declare @i int = 100 while @i > 0 begin insert test (idRecord, note) values ('abcde' + CAST(@i as varchar(10)), 'valore che scrivo nella riga') set @i -= 1 end go select COUNT(*) as numRows from test go

La select count torna numRows = 100, ovvero significa che ho 100 righe al livello foglia del nostro albero BTree (e, quindi, dovrei avere 100 data pages).

Vediamo se è vero analizzando come è stato costruito l'albero dei nostri dati:

select index_type_desc, alloc_unit_type_desc, index_depth, index_level, page_count, record_count, avg_record_size_in_bytes from sys.dm_db_index_physical_stats(db_id(N'testRootPage'), object_id('dbo.test'), NULL, NULL, 'DETAILED') order by index_level desc go

image

Con il risultato ottenuto posso vedere che la struttura ad albero costruita dal database engine per memorizzare i miei dati è costituta da 3 livelli.

Il livello della ROOT è il livello più alto (2 nel mio esempio) fino ad arrivare a 0 per i dati. Ovvero:

  • la pagina di root (index_level = 2, page_count = 1, record_count =2)
  • un secondo livello (index_level = 1, page_count = 2, record_count = 100)
  • il livello foglia, ovvero i dati (index_level = 0, page_count = 100, record count = 100)

Riepilogando:

  • nella pagina di root ho due righe (quindi so che la root avrà due figli ed infatti il livello successivo è fatto di due pagine)
  • nel livello 1 ho due pagine che contengono un totale di 100 righe (infatti il livello foglia è fatto di 100 pagine)
  • nel livello foglia ho 100 pagine dati (una per record)

A questo punto voglio visualizzare informazioni su tutte le pagine utilizzate.

Per farlo utilizzo il comando DBCC IND anando a memorizzare il suo risultato all'interno di una tabella temporanea (per avere una successiva semplicità di interrogazione):

if object_id('tempdb..#ind') is not null drop table #ind go create table #ind ( PageFID tinyint, PagePID int, IAMFID tinyint, IAMPID int, ObjectID int, IndexID tinyint, PartitionNumber tinyint, PartitionID bigint, iam_chain_type varchar(30), PageType tinyint, IndexLevel tinyint, NextPageFID tinyint, NextPagePID int, PrevPageFID tinyint, PrevPagePID int ) go /* eseguo il comando */ insert #ind exec ('DBCC IND (''testRootPage'', ''test'', 1) with tableresults') go /* interrogo il risultato */ select pageFID, pagePID, pageType, indexLevel, case when pageType = 10 then 'IAM' when pageType = 2 then 'index page' when pageType = 1 then 'data pages' else 'non definito' end as pageType, nextPagePID, prevPagePID from #ind order by isnull(indexLevel,255) desc go

Questo il risultato della SELECT:

image

Ovvero 104 righe rappresentanti:

  1. una riga per la pagina IAM
  2. una riga per la pagina di ROOT
  3. due righe per le due pagine del primo livello
  4. cento righe per il livello foglia (le pagine dati)

Cosa posso leggere sul risultato?

  • pagina numero 55: IAM
  • pagina numero 248: pagina di ROOT del nostro albero BTree+
  • pagine numero 249, 77: pagine del livello 1
  • ... tutte le pagine dati ...

Se volessi rappresentare l'albero in forma grafica:

image

Nota: l'immagine, per problemi di spazio, non contiene tutte le pagine del livello foglia (100).

 

Ora che conosco "l'indirizzo" delle mie pagine (conosco il PageFID ed il PagePID, ovvero ID del file e ID della pagina) posso andare a metterci il naso per vederne il contenuto.

Per esempio apriamo la pagina di ROOT per vedere le informazioni che contiene.

Per farlo utilizziamo il comando (non documentato):
dbcc page ( {'dbname' | dbid}, filenum, pagenum [, printopt={0|1|2|3} ])

Le opzioni di stampa (printopt) del risultato:
0 - solo page header
1 - page header + per-row hex dumps e dump page slot array
2 - page header + page hex dump
3 - page header + detailed per-row

Prima di lanciare il comando, devo fare in modo che il risultato mi venga presentato a video (e non tracciato soltanto nel log) abilitando il flag 3604 come:

DBCC TRACEON (3604) DBCC Page (testRootPage, 1, 248, 3)

Questo il contenuto della pagina di ROOT:

image

In pratica abbiamo soltanto due righe, come già mi aspettavo (quando abbiamo analizzato poco sopra come è stato costruito l'albero dei nostri dati l'engine ci aveva già detto che la ROOT aveva due record).

Vedo: ChildPageId = 77 e 249 (che sono proprio le pagine del livello 1)

Se visualizzo il contenuto delle pagine del livello 1 potrò quindi ottenere l'elenco delle pagine figlie, ovvero delle foglie di ciascun nodo.

La pagina 77:

DBCC Page (testRootPage, 1, 77, 3)

image

La pagina 249:

DBCC Page (testRootPage, 1, 249, 3)

image

Se voglio mettere il naso nelle pagine del livello foglia, ovvero dove sono contenuti i dati, ricorro sempre al comando DBCC PAGE:

DBCC Page (testRootPage, 1, 270, 3)

Nel risultato, che mi mostra il contenuto della pagina, posso anche vedere il contenuto del record:

image

[SQL 2008] Panoramica FullText
30 luglio 08 12.27 | abenedetti | 1 comment(s)

Le funzionalità di indicizzazione nella nuova versione di SQL Server sono molto differenti rispetto alla precedente.

Differenti sia da un punto di vista di architettura, sia da un punto di vista di programmazione ed utilizzo.

Il motore fulltext, oggi, non è più in un processo separato, ma risiede direttamente all'interno dello stesso processo del motore del database (non esiste più il servizio MSFTESQL).

Allo stesso modo non è più necessario avere un filegroup dedicato (è tutto nel db – che si traduce anche a poter eseguire un backup / restore senza dover ricostruire gli indici).

Questa l’architettura in SQL Server 2008:

clip_image001

Il motore fornisce due attività ben precise:

  • supporto all'indicizzazione
  • supporto alle interrogazioni

Amminstrare queste caratteristiche si traduce in quattro diverse attività:

  • creare indici e cataloghi fulltext
  • modificare indici e cataloghi fulltext
  • cancellare indici e cataloghi fulltext
  • schedulare e mantenere il corretto popolamento (allineamento) degli indici

Nota: in SQL Server 2008, di default, tutti i database sono abilitati al fulltext

Per poter utilizzare le funzionalità è necessario definire le colonne delle tabelle che vogliamo siano accessibili tramite il motore di indicizzazione.

Ovvero:

  1. creare un catalogo fulltext che possa mantenere gli indici
  2. creare un indice fulltext sulla tabella (o vista indicizzata)

Proviamo a disegnare uno scenario, magari utilizzando il FILESTREAM di SQL Server 2008, e vediamo come poter creare ed utilizzare il motore di indicizzazione.

Abbiamo una tabella [documenti], modellata come segue:

CREATE TABLE [dbo].[documenti]( [idDocumento] [uniqueidentifier] ROWGUIDCOL NOT NULL PRIMARY KEY, [nomeFile] [varchar](35) NULL, [body] [varbinary](max) FILESTREAM NULL ) ON [PRIMARY] FILESTREAM_ON [FILESTREAMGROUP]

Vediamo i passi necessari per costruire un catalogo ed interrogarlo:

use myDatabase go /* creo il catalogo */ CREATE FULLTEXT CATALOG myDatabaseFTCat; go /* creo indice univoco */ CREATE UNIQUE INDEX ui_documenti ON dbo.documenti(idDocumento); go /* creo una stoplist */ CREATE FULLTEXT STOPLIST myDatabaseStopList; GO /* creo indice fulltext */ CREATE FULLTEXT INDEX ON dbo.documenti ( body TYPE COLUMN nomeFile Language 1040 ) KEY INDEX ui_documenti ON myDatabaseFTCat WITH CHANGE_TRACKING AUTO GO /* qualche esempio di ricerca */ DECLARE @SearchWord varchar(30) SET @SearchWord ='"schemi" AND "relazionali"' SELECT * FROM dbo.documenti WHERE CONTAINS(body, @SearchWord); go DECLARE @SearchWord varchar(30) SET @SearchWord ='relazionali er' SELECT * FROM dbo.documenti WHERE FREETEXT(body, @SearchWord); DECLARE @SearchWord varchar(30) SET @SearchWord = 'schemi' SELECT nomeFile, T.rank FROM dbo.documenti D join FREETEXTTABLE(dbo.Documenti, body, @SearchWord) as T on D.idDocumento = T.[key] order by T.rank desc

 

 

/* *** NOTE *** */ /* per vedere se il db ha il fulltext abilitato */ SELECT DATABASEPROPERTYEX('myDatabase', 'IsFullTextEnabled'); /* per verificare proprietà del catalogo */ SELECT FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'Populatestatus') as populateStatus, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'AccentSensitivity') as AccentSensitivity, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'IndexSize') as IndexSize, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'ItemCount') as ItemCount, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'LogSize') as LogSize, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'UniqueKeyCount') as UniqueKeyCount, FULLTEXTCATALOGPROPERTY('myDatabaseFTCat', 'ImportStatus') as ImportStatus; go /* la tabella delle lingue */ select * from sys.fulltext_languages order by name /* informazioni sul contenuto dell'indice */ SELECT * FROM sys.dm_fts_index_keywords(db_id('myDatabase'), object_id('dbo.documenti')) /* Query per recuperare il numero di frammenti di ogni indice full-text Se ne esistono diversi --> REORGANIZE per migliorare prestazioni */ select t.name as TableName , f.data_size , f.row_count , case f.status when 0 then 'Newly created and not yet used' when 1 then 'Being used for insert' when 4 then 'Closed ready for query' when 6 then 'Being used for merge inpurt and ready for query' when 8 then 'Marked for deletion. Will not be used for query and merge source' else 'Unknown status code' end from sys.fulltext_index_fragments f join sys.tables t on f.table_id = t.object_id;

clip_image003

Qualche query per mettere il naso dentro il motore...

SELECT * FROM sys.dm_fts_index_keywords_by_document (db_id('myDatabase'), object_id('dbo.documenti'))

clip_image005

 

select * from sys.dm_fts_parser ('"schemi relazionali"', 1040, (select stoplist_id from sys.fulltext_indexes),0)

 

clip_image007

select * from sys.dm_fts_parser ('"sia schemi che relazionali"', 1040, (select stoplist_id from sys.fulltext_indexes),0)

clip_image009

Un esempio di piano di esecuzione:

clip_image011

Nota: di default SQL Server non ha la capacità di indicizzare file PDF.

E' però possibile installare il filtro che Adobe rilascia gratuitamente.

http://www.adobe.com/support/downloads/detail.jsp?ftpID=2611

[SQL Server] Spostare il database tempdb
24 luglio 08 12.14 | abenedetti | with no comments

Può capitare la necessità di spostare il database di sistema tempdb su un percorso differente.

Magari potremmo volerlo fare dopo che il nostro server è stato attrezzato di un disco che sarà dedicato allo storage di questo database :-)

Spostare i files del db è un'operazione molto semplice che richiede, tuttavia, il riavvio dell'istanza.

Per prima cosa identifico i nomi logici dei file (mi serviranno per l'operazione di ALTER DATABASE), quindi eseguo una modifica sul database invocando una MODIFY FILE.

Ovvero:

USE tempdb GO EXEC sp_helpfile GO

image

USE master GO ALTER DATABASE tempdb MODIFY FILE (NAME = tempdev, FILENAME = 'C:\temp\tempdb.mdf') GO ALTER DATABASE tempdb MODIFY FILE (NAME = templog, FILENAME = 'C:\temp\tempdb.ldf') GO

image

A questo punto riavvio l'istanza e la modifica sarà operativa.

Verificare, in maniera certa, un backup: faccio il restore!
17 luglio 08 04.27 | abenedetti | 2 comment(s)

Come fa anche l'amico Luca, mi sforzo spesso di ripetere che l'unico modo sicuro di essere certi di un backup è quello di farne un restore.

Purtroppo sembra che questa legge non sia così conosciuta...

La cosa interessante è che possiamo automatizzare tutto il processo di verifica con uno sforzo pari a zero!

Ovvero costruire una procedura che prenda in ingresso:

  1. il backup da verificare
  2. un path su cui "appoggiare" i file del database di cui faremo il restore

Chiaramente effettueremo un restore su un db differente da quello di produzione, penso fosse chiaro ma preferisco esplicitarlo :-)

Per risolvere il nostro problema abbiamo una precisa strada da percorrere:

  1. tenere traccia della versione di SQL Server in uso (il comando RESTORE FILELISTONLY produce risultati differenti)
  2. recuperare, tramite la RESTORE FILELISTONLY, i nomi logici e fisici dei file che compongono il database backuppato
  3. costruire un'opportuna istruzione di RESTORE con
    • un nome di database che non esista (io uso un guid per sicurezza)
    • clausole MOVE per creare nuovi file fisici
  4. eseguire l'istruzione di RESTORE
  5. eseguire l'istruzione di DROP DATABASE per ripulire le attività

Ovvero, traducendo in TSQL, qualcosa come:

 

if object_id('dbo.up_verifyBackup') IS NOT NULL drop procedure dbo.up_verifyBackup GO create procedure dbo.up_verifyBackup ( @pathBackupDb nvarchar(200), @pathTestRestore nvarchar(200) ) as set nocount on /* Parametri in ingresso: @pathTestRestore: il path dove voglio andare a scrivere i file che compongono il db da restorare @pathBackupDb: il path completo del backup di cui voglio fare test di restore Esempio di esecuzione: exec dbo.up_verifyBackup @pathBackupDb = 'C:\myBackupFolder\myBackup.bak', @pathTestRestore = 'C:\myFolder' */ /* dichiarazione variabili d'appoggio */ declare @tsql nvarchar(4000) declare @databaseName varchar(36) declare @posizioneVirgola int print convert(varchar(10), GETDATE(), 103) + ' ' + convert(varchar(8), GETDATE(), 108) + ' Impostazione variabili' /* tengo traccia della versione di SQL Server 9 = sql server 2005 10 = sql server 2008 questo perchè il risultato ritornato dalla RESTORE FILELISTONLY è differente tra le due versioni */ declare @sqlServerVersion tinyint select @sqlServerVersion = LEFT(CAST( SERVERPROPERTY('productversion') as varchar(15)), CHARINDEX ('.', CAST( SERVERPROPERTY('productversion') as varchar(15))) -1) print '@sqlServerVersion = ' + cast(@sqlServerVersion as varchar(2)) /* comando per recuperare i nomi logici dei file database */ set @tsql = 'RESTORE FILELISTONLY FROM DISK = ''' + @pathBackupDb + '''' print '@pathTestRestore = ' + @pathTestRestore print '@pathBackupDb = ' + @pathBackupDb print '@tsql = ' + @tsql print '' /* creo una tabella temporanea che conterrà il risultato della RESTORE FILELISTONLY devo interrogarla successivamente per recuperare i nomi file */ if object_id('tempdb..#fileListOnly') is not null drop table #fileListOnly create table #fileListOnly ( LogicalName nvarchar(128), PhysicalName nvarchar(260), Type char(1), FileGroupName nvarchar(128), Size numeric(20,0), MaxSize numeric(20,0), FileID bigint, CreateLSN numeric(25,0), DropLSN numeric(25,0) NULL, UniqueID uniqueidentifier, ReadOnlyLSN numeric(25,0) NULL, ReadWriteLSN numeric(25,0) NULL, BackupSizeInBytes bigint, SourceBlockSize int, FileGroupID int, LogGroupGUID uniqueidentifier NULL, DifferentialBaseLSN numeric(25,0) NULL, DifferentialBaseGUID uniqueidentifier, IsReadOnly bit, IsPresent bit ) if (@sqlServerVersion = 10) begin alter table #fileListOnly add TDEThumbprint nvarchar(max) /* nuovo in SQL Server 2008 */ end /* eseguo la RESTORE FILELISTONLY */ print convert(varchar(10), GETDATE(), 103) + ' ' + convert(varchar(8), GETDATE(), 108) + ' Esecuzione @tsql' print '' insert #fileListOnly exec sp_executesql @tsql /* se voglio vedere il risultato della RESTORE FILELISTONLY: select * from #fileListOnly */ /* costruisco il comando di restore */ set @databaseName = (select cast(NEWID() as varchar(36))) print '@databaseName = ' + @databaseName set @tsql = ' RESTORE DATABASE [' + @databaseName + '] FROM DISK = ''' + @pathBackupDb + ''' WITH ' select @tsql = @tsql + ', MOVE N''' + logicalName + ''' TO N''' + @pathTestRestore + '\testRestore_' + RIGHT ( PhysicalName, CHARINDEX ( '\', REVERSE( PhysicalName ) ) - 1) + ''' ' from #fileListOnly /* elimino la prima virgola */ set @posizioneVirgola = (select CHARINDEX(',', @tsql)) set @tsql = STUFF(@tsql, @posizioneVirgola, 1,'') /* vedo il comando di restore */ print '@tsql = ' + @tsql print '' /* se voglio mandare in esecuzione il comando di restore: */ print convert(varchar(10), GETDATE(), 103) + ' ' + convert(varchar(8), GETDATE(), 108) + ' Esecuzione RESTORE' print '' exec sp_executesql @tsql /* se tutto va a buon fine significa che il backup è valido posso droppare il db appena creato: */ print convert(varchar(10), GETDATE(), 103) + ' ' + convert(varchar(8), GETDATE(), 108) + ' Esecuzione DROP DATABASE' print '' set @tsql = 'drop database [' + @databaseName + ']' exec sp_executesql @tsql print '@tsql = ' + @tsql print convert(varchar(10), GETDATE(), 103) + ' ' + convert(varchar(8), GETDATE(), 108) + ' Fine operazioni' go

Per provarla, supponendo di avere un database "Test":

backup database test to disk = 'c:\temp\myBackupTest.bak'

Chiamo la procedura di verifica backup:

exec dbo.up_verifyBackup @pathBackupDb = 'c:\temp\myBackupTest.bak', @pathTestRestore = 'C:\temp' go

Questo il risultato che appare nella mia finestra Messages:

17/07/2008 16:24:35 Impostazione variabili
@sqlServerVersion = 10
@pathTestRestore = C:\temp
@pathBackupDb = c:\temp\myBackupTest.bak
@tsql = RESTORE FILELISTONLY FROM DISK = 'c:\temp\myBackupTest.bak'

17/07/2008 16:24:35 Esecuzione @tsql
@databaseName = FE1AD55D-A54F-429C-88E2-C6DDE2F13A17
@tsql =  RESTORE DATABASE [FE1AD55D-A54F-429C-88E2-C6DDE2F13A17]
  FROM  DISK = 'c:\temp\myBackupTest.bak'
  WITH  MOVE N'test' TO N'C:\temp\testRestore_test.mdf' , MOVE N'test_log' TO N'C:\temp\testRestore_test_log.LDF'

17/07/2008 16:24:35 Esecuzione RESTORE
Processed 176 pages for database 'FE1AD55D-A54F-429C-88E2-C6DDE2F13A17', file 'test' on file 1.
Processed 2 pages for database 'FE1AD55D-A54F-429C-88E2-C6DDE2F13A17', file 'test_log' on file 1.
RESTORE DATABASE successfully processed 178 pages in 0.054 seconds (25.625 MB/sec).

17/07/2008 16:24:35 Esecuzione DROP DATABASE
@tsql = drop database [FE1AD55D-A54F-429C-88E2-C6DDE2F13A17]

17/07/2008 16:24:36 Fine operazioni

Mi sembra comodo, no?

SQL Server 2008 Whitepapers
08 luglio 08 02.49 | abenedetti | with no comments

Il nostro team SolidQ sta lavorando su diversi withepapers sulle nuove caratteristiche e funzionalità di SQL Server 2008.

Il primo di questi, dell'amico Itzik, è dedicato alle novità del linguaggio T-SQL: "An Introduction to New T-SQL Programmability Features in SQL Server 2008"

Lo trovate qui: http://msdn.microsoft.com/en-gb/library/cc721270(SQL.100).aspx

[SQL 2008] Filestream che non si riesce ad abilitare
08 luglio 08 01.48 | abenedetti | with no comments

Questa mattina avevo la necessità di abilitare il FILESTREAM su un'istanza SQL Server 2008.

La macchina ha installate sia una istanza SQL 2005, che una SQL 2008 in cui, in fase di setup, non era stata abilitata la funzionalità.

Per abilitare apro SQL Server Configuration, tasto DX sul servizio SQL 2008 e poi, sulla TAB di FileStream abilito le proprietà che mi interessano, come in fugura:

clip_image002

Clicco su "Apply", apro il Management Studio, nuova query e digito:

EXEC sp_configure filestream_access_level, 2 RECONFIGURE

In tutta risposta ottengo un bel messaggio di errore:

Configuration option 'filestream access level' changed from 2 to 2. Run the RECONFIGURE statement to install.

In realtà non era stato attivato proprio nulla (se torno sulle proprietà dell'istanza, infatti, le checkbox non sono flaggate).

Sul log di SQL Server trovo:

clip_image002[4]

Girando per la rete trovo un posto mooolto interessante sul blog del team dello Storage Engine, questo:

http://blogs.msdn.com/sqlserverstorageengine/archive/2008/06/09/enabling-filestream-post-sql2008-setup-a-known-issue-in-sql-config-manager.aspx

Guarda caso la situazione è proprio questa:

This happens only if:

    1) you want to enable FILESTREAM after SQL setup

    2) another instance of SQL 2005 or SQL 2000 (Pre-SQL2008) is installed on the same machine

 

Il post dice di disinstallare SQL 2005 (cosa ?!?!), non ci penso nemmeno e vado a modificare il comando d'esecuzione dello script vbs in questo modo:

cscript filestream_enable.vbs /Machine:REDMOND /Instance:SQL2008 /Level:2 /Share:MYSHARESQL2008

Questo il risultato:

clip_image002[7]

Torno ad eseguire la mia query:

EXEC sp_configure 'filestream_access_level', 2 RECONFIGURE

Ok, tutto perfetto, FILESTREAM abilitato!

This Blog

Syndication