SQL Server: set a variable with more than one possible value - sql

I need your help. I have a command to update rows, this works if in a variable #ID contain one value.
DECLARE #ID nvarchar(100)
SET #ID = (select top 1 id from [SERVICES])
DECLARE #UPDATE nvarchar(max)
SET #UPDATE ='UPDATE SERVICES
SET SERVICES.options = t1.options
FROM SERVICES t
JOIN (SELECT *
FROM OPENQUERY([ORI], ''SELECT ID, options
FROM log
WHERE ID = ''''' + #ID + ''''' '')) t1 ON t1.id = t.id'
EXEC (#UPDATE)
but I need to update more than 1 rows.
If I specify a condition like this:
SET #ID = (SELECT id FROM [ReportSM].[dbo].[SERVICES])
I get an error:
Subquery returned more than 1 value.
How to fix it?

It sounds like you really want to pass a table valued parameter into the open query, but that's not supported. You can remove that filter and let the join take care of the update accuracy but that will result in a potentially much more expensive remote query than necessary. That solution would just look like this:
UPDATE
t
SET
t.options = t1.options
FROM
Services t
JOIN (SELECT ID, options FROM OPENQUERY([ORI], 'SELECT ID, options FROM
log')) t1 ON t1.id = t.id
However, if you have control over the ORI linked server, you could set up a linked server there back to your ReportSM server. That would let you create a view on your ORI server that contains all of the IDs from your [ReportSM].[dbo].[SERVICES] table which is what you are trying to filter your log table on. That means you could perform the ID filtering on the ORI side and then run a simpler update on the ReportSM side. Something like this on the ORI side:
CREATE VIEW vReportServiceIDs
AS
SELECT
ID
FROM
[ReportSM].[dbo].[SERVICES]
CREATE VIEW vReportServiceLogs
AS
SELECT
reportService.ID,
oriLog.options
FROM
vReportServiceIDs reportService
JOIN [log] oriLog ON reportService.ID = [log].ID
And then on your ReportSM side:
UPDATE
t
SET
t.options = t1.options
FROM
SERVICES t
JOIN (
SELECT
ID, options
FROM
OPENQUERY([ORI], 'SELECT ID, options FROM vReportServiceLogs')
If you do not have that kind of access to the ORI server and the logs table has too much data for you to just query it all and exclude what you don't need during the join, you might want to consider creating a cache of the logs table that you update from a job on the ReportSM server and then just joining on that during your update.

Option 1
In your current setup, you could pass #ID as a CSV to OPENQUERY such that WHERE IN would work.
WHERE ID = ' + #ID + '
could then be replaced with
WHERE ID IN (' + #IDs + ')'
Look here to convert your ID column into a CSV: SQL Server convert select a column and convert it to a string
Be aware of the limit on the length of the IN clause. See this https://dba.stackexchange.com/questions/14161/what-is-the-maximum-number-of-parameters-i-can-pass-using-the-sql-in-clause-in-s
Option 2
Since concatenating data directly into a query has SQL injection concerns, you could also look at a more structured approach of using FOR XML to convert the IDs into an xml fragment and passing that into OPENQUERY and within that reading the ids out using OPENXML.
If both your servers are SQL Server 2016 or above, you could also use JSON as your format for transferring the ids instead of XML. You would use FOR JSON to create a JSON array containing the ids and use OPENJSON on the destination SQL server to convert the JSON back into a rowset that you can join.
declare #json varchar(max) = (select id from [ReportSM].[dbo].[SERVICES] FOR JSON PATH)
This will generate a string like this
[{"id":1},{"id":2},{"id":3},{"id":4}]
You can add this into a variable in the query you are preparing and read it using below
SELECT ID
FROM OPENJSON (#json, '$')
WITH (ID IN '$.id')
Putting it together your query would look like this:
declare #json varchar(max) = (select id from [ReportSM].[dbo].[SERVICES] FOR JSON PATH)
DECLARE #UPDATE nvarchar(max)
SET #UPDATE ='UPDATE SERVICES
SET SERVICES.options = t1.options
FROM SERVICES t
JOIN (SELECT *
FROM OPENQUERY([ORI], ''DECLARE #json nvarchar(max) = ''''' + #json + '''''
SELECT ID, options
FROM log
WHERE ID IN (SELECT ID FROM OPENJSON (#json, ''$'') WITH (ID IN ''$.id'')))) t1 ON t1.id = t.id'
EXEC (#UPDATE)

Related

How to use the EXEC command

I am trying to get a result from a database that will be selected after knowing the country of a user given his email.
I tried using EXEC() but now I get too many results from different users when I clearly indicate that I only want the result from certain user in the 'where' clause.
I first tried making the query with pure inner joins but it failed, it indicated that there was a syntax error, but if i ran it separately without the exec it worked.
After that I decided to use sub-queries, but as I mentioned above, it is returning all of the values, as if it wasn't considering the 'where'
What am I doing wrong?
Here is a sample of the query:
DECLARE #email nvarchar(150) = 'name.lastname#mx.company.com'
--Getting the country code of user
DECLARE #country_code nvarchar(3) = (SELECT country_code FROM general.countries WHERE id_country = (SELECT fk_country FROM databases_access.staff WHERE email = #email))
--Setting user database to search for job title & department
DECLARE #dbname NVARCHAR(25)
SET #dbname = 'dsv_global_' + #country_code
Declare #query nvarchar(500)
-- Query to be run to to get the user department and job title
SET #query =
'
USE '+#dbname+'
SELECT
id_staff,
email,
(SELECT complete_name_dept FROM dsv_global.departments WHERE id_department = fk_department),
(SELECT CONCAT(title,'' '',description) FROM dsv_global.job_titles WHERE id_job_title = (SELECT fk_title FROM dsv_global.staff_information WHERE fk_staff = id_staff)) ,
(SELECT COUNT(fk_staff) FROM dsv_global.staff_managers WHERE fk_manager = fk_staff)
FROM dsv_global.staff
WHERE email = '''+#email+''' AND status = ''ACTIVE''
'
----Storing department & title from user in temp table
--DECLARE #user_info TABLE (id_staff int, email nvarchar(200),complete_name_dept nvarchar(100), title nvarchar(200),num_of_errors int)
--INSERT INTO #user_info
EXEC(#query)
Edit:
I expect to receive:
But I receive:
It's worth to use Common Table Expressions when you deal with complex queries. You can put WITH to define a temporary named result set that available temporarily in the execution scope of a statement. And, by another hand, put GROUP BY for your COUNT function. Also, you need to put id_staff inside the WITH block, it looks like:
WITH cte_titles ( job_title)
AS (
SELECT CONCAT(title,'' '',description)
FROM dsv_global.job_titles
WHERE id_job_title IN
(SELECT fk_title
FROM dsv_global.staff_information
WHERE fk_staff = id_staff)
),
cte_staff (count_staff) AS
(
SELECT COUNT(fk_staff)
FROM dsv_global.staff_managers
WHERE fk_manager = fk_staff
GROUP BY fk_staff
)
SELECT
dsv.id_staff,
dsv.email,
job_title,
count_staff,
FROM dsv_global.staff dsv
cte_staff,
cte_titles
WHERE email = '''+#email+''' AND status = ''ACTIVE''
The problem is that i was not assigning enough space to store the query inside a string :
I had:
Declare #query nvarchar(500)
i changed it to :
Declare #query nvarchar(500)

No column name was specified for column 1 of 'src' Merge statement SQL server

I am using the MERGE statement in SQL server to refresh the data but I am repeatedly getting this error int the ON clause of the MERGE statement.
The code is
DECLARE #instance varchar(50)
DECLARE #db varchar (50)
DECLARE #queryEntity nvarchar(max)
SET #instance = (select value from Parameter where name = 'SERVERALIAS')
SET #db = (select value from Parameter where name = 'SERVERDB')
SET #queryEntity = 'Select EntityId,EntityName,CreatedDate,ModifiedDate,Active,TENANTID,PriorityId From [' + #instance + '].[' + #db + '].metadata.Entity Where TENANTID = 1'
MERGE [metadata].[Entity] AS trgt
USING ( VALUES(#queryEntity) ) AS src
ON ( **trgt.EntityId = src.EntityId** )
WHEN matched
--AND trgt.ModifiedDate <= src.ModifiedDate
THEN
-- if the master has a row newer than the client
-- update the client
UPDATE SET trgt.EntityId = src.EntityId,
trgt.EntityName = src.EntityName,
trgt.Createddate = src.CreatedDate,
trgt.ModifiedDate = src.ModifiedDate,
trgt.Active = src.Active,
trgt.TENANTID = src.TENANTID,
trgt.PriorityId = src.PriorityId
WHEN NOT matched BY SOURCE
THEN
DELETE
WHEN NOT matched BY TARGET
THEN
INSERT ( EntityId, EntityName, CreatedDate, ModifiedDate, Active, TENANTID, PriorityId)
VALUES ( src.EntityId, src.EntityName, src.CreatedDate, src.ModifiedDate, src.Active, src.TENANTID, src.PriorityId);
You haven't specified a column name for src -- the message is pretty clear. Try this:
MERGE [metadata].[Entity] AS trgt
USING ( VALUES(#queryEntity) ) AS src(EntityId)
--------------------------------------^
I should point out that this is only the beginning. src also doesn't have a host of other columns, specified in the rest of the MERGE. In fact, it is merely a string. MERGE doesn't execute a string just because it looks like a query.
You have three options. The first is to dispense with the variable and put the query string in the MERGE. But that doesn't seem possible because you have variable identifier names.
The second is to use dynamic SQL with the MERGE.
My recommendation, though, is to use dynamic SQL to create a view or to populate a table with a canonical name. Then use that for the MERGE statement.

How to write an attribute name to the select query dynamically

I have a table including:
ID Name Period0Id Period1Id Period2Id
What I would like to receive data based on a user-defined parameter #check.
Lets assume:
declare #check int = 1;
In this case I need to get Period1Id value from the table. So I need to have something like that:
Select ID, Name, StatusId = Period + #check + Id -- #check is the parameter
From mytable
However, my query is not working. How can I fix this?
Your table looks like it is not in first normal form.
Instead of three columns for Period0Id to Period2Id you could have a column for PeriodIndex with values of (0,1,2) and a single column for PeriodId and then it would be just a WHERE PeriodIndex = #Check
You can't select a column using string interpolation with a variable as you are attempting. You can use dynamic SQL to create the SQL String dynamically. Or simply hardcode the options if they all have the same dataype.
Select ID,
Name,
StatusId = CASE #Check WHEN 0 THEN Period0Id
WHEN 1 THEN Period1Id
WHEN 2 THEN Period2Id
END
From mytable
Here is an alternative way that will create dynamic columns, which is essentially using your original query:
DECLARE #check VARCHAR = 1
DECLARE #sqlquery NVARCHAR(MAX)
SET #sqlquery = N'SELECT ID, Name, StatusId = Period'+#check+'Id
FROM mytable'
EXEC sp_executesql #sqlquery

Execute table valued function from row values

Given a table as below where fn contains the name of an existing table valued functions and param contains the param to be passed to the function
fn | param
----------------
'fn_one' | 1001
'fn_two' | 1001
'fn_one' | 1002
'fn_two' | 1002
Is there a way to get a resulting table like this by using set-based operations?
The resulting table would contain 0-* lines for each line from the first table.
param | resultval
---------------------------
1001 | 'fn_one_result_a'
1001 | 'fn_one_result_b'
1001 | 'fn_two_result_one'
1002 | 'fn_two_result_one'
I thought I could do something like (pseudo)
select t1.param, t2.resultval
from table1 t1
cross join exec sp_executesql('select * from '+t1.fn+'('+t1.param+')') t2
but that gives a syntax error at exec sp_executesql.
Currently we're using cursors to loop through the first table and insert into a second table with exec sp_executesql. While this does the job correctly, it is also the heaviest part of a frequently used stored procedure and I'm trying to optimize it. Changes to the data model would probably imply changes to most of the core of the application and that would cost more then just throwing hardware at sql server.
I believe that this should do what you need, using dynamic SQL to generate a single statement that can give you your results and then using that with EXEC to put them into your table. The FOR XML trick is a common one for concatenating VARCHAR values together from multiple rows. It has to be written with the AS [text()] for it to work.
--=========================================================
-- Set up
--=========================================================
CREATE TABLE dbo.TestTableFunctions (function_name VARCHAR(50) NOT NULL, parameter VARCHAR(20) NOT NULL)
INSERT INTO dbo.TestTableFunctions (function_name, parameter)
VALUES ('fn_one', '1001'), ('fn_two', '1001'), ('fn_one', '1002'), ('fn_two', '1002')
CREATE TABLE dbo.TestTableFunctionsResults (function_name VARCHAR(50) NOT NULL, parameter VARCHAR(20) NOT NULL, result VARCHAR(200) NOT NULL)
GO
CREATE FUNCTION dbo.fn_one
(
#parameter VARCHAR(20)
)
RETURNS TABLE
AS
RETURN
SELECT 'fn_one_' + #parameter AS result
GO
CREATE FUNCTION dbo.fn_two
(
#parameter VARCHAR(20)
)
RETURNS TABLE
AS
RETURN
SELECT 'fn_two_' + #parameter AS result
GO
--=========================================================
-- The important stuff
--=========================================================
DECLARE #sql VARCHAR(MAX)
SELECT #sql =
(
SELECT 'SELECT ''' + T1.function_name + ''', ''' + T1.parameter + ''', F.result FROM ' + T1.function_name + '(' + T1.parameter + ') F UNION ALL ' AS [text()]
FROM
TestTableFunctions T1
FOR XML PATH ('')
)
SELECT #sql = SUBSTRING(#sql, 1, LEN(#sql) - 10)
INSERT INTO dbo.TestTableFunctionsResults
EXEC(#sql)
SELECT * FROM dbo.TestTableFunctionsResults
--=========================================================
-- Clean up
--=========================================================
DROP TABLE dbo.TestTableFunctions
DROP TABLE dbo.TestTableFunctionsResults
DROP FUNCTION dbo.fn_one
DROP FUNCTION dbo.fn_two
GO
The first SELECT statement (ignoring the setup) builds a string which has the syntax to run all of the functions in your table, returning the results all UNIONed together. That makes it possible to run the string with EXEC, which means that you can then INSERT those results into your table.
A couple of quick notes though... First, the functions must all return identical result set structures - the same number of columns with the same data types (technically, they might be able to be different data types if SQL Server can always do implicit conversions on them, but it's really not worth the risk). Second, if someone were able to update your functions table they could use SQL injection to wreak havoc on your system. You'll need that to be tightly controlled and I wouldn't let users just enter in function names, etc.
You cannot access objects by referencing their names in a SQL statement. One method would be to use a case statement:
select t1.*,
(case when fn = 'fn_one' then dbo.fn_one(t1.param)
when fn = 'fn_two' then dbo.fn_two(t1.param)
end) as resultval
from table1 t1 ;
Interestingly, you could encapsulate the case as another function, and then do:
select t1.*, dbo.fn_generic(t1.fn, t1.param) as resultval
from table1 t1 ;
However, in SQL Server, you cannot use dynamic SQL in a user-defined function (defined in T-SQL), so you would still need to use case or similar logic.
Either of these methods is likely to be much faster than a cursor, because they do not require issuing multiple queries.

Using IN with LIKE

OK I have a question to which an answer might be simple but im not sure how to do it:
Question:
How do i use IN with LIKE?
Why Not Duplicate:
Well I know if i have multiple strings i can use OR to check. But this is not the case with what i am trying to do.
Question Explaination:
I have an SP with a parameter #path, now i would like to send multiple paths, separated by delimiter, (to avoid calling sp multiple times). I split the string using a custom function which returns a table with splited values.
Now how would i go about using the values from that splited values table to be used with LIKE operator.
What I have done so far:
declare
#path varchar(max) = 'CompanyRules/Billing/IntegrationServices|CompanyRules/Reports/IntegrationServices',
#default_code varchar(max) = '1'
declare #tempTable TABLE(path varchar(max))
INSERT INTO #tempTable (path)
SELECT split from fn_splitby(#path, '|')
select prg.path, prg.default_code, prmd.optional_property_1, prmd.optional_property_2, prmd.optional_property_3, prmd.optional_property_4, prmd.optional_property_5, prmd.optional_property_6
from pdm_rule_group prg, pdi_rule_master prmd
where prg.path = prmd.path
AND prg.path in (select path from #tempTable)
AND prg.default_code != #default_code
The this will not yield any result.
Possible Solution:
What i though was that i have to loop through the #tempTable and then create separate strings to be used with LIKE. Which im sure is a bad solution, and may have some other solution to it.
Replace this statement
AND prg.path in (select path from #tempTable)
with
AND EXISTS (select 1 from #tempTable where prg.path like "%"+path+"%" )
Just join to the table:
select prg.path, prg.default_code, prmd.optional_property_1, prmd.optional_property_2, prmd.optional_property_3, prmd.optional_property_4, prmd.optional_property_5, prmd.optional_property_6
from
pdm_rule_group prg
inner join
pdi_rule_master prmd
on prg.path = prmd.path
inner join
#tempTable tt
on
prg.path like tt.path
where
prg.default_code != #default_code