Dynamic SQL: String truncation in EXECUTE-Statment - sql-server

I wanted to monitor sizes of any databases in an SQL Server instance so I came up with this query that builds a temporary table which delivers what I want
/*DROP TEMP TABLE IF STILL EXISTENT*/
IF OBJECT_ID('tempdb..#sizes') IS NOT NULL
DROP TABLE #sizes;
/*DECLARE CURSOR curs1 to query the names of all relevant databases*/
DECLARE curs1 INSENSITIVE CURSOR FOR
select CAST(name AS varchar(MAX)) from sys.databases WHERE state_desc ='ONLINE' AND name NOT IN ('tempdb', 'master', 'model', 'msdb', 'sysdb', 'tempdb2')
/*Open Cursor*/
OPEN curs1
/*Declare Variable to hold the value of current db*/
declare #dbname varchar(MAX) = 'dummy'
/*Create the temporary table for results*/
CREATE TABLE #sizes (DBname varchar(MAX), physicalname varchar(MAX), TotalSizeMB INT, AvailableSpaceMB INT)
/*Query the datafile statistics for every database dynamically*/
WHILE(1=1)
BEGIN
FETCH NEXT FROM curs1 INTO #dbname
IF ##FETCH_STATUS != 0
BREAK;
EXECUTE('USE ' + #dbname + ';INSERT INTO #sizes
SELECT DB_NAME(), f.physical_name,
CAST((f.size/128.0) AS DECIMAL(15,2)),
CAST(f.size/128.0 - CAST(FILEPROPERTY(f.name, ''SpaceUsed'') AS int)/128.0 AS DECIMAL(15,2))
FROM sys.database_files f;')
END
Select * from #sizes
/*Cleanup*/
DROP TABLE #sizes
CLOSE curs1
DEALLOCATE curs1
When the cursor fetches a string containing a "-", it splits the string at that point, leaving an incomplete database name, which can not be found. I isolated the EXECUTE function to be the problem. I assume this has to do with the change of of context when changing the database with the use command. But I also tried the datatypes nvarchar, text and ntext (which are not valid for local variables), char and nchar as well as sysname, which is the fieldtype of the sys.databases' "name"-column.
Any suggestions on how to stop this behaviour?

Try escaping the database name:
'USE [' + #dbname + ']; ...

Here is a quick example of how you can do this without using a cursor. It still requires dynamic sql but no cursor. :) I realize you are using FILEPROPERTY which will be NULL here but I tossed this example together quickly. And since I suspect you will probably continue to use the cursor version you already have I didn't refactor that piece.
declare #SQL nvarchar(max) = ''
select #SQL = #SQL +
'SELECT DatabaseName = ''' + name + ''', f.physical_name collate SQL_Latin1_General_CP1_CI_AS,
CAST((f.size/128.0) AS DECIMAL(15,2)),
CAST(f.size/128.0 - CAST(FILEPROPERTY(f.name, ''SpaceUsed'') AS int)/128.0 AS DECIMAL(15,2))
FROM [' + name + '].sys.database_files f union all '
from sys.databases
WHERE state_desc ='ONLINE'
AND name NOT IN ('tempdb', 'master', 'model', 'msdb', 'sysdb', 'tempdb2')
select #SQL = left(#SQL, len(#SQL) - 10)
select #SQL
--Uncomment the next line when you are comfortable the dynamic sql will work
--exec sp_executesql #SQL

Related

Database name with special character 'İ'

Somebody create database with special characters name like 'PRİNCE' long time ago.So we can't change database name anymore.
Because of ' İ ' we can't call database from sys.databases.
select name
from dbo.sysdatabases
where name=cast('PRİNCE' as varchar(100)) COLLATE SQL_Latin1_General_CP1253_CI_AI
it works well
But there is a procedure which we using gives error
'Database 'PRİNCE' does not exist. Make sure that the name is entered correctly.'
DECLARE #dbname VARCHAR(50) -- database name
Declare #strSQL nvarchar(max)
DECLARE c CURSOR FOR
select name from MASTER.dbo.sysdatabases where (name not in('master','model','msdb','tempdb'))
OPEN c;
FETCH NEXT FROM c INTO #dbname;
WHILE ##FETCH_STATUS = 0
begin
select #strSQL='use '+cast(#dbname as varchar(100))COLLATE SQL_Latin1_General_CP1253_CI_AI+' Select Db_name(DB_ID()) as Dbname from sys.database_files ;'
exec sp_executesql #strSQL
fetch next from c into #dbname
end
close c
deallocate c
The problem is your use of varchar throughout and not nvarchar. The specific problem statement is this one:
select #strSQL='use '+cast(#dbname as varchar(100))COLLATE SQL_Latin1_General_CP1253_CI_AI+' Select Db_name(DB_ID()) as Dbname from sys.database_files ;'
I am guessing your database (master I assume) is not in the collation SQL_Latin1_General_CP1253_CI_AI so as soon as the assignment happens, the value is lost.
See, for example, the below:
DECLARE #Database sysname = N'USE ' + 'İ' COLLATE SQL_Latin1_General_CP1253_CI_AI + ';';
SELECT #Database;
This returns USE I; not USE İ;. As object names are a sysname (a synonym of nvarchar(128) NOT NULL) then make sure you use that data type through your code:
USE master;
GO
DECLARE #dbname sysname; -- database name
DECLARE #strSQL nvarchar(max);
DECLARE c CURSOR FOR
SELECT [name]
FROM sys.databases --Don't use the old objects!
WHERE ([name] NOT IN(N'master',N'model',N'msdb',N'tempdb'));
OPEN c;
FETCH NEXT FROM c INTO #dbname;
WHILE ##FETCH_STATUS = 0 BEGIN
SET #strSQL = N'USE ' + QUOTENAME(#dbname) + N'; SELECT DB_NAME(DB_ID()) AS DBName from sys.database_files;'; --Note QUOTENAME as well for proper quoting.
EXEC sys.sp_executesql #strSQL;
FETCH NEXT FROM c INTO #dbname;
END;
CLOSE c;
DEALLOCATE c;
Ideally, I'd get rid of the CURSOR as well, but that's a completely different question.

T-SQL How to copy data from table with parameterized name to temporary table

My procedure has two parameters:
#schemaName as sysname
#tableName as sysname
Inside the procedure, I want to copy data from table schemaName.tableName to the new temporary table # tmpTable1 .
My default schema in the database is schemaX, not dbo and I am not the dbo user.
Create procedure copy_data_to_temp
#schemaName as sysname,
#tableName as sysname
AS
Begin
Exec('select * into #tmpTable1 from ' + #schemaName + '.' + #tableName)
Select * from #tmpTable1 – does not work, because after dynamic SQL #tmpTable1 does not exist
END
I have tried:
-- Exec('Select * into Tdummy from ' + #schemaName + ’.’ + #tableName + ' where 1=2'’)
-- Select * into #tmpTable1 from Tdummy -- Gives error: Invalid object name 'Tdummy', when I am not dbo user and my default schema is schemaX not dbo.
-- Exec('Insert into #tmpTable1 select * from '+ #schemaName + '.' + #tableName)
There are 2 problems here. Firstly, what you have is dangerously open to Injection. That must be fixed. I cannot stress that more than anything in this answer. If you learn nothing else from this, learn to write secure dynamic statements.
Secondly, temporary objects only persist for the duration of the scope you define them in. For the above, that's the duration of the dynamic statement, and that is it.
This, however, has a strong "code smell" of being an XY Problem but I'll go on to answering this anyway.
You'll need to create a persisted object and then SELECT from that in the procedure, and then "clean up":
CREATE PROC dbo.copy_data_to_temp #SchemaName sysname, #TableName sysname AS
BEGIN
DECLARE #SQL nvarchar(MAX);
DROP TABLE IF EXISTS tempdb.dbo.tmpTable;
SELECT #SQL = N'SELECT * INTO tempdb.dbo.tmpTable FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';'
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
WHERE s.name = #SchemaName
AND t.[name] = #TableName;
EXEC sys.sp_executesql #SQL;
SELECT *
FROM tempdb.dbo.tmpTable;
--DROP TABLE tempdb.dbo.tmpTable;
END;
Of course, the SELECT ... INTO tmpTable ... FROM {dynamic object} followed by the SELECT ... FROM tmpTable might as well just be a SELECT ... FROM {dynamic object}, and why this looks like a XY Problem.
When using Dynamic SQL, you need to make sure you use it properly and safely. Rather than repeating myself, you can learn a lot from my article Dos and Don'ts of Dynamic SQL.
Declare the #temp table before using exec and then also add INSERT INTO
create table #temp
(
...
)
insert into #temp
Exec (...)
-- return results from exec
Select * from #temp

Creating table of DB and table info on SQL Server Instance using while loop

I was wondering why i can't substitute the DB name for the variable #DBNAME in the following while loop:
I can use the variable as the DB_NAME but it wont let me substitute the DB name for sys.tables
it is giving me the following error: Incorrect syntax near '.'.
DROP TABLE #TABLE_INFO;
CREATE TABLE #TABLE_INFO
(
DB_NAME VARCHAR(100),
TABLE_NAME VARCHAR(100),
CREATE_DATE DATE
);
DECLARE #COUNT AS INT = 1
DECLARE #DBNAME AS VARCHAR(100)
WHILE #COUNT < (SELECT MAX(DATABASE_ID) +1 FROM SYS.DATABASES)
BEGIN
SET #DBNAME = (SELECT NAME FROM SYS.DATABASES WHERE DATABASE_ID = #COUNT)
INSERT INTO #TABLE_INFO
SELECT
#DBNAME AS DB_NAME,
A1.NAME AS TABLE_NAME,
A1.CREATE_DATE
FROM #DBNAME.SYS.TABLES A1
SET #COUNT = #COUNT+1
END
SELECT * FROM #TABLE_INFO;
T-SQL does not allow database object-identifiers to be parameteried.
...So you have to use Dynamic SQL (i.e. building up a query inside a string value and passing it to sp_executesql).
Obviously this is dangerous due to the risk of SQL injection, or simply forgetting to escape names correctly (use QUOTENAME) which may cause inadvertent data-loss (e.g. if someone actually named a table as [DELETE FROM Users]).
So it's important to use Dynamic SQL as little as possible.
What you can do is use INSERT INTO #tableVariable EXECUTE sp_executesql #query which allows you to store the results of a stored procedure - or any dynamic SQL - into a table-variable or temporary-table without them being output directly to your client connection and then process the results using non-dynamic SQL.
You can also use CROSS APPLY too if you can move all of your dynamic-SQL logic to a stored-procedure.
Try this:
BTW, I changed your WHILE loop to use a STATIC READ_ONNLY CURSOR which avoids your assumptions about how sys.databases's database_id work (e.g. what if sys.databases lists only these 4 database_id values 1, 2, 99997, 99998? Your WHILE loop would be wasting time checking for 3, 4, 5, 6, ..., 99996).
I also use a table-variable rather than a temporary-table because the scope and lifetime of a table-variable is easier to reason about compared to a temporary-table. That said, there aren't really any performance benefits of TVs over TTs (though you can pass TVs as table-valued-parameters, which is nice and generally much better than passing data to sprocs using TTs).
DECLARE #results TABLE (
DatabaseName nvarchar(100) NOT NULL,
SchemaName nvarchar(50) NOT NULL,
TableName nvarchar(100) NOT NULL,
Created datetime NOT NULL
);
DECLARE #dbName nvarchar(100);
DECLARE c CURSOR STATIC READ_ONLY FOR
SELECT QUOTENAME( [name] ) FROM sys.databases;
OPEN c;
FETCH NEXT FROM c INTO #dbName;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #dbQuery nvarchar(1000) = N'
SELECT
''' + #dbName + N''' AS DatabaseName,
s.[name] AS SchemaName,
t.[name] AS TableName,
t.create_date AS Created
FROM
' + #dbName + N'.sys.tables AS t
INNER JOIN sys.schemas AS s ON t.schema_id = s.schema_id
ORDER BY
s.[name],
t.[name]
';
INSERT INTO #results
( DatabaseName, SchemaName, TableName, Created )
EXECUTE sp_executesql #dbQuery;
FETCH NEXT FROM c INTO #dbName;
END;
CLOSE c;
DEALLOCATE c;
--------------------------
SELECT
*
FROM
#results
ORDER BY
DatabaseName,
SchemaName,
TableName;
I am able to copy+paste this query into SSMS and run it against my SQL Server 2017 box without modifications and it returns the expected results.

