agosto 2008 - Posts

[Filestream] Non ho mai amato gli amanti delle SELECT * ...
23 agosto 08 02.05 | abenedetti | with no comments

Come scrivo nel titolo non ho mai amato gli amanti delle SELECT *  FROM tabella... per mille motivi.

Adesso, che si cominciano a vedere tabelle con FILESTREAM voglio vedere se continueranno a fare SELECT * ...

Auguri... :-)

PS: forse è meglio dire "buone ferie" piuttosto che "auguri". Da domani, anzi tra 6 ore, mi aspetta la Valle d'Aosta. Per una settimana di marmotte, torrenti, ghiacciai, fontina, ... A presto!

[SQL Server Tip] Cercare un testo in tutto il database
20 agosto 08 03.18 | abenedetti | with no comments

Durante lo sviluppo di un'applicazione è nata l'esigenza di poter effettuare delle ricerche di testo all'interno di tutte le colonne (testo) di tutte le tabelle. Naturalmente facendo in modo che questo meccanismo continui a funzionare anche nel momento in cui dovessero essere aggiunte tabelle e/o colonne.

In pratica poter chiamare una procedura, passando una stringa di ricerca, ed avere come risultato l'elenco delle tabelle (e colonne) in cui questo testo è stato trovato.

Quindi avere delle istruzioni in grado di:

  1. recuperare l'elenco di tutte le colonne testo di tutte le tabelle
  2. costruire, per ciascuna riga del nostro elenco, un'istruzione di select con opportuna clausola WHERE ... LIKE ...
  3. memorizzare la [tabella].[colonna] che ha prodotto il risultato

Abbiamo esteso da subito questa procedura in modo tale da poter avere, se richiesto, anche il numero di occorrenze trovate.

Ovvero abbiamo messo a disposizione due parametri:

  1. @str nvarchar(200) : per poter passare il testo da ricercare
  2. @countOccurence bit = 0 : (opzionale) per specificare se contare anche il numero di occorrenze

Per arrivare al risultato e poter fare un test costruisco un database di esempio:

use master go if exists (select name from sys.databases where name = N'test') drop database test GO create database test go use test go if object_id('clienti') is not null drop table clienti if object_id('fornitori') is not null drop table fornitori if object_id('magazzini') is not null drop table magazzini go create table clienti (id int identity(1,1), codCliente varchar(100)) create table fornitori (id int identity(1,1), codFornitore varchar(100)) create table magazzini (id int identity(1,1), codMagazzino varchar(100)) go insert clienti values ('rossi') insert clienti values ('rossini') insert clienti values ('bianchi') insert fornitori values ('neri') insert fornitori values ('blu') insert fornitori values ('gialli') insert magazzini values ('rossi') insert magazzini values ('rossi') insert magazzini values ('viola') go select * from clienti select * from fornitori select * from magazzini go

Queste le tabelle popolate con i dati di prova:

image

Questa la procedura:

