SQL Server - Select columns that meet certain conditions? - sql

My COLUMNS can contain only three values or var chars - economy, basic, luxury. I want to select a ROW and display only those COLUMNS which contain luxury. The problem is that there are many such columns - about 50. I don't want to type the names of all those columns in my select query. Is there a shorter and simpler alternative to this ? Which query should I use ?
I am thinking of something like this (this is a FAKE query) -
#declare Column_Name varchar(30)
select Column_Name where Column_Value = 'luxury'
from ATable
where rowId = 'row 5';
Table structure -
rowId | Column1 | Column2 | Column3.....

I've created a stored procedure for you.
This procedure examines the MSSQL meta to build a dynamic SQL string that returns a result containing column names N and their values V, and the corresponding row key K from which that value was retrieved, for a specified table.
When this is executed, the results stored in a global temporary table called ##ColumnsByValue, which can then be queried directly.
Create the GetColumnsByValue stored procedure, by executing this script:
-- =============================================
-- Author: Ben Roberts (sepster#internode.on.net)
-- Create date: 22 Mar 2013
-- Description: Returns the names of columns that contain the specified value, for a given row
-- =============================================
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF OBJECT_ID ( 'dbo.GetColumnsByValue', 'P' ) IS NOT NULL
DROP PROCEDURE dbo.GetColumnsByValue;
GO
CREATE PROCEDURE dbo.GetColumnsByValue
-- Add the parameters for the stored procedure here
#idColumn sysname,
#valueToFind nvarchar(255),
#dbName sysname,
#tableName sysname,
#schemaName sysname,
#debugMode int = 0
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #SQL nvarchar(max);
DECLARE #SQLUnion nvarchar(max);
DECLARE #colName sysname;
DECLARE #dbContext nvarchar(256);
DECLARE #Union nvarchar(10);
SELECT #dbContext = #dbName + '.' + #schemaName + '.sp_executeSQL';
SELECT #SQLUnion = '';
SELECT #Union = '';
IF OBJECT_ID ( 'tempdb..##GetColumnsByValueIgnoreList') IS NULL -- no columns to ingore have been specified, need to create an empty list.
BEGIN
CREATE TABLE ##GetColumnsByValueIgnoreList (column_name nvarchar(255));
END
DECLARE DBcursor CURSOR FOR
SELECT
COLUMN_NAME
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = #tableName
AND
TABLE_SCHEMA = #schemaName;
OPEN DBcursor;
FETCH DBcursor INTO #colName;
WHILE (##FETCH_STATUS = 0)
BEGIN
IF (
#colName != #idColumn
AND
#colName NOT IN (SELECT column_name FROM ##GetColumnsByValueIgnoreList)
)
BEGIN
SELECT #SQL = 'SELECT '+#idColumn+' as K, '''+#colName+''' as N, ' +#colName+ ' as V FROM ' + #dbName + '.' + #schemaName + '.' + #tableName;
--PRINT #SQL;
SELECT #SQLUnion = #SQL + #Union + #SQLUnion;
SELECT #Union = ' UNION ';
END
FETCH DBcursor INTO #colName;
END; -- while
CLOSE DBcursor; DEALLOCATE DBcursor;
IF (#debugMode != 0)
BEGIN
PRINT #SQLUnion;
PRINT #dbContext;
END
ELSE
BEGIN
-- Delete the temp table if it has already been created.
IF OBJECT_ID ('tempdb..##ColumnsByValue') IS NOT NULL
BEGIN
DROP TABLE ##ColumnsByValue
END
-- Create a new temp table
CREATE TABLE ##ColumnsByValue (
K nvarchar(255), -- Key
N nvarchar(255), -- Column Name
V nvarchar(255) -- Column Value
)
-- Populate it with the results from our dynamically generated SQL.
INSERT INTO ##ColumnsByValue EXEC #dbContext #SQLUnion;
END
END
GO
The SP takes several inputs as parameters, these are explained in the following code.
Note also I've provided a mechanism to add an "ignore list" as an input:
This allows you to list any column names that should not be included
in the results.
You do NOT need to add the columnn that you're using as your key, ie the row_id from your example structure.
You MUST include other columns that are not varchar as
these will cause an error (as the SP just does a varchar comparison
on all columns it looks at).
This is done via a temp table that you must create/populate
Your example table structure suggests
the table contains only columns of interest, so this may not apply to
you.
I've included example code for how to do this (but only do this if you need to):
IF OBJECT_ID ( 'tempdb..##GetColumnsByValueIgnoreList') IS NOT NULL
BEGIN
DROP TABLE ##GetColumnsByValueIgnoreList;
END
CREATE TABLE ##GetColumnsByValueIgnoreList (column_name nvarchar(255));
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('a_column');
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('another_column');
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('yet_another_column');
Now, to fire off the procedure that build your temp table of results, use the following code (and modify as appropriate, of course).
-- Build the ##ColumnsByValue table
EXEC dbo.GetColumnsByValue
#idColumn = 'row_id', -- The name of the column that contains your row ID (eg probably your PK column)
#dbName = 'your_db_name',
#tableName = 'your_table_name',
#schemaName = 'dbo',
#debugMode = 0 -- Set this to 1 if you just want a print out of the SQL used to build the temp table, to 0 if you want the temp table populated
This leaves you with ##ColumnsByValue, on which you can perform whatever search you need, eg:
select * from ##ColumnsByValue WHERE v = 'luxury' and k = 5 --some_row_id
You'd need to re-execute the stored procedure (and if relevant, create/modify the ignore list table prior to it) for each table you want to examine.
A concern with this approach is the nvarchar length might get exceeded in your case. You'd prob. need to use different datatype, reduce the column name lengths etc. Or break it up into sub-steps and union the results together to get the resultset you're after.
Another concern I have is that this is complete overkill for your particular scenario, where a one-off script-to-query-window will give you the basis of what you need, then some clever text editing in eg Notepad++ will get you all the way there... and hence this problem will likely (and quite reasonably) put you off doing it this way! But it is a good general-case question, and so deserves an answer for anyone interested in future ;-)

Related

T-SQL script to update numerous prod tables from temp tables

I am trying to simplify the updating of numerous production tables from temp tables. I currently have a long script that uses the MERGE command, but as I increase the number of tables to be processed, the MERGE code is getting unwieldy. To simplify, my idea is to create a script that will get the table names into a cursor, get the field names for that table into a different cursor, then compare temp and prod values. If different, I will update the prod table.
The table 'TableManifest' has only 1 column called TableName.
So 2 questions
1 - Is this an efficient approach to the problem? I'd love to get other suggestions.
2 - This code fails with an error "Must declare the table variable #Temp_TableName" on the line that starts with 'IF(SELECT..'.
DECLARE #TableName varchar(MAX) -- TARGET Table to update
DECLARE #Temp_TableName varchar(MAX) -- SOURCE Table for compare
DECLARE #ColumnNames varchar(MAX) -- Table Column to compare
-- Create CURSOR of Tables to process
DECLARE C_TableNames CURSOR FOR SELECT TableName FROM TableManifest
OPEN C_TableNames
-- Get 1st table name
FETCH NEXT FROM C_TableNames INTO #TableName
WHILE ##FETCH_STATUS = 0
BEGIN
-- Create variable for TEMP table name
SELECT #Temp_TableName = CONCAT('TEMP_',#TableName)
-- Create CURSOR of Column Names in the table
DECLARE C_ColumnNames CURSOR FOR SELECT name FROM sys.columns WHERE object_id = OBJECT_ID(#TableName)
OPEN C_ColumNames
-- Get 1st column name
FETCH NEXT FROM C_ColumnNames INTO #ColumnNames
WHILE ##FETCH_STATUS = 0
BEGIN
-- If the column name is not 'ID' and the values are different, update the TARGET table
IF(SELECT #ColumnNames FROM #Temp_TableName) <> (SELECT #ColumnNames FROM #TableName) AND #ColumnNames <> 'id'
UPDATE #TableName SET #ColumnNames = (SELECT #ColumnNames from #Temp_TableName)
-- Get next column name
FETCH NEXT FROM C_ColumnNames INTO #ColumnNames
END
-- Get next table name
FETCH NEXT FROM C_TableNames INTO #TableName
END
-- Clean up
CLOSE C_ColumNames
DEALLOCATE C_ColumNames
CLOSE C_TableNames
DEALLOCATE C_TableNames

Does sp_executesql support multiple values in one parameter and return multiple records?

I have created a stored procedure as shown below, but it's returning only one row instead of 3:
CREATE PROCEDURE [dbo].[tempsp]
(#RecycleIds NVARCHAR(MAX) = NULL)
AS
BEGIN
DECLARE #Err INT
DECLARE #WhereClause NVARCHAR(MAX)
DECLARE #SQLText1 NVARCHAR(MAX)
DECLARE #SQLText NVARCHAR(MAX)
SET #SQLText1 = 'SELECT FROM dbo.SKU '
IF #RecycledSkuIds IS NOT NULL
BEGIN
SET #SQLText = 'SELECT FROM dbo.SKU WHERE SKU.SkuId IN (#RecycleIds)'
EXEC sp_executesql #SQLText, N'#RecycleSkuIds nvarchar', #RecycleIds
END
ELSE
BEGIN
EXEC(#SQLText1)
END
SET #Err = ##ERROR
RETURN #Err
END
-------end of stored procedure--------
EXEC tempsp #RecycleIds = '5,6,7'
After running this SQL statement, it only returns one row instead of 3, with the id's of 5, 6, 7.
Can anyone tell me what I am doing wrong?
i wanted to use sp_executesql, so that it can be safe against sql injection with strong type defined.
Use a table type parameter, with a strongly typed column:
CREATE TYPE dbo.IDs AS table (ID int);
GO
CREATE PROCEDURE [dbo].[tempsp] #RecycleIds dbo.IDs READONLY AS
BEGIN
IF EXISTS (SELECT 1 FROM #RecycleIds)
SELECT * --Replace with needed columns
FROM dbo.SKU S
--Using EXISTS in case someone silly puts in the same ID twice.
WHERE EXISTS (SELECT 1
FROM #RecycleIds R
WHERE R.ID = S.SkuID);
ELSE
SELECT * --Replace with needed columns
FROM dbo.SKU S
END;
GO
Then you could execute it like so:
EXEC dbo.tempsp; --All Rows
GO
DECLARE #RecycleIds dbo.IDs;
INSERT INTO #RecycleIds
VALUES(1),(40),(182);
EXEC dbo.tempsp #RecycleIds;
I was trying to retrive the rows whose id matches within the IN clause.
SET #INClauseIds='''' + replace(#Ids, ',', ''',''') + ''''
Above statement would convert the ID's ='1,2,3' to '1','2','3' which i can directly place in the IN clause.
SET #SQLText1 ='EXEC(''SELECT Name,SEOFriendlyName FROM SKU Where Id IN ( ''+ #Ids+'' ) )'
EXEC sp_executesql #SQLText1 ,N'#INClauseIds nvarchar(max)',#Ids=#INClauseIds
If you want to avoid the usage of Temp Table which would add extra caliculation time. you can you the above strategy to retrive n number of records. Safe with strongly coupled with sp_executesql and without any sql injection.
You cannot use IN. Or, more accurately, you have a string and you are confusing it with a list. One method is to instead use LIKE:
SET #SQLText = '
SELECT *
FROM dbo.SKU
WHERE CONCAT('','', #RecycleIds, '','') LIKE CONCAT(''%,'', SKU.SkuId, '',%'')
';

Store the value of a query into a SQL Server variable

The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns. This is my code for the stored procedure:
ALTER proc [dbo].[usp_ColumnFieldValidator]
(
#TblName nvarchar(30),
#ColumnName nvarchar(30),
#RetVal bit output
)
as
begin
declare #CountOfRowsQuery as nvarchar(300)
set #CountOfRowsQuery = 'select count('+quotename(#ColumnName)+') from '+quotename(#TblName)+' having count(' +quotename(#ColumnName)+') = nullif(count('+quotename(#ColumnName)+'),0)'
execute sp_executesql #CountOfRowsQuery
select #RetVal = dbo.fn_ColumnValidator(#CountOfRowsQuery)
end
As you can see, a user-defined function is being called to set the value of #RetVal. This is my code for the user-defined function.
ALTER function [dbo].[fn_ColumnValidator]
(
#NullChecker as nvarchar(max)
)
returns bit
as
begin
declare #returnVar as bit
if #NullChecker is null
set #returnVar = 0
else
set #returnVar = 1
return #returnVar
end
The output of #RetVal is always 1 and I have attributed this error to #CountOfRowsQuery storing the entire string rather than the value of the query ie: #CountOfRowsQuery = null if the count of rows is zero else, #CountOfRowsQuery = the number of rows present in the column. To make things clearer I am attaching screenshots of the output when I run the program.
Output of a table that contains rows with data
Output of a table that contains no rows with no data
As you can see in list item.2, the sp returns null but the function_returned_value is being set to 1 instead of 0.
The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns.
Man, if this is not an over-complication I don't know what is.
Here's a much simpler (and more efficient) query that does the work:
SELECT CAST(IIF(EXISTS(
SELECT 1
FROM TableName
WHERE ColumnName IS NOT NULL
), 1, 0) As Bit)
Now, to change that to a procedure using dynamic SQL in a way that will not expose you to SQL Injection threats you can do this:
ALTER PROCEDURE [dbo].[usp_ColumnFieldValidator]
(
#TblName sysname,
#ColumnName sysname,
#RetVal bit output
)
AS
BEGIN
IF NOT EXISTS(
SELECT 1
FROM Information_Schema.Columns
WHERE Table_Name = #TblName
AND Column_Name = #ColumnName
)
RETURN;
DECLARE #Sql nvarchar(1000) =
N'SELECT #RetVal = CAST(IIF(EXISTS(
SELECT 1
FROM '+ QUOTENAME(#TblName) + N'
WHERE '+ QUOTENAME(#ColumnName) + N' IS NOT NULL
), 1, 0) As Bit)'
EXEC sp_executesql #Sql, N'#RetVal bit output', #RetVal OUTPUT;
END
Key notes:
I've changed the #TblName and #ColumnName variables to data type sysname instead of your original nvarchar(30) - since that is the data type SQL Server use internally to store identifiers.
Since identifiers can't be parameterized, I've white-listed them.
I'm using sp_executeSql to get back the value of the dynamic query directly into my output parameter.
For more tips and tricks on dynamic SQL, you can read my blog post entitled The do’s and don’ts of dynamic SQL for SQL Server

How to check existence of tables in different databases in a proc - SQLServer

I am working on a proc which is present in one database, it picks table from other source database (passed as parameter) and insert missing values to other destination database (again passed as parameter).
My insert query is a dynamic query and before executing it, I want to check existence of both the source and destination tables. I don't want to execute dynamic queries again just to check existence of tables as they are not advisable to use in bulk,.
Is there any way I can achieve something like below
USE #DbName
GO
IF EXISTS (
SELECT 1
FROM sys.tables
WHERE NAME = #table
AND type = 'U'
)
BEGIN
code here...
END
or
IF EXISTS (
SELECT 1
FROM #fulltableName -- where variable consists 'dbname.sys.tables'
WHERE NAME = #table
AND type = 'U'
)
BEGIN
code here...
END
with the help of only variables and without executing dynamic queries.
Check if the OBJECT_ID returns a value. If you are checking for tables, use U as 2nd parameter.
IF OBJECT_ID('DatabaseName.dbo.TableName', 'U') IS NOT NULL
BEGIN
-- Table Exists
END
With variables:
DECLARE #DatabaseName VARCHAR(100) = 'MyCustomDatabase'
DECLARE #SchemaName VARCHAR(100) = 'dbo'
DECLARE #TableName VARCHAR(100) = 'Countries'
IF OBJECT_ID(QUOTENAME(#DatabaseName) + '.' + QUOTENAME(#SchemaName) + '.' + QUOTENAME(#TableName), 'U') IS NOT NULL
BEGIN
-- Do stuff
END
Make sure to execute with a login with enough privileges.

How can I spot in what database is a stored procedure with name 'myStoredProcedure'?

There are bunch of databases to the SQL server I am connected.
How should I query the sysobjects in order to spot in what database a stored procedure with name 'myStoredProcedure' is located ?
The query should return the database name.
Thanks
I know you are not asking for this, but I'd really download RedGate's Sql Search add-in for SSMS and use that. It allows you to find any object (proc, table, view, column, etc) on any database easily.
And it's free!
I'd give this a try:
CREATE TABLE ##DatabaseList
(
DatabaseName varchar(50)
)
EXECUTE SP_MSForEachDB 'USE [?]; INSERT INTO ##DatabaseList SELECT DB_NAME() FROM [sys].[objects] WHERE name = "MyStoredProcedure" AND type_desc = "SQL_STORED_PROCEDURE"'
SELECT * FROM ##DatabaseList
DROP TABLE ##DatabaseList
That's using the undocumented/ unsupported system stored procedure SP_MSForEachDb and writing any hits to a global temp table, then outputting the contents to the Results window before dropping the table. If you just need to know which database (or databases - there may of course be more than one) has an appropriately named SP, this should do it. If you want to use the output elsewhere as a parameter, it may take a little more work.
By the way, I'm only learning this stuff myself over the last few months so if anyone can critique the above and suggest a better way to go at it I'm happy to receive feedback. Equally, I can answer any further questions posted here to the best of my ability.
Cheers
So out of curiosity I decided to try write this myself, especially since ADG mentioned his solution was using an unsupported, undocumented procedure. This could also be expanded to take a 2nd parameter so where it checks the type = P (stored Proc) you could probably change it to look for other things like views / tables etc.
My solution is a bit long but here goes:
CREATE PROCEDURE spFindProceduresInDatabases
(
#ProcedureName NVARCHAR(99)
)
AS
BEGIN
-- Get all the database names and put them into a table
DECLARE #Db TABLE (DatabaseName Varchar(99))
INSERT INTO #Db SELECT name FROM Sys.databases
-- Declare a table to hold our results
DECLARE #results TABLE (DatabaseName VARCHAR(99))
-- Make a Loop
-- Declare a variable to be incremented
DECLARE #count INT
SET #count = 0
-- Declare the end condition
DECLARE #endCount INT
SELECT #endCount = COUNT(*) FROM #Db
-- Loop through the databases
WHILE (#count < #endCount )
BEGIN
-- Get the database we are going to look into
DECLARE #dbWeAreChecking VARCHAR(99)
SELECT TOP 1 #dbWeAreChecking = DatabaseName FROM #Db
DELETE FROM #Db WHERE DatabaseName = #dbWeAreChecking
-- Create and execute our query
DECLARE #Query NVARCHAR(3000)
SET #Query = N'SELECT #outParam = COUNT(*) FROM '+#dbWeAreChecking+'.sys.sysobjects WHERE type = ''P'' and name = #ProcedureName'
Declare #outParam INT
print (#Query)
DECLARE #ParmDefinition NVARCHAR(500)
DECLARE #IntVariable INT
SET #ParmDefinition = N'#ProcedureName VARCHAR(99),#outParam INT OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql
#Query ,
#ParmDefinition,
#ProcedureName,
#outParam = #outParam OUTPUT
-- If we have a result insert it into the results table
If (#outParam > 0)
BEGIN
INSERT INTO #results(DatabaseName) VALUES(#dbWeAreChecking)
END
-- Increment the counter
SET #count = (#count + 1)
END
-- SELECT ALL OF THE THINGS!!!
SELECT * FROM #results
END