Variable = MAX() Causing Incorrect Syntax - tsql

Have the following SQL below which will be part of a larger SP. I'm receiving a syntax error and I'm assuming it's due to the selected variable #ColumnMM containing a MAX function when trying to be executed withing the string of the variable #SQL. For example, changing Line 25 (#Column1) to
SELECT #Column01 = 'abcd01' FROM MonthlySummary works just fine.
How do I best resolve this?
IF OBJECT_ID('New_Report', 'U') IS NOT NULL
DROP TABLE New_Report
;
CREATE TABLE New_Report (
Area NVARCHAR(255)
, Division NVARCHAR(255)
)
;
DECLARE #Column01 VARCHAR(6)
DECLARE #Column02 VARCHAR(6)
DECLARE #Column03 VARCHAR(6)
DECLARE #Column04 VARCHAR(6)
DECLARE #Column05 VARCHAR(6)
DECLARE #Column06 VARCHAR(6)
DECLARE #Column07 VARCHAR(6)
DECLARE #Column08 VARCHAR(6)
DECLARE #Column09 VARCHAR(6)
DECLARE #Column10 VARCHAR(6)
DECLARE #Column11 VARCHAR(6)
DECLARE #Column12 VARCHAR(6)
;
SELECT #Column01 = MAX(reporting_year)+'01' FROM MonthlySummary
SELECT #Column02 = MAX(reporting_year)+'02' FROM MonthlySummary
SELECT #Column03 = MAX(reporting_year)+'03' FROM MonthlySummary
SELECT #Column04 = MAX(reporting_year)+'04' FROM MonthlySummary
SELECT #Column05 = MAX(reporting_year)+'05' FROM MonthlySummary
SELECT #Column06 = MAX(reporting_year)+'06' FROM MonthlySummary
SELECT #Column07 = MAX(reporting_year)+'07' FROM MonthlySummary
SELECT #Column08 = MAX(reporting_year)+'08' FROM MonthlySummary
SELECT #Column09 = MAX(reporting_year)+'09' FROM MonthlySummary
SELECT #Column10 = MAX(reporting_year)+'10' FROM MonthlySummary
SELECT #Column11 = MAX(reporting_year)+'11' FROM MonthlySummary
SELECT #Column12 = MAX(reporting_year)+'12' FROM MonthlySummary
;
DECLARE #SQL NVARCHAR(MAX)
;
SET #SQL = 'ALTER TABLE New_Report ADD ' + #Column01 + ' VARCHAR(6)';
EXEC sys.sp_executesql #SQL;
AND FYI -- The result of SELECT MAX(reporting_year)+'01' FROM MonthlySummary would be 202001. reporting_year is a VARCHAR(4) in MonthlySummary although I know that doesn't matter after using an aggregate function on it.

Take a look at this link from the documentation:
https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver15#rules-for-regular-identifiers
Basically, you can't use a digit character as the first character of a table name, column name, etc.
That aside, I also see a type mismatch problem. MAX(reporting_year) is a number, but '01', '02', etc are text, and so you need to cast to combine them together. Additionally, the code is unnecessarily repetitive, both in terms of how it's written and for the work it causes the server to do. There's no good need to run the same aggregation across the entire table multiple times.
DECLARE #MaxYear AS VARCHAR(4)
SELECT #MaxYear = CAST(MAX(reporting_year) As varchar(4)) FROM MonthlySummary
DECLARE #Column01 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'01')
DECLARE #Column02 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'02')
DECLARE #Column03 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'03')
DECLARE #Column04 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'04')
DECLARE #Column05 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'05')
DECLARE #Column06 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'06')
DECLARE #Column07 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'07')
DECLARE #Column08 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'08')
DECLARE #Column09 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'09')
DECLARE #Column10 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'10')
DECLARE #Column11 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'11')
DECLARE #Column12 VARCHAR(9) = QUOTENAME('_'+#MaxYear+'12')
Finally, as a matter of good practice I object to naming the columns like this. If you want pivoted data, that's fine, but then let your client tool worry about the names and adjust as needed. Do this, and it's likely you can significantly simplify this entire procedure, possibly down the point of a single SELECT statement.

Related

How to use sp_execute passing table parameter