if exists (select 1 from INFORMATION_SCHEMA.ROUTINES where SPECIFIC_SCHEMA = N'dbo' AND SPECIFIC_NAME = N'up_searchDb') drop procedure dbo.up_searchDb go create procedure dbo.up_searchDb ( @str nvarchar(200), @countOccurence bit = 0 ) as begin set nocount on set @str = QUOTENAME('%' + @str + '%','''') declare @tsql nvarchar(2000) declare @tableName nvarchar(200) declare @value nvarchar(200) select table_schema + '.' + table_name as tableName, column_name as columnName into #columnList from INFORMATION_SCHEMA.COLUMNS where data_type IN ('char', 'varchar', 'nchar', 'nvarchar') order by table_name, table_schema create table #search ( tableName nvarchar(200),columnName nvarchar(200), value nvarchar(200), occurence int ) while exists (select 1 from #columnList) begin select top 1 @tableName = tableName, @value = columnName from #columnList if (@countOccurence = 1) begin set @tsql = 'SELECT ''' + @tableName + ''',''' + @value + ''', ' + @value + ', count(1) FROM ' + @tableName + ' (NOLOCK) WHERE ' + @value + ' LIKE ' + @str + ' GROUP BY ' + @value end else begin set @tsql = 'SELECT TOP 1 ''' + @tableName + ''',''' + @value + ''', '''', NULL FROM ' + @tableName + ' (NOLOCK) WHERE ' + @value + ' LIKE ' + @str end print @tsql insert #search exec sp_executesql @tsql delete from #columnList where tableName = @tableName and columnName = @value end select * from #search order by tableName, value end go

E due esempi di chiamata:

exec dbo.up_searchDb @str = 'rossi', @countOccurence = 0

image

exec dbo.up_searchDb @str = 'rossi', @countOccurence = 1

image

Filed under: , , , ,
Svuotare un database: un meccanismo automatico
18 agosto 08 10.05 | abenedetti | 1 comment(s)

Può capitare, per diversi motivi, di avere la necessità di svuotare un database per riportarlo allo stato "iniziale".

Per stato "iniziale" intendo il momento dell'installazione: nessuna riga (se non in eventuali tabelle che devono essere già popolate) e con i contatori delle colonne autoincrementanti a zero.

Ci sono diverse strade per poter fare questo:

  • droppare il database e ricrearlo
  • effettuare il restore del db iniziale, sovrascrivendo quello attuale
  • ...

Può però anche esserci la necessità, per diversi motivi, di dover effettuare una pulizia via codice.

Abbiamo quindi a disposizione l'istruzione TRUNCATE e l'istruzione DELETE.

Non sto qui a ricordare le differenze sostanziali che passano tra le due istruzioni, mi limito solo a ricordare che in presenza di chiavi esterne non è possibile eseguire la prima delle due:

Msg 4712, Level 16, State 1, Line 1
Cannot truncate table 'xyz' because it is being referenced by a FOREIGN KEY constraint.

Possiamo però costruire, ed è quello che farò nel post, un meccanismo di DELETE automatico che sia in grado di:

  1. cancellare le righe di tutte le tabelle che vogliamo svuotare (istruzione DELETE)
  2. riportare i contatori al loro stato iniziale (comando DBCC CHECKIDENT).

Due sono le note che voglio sottilineare, una per il primo ed una per il secondo punto.

Punto (1): in presenza di tabelle con relazioni eseguire una DELETE sulla tabella figlio, prima che sia svuotata la tabella padre, porta ad un errore: The DELETE statement conflicted with the REFERENCE constraint ...

Punto (2): sottolineo il comportamento che si ottiene eseguendo il comando DBCC su tabelle mai utilizzate, o su tabelle su cui sono stati fatti degli inserimenti, come spiegato perfettamente nei booksonline, quando eseguiamo il comando come:

DBCC CHECKIDENT (' + @tName + ', RESEED, 1)

Current identity value is set to the new_reseed_value. If no rows have been inserted to the table since it was created, the first row inserted after you run DBCC CHECKIDENT uses new_reseed_value as the identity. Otherwise, the next row inserted uses new_reseed_value + 1.

 

Per entrambe le note esiste una soluzione:

  1. rieseguire le cancellazioni del db, così che la tabella figlia venga pulita
  2. controllare, tramite la funzione scalare IDENT_CURRENT, il valore dell'ultimo identity generato e comportarsi di conseguenza

 

Per realizzare il meccanismo automatico utilizzo un database molto semplice, costituito da tre tabelle, con un paio di relazioni.

Queste le istruzioni per costruirlo:

set nocount on use master go if exists (select [name] from sys.databases WHERE name = N'identityTest') drop database identityTest go create database identityTest go use identityTest go create table a ( idRecord int primary key identity(1,1), valore varchar(10) ) go create table b ( idRecord int primary key identity(1,1), valore varchar(10) ) go create table c ( idRecord int primary key identity(1,1), valore varchar(10), idFKA int foreign key references a(idRecord), idFKB int foreign key references b(idRecord) ) go

Questo il diagramma del db costruito per i nostri scopi:

image

A questo punto inserisco alcuni dati di prova e vedo le tabelle:

/* inserisco righe nella tabella "a" e nella tabella "c" */ insert a values ('aaa') insert a values ('aaa') insert c values ('ccc', 1, NULL) insert c values ('ccc', 1, NULL) select * from a select * from b select * from c

image

Procediamo con la costruzione del meccanismo di cancellazione che sia in grado di:

    • recuperare la lista delle tabelle del mio database (se devo escluderne alcune è sufficiente modificare la clausola WHERE di questa istruzione)
    • contare, per ciascuna tabella, il numero di righe presenti (se = 0 è inutile che esegua la procedura di cancellazione)
    • verificare se la tabella è mai stata utilizzata e, a seconda del risultato, eseguire:
      • DBCC CHECKIDENT ('nomeTabella', RESEED, 1)
      • DBCC CHECKIDENT ('nomeTabella', RESEED, 0)

Questo il codice TSQL, facilmente trasformabile in una stored procedure da poter chiamare nel momento in cui si presenti la necessità di svotare il database:

/* Meccanismo di delete automatico */ set nocount on print '*** delete' declare @tName nvarchar(100) declare @tSQL nvarchar(2000) declare @nrRows bigint declare @rowCount bigint if object_id('tempdb..#temp') is not null drop table #temp if object_id('tempdb..#results') is not null drop table #results create table #results ( tableName varchar(50), nrRows bigint, reseed int ) /* Recupero la lista delle tabelle del mio db */ select table_name into #temp from information_schema.tables where table_type = 'base table' /* Recupero il numero di righe presenti in ciascuna tabella */ while exists (select 1 from #temp) begin select top 1 @tName = table_name from #temp set @tSQL = 'select ''' + @tName + ''' as TableName, count(*) as NrRows, 0 from [' + @tName + ']' insert into #results exec sp_executesql @tSQL delete from #temp where table_name = @tName end /* verifico se ho delle righe nel db */ select @nrRows = sum(nrRows) from #results select @nrRows as nrRigheIniziali /* se voglio vedere l'elenco delle tabelle con il numero di righe */ select * from #results /* ciclo finchè ho righe */ while (@nrRows > 0) begin /* ciclo finchè ho tabelle su cui eseguire l'operazione di delete */ while exists (select 1 from #results) begin /* costruisco l'operazione di delete */ select top 1 @tName = TableName, @nrRows = NrRows from #results if (@nrRows > 0) begin set @tSQL = 'delete from ' + @tName print @tSQL begin try exec sp_executesql @tSQL set @rowCount = @@rowcount print '@rowCount: ' + cast(@rowCount as varchar(10)) end try begin catch print error_message() end catch end else begin print 'tabella ' + @tName + ' risulta vuota' end print '' /* elimino dal mio contenitore temporaneo la tabella su cui ho eseguito la delete */ delete from #results where tableName = @tName end /* svuoto le mie tabelle temporanee */ truncate table #temp truncate table #results /* riprendo l'elenco delle tabelle */ insert #temp select table_name from information_schema.tables where table_type = 'base table' /* ricalcolo il numero di righe di ciascuna tabella */ while exists (select 1 from #temp) begin select top 1 @tName = table_name from #temp set @tSQL = 'select ''' + @tName + ''' as TableName, count(*) as NrRows, 0 from [' + @tName + ']' insert into #results exec sp_executesql @tSQL delete from #temp where table_name = @tName end /* riconto il numero di righe totali del db */ select @nrRows = sum(nrRows) from #results end /* (re)inizializzazione dei contatori */ print '*** reseed' /* svuoto le mie tabelle temporanee */ truncate table #temp truncate table #results /* riprendo l'elenco delle tabelle */ insert #temp select table_name from information_schema.tables where table_type = 'base table' /* calcolo il numero di righe di ciascuna tabella ed eseguo l'operazione: IDENT_CURRENT('nomeTabella') - IDENT_SEED('nomeTabella') */ while exists (select 1 from #temp) begin select top 1 @tName = table_name from #temp set @tSQL = 'select ''' + @tName + ''' as TableName, count(*) as NrRows, IDENT_CURRENT(''' + @tName + ''') - IDENT_SEED(''' + @tName + ''') from [' + @tName + ']' insert into #results exec sp_executesql @tSQL delete from #temp where table_name = @tName end /* elimino eventuali tabelle che: 1. hanno righe 2. non necessitano la reinizializzazione (non hanno identity) */ delete from #results where nrRows <> 0 or reseed IS NULL declare @reseed int select top 1 @tName = tableName, @reseed = reseed from #results /* Se: IDENT_CURRENT('nomeTabella') - IDENT_SEED('nomeTabella') = 0 allora: DBCC CHECKIDENT ('nomeTabella', RESEED, 1) altrimenti: DBCC CHECKIDENT ('nomeTabella', RESEED, 0) */ while exists (select 1 from #results) begin if (@reseed = 0) set @tsql = ' DBCC CHECKIDENT (' + @tName + ', RESEED, 1) ' else set @tsql = ' DBCC CHECKIDENT (' + @tName + ', RESEED, 0) ' print @tsql begin try exec sp_executesql @tsql end try begin catch end catch delete from #results where tableName = @tName select top 1 @tName = tableName, @reseed = reseed from #results end print '*** end ***'
Gestione degli errori: manca ancora qualcosa
13 agosto 08 12.55 | abenedetti | with no comments

Con SQL Server 2005 (e l'introduzione delle keyword TRY / CATCH) la gestione degli errori è notevolmente migliorata.

Dalla precedente versione possiamo ad esempio sfruttare, all'interno di un blocco BEGIN CATCH ... END CATCH le funzioni scalari ERROR_NUMBER(), ERROR_MESSAGE(), ... per tracciare tutte le informazioni relative all'eccezione generata, così che possa loggarle, memorizzarle in una mia tabella, ecc...

Il problema però è che, essendo scalari, queste funzioni ritornano soltanto un valore e non una possibile lista di errori generati.

Mi spiego con un esempio...

Creo una tabella molto semplice, con una sola colonna, su cui applico un vincolo UNIQUE.

In questo modo:

USE tempdb GO IF OBJECT_ID('dbo.tabTest', 'U') IS NOT NULL DROP TABLE dbo.tabTest GO CREATE TABLE tabTest (codice INT) GO ALTER TABLE tabTest WITH CHECK ADD CONSTRAINT u_tabTest UNIQUE (codice) go

A questo punto, chissà per quale motivo, viene rieseguita, all'interno di un blocco TRY / CATCH, l'istruzione di creazione del medesimo constraint:

BEGIN TRY ALTER TABLE tabTest WITH CHECK ADD CONSTRAINT u_tabTest UNIQUE (codice) END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_SEVERITY() AS ErrorSeverity, ERROR_STATE() AS ErrorState, ERROR_PROCEDURE() AS ErrorProcedure, ERROR_LINE() AS ErrorLine, ERROR_MESSAGE() AS ErrorMessage; END CATCH

Cosa ottengo?

image

Una sola riga con l'ultimo errore generato: non posso creare il vincolo. Vedi gli errori precedenti.

Ok, ma come diavolo faccio a vedere gli errori precedenti ?!?!

Infatti, nella realtà, gli errori sarebbero due ed il primo molto più chiaro del secondo:

Msg 2714, Level 16, State 4, Line 1
There is already an object named 'u_tabTest' in the database.
Msg 1750, Level 16, State 0, Line 1
Could not create constraint. See previous errors.

Purtroppo non esiste il modo di recuperare entrambi gli errori, nè esiste il modo di intercettare i messaggi che vengono visualizzati sulla finestra "Messages" del Management Studio.

Con SQL 2008 cambia qualcosa?

Putroppo no, o meglio, cambia solo il numero di errore e la descrizione del primo messaggio:

Msg 1913, Level 16, State 1, Line 2
The operation failed because an index or statistics with name 'u_tabTest' already exists on table 'tabTest'.
Msg 1750, Level 16, State 0, Line 2
Could not create constraint. See previous errors.

This Blog

Syndication