Make value of a parameter the column name without dynamic SQL - sql

Is there a way to name a column the value of what's in a parameter, without using dynamic SQL?
I need to somehow output the value of what is in the #input parameter as the name of the column in the SQL statement. I need to avoid using dynamic SQL.
DECLARE #input VARCHAR(10)=' Person ';
SELECT count(*) AS #input + ' Open Data'
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid

One ugly way, without Dynamic-SQL is using temporary table and rename column:
DECLARE #input VARCHAR(10) = ' Person ';
DECLARE #new_name VARCHAR(100) = #input + ' Open Data';
SELECT [rename_me] = COUNT(*)
INTO #temp
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid;
EXEC tempdb..sp_rename '#temp.rename_me', #new_name, 'COLUMN';
SELECT *
FROM #temp;

You need to use dynamic SQL:
DECLARE #input VARCHAR(10) = ' Person ';
DECLARE #sql NVARCHAR(MAX) = '
SELECT count(*) AS [#Input Open Data]
FROM #Accounts a JOIN
dbo.FileFeed t
On t.ID = a.AccountID
GROUP BY a.accountid';
SET #sql = REPLACE(#sql, '#Input', #Input);
exec sp_executesql #sql;
However, I don't really think this is a good idea. If you need to rename a column, do it in the application code.

No. Databases use a process similar to compiling to turn your query into an execution plan. Part of this process involves determining whether the user running the query has permissions to access the tables and columns used by the query. If those tables and columns are not determined until execution time, the compilation step can't finish. It seems strange, but the same thing applies to the result set.
Dynamic SQL (which creates a new query, with a new compilation step where the tables and column names are known up front) will be the only way to accomplish this.

The answer is "No". Dynamic SQL, by definition, means that the code can alter itself. The only alternative to Dynamic SQL is Hard-Coded SQL, which is when you specifically write every piece of it yourself.
You're asking if you can change a field name based on a variable, which can only be done with Dynamic SQL.

Unless you rename the columns in the application, this is the way i would do it.
DECLARE #input VARCHAR(10)=' Person ';
Declare #sql varchar(max);
set #sql'SELECT count(*) as ['+ #input +' Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t On t.ID = a.AccountID
GROUP BY a.accountid';
EXEC(#sql);

IF it is the case that the values of what can be in the #input variable / parameter are limited (i.e. not open-ended user input or something of that nature), then you can do this without DynamicSQL. You just need to have multiple copies of the query in this Stored Procedure, each being exactly the same except for the alias of the column, and pick the one you want based on the value of the variable. For example:
DECLARE #input VARCHAR(10)=' Person ';
IF (#input = 'Person')
BEGIN
SELECT COUNT(*) AS [Person Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t
ON t.ID = a.AccountID
GROUP BY a.accountid;
END;
IF (#input = 'Account')
BEGIN
SELECT COUNT(*) AS [Account Open Data]
FROM #Accounts a
JOIN dbo.FileFeed t
ON t.ID = a.AccountID
GROUP BY a.accountid;
END;
...

If you've got a later version of SQL Server then you might look at this option:
EXEC <stored_procedure> WITH RESULT SETS (...)
I imagine you're already using a stored procedure. This way you'd at least be able to keep the main logic completely static in a second procedure and just interface to a procedure with the dynamic exec call to accomplish the renaming of column(s).

Related

In One DB I have 100+ tables, but I need staging table (start with STR_) wise column wise(Which is Status ) row count. in Dynamic query [duplicate]

I am trying to write this query to find all tables with specific column with some specific value. This is what I've done so far -
EXEC sp_MSforeachtable
#command1='
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=PARSENAME("?",2) AND TABLE_NAME=PARSENAME("?",1) AND COLUMN_NAME="EMP_CODE")
BEGIN
IF (SELECT COUNT(*) FROM ? WHERE EMP_CODE="HO081")>0
BEGIN
SELECT * FROM ? WHERE EMP_CODE="HO081"
END
END
'
I hope my intensions are clear, I just want to select only those tables where the column EMP_CODE is present and in those tables I want to select those rows where EMP_CODE='HO081'.
Edit -
Now it stands like this. But I'm not able to replace #EMPCODE variable in the query.
DECLARE #EMPCODE AS VARCHAR(20)
SET #EMPCODE='HO081'
EXEC sp_MSforeachtable
#command1='
DECLARE #COUNT AS INT
SELECT #COUNT=COUNT(*) FROM ? WHERE EMP_CODE='''+#EMPCODE+'''
IF #COUNT>0
BEGIN
PRINT PARSENAME("?",1)+'' => ''+CONVERT(VARCHAR,#COUNT)+'' ROW(S)''
--PRINT ''DELETE FROM ''+PARSENAME("?",1)+'' WHERE EMP_CODE='''''+#EMPCODE+'''''''
END
',#whereand='AND O.ID IN (SELECT OBJECT_ID FROM SYS.COLUMNS C WHERE C.NAME='''+#EMPCODE+''')'
You know how sp_MSforeachtable is undocumented, and may go away at any time/be modified?
Well, if you're happy to ignore that, it has another parameter called #whereand, which is appended to the WHERE clause of the internal query that is being used to find the tables (and should start with an AND).
You also have to know that there's an alias, o against sysobjects, and a second alias syso against sys.all_objects.
Using this knowledge, you might craft your #whereand parameter as:
EXEC sp_MSforeachtable
#command1='...',
#whereand='AND o.id in (select object_id from sys.columns c where c.name=''EMP_CODE'')'
You can now also simplify your command1, since you know it will only be run against tables containing an EMP_CODE column. I'd probably take out the COUNT(*) condition also, since I don't see what value it's adding.
Updated based on your further work, and tested against one table:
DECLARE #EMPCODE AS VARCHAR(20)
SET #EMPCODE='HO081'
declare #sql nvarchar(2000)
set #sql = '
DECLARE #COUNT AS INT
SELECT #COUNT=COUNT(*) FROM ? WHERE EMP_CODE='''+#EMPCODE+'''
IF #COUNT>0
BEGIN
PRINT PARSENAME("?",1)+'' => ''+CONVERT(VARCHAR,#COUNT)+'' ROW(S)''
--PRINT ''DELETE FROM ''+PARSENAME("?",1)+'' WHERE EMP_CODE='''''+#EMPCODE+'''''''
END
'
EXEC sp_MSforeachtable
#command1=#sql,#whereand='AND O.ID IN (SELECT OBJECT_ID FROM SYS.COLUMNS C WHERE C.NAME=''EMP_CODE'')'
(I've reverted the #whereand to query for EMP_CODE, since you don't want to replace the value there).
The issue is that, you can pass parameters to a stored procedure, or literals, but you can't perform calculations/combining actions between them - so I moved the construction of the sql statement out into a separate action.
I guess you get an error of some kind, perhaps Invalid column name 'EMP_CODE'?
It's because the code is compiled before you check for the column.
You could do like this instead.
EXEC sp_MSforeachtable
#command1='
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=PARSENAME("?",2) AND TABLE_NAME=PARSENAME("?",1) AND COLUMN_NAME="EMP_CODE")
BEGIN
EXEC(''
IF (SELECT COUNT(*) FROM ? WHERE EMP_CODE="HO081")>0
BEGIN
SELECT * FROM ? WHERE EMP_CODE="HO081"
END
'')
END
'

Use results of an `exec` query as a join or "in" statement?

I have a query I build into a NVARCHAR due to an issue between SQL Server and a linked Oracle server forcing me to use OpenQuery. It works fine and I get the results I need when I run exec (#OPENQUERYFULL).
My results are a single column of numbers I need for another query. I would like to be able to use those results as an "IN ()" statement or as a JOIN but what I have tried so far has failed.
Is there a way to use the results of an exec query directly in another query?
UPDATE:
To be clear I was trying to avoid using a temp table.
Declare a table variable and insert the results of the other SP into that variable:
declare #results table (
numbers int
);
GO
insert into #results(numbers)
exec otherSP
Then you perform your join on the table variable.
You can send the linked server query with OPENQUERY instead of 'exec' by building a dynamic query with OPENQUERY and a join. It just takes some annoying string escaping, eg:
declare #oracleSQL nvarchar(max) = 'select 1 a, ''hello'' b from dual';
declare #sql nvarchar(max) = concat(
N'
with q as
(
SELECT * FROM OPENQUERY (OracleSvr, N''', replace(#oracleSQL,'''','''''') ,N''')
)
select *
from q
cross join sys.objects o
')
print #sql
exec ( #sql )
But as you can see the resulting code is a bit complicated. So I would almost always just use a temp table.

How do you call a variable (that is a list of values) in a conditional statement?

I am trying to create a procedure where the conditional statement changes based on the value that the user inputs. A simplified example of what I'm trying to achieve is below.
Create Procedure [LossRatioReport] (#construction AS VARCHAR(2), #state AS VARCHAR(2)) AS
DECLARE #construction_condition AS NVARCHAR(MAX)
DECLARE #construction AS VARCHAR(2)
SET #construction = '01'
SET #construction_condition = CASE #construction WHEN '01'
THEN #construction_condition = '('01', 'Frame Construction', 'Frame')'
ELSE #construction_condition = '00'
END
BEGIN
SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction = #construction_condition)
END
GO
I want to do it this way because there is a list of both integer and strings for each type and I don't want to rewrite the code over and over for each condition. But when I try and run my code, I keep getting incorrect syntax messages for the variables in the WHERE statement.
You'll need to use dynamic sql for this:
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = 'SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction IN( ' + #construction_condition
EXEC sp_executesql #sql
As Sean Lange stated, something should probably be mentioned on SQL Injection. Be sure you are aware of the danger before implementing a solution with dynamic SQL. Here is an overview, and Google can tell you much, much more.

Dynamically joining tables that may not exist

What's a good way to dynamically create tables, then join together all the tables that I have created?
Background
I'm trying to write a stored procedure that collects data from various parts of the database and presents it for use. For example if I wanted to send invoices to both Alice and Bob, this stored procedure would find all the information for the invoice. The trick is that Alice and Bob don't always need all the information I have. If Bob is tax exempt but trades in simoleons, I'll need to include information about currency exchange rates but leave out tax info. I want to give the stored procedure an order number and get back results formatted to the customer.
My solution so far is to create a table holding customer requirements. Then my stored procedure checks the table: if currency data is required I create and populate #Currency, or #Taxes holds tax data and so on. I check the requirement, then create the table as needed. At the end I need to Join the data into one record set.
My problem is that I have a error saying
Incorrect syntax near the keyword 'table'
AND an error saying
Must declare the table variable #Summary
on line 1 where i define #Summary These errors only show up when I use dynamic SQL to join the tables. When I comment out the dynamic SQL and copy-paste the statements into the one it should create, I have the result I want.
Code
I tried to use the following dynamic SQL in Microsoft SQL Server Management Studio
create procedure <procedure> (#OrderNumber varchar(20)) AS
DECLARE #CustomerName varchar(35) -- retrieved based on #OrderNumber
DECLARE #SQLQuery nvarchar(500)
DECLARE #ParamDef nvarchar(500)
DECLARE #SummaryTable table
(
ID varchar(20) --=#OrderNumber
, <Stuff>
)
SET #SQLQuery = 'Select * From #SummaryTable'
SET #ParamDef = '#SummaryTable table'
IF EXISTS(Select <TableName> from CustRequirements where <TableName> = 1 AND Customer = #CustomerName)
BEGIN
--Create table
DECLARE #<TableName> table
(
ID varchar(20)
, <Stuff>
)
--Populate
Insert into <TableName> Select <RelevantData> From <DataSource> where <Condition based on #OrderNumber or #CustomerName>
--Pepare Dynamic SQL
Set #SQLQuery = #SQLQuery + ' FULL OUTER JOIN #<TableName> ON <TableName>.ID = SummaryTable.ID'
SET #ParamDef = ', #<TableName> table'
END
<repeat for other tables>
EXECUTE sp_executesql #SQLQuery, #ParamDef, #Summary, #LineItems, #Taxes, #Currency
Question
Is there something wrong with my code? Is there a better way to do this? The other option I though of was to have IF Exists trees with the entire JOIN statement at the bottom of each branch (Since I can't seem to interrupt the JOIN clause with IF's). The problem with that is that I'll need 2^n JOIN statements to join n possible tables. n in this case might be as high has 20 or 30.
I think I see a problem in the code (maybe 3 problems -- see "questionable" 1 & 2 below) --
1) [Changed on 10/21, after OP's comments] The big problem: table parameters passed in the final "EXECUTE sp_executesql #SQLQuery..." are sometimes not declared.
1a) #Summary is actually never declared... you declared and set #SummaryTable, then use #Summary.
Just change that to #SummaryTable (I did that in (4) below), and I think that will prevent your second error message ("Must declare the table variable #Summary").
1b) All the other tables are sometimes declared: each of their DECLARE statements are within an "IF EXISTS". I suggest either (I) make the declares unconditional (outide IF EXISTS), but still conditionally INSERT... or (II) make the format of the EXECUTE command vary with what's available.
(I doubt the unused table variables need anything in them....)
In point 4 below (added 10/21), I give an example that I have NOT TESTED with my own databases (that test would take a lot more time)... so please let me know how it goes...
2) [questionable 1] Simple case of mixed case ;) Note that the line...
Set #SQLQuery = #SqlQuery + ' FULL OUTER JOIN #<TableName> ON <TableName>.ID = SummaryTable.ID'
... first has an uppercase "SQL", then mixed-case "Sql".
Why I said this is "questionable" -- if your server's COLLATION is case-insensitive, what you typed above would be fine.
3) [questionable 2] You have both '#TableName' and '#Table Name' (with a space). I realize that may just be how you typed it, in posting your question.
Point (4), added in update to answer -- possible code
create procedure <procedure> (#OrderNumber varchar(20)) AS
DECLARE #CustomerName varchar(35) -- retrieved based on #OrderNumber
DECLARE #SQLQuery nvarchar(500)
DECLARE #ParamDef nvarchar(500)
DECLARE #SummaryTable table
(
ID varchar(20) --=#OrderNumber
, <Stuff>
)
SET #SQLQuery = 'Select * From #SummaryTable'
SET #ParamDef = '#SummaryTable table'
--Create table variables, though they may not be populated
DECLARE #LineItems
(
ID varchar(20)
, <Stuff>
)
DECLARE #Taxes
(
ID varchar(20)
, <Stuff>
)
DECLARE #Currencytable
(
ID varchar(20)
, <Stuff>
)
IF EXISTS(Select <TableName> from CustRequirements where <TableName> = 1 AND Customer = #CustomerName)
BEGIN
--Populate
Insert into <TableName> Select <RelevantData> From <DataSource> where <Condition based on #OrderNumber or #CustomerName>
--Prepare Dynamic SQL
Set #SQLQuery = #SQLQuery + ' FULL OUTER JOIN #<TableName> ON <TableName>.ID = SummaryTable.ID'
SET #ParamDef = ', #<TableName> table'
END
<repeat for other tables>
EXECUTE sp_executesql #SQLQuery, #ParamDef, #SummaryTable, #LineItems, #Taxes, #Currency
The solution I've decided tp use is to create a table then use ALTER TABLE to add columns as needed.
CREATE Table #Output
(
InvoiceNumber varchar(20)
, <stuff>
)
IF EXISTS(Select <ThisSegment> FROM CustRequirements WHERE <ThisSegment> = 1 AND Customer = #CustomerName)
BEGIN
ALTER TABLE #Output ADD <ThisSegment> <dataType>
UPDATE #Output
SET <ThisSegment> = <data> from <DataSource>
WHERE <InvoiceNumber = DataSource.invoice> AND <Other conditions>
END
<Repeat as needed>

Passing multiple values for one SQL parameter

I have a CheckBoxList where users can select multiple items from the list. I then need to be able to pass these values to my Stored Procedure so they can be used in a WHERE condition like:
WHERE ID IN (1,2,3)
I tried doing this so that its a nvarchar parameter and i pass the string 1,2,3 with:
WHERE ID IN (#IDs)
But this returned the following error:
Conversion failed when converting the nvarchar value '1,2,3' to data type int
Any help would be much appreciated!
There's a few ways of doing it.
You could pass in the parameter as an XML blob like this example:
CREATE PROCEDURE [dbo].[uspGetCustomersXML]
#CustomerIDs XML
AS
BEGIN
SELECT c.ID, c.Name
FROM [dbo].[Customer] c
JOIN #CustomerIDs.nodes('IDList/ID') AS x(Item) ON c.ID = Item.value('.', 'int' )
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersXML] '<IDList><ID>1</ID><ID>10</ID><ID>100</ID></IDList>'
Or pass in the values as CSV and use a split function to split the values out into a table variable (there's a lot of split functions out there, quick search will throw one up).
CREATE PROCEDURE [dbo].[uspGetCustomersCSV]
#CustomerIDs VARCHAR(8000)
AS
BEGIN
SELECT c.Id, c.Name
FROM [dbo].[Customer] c
JOIN dbo.fnSplit(#CustomerIDs, ',') t ON c.Id = t.item
END
GO
--Example Use:
EXECUTE [dbo].[uspGetCustomersCSV] '1,10,100'
If you were using SQL 2008 or later, you could have used Table Valued Parameters which allow you to pass a TABLE variable in as a parameter. I blogged about these 3 approaches a while back, with a quick performance comparison.
alter procedure c2
(#i varchar(5))
as
begin
declare #sq nvarchar(4000)
set #sq= 'select * from test where id in (<has_i>) '
SET #sq= REPLACE(#sq, '<has_i>', #i)
EXECUTE sp_executesql #sq
end
exec c2 '1,3'
I did find a solution for a similar problem.
It is used for a data driven subscription, but can be easily altered for use in a parameter.
check my blog post here with a detailed description
If you are having problem converting it to a stored procedure call, just let me know.