How to find the name of the database from the given table name, using stored procedure?

I am newbie here. I have many databases in my SSMS, so I need to find the database name using the given table name using stored procedures.
And I am not good at writing SP's and handling errors.
I apologize for my English.
Thank you
I tried it using cursors in stored procedure.
But I am getting errors as I am not good at handling errors.
You could create the stored procedure in the following:
CREATE PROCEDURE sp_Get_Tables
#schema VARCHAR(50) = 'dbo',
#table_name VARCHAR(100) = 'Default_Table_Name'
AS
SELECT name
FROM sys.databases
WHERE CASE WHEN state_desc = 'ONLINE' THEN OBJECT_ID(QUOTENAME(name) + '.' + #schema + '.' + #table_name, 'U') END IS NOT NULL
And execute the stored procedure you can in the following:
EXEC sp_Get_Names 'Schema', 'Table_Name'
Try This:
Create PROCEDURE Pro_FindTable
(#tableName VARCHAR(MAX))
AS
BEGIN
SET NOCOUNT ON;
DECLARE #name VARCHAR(MAX),
#dbid INT;
DECLARE C CURSOR FAST_FORWARD FOR(
SELECT name,
database_id
FROM sys.databases);
OPEN C;
FETCH NEXT FROM C
INTO #name,
#dbid;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #query NVARCHAR(MAX)
= 'IF EXISTS(SELECT name FROM(SELECT name, COUNT(*)Over(Order By (Select Null)) as RN FROM(SELECT '''
+ #name + ''' AS name UNION ALL SELECT name FROM [' + #name
+ '].sys.tables WHERE type=''U'' AND name = ''' + #tableName
+ ''') as K)as K Where RN>1)
Select '''+ #name + '''';
EXEC (#query);
FETCH NEXT FROM C
INTO #name,
#dbid;
END;
CLOSE C;
DEALLOCATE C;
END;
And call it like this:
EXEC Pro_FindTable 'MyTable'
Result will be all databases which has a table named 'MyTable'

SQL search across multiple columns in any order

My users are trying to find records in my SQL db by providing simple text strings like this:
SCRAP 000000152 TMB-0000000025
These values can be in any order and any may be excluded. For example, they may enter:
SCRAP
TMB-0000000025 SCRAP
000000152 SCRAP
SCRAP 000000152
TMB-0000000025 000000152
All should work and include the same record as the original search, but they may also contain additional records because fewer columns are used in the match.
Here is a sample table to use for the results:
DECLARE #search1 varchar(50) = 'SCRAP 000000152 TMB-0000000025'
DECLARE #search2 varchar(50) = 'SCRAP'
DECLARE #search3 varchar(50) = 'TMB-0000000025 SCRAP'
DECLARE #search4 varchar(50) = '000000152 SCRAP'
DECLARE #search5 varchar(50) = 'SCRAP 000000152'
DECLARE #search6 varchar(50) = 'TMB-0000000025 000000152'
DECLARE #table TABLE (WC varchar(20),WO varchar(20),PN varchar(20))
INSERT INTO #table
SELECT 'SCRAP','000000152','TMB-0000000025' UNION
SELECT 'SCRAP','000012312','121-0000121515' UNION
SELECT 'SM01','000000152','121-0000155' UNION
SELECT 'TH01','000123151','TMB-0000000025'
SELECT * FROM #table
One additional wrinkle, the user does not have to enter 000000152, they can enter 152 and it should find the same results.
I can use patindex, but it requires the users to enter the search terms in a specific order, or for me to have an exponentially larger string to compare as I try to put them in all possible arrangements.
What is the best way to do this in SQL? Or, is this outside the capabilities of SQL? It is quite possible that the table will have well over 10,000 records (for some instances even over 100,000), so the query has to be efficient.
Agree with #MitchWheat (as usual). This database is not designed for queries like that, nor would any kind of "basic query" help. Best way would be to build a list of strings appearing in any column of the database, mapped back to the source column and row, and search that lookup table for your strings. This is pretty much what Lucene and any other full-text search library will do for you. SQL has a native implementation, but if the pros say go with a third party implementation, I'd say it's worth a look-see.
You can try this SP:
USE master
GO
CREATE PROCEDURE sp_FindStringInTable #stringToFind VARCHAR(100), #schema sysname, #table sysname
AS
DECLARE #sqlCommand VARCHAR(8000)
DECLARE #where VARCHAR(8000)
DECLARE #columnName sysname
DECLARE #cursor VARCHAR(8000)
BEGIN TRY
SET #sqlCommand = 'SELECT * FROM [' + #schema + '].[' + #table + '] WHERE'
SET #where = ''
SET #cursor = 'DECLARE col_cursor CURSOR FOR SELECT COLUMN_NAME
FROM ' + DB_NAME() + '.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ''' + #schema + '''
AND TABLE_NAME = ''' + #table + '''
AND DATA_TYPE IN (''char'',''nchar'',''ntext'',''nvarchar'',''text'',''varchar'')'
EXEC (#cursor)
OPEN col_cursor
FETCH NEXT FROM col_cursor INTO #columnName
WHILE ##FETCH_STATUS = 0
BEGIN
IF #where <> ''
SET #where = #where + ' OR'
SET #where = #where + ' [' + #columnName + '] LIKE ''' + #stringToFind + ''''
FETCH NEXT FROM col_cursor INTO #columnName
END
CLOSE col_cursor
DEALLOCATE col_cursor
SET #sqlCommand = #sqlCommand + #where
--PRINT #sqlCommand
EXEC (#sqlCommand)
END TRY
BEGIN CATCH
PRINT 'There was an error. Check to make sure object exists.'
IF CURSOR_STATUS('variable', 'col_cursor') <> -3
BEGIN
CLOSE col_cursor
DEALLOCATE col_cursor
END
END CATCH
This will have results as follow:
USE AdventureWorks
GO
EXEC sp_FindStringInTable 'Irv%', 'Person', 'Address'
USE AdventureWorks
GO
EXEC sp_FindStringInTable '%land%', 'Person', 'Address'
That's all there is to it. Once this has been created you can use this against any table and any database on your server.(Read More)

Resources