I need to pass a table var to sp_executesql statement. Do you know how can I pass the table variable to sp_executesql?
Here is how I pass the regular variable (not table variable) to sp_executesql
EXEC sp_executesql #statement, N'#Status INT',#Status
Typically you don't pass a table variable to execute SQL with sp_executesql. You make a statement up out of text and execute that. Like so:
IF OBJECT_ID('tempdb..#People') IS NOT NULL
DROP TABLE tempdb..#People
CREATE TABLE #People (PersonId INT IDENTITY, PersonName VARCHAR(128));
INSERT INTO #People (PersonName) VALUES ('Brett'), ('John'), ('Mark'), ('Shawn'), ('Ryan'), ('Kevin');
DECLARE #SQL NVARCHAR(Max) = 'Select * from #People'
EXEC sp_executesql #Sql
UPDATE 1-27-17
IF OBJECT_ID('tempdb..#People') IS NOT NULL DROP TABLE tempdb..#People
IF OBJECT_ID('tempdb..#People2') IS NOT NULL DROP TABLE tempdb..#People2
CREATE TABLE #People (PersonId INT IDENTITY, PersonName VARCHAR(128));
CREATE TABLE #People2 (PersonId INT IDENTITY(7,1), PersonName VARCHAR(128));
SET NOCOUNT ON;
INSERT INTO #People (PersonName) VALUES ('Brett'), ('John'), ('Mark'), ('Shawn'), ('Ryan'), ('Kevin');
INSERT INTO #People2 (PersonName) VALUES ('Emily'), ('Beth'), ('Jane'), ('Hannah');
--I. getting an output for a single output variable dynamically
--Say I just want to get Ryan by his Id dynamically and output it
--I need to define one or many parameters OUTSIDE the scope of the Dynamic Sql
DECLARE #Output VARCHAR(8)
DECLARE #PersonId INT = 5
--I then need to associate the parameters as an array, for the purposes of explanation I will use DIFFERENT NAMES you may use the same
DECLARE #ParmDefinition NVARCHAR(500) = N'#PersonIdInside Int, #OutputInside varchar(8) OUTPUT'
--I then use the names ABOVE in the dynamic sql
DECLARE #SQL NVARCHAR(Max) = N'Select #OutputInside = PersonName from #People Where PersonId = #PersonIdInside'
-- I then do the following AFTER the sp_executesql 1. The Dynamic sql nvarchar, 2. The params nvarchar 3. one or many variables and how they associate
EXEC sp_executesql #Sql, #ParmDefinition, #PersonIdInside = #PersonId, #OutputInside = #Output OUTPUT
-- I have an output so now it should show what I want
SELECT #Output
-- II. getting a result set dymamically to another record set or table OUTSIDE the scope of the internal
-- Create another table, I use a #table for example purposes
IF OBJECT_ID('tempdb..#Output') IS NOT NULL DROP TABLE tempdb..#Output
CREATE TABLE #Output (PersonId INT IDENTITY, PersonName VARCHAR(8))
--Get a truncated list for an 'in' statement later of person Id's in a variable
DECLARE #People NVarchar(32) = N'1, 5, 10'
--I then use the #People ABOVE in the dynamic sql putting it together and then do an 'insert statement first'
DECLARE #SQL2 NVARCHAR(Max) = N'Insert Into #Output SELECT PersonName FROM (SELECT * FROM #People UNION SELECT * FROM #People2) as x Where PersonId in (' + #People + ')'
--execute yields nothing
EXEC sp_executesql #Sql2
-- or does it?
Select *
From #Output
-- !!! WARNING !!!
-- With dynamic sql you cannot nest multiple dynamic sql statements inside of procs. EG: Proc1 cannot call Proc2 and both of them have dynamic sql in them. Engine limitation.

