How to call table variables outside stored procedure - sql

I want to access outside the scope of a stored procedure a table variable that it defines. I have tried this one, but my case is different because my procedure receives a varchar as a parameter and then it creates a varchar that is executed, someway like this:
create procedure p_x
#Table varchar(250)
as
begin
declare #Sql varchar(450)
select #sql = 'declare ' + #Table + 'table(col1 varchar(10), col2 float, col3 float, col4 float)'
select #sql = #sql + ' insert' + #Table + ' values('a', 1,1,1)'
select #sql = #sql + ' insert' + #Table + ' values('b', 2,2,2)'
select #sql = #sql + ' select * from'
execute(#sql)
end
go
The solution the other question does is to pass as a parameter the new table to be processed. But in my case, because the code is written in a decisive part inside a varchar concatenation (and also it creates auxiliary tables concatenating at the end of the varchar #Table), I don't know what to do.
To give some context: there is this procedure that uses global temporary tables, which were called inside queries. Everything was working fine until we need to change the query to transform it into a table-valued function. The query just access the tables defined inside the procedure through global scope, but the table-valued function doesn't allow to access these global temporary tables. It seems that table variables can't be global.
In short, to change the query to a table-valued function, I need to change the procedure's temporary tables into table variables that I can access inside the table-valued function.
The big picture: 1) Today I have a query that works and this query calls a procedure.
2) I want to be able to call this query from a API without having to paste 100 lines of query
3) I received the suggestion of converting the query into a TFV
4) I did it, but it doesn't work, because TFV can't use temporary tables
5) I want a workaround to create a copy of the procedure with some minor changes that I can call from the TVF.

You cannot do this with a table variable. You can do it with a Temp Table, but table variables are automatically out of scope and inaccessible outside of the procedure that creates them.

Related

Should I always use dynamic sql when programatically use some stored procedure?

I have a stored procedure that can get the number of records in a table, in which the #tableName is the parameter of the stored procedure. Let's call it FastCount:
SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID(#tableName)
AND index_id < 2
GROUP BY OBJECT_NAME(object_id);
Now, let's say I have 50 tables, like data_1950, data_1951, .....data_2000. I wrote a batch, query each table's records count, and put them into a temporary table. It works like a charm
CREATE TABLE #Temp
(
TableName varchar(30),
RecordsCount int
)
DECLARE #sql as varchar(max)
DECLARE #yearN as int = 1950
DECLARE #tbName as sysname
WHILE #yearN <= 2000
BEGIN
SET #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
SET #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
INSERT INTO #Temp
EXEC (#sql)
SET #yearN = #yearN + 1
END
SELECT * FROM #Temp
DROP TABLE #Temp
However, if I replace the dynamic SQL string part
SET #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
INSERT INTO #Temp
EXEC (#sql)
with a straightforward call
INSERT INTO #Temp
EXEC [dbo].FastCount #tableName = #tbName
Then the whole batch just not work...
So I don't understand why... Should I always use dynamic SQL string and exec(#sql) when programmatically using the stored procedure. A big thanks for taking the time to look.
OK, here is what is happening in the two scenarios that you posed in your original question. (Yes, the reality is that there are probably better ways to achieve your end result, but let's look at the actual problem that you posed .... why is the behaviour of your INSERT / EXEC different, depending on how you made the call).
First, you have your variable declared, that will contain your table name:
DECLARE #tbName as sysname
Then you have your looping block that incrementally increases the year number, to generate the different table names. There's nothing inherently wrong with the looping block, so let's just look at an example using one of the table names, to see what's happening within the WHILE block. Take the first table name as the example, which would be [dbo].data_1950.
Your statement:
set #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
ultimately takes the string "[dbo].data_1950" - which comes from concatenating '[dbo].data_' with the year number (in this case, 1950) converted to a string (varchar) - and passes it to the QUOTENAME() function. The QUOTENAME() function takes its input and a second parameter, which is the character that the input should be quoted with (if the 2nd parameter is not passed, then the default is []). Thus, if we then converted the #tbName variable to a string, it would appear like this:
[[dbo].data_1950]
Now we get to see the funky way that SQL deals with "sysname" data-types. (In fact, as you read further down, maybe the issue is not primarily tied to the "sysname" data-type, but anyhow, take away from this what you will). To be honest, "sysname" is, in itself, a little bit of a funky data-type anyway, which I tend to steer away from, unless absolutely necessary. But anyhow, on to the details of the issue that you were seeing.
Step 1 - I created a version of your stored proc, but I included a statement that would output the value of the #tableName parameter that was passed in. This gives us an opportunity to see what SQL is doing in the two different scenarios, and then explain why the results are different.
CREATE PROC [dbo].FastCount
(
#tableName varchar(100)
)
AS
BEGIN
PRINT #tableName;
SELECT OBJECT_NAME(object_id), SUM(row_count) AS rows
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID(#tableName)
AND index_id < 2
GROUP BY OBJECT_NAME(object_id);
END
Step 2 - our first scenario is executing the dynamic SQL.
set #tbName = QUOTEName(N'[dbo].data_' + Convert(varchar,#yearN))
set #sql = N'Exec [dbo].FastCount #tableName=' + #tbName
Insert Into #Temp Exec(#sql)
Now, we know that the #tbName variable contains
[[dbo].data_1950]
and therefore we can then infer that the #sql variable contains
Exec [dbo].FastCount #tableName=[[dbo].data_1950]
so that is effectively the statement that is executed by the Exec(#sql) command.
When this runs, and we look at the output of the PRINT command, we see
[dbo].data_1950
and we see a result from our query (the table name and row count). This makes sense, of course, because our table name is "data_1950", and the schema of the table is "dbo", so the SELECT statement to get the row count is going to work as expected.
Step 3 - run the EXEC command directly, without the use of the #sql variable, ie.
Insert Into #Temp Exec [dbo].FastCount #tableName = #tbName
Now, when we look at the output of the PRINT command for this execution of the "FastCount" stored procedure, we see
[[dbo].data_1950]
Of course, this is now NOT going to produce the results that we expect, because we're telling SQL to find the row count for a table named "[dbo].data_1950" (in the absence of the specific schema, SQL will just assume the default schema. In this case, with a schema of [dbo], we'd be telling SQL to get the row count from a table named [dbo].[[dbo].data_1950] - which is clearly NOT the table name).
You should see the obvious difference - in one scenario, the parameter value that is passed into the stored is the "correct" reference to the table name, and in the other scenario it is not.
As a final step, let's look at how the "non-dynamic" SQL would be executed, to achieve the results that we need. In this instance, there's no need for the QUOTENAME() function:
set #tbName = N'[dbo].data_' + Convert(nvarchar,#yearN)
Insert Into #Temp Exec [dbo].FastCount #tableName = #tbName
When we run it in this way, we see the expected output ([dbo].data_1950) from the PRINT command, and we see the expected query results (containing the table name and row count).
Can I explain this behaviour, exactly? Errr, not necessarily ... maybe someone else will be able to explain specifically what is happening, and why. My only interpretation is that when the EXEC() statement is passed the dynamic sql (ie. #sql variable) it is first interpreting the entire string and stripping out identifiers (in the case, the surrounding [] ... on what basis is it making that decision, I don't know). As opposed to the non-dynamic execution, where the #tbName value ([[dbo].data_1950]) is just being passed straight in as the parameter, with no modification (and thus causing the unexpected end result that we saw).
Hopefully this information is useful to you (or, at least, to someone else in the future!).
In general you should avoid dynamic SQL, and you should avoid granting rights to execute dynamic SQL, unless absolutely necessary. This is for performance and security reasons.
The typical way to deal with this situation is to use a partitioned view:
CREATE VIEW DataView
AS
SELECT '1950' TableName, * FROM Data_1950
UNION ALL
SELECT '1951' TableName, * FROM Data_1951
UNION ALL
SELECT '1952' TableName, * FROM Data_1952
UNION ALL
SELECT '1953' TableName, * FROM Data_1953
UNION ALL
SELECT '1954' TableName, * FROM Data_1954
UNION ALL
SELECT '1955' TableName, * FROM Data_1955
(Keep adding select statements until you have covered all of your tables.)
Now to get your table counts all you need to do is execute this:
SELECT TableName, COUNT(*) RecordCount
FROM DataView
GROUP BY TableName
Isn't that much easier?

How to select newly inserted record(s) in MSSQL stored procedure in case of execution of dynamic insert script?

I want to select newly inserted record in my sql server database.
I know that the right solution for me is this:
DECLARE #OutputTbl TABLE (ID INT)
INSERT INTO MyTable(Name, Address, PhoneNo)
OUTPUT INSERTED.ID INTO #OutputTbl(ID)
VALUES ('Yatrix', '1234 Address Stuff', '1112223333')
But my question is about create insert script and execute it in specific stored procedure.
I wrote these two lines at the end of my stored procedure to insert my data to any table I need.
SET #sqlQuery = 'INSERT INTO ' + #tblName + '(' + #param_Collection + ') ' + 'OUTPUT INSERTED.' + #IDENT_Field + ' values('+ #value_Collection +')';
EXEC sys.sp_executesql #sqlQuery;
All variables declared before.
I don't know how to access the identity field to select proper newly inserted record(s).
No, you can't.
A table-valued parameter must be declared with the READONLY option. Thus you can't insert into it.
Even if you declare it in the dynamic SQL, you can insert the data into it, but you can't get the data from the table, because the table variable is isolated from the scope of the dynamic SQL.

How to create a simple stored procedure with table name as an input

I am using SQL Server 2017 and I would like to create a stored procedure with a single table name as an input variable. All I want the procedure to do is update that table in a variety of ways. This project will be done twice a year, and the columns will always be the same, so I would like to try this as a stored procedure, so I do not have to highlight several lines of code and executing each time.
Is there a simple way to pass a table name through a stored procedure which updates the table (adding columns, calculating columns, replacing nulls in columns etc). In a basic example, one task would be just replaces nulls with 0s in a column. I am not sure how to set this up though. DO I have to declare every column in the table too?
CREATE PROCEDURE updteleform
#tablename TABLE
AS
BEGIN
UPDATE #tablename
SET Recog = 0
WHERE Recog IS NULL
END
GO
I'm assuming you want to update a physical table. SQL Server table variables don't work that way, rather they are a way to pass a transient result set to a stored procedure. There is no persistence if your stored procedure does not do so.
If you are looking to update the same table, then just write the procedure to work on that table.
If you are looking to update multiple tables using the same script then you should change your procedure to accept a string parameter that would be the name of the table you want it to work on and then use dynamic SQL in your stored procedure.
Something like
CREATE PROCEDURE updteleform #tablename sysname
AS
BEGIN
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'
update ' + QUOTENAME(#tablename) + '
set Recog= 0
where Recog is null;';
EXEC sp_executesql #sql;
END
GO
And then call it with something like:
EXEC updteleform #tablename = 'table1';
EXEC updteleform #tablename = 'table2';
EXEC updteleform #tablename = 'table3';
...

Select from temp table fails

I'm inserting data into a temp table and querying the temp table fails
DECLARE #SQLQuery AS NVARCHAR(500)
SET #SQLQuery = 'SELECT Top 100 *
INTO #tempTable
FROM ' + #origDB + '.dbo.' + #origTable + ' o WITH (NOLOCK) '
EXECUTE sp_executesql #SQLQuery
and when I try to query the temp table , like so
select * from #tempTable
I get the following error :
Invalid object name '#tempTable'.
Courtesy of MSDN
The problem that you have is with the scope. The TEMP table is creatd at the scope of the EXEC() method and hence it is not available after the function returns. To fix this, create your temp table before calling EXEC() and use an INSERT INTO instead of SELECT INTO.
As others have said, the scope of a temporary table is limited to the session context in which it is created - a stored procedure runs in its own context.
You could use a global temporary table ##tempTable, but it's generally a bad idea as it would be available to other sessions than the one creating it.

Call dynamic SQL from function

I am writing a function that returns a table. There are two parameters that are passed to the function and a query is built and executed and inserted into the returning table. However I am receiving this error.
Only functions and some extended stored procedures can be executed from within a function.
I would like to not use a stored procedure as this is a simple utility function. Does anyone know if this can be done. My function is coded below, it checks for dupes for a certain column within a certain table.
-- =============================================
-- AUTHOR: JON AIREY
-- THIS FUNCTION WILL RETURN A COUNT OF HOW MANY
-- TIMES A CERTAIN COLUMN VALUE APPEARS IN A
-- TABLE. THIS IS HELPFUL FOR FINDING DUPES.
-- THIS FUNCTION WILL ACCEPT A COLUMN NAME, TABLE
-- NAME (MUST INCLUDE SCHEMA), AND OPTIONAL
-- DATABASE TO USE. RESULTS WILL BE RETURNED AS
-- A TABLE.
-- =============================================
ALTER FUNCTION [dbo].[fn_FindDupe]
(
-- Add the parameters for the function here
#Column VARCHAR(MAX),
#Table VARCHAR(100),
#Database VARCHAR(100) = ''
)
RETURNS
#TempTable TABLE
([Column] varchar(100)
,[Count] int)
AS
BEGIN
DECLARE #SQL VARCHAR(MAX)
SET #Table = CASE
WHEN #Database = ''
THEN #Table
ELSE #Database + '.' + #Table
END
SET #SQL =
'
INSERT INTO #TempTable
SELECT ' + #Column + '
,COUNT(' + #Column + ') AS CNT
FROM ' + #Table + '
GROUP BY ' + #Column + '
ORDER BY CNT DESC
'
EXEC SP_EXECUTESQL #SQL
RETURN
END
GO
You can't use dynamic sql in a udf:
This very simple: you cannot use dynamic SQL from used-defined
functions written in T-SQL. This is because you are not permitted do
anything in a UDF that could change the database state (as the UDF may
be invoked as part of a query). Since you can do anything from dynamic
SQL, including updates, it is obvious why dynamic SQL is not
permitted.
...
In SQL 2005 and later, you could implement your function as a CLR
function. Recall that all data access from the CLR is dynamic SQL.
(You are safe-guarded, so that if you perform an update operation from
your function, you will get caught.) A word of warning though: data
access from scalar UDFs can often give performance problems.