check which well known text can be transformed using geometry::STPolyFromText(

I have some data which I bulk import into this table structure:
CREATE TABLE #Temp
(
WellKnownText NVARCHAR(MAX)
)
Some of the entries are not valid. So something like this:
SELECT geometry::STPolyFromText(WellKnownText,4326) FROM #Temp
does not work for all rows and thus falls over.
What is the best way to detect which WellKnownText are not valid? I have used MakeValid in the past - so ideally I would like to fix entries as much as possible.
PS:
This does not work:
SELECT * FROM #Temp
WHERE geometry::STPolyFromText(WellKnownText,4326).STIsValid() = 0
PPS:
I chose a loop based approach in the end along those lines:
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL DROP TABLE #Temp;
IF OBJECT_ID('tempdb..#Temp1') IS NOT NULL DROP TABLE #Temp1;
DECLARE #LoopCounter INT = 1;
DECLARE #MaxCounter INT;
DECLARE #Valid BIT;
DECLARE #ValidCounter INT;
DECLARE #WellKnownText NVARCHAR(MAX);
CREATE TABLE #Temp
(
Guid UNIQUEIDENTIFIER,
PostcodeFraction NVARCHAR(50),
WellKnownText NVARCHAR(MAX),
GeoJson NVARCHAR(MAX)
);
CREATE TABLE #Temp1
(
Guid UNIQUEIDENTIFIER,
PostcodeFraction NVARCHAR(50),
WellKnownText NVARCHAR(MAX),
GeoJson NVARCHAR(MAX)
);
BULK INSERT #Temp FROM 'D:\PolygonData.txt' WITH (FIELDTERMINATOR = '\t', FIRSTROW = 2, ROWTERMINATOR = '\n');
ALTER TABLE #Temp ADD Id INT IDENTITY(1,1);
SELECT #MaxCounter = MAX(Id) FROM #Temp
SET #ValidCounter = 0;
WHILE(#LoopCounter <= #MaxCounter)
BEGIN
BEGIN TRY
SELECT #WellKnownText = WellKnownText FROM #Temp WHERE Id = #LoopCounter;
SET #Valid = GEOMETRY::STGeomFromText(#WellKnownText,4326).STIsValid();
SET #ValidCounter = #ValidCounter + 1;
END TRY
BEGIN CATCH
SET #Valid = 0;
END CATCH
IF(#Valid = 1)
BEGIN
INSERT INTO #TEMP1
SELECT Guid, PostcodeFraction, WellKnownText, GeoJson FROM #Temp WHERE Id = #LoopCounter;
END
SET #LoopCounter = #LoopCounter + 1;
END
PRINT #ValidCounter;
SELECT * FROM #TEMP1;
As requested in the comments, some possible solutions
I guess you're really looking for a function that can be CROSS APPLYed, something like
SELECT * FROM #Temp T
CROSS APPLY IsWKTValidFunc(T.WellKnownText, 4326) F
WHERE F.IsValid = <somecondition>
(Or even added to as computed column to give you a flag that's set on inserting your WKT)
Stored Proc
https://gis.stackexchange.com/questions/66642/detecting-invalid-wkt-in-text-column-in-sql-server has a simple SP that wraps GEOMETREY::STGeomFromText in a try catch block.
However, stored procs cannot be CROSS APPLYed (or called from a UDF that can be) so this would result in a cursor based solution.
UDF
A UDF can be cross applied, but can't have a TRY-CATCH block. You also can't call the above SP from a UDF. So not much use there.
CLR UDF
Wrap the GEOMETREY::STGeomFromText call in a CLR UDF that can be CROSS APPLIED, can have try catch and other error checking, rules etc, and return a flag indicating valid text. I haven't tried this one out but this sounds like the best option if CLR is enabled in your environment.
Hope this gives you some ideas. Feedback in the comments to these suggestions appreciated.

Why does setting a varible like this make a difference?

I have a scalar value function that returns a VarChar(MAX) In my stored procedure I do this
declare #p_emailAddr varchar(MAX) = (select db.dbo.GetEmails(10))
If I do print #p_emailAddr it shows me it was populated with the correct information but the rest of the code doesn't work correctly using it. (I have no clue why, it doesn't make sense!)
Now if I change it like this
declare #p_emailAddr varchar(MAX) = 'test#email.com;'
The rest of my code works perfect as it should!
What is the difference between the two methods of setting #p_emailAddr that is breaking it?
This is get emails code
ALTER FUNCTION [dbo].[GetEmails](#p_SubID int)
RETURNS varchar(max)
AS
BEGIN
DECLARE #p_Emails varchar(max)
SELECT #p_Emails = COALESCE(#p_Emails + ';', '') + E.EmailAddress
FROM db.dbo.UserEmailAddr E JOIN
db.dbo.EmailSubscriptionUsers S on e.ClockNumber = s.Clock AND S.SubID = #p_SubID
SET #p_Emails = #p_Emails + ';'
RETURN #p_Emails
END
What's coming back from GetEmails(10)? varchar(max) is a string value and is expecting a single value. you could have a table variable or if dbo.getemails(10) is a table just join it where you're expecting to use #p_emailaddr
best
select *
from table1 t1
join dbo.GetEmails(10) e
on e.email = t1.email
alternative
create table #GetEmails (emails varchar(max))
insert into #GetEmails values ('email#test.com'), ('test#email.com')
declare #p_emailAddr table (emails varchar(max))
insert into #p_emailAddr(emails)
select *
from #GetEmails
select *
from #p_emailAddr

two table input parameters in stored procedure

I am working on C# project which needs a stored procedure which will take two table names as inputs.
First table will copy data to a temp table which has two columns URL & channelID. This URL column is then matched with other input table's URL column & if match is found then it will update channel id from temp table to other tables channel ID.
I have written stored procedure as
CREATE PROCEDURE [dbo].[UpdateTables]
#excelTable NVARCHAR(128) ,
#TableName NVARCHAR(128)
AS
Declare #channel_Id nvarchar(50)
Declare #url varchar(400)
BEGIN
Select *
Into #Temp
From QUOTENAME(#excelTable)
END
While EXISTS(SELECT * From #Temp ) > 0
Begin
Select Top 1
#channel_Id = channel_Id, #url = url
From #Temp
update QUOTENAME(#TableName)
set channelid = #channelid
where pagefullurl like '%'+ #url + '%'
Delete #Temp
Where channelid = #channelid
End
I don't have much knowledge in TSQL and my above code has errors.
Incorrect syntax near '>'.
Msg 137, Level 15, State 2, Procedure UpdateTables, Line 20
Must declare the scalar variable "#channelid".
Msg 137, Level 15, State 2, Procedure UpdateTables, Line 22
Must declare the scalar variable "#channelid".
Please suggest what changes needs to done
I don't have MS SQL server handy to test it, but you declare your variable as #channel_Id, and later try to use it as #channelid (without the underscore) so you get errors about the undeclared variable.
I've corrected your SP and this is how it should look
CREATE PROCEDURE [dbo].[UpdateTables]
#excelTable NVARCHAR(128) ,
#TableName NVARCHAR(128)
AS
Declare #channel_Id nvarchar(50)
Declare #url varchar(400)
BEGIN
Select *
Into #Temp
From QUOTENAME(#excelTable)
While EXISTS(SELECT * From #Temp )
Begin
Select Top 1
#channel_Id = channel_Id, #url = url
From #Temp
update QUOTENAME(#TableName)
set channelid = #channel_Id
where pagefullurl like '%'+ #url + '%'
Delete #Temp
Where channelid = #channel_Id
End
END

Dynamic SQL and Functions

is there any way of accomplishing something like the following:
CREATE FUNCTION GetQtyFromID
(
#oricod varchar(15),
#ccocod varchar(15),
#ocmnum int,
#oinnum int,
#acmnum int,
#acttip char(2),
#unisim varchar(15)
)
AS
RETURNS DECIMAL(18,8)
BEGIN
DECLARE #Result decimal(18,8)
DECLARE #SQLString nvarchar(max);
DECLARE #ParmDefinition nvarchar(max);
--I need to execute a query stored in a cell which returns the calculated qty.
--i.e of AcuQry: select #cant = sum(smt) from table where oricod = #oricod and ...
SELECT #SQLString = AcuQry
FROM OinActUni
WHERE (OriCod = #oricod) AND (ActTipCod = #acttip) AND (UniSim = #unisim) AND (AcuEst > 0)
SET #ParmDefinition = N'
#oricod varchar(15),
#ccocod varchar(15),
#ocmnum int,
#oinnum int,
#acmnum int,
#cant decimal(18,8) output';
EXECUTE sp_executesql #SQLString, #ParmDefinition,
#oricod = #oricod,
#ccocod = #ccocod,
#ocmnum = #ocmnum,
#oinnum = #oinnum,
#acmnum = #acmnum,
#cant = #result OUTPUT;
RETURN #Result
END
The problem with this approach is that it is prohibited to execute sp_excutesql in a function...
What I need is to do something like:
select id, getQtyFromID(id) as qty
from table
The main idea is to execute a query stored in a table cell, this is because the qty of something depends on it's unit. the unit can be days or it can be metric tons, so there is no relation between the units, therefore the need of a specific query for each unit.
What about using an if then or case expression in a stored procedure to check the unit, then perform specific calculations based on the type of unit?