dynamic query with dynamic subquery - sql-server

I am trying (and failing) to create a dynamic query with a subquery.
I have a table of questions where the possible answers to each question need to be created dynamically from one or several other tables. I have a varchar field in the questions table that I want to use dynamically to query the possible answers. Example:
Questions Table:
-----------------------------------------------------------------------------
id | question | answer_query
-----------------------------------------------------------------------------
1 | Can this be done? | SELECT field1 + ' ' + field2 answers FROM table1 a JOIN table2 b ON b.field1 = a.field2 WHERE b.id = '#id'
Then I want a stored proc that creates a dynamic query like this:
DECLARE #id int
SET #id.......
DECLARE #sql_query varchar(3000);
SET #sql_query =
'SELECT q.id, q.question, (REPLACE(q.answer_query, ''#id'', #id))
FROM Questions q
JOIN Other Table ON ....
WHERE .....';
EXECUTE(#sql_query);
Apologies for the poor formatting!
Is what I am trying to do possible?

Try this:
DECLARE #id int
SET #id.......
DECLARE #sql_query varchar(3000);
SET #sql_query =
'SELECT q.id, q.question, '
+ (REPLACE(q.answer_query, '#id', #id))
+ ' FROM Questions q
JOIN Other Table ON ....
WHERE .....';
EXECUTE(#sql_query);

You want
SET #sql_query = (SELECT REPLACE(q.answer_query, '#id', #id)
FROM Questions q
JOIN Other Table ON ....
WHERE .....)
or
SELECT #sql_query = REPLACE(q.answer_query, '#id', #id)
FROM Questions q
JOIN Other Table ON ....
WHERE .....)
However this seems like a really bad idea for many reasons. Is there any other way to solve this problem.
For example -- instead of having the select statement in the table, reverence a view name. This solves many problems, the compiler can optimize you are not open to sql injection -- so much good.

I am not able to understand the query properly. Assumes that you want to replace the string '#id' from the answer_query with the values in the variable #id.
If so, please try the below query:
DECLARE #id int
SET #id=.....
DECLARE #sql_query varchar(3000);
SET #sql_query =
'SELECT q.id, q.question, (REPLACE(q.answer_query, ''#id'', '+CONVERT(VARCHAR(10),#id)+'))
FROM Questions q
JOIN Other Table ON ....
WHERE .....';
EXECUTE(#sql_query);

Thanks all for your comments. I have come up with the following solution, which is not as generic as I would like.
Table structure
Stored Proc:
CREATE PROCEDURE [dbo].[spQuestionsByTypeAndOther]
#type_id uniqueidentifier,
#other_param_id uniqueidentifier
AS
BEGIN
SET NOCOUNT ON;
SELECT q.id, q.question, q.required, dbo.fnGetAnswers(q.id, #other_param_id)
FROM Questions q
JOIN QuestionsToTypes t on t.question_id = q.id
WHERE t.type_id = #type_id
END
And a function (fnGetAnswers) which uses a case statement to return the answers based on the question id and one other parameter. Each case performs a different query against other tables in the database and flattens/concatenates the results into a csv string.

Related

Searching for multiple patterns in a string in T-SQL

In t-sql my dilemma is that I have to parse a potentially long string (up to 500 characters) for any of over 230 possible values and remove them from the string for reporting purposes. These values are a column in another table and they're all upper case and 4 characters long with the exception of two that are 5 characters long.
Examples of these values are:
USFRI
PROME
AZCH
TXJS
NYDS
XVIV. . . . .
Example of string before:
"Offered to XVIV and USFRI as back ups. No response as of yet."
Example of string after:
"Offered to and as back ups. No response as of yet."
Pretty sure it will have to be a UDF but I'm unable to come up with anything other than stripping ALL the upper case characters out of the string with PATINDEX which is not the objective.
This is unavoidably cludgy but one way is to split your string into rows, once you have a set of words the rest is easy; Simply re-aggregate while ignoring the matching values*:
with t as (
select 'Offered to XVIV and USFRI as back ups. No response as of yet.' s
union select 'Another row AZCH and TXJS words.'
), v as (
select * from (values('USFRI'),('PROME'),('AZCH'),('TXJS'),('NYDS'),('XVIV'))v(v)
)
select t.s OriginalString, s.Removed
from t
cross apply (
select String_Agg(j.[value], ' ') within group(order by Convert(tinyint,j.[key])) Removed
from OpenJson(Concat('["',replace(s, ' ', '","'),'"]')) j
where not exists (select * from v where v.v = j.[value])
)s;
* Requires a fully-supported version of SQL Server.
build a function to do the cleaning of one sentence, then call that function from your query, something like this SELECT Col1, dbo.fn_ReplaceValue(Col1) AS cleanValue, * FROM MySentencesTable. Your fn_ReplaceValue will be something like the code below, you could also create the table variable outside the function and pass it as parameter to speed up the process, but this way is all self contained.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION fn_ReplaceValue(#sentence VARCHAR(500))
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #ResultVar VARCHAR(500)
DECLARE #allValues TABLE (rowID int, sValues VARCHAR(15))
DECLARE #id INT = 0
DECLARE #ReplaceVal VARCHAR(10)
DECLARE #numberOfValues INT = (SELECT COUNT(*) FROM MyValuesTable)
--Populate table variable with all values
INSERT #allValues
SELECT ROW_NUMBER() OVER(ORDER BY MyValuesCol) AS rowID, MyValuesCol
FROM MyValuesTable
SET #ResultVar = #sentence
WHILE (#id <= #numberOfValues)
BEGIN
SET #id = #id + 1
SET #ReplaceVal = (SELECT sValue FROM #allValues WHERE rowID = #id)
SET #ResultVar = REPLACE(#ResultVar, #ReplaceVal, SPACE(0))
END
RETURN #ResultVar
END
GO
I suggest creating a table (either temporary or permanent), and loading these 230 string values into this table. Then use it in the following delete:
DELETE
FROM yourTable
WHERE col IN (SELECT col FROM tempTable);
If you just want to view your data sans these values, then use:
SELECT *
FROM yourTable
WHERE col NOT IN (SELECT col FROM tempTable);

Setting the value of a column in SQL Server based on the scope_identity() of the insert

I have a stored procedure in a program that is not performing well. Its truncated version follows. The MyQuotes table has an IDENTITY column called QuoteId.
CREATE PROCEDURE InsertQuote
(#BinderNumber VARCHAR(50) = NULL,
#OtherValue VARCHAR(50))
AS
INSERT INTO MyQuotes (BinderNumber, OtherValue)
VALUES (#BinderNumber, #OtherValue);
DECLARE #QuoteId INT
SELECT #QuoteId = CONVERT(INT, SCOPE_IDENTITY());
IF #BinderNumber IS NULL
UPDATE MyQuotes
SET BinderNumber = 'ABC' + CONVERT(VARCHAR(10),#QuoteId)
WHERE QuoteId = #QuoteId;
SELECT #QuoteId AS QuoteId;
I feel like the section where we derive the binder number from the scope_identity() can be done much, much, cleaner. And I kind of think we should have been doing this in the C# code rather than the SQL, but since that die is cast, I wanted to fish for more learned opinions than my own on how you would change this query to populate that value.
The following update avoids needing the id:
UPDATE MyQuotes SET
BinderNumber = 'ABC' + CONVERT(VARCHAR(10), QuoteId)
WHERE BinderNumber is null;
If selecting QuoteId as a return query is required then using scope_identity() is as good a way as any.
Dale's answer is better, however this can be useful way too:
DECLARE #Output TABLE (ID INT);
INSERT INTO MyQuotes (BinderNumber, OtherValue) VALUES (#BinderNumber, #OtherValue) OUTPUT inserted.ID INTO #Output (ID);
UPDATE q SET q.BinderNumber = 'ABC' + CONVERT(VARCHAR(10),o.ID)
FROM MyQuotes q
INNER JOIN #Output o ON o.ID = q.ID
;
Also, if BinderNumber is always linked to ID, it would be better to just create computed column
AS 'ABC' + CONVERT(VARCHAR(10),ID)

Does a WHERE clause that matches a column value to itself get optimized out in SQL Server 2008+?

I'm trying to clean up some stored procedures, and was curious about the following. I did a search, but couldn't find anything that really talked about performance.
Explanation
Imagine a stored procedure that has the following parameters defined:
#EntryId uniqueidentifier,
#UserId int = NULL
I have the following table:
tbl_Entry
-------------------------------------------------------------------------------------
| EntryId PK, uniqueidentifier | Name nvarchar(140) | Created datetime | UserId int |
-------------------------------------------------------------------------------------
All columns are NOT NULL.
The idea behind this stored procedure is that you can get an Entry by its uniqueidentifier PK and, optionally, you can validate that it has the given UserId assigned by passing that as the second parameter. Imagine administrators who can view all entries versus a user who can only view their own entries.
Option 1 (current)
DECLARE #sql nvarchar(3000);
SET #sql = N'
SELECT
a.EntryId,
a.Name,
a.UserId,
b.UserName
FROM
tbl_Entry a,
tbl_User b
WHERE
a.EntryId = #EntryId
AND b.UserId = a.UserId';
IF #UserId IS NOT NULL
SET #sql = #sql + N' AND a.UserId = #UserId';
EXECUTE sp_executesql #sql;
Option 2 (what I thought would be better)
SELECT
a.EntryId,
a.Name,
a.UserId,
b.UserName
FROM
tbl_Entry a,
tbl_User b
WHERE
a.EntryId = #EntryId
AND a.UserId = COALESCE(#UserId, a.UserId)
AND b.UserId = a.UserId;
I realize this case is fairly, simple, and could likely be optimized by a single IF statement that separates two queries. I wrote a simple case to try and concisely explain the issue. The actual stored procedure has 6 nullable parameters. There are others that have even more nullable parameters. Using IF blocks would be very complicated.
Question
Will SQL Server still check a.UserId = a.UserId on every row even though that condition will always be true, or will that condition be optimized out when it sees that #UserId is NULL?
If it would check a.UserId = a.UserId on every row, would it be more efficient to build a string like in option 1, or would it still be faster to do the a.UserId = a.UserId condition? Is that something that would depend on how many rows are in the tables?
Is there another option here that I should be considering? I wouldn't call myself a database expert by any means.
You will get the best performance (and the lowest query cost) if you replace the COALESCE with a compound predicate as follows:
(#UserId IS NULL OR a.UserId = #UserId)
I would also suggest when writing T-SQL that you utilize the join syntax rather than the antiquated ANSI-89 coding style. The revised query will look something like this:
SELECT a.EntryId, a.Name, a.UserId, b.UserName
FROM tblEntry a
INNER JOIN tblUser b ON a.UserId = b.UserId
WHERE a.EntryId = #EntryId
AND (#UserId IS NULL OR a.UserId = #UserId);

How can I replace all key fields in a string with replacement values from a table in T-SQL?

I have a table like:
TemplateBody
---------------------------------------------------------------------
1.This is To inform #FirstName# about the issues regarding #Location#
Here the key strings are #FirstName# and #Location# which are distinguished by hash tags.
I have another table with the replacement values:
Variables | TemplateValues
-----------------------------
1.#FirstName# | Joseph William
2.#Location# | Alaska
I need to replace these two key strings with their values in the first table.
There are several ways this can be done. I'll list two ways. Each one has advantages and disadvantages. I would personally use the first one (Dynamic SQL).
1. Dynamic SQL
Advantages: Fast, doesn't require recursion
Disadvantages: Can't be used to update table variables
2. Recursive CTE
Advantages: Allows updates of table variables
Disadvantages: Requires recursion and is memory intensive, recursive CTE's are slow
1.A. Dynamic SQL: Regular tables and Temporary tables.
This example uses a temporary table as the text source:
CREATE TABLE #tt_text(templatebody VARCHAR(MAX));
INSERT INTO #tt_text(templatebody)VALUES
('This is to inform #first_name# about the issues regarding #location#');
CREATE TABLE #tt_repl(variable VARCHAR(256),template_value VARCHAR(8000));
INSERT INTO #tt_repl(variable,template_value)VALUES
('#first_name#','Joseph William'),
('#location#','Alaska');
DECLARE #rep_call NVARCHAR(MAX)='templatebody';
SELECT
#rep_call='REPLACE('+#rep_call+','''+REPLACE(variable,'''','''''')+''','''+REPLACE(template_value,'''','''''')+''')'
FROM
#tt_repl;
DECLARE #stmt NVARCHAR(MAX)='SELECT '+#rep_call+' FROM #tt_text';
EXEC sp_executesql #stmt;
/* Use these statements if you want to UPDATE the source rather than SELECT from it
DECLARE #stmt NVARCHAR(MAX)='UPDATE #tt_text SET templatebody='+#rep_call;
EXEC sp_executesql #stmt;
SELECT * FROM #tt_text;*/
DROP TABLE #tt_repl;
DROP TABLE #tt_text;
1.B. Dynamic SQL: Table variables.
Requires to have the table defined as a specific table type. Example type definition:
CREATE TYPE dbo.TEXT_TABLE AS TABLE(
id INT IDENTITY(1,1) PRIMARY KEY,
templatebody VARCHAR(MAX)
);
GO
Define a table variable of this type, and use it in a Dynamic SQL statement as follows. Note that updating a table variable this way is not possible.
DECLARE #tt_text dbo.TEXT_TABLE;
INSERT INTO #tt_text(templatebody)VALUES
('This is to inform #first_name# about the issues regarding #location#');
DECLARE #tt_repl TABLE(id INT IDENTITY(1,1),variable VARCHAR(256),template_value VARCHAR(8000));
INSERT INTO #tt_repl(variable,template_value)VALUES
('#first_name#','Joseph William'),
('#location#','Alaska');
DECLARE #rep_call NVARCHAR(MAX)='templatebody';
SELECT
#rep_call='REPLACE('+#rep_call+','''+REPLACE(variable,'''','''''')+''','''+REPLACE(template_value,'''','''''')+''')'
FROM
#tt_repl;
DECLARE #stmt NVARCHAR(MAX)='SELECT '+#rep_call+' FROM #tt_text';
EXEC sp_executesql #stmt,N'#tt_text TEXT_TABLE READONLY',#tt_text;
2. Recursive CTE:
The only reasons why you would write this using a recursive CTE is that you intend to update a table variable, or you are not allowed to use Dynamic SQL somehow (eg company policy?).
Note that the default maximum recursion level is 100. If you have more than a 100 replacement variables you should increase this level by adding OPTION(MAXRECURSION 32767) at the end of the query (see Query Hints - MAXRECURSION).
DECLARE #tt_text TABLE(id INT IDENTITY(1,1),templatebody VARCHAR(MAX));
INSERT INTO #tt_text(templatebody)VALUES
('This is to inform #first_name# about the issues regarding #location#');
DECLARE #tt_repl TABLE(id INT IDENTITY(1,1),variable VARCHAR(256),template_value VARCHAR(8000));
INSERT INTO #tt_repl(variable,template_value)VALUES
('#first_name#','Joseph William'),
('#location#','Alaska');
;WITH cte AS (
SELECT
t.id,
l=1,
templatebody=REPLACE(t.templatebody,r.variable,r.template_value)
FROM
#tt_text AS t
INNER JOIN #tt_repl AS r ON r.id=1
UNION ALL
SELECT
t.id,
l=l+1,
templatebody=REPLACE(t.templatebody,r.variable,r.template_value)
FROM
cte AS t
INNER JOIN #tt_repl AS r ON r.id=t.l+1
)
UPDATE
#tt_text
SET
templatebody=cte.templatebody
FROM
#tt_text AS t
INNER JOIN cte ON
cte.id=t.id
WHERE
cte.l=(SELECT MAX(id) FROM #tt_repl);
/* -- if instead you wanted to select the replaced strings, comment out
-- the above UPDATE statement, and uncomment this SELECT statement:
SELECT
templatebody
FROM
cte
WHERE
l=(SELECT MAX(id) FROM #tt_repl);*/
SELECT*FROM #tt_text;
As long as the values for the Variables are unique ('#FirstName#' etc.) you can join each variable to the table containing TemplateBody:
select replace(replace(t.TemplateBody,'#FirstName#',variable.theVariable),'#Location#',variable2.theVariable)
from
[TemplateBodyTable] t
left join
(
select TemplateValues theVariable,Variables
from [VariablesTable] v
) variable on variable.Variables='#FirstName#'
left join
(
select TemplateValues theVariable,Variables
from [VariablesTable] v
) variable2 on variable2.Variables='#Location#'
A common table expression would allow you to loop through your templates and replace all variables in that template using a variables table. If you have a lot of variables, the level of recursion might go beyond the default limit of 100 recursions. You can play with the MAXRECURSION option according to your need.
DECLARE #Templates TABLE(Body nvarchar(max));
INSERT INTO #Templates VALUES ('This is to inform #FirstName# about the issues regarding #Location#');
DECLARE #Variables TABLE(Name nvarchar(50), Value nvarchar(max));
INSERT INTO #Variables VALUES ('#FirstName#', 'Joseph William'),
('#Location#', 'Alaska');
WITH replacing(Body, Level) AS
(
SELECT t.Body, 1 FROM #Templates t
UNION ALL
SELECT REPLACE(t.Body, v.Name, v.Value), t.Level + 1
FROM replacing t INNER JOIN #Variables v ON PATINDEX('%' + v.Name + '%', t.Body) > 0
)
SELECT TOP 1 r.Body
FROM replacing r
WHERE r.Level = (SELECT MAX(Level) FROM replacing)
OPTION (MAXRECURSION 0);

Select Values Based on ItemID OR String Value

I am wanting to build a query that looks at an existing Models table.
Table Structure:
ModelID | ManufacturerID | CategoryID | ModelName
What I want to do is pass two things to the query, ModelID and ModelName, so that it returns the specific model and also similar models.
ModelName could be made up of several words e.g iPhone 5s 16GB, so what I would like my query to do is:
SELECT
M.*
FROM
Models AS M
WHERE
(M.ModelID = 1840 OR M.ModelName LIKE '%iPhone%'
OR M.ModelName LIKE '%5s%' OR M.ModelName LIKE '%16GB%')
Is there a way that I can pass the ModelName to the query as a string and then have the query split the string to generate the OR statements?
Do a web search for T-SQL split function. There are loads out there. They take a string (comma-delimited or space delimited or whatever) and return a table of values. Then just do a JOIN against that result set.
SELECT DISTINCT M.*
FROM Models AS M
JOIN dbo.fn_split(#model_name, ' ') AS model_names
ON M.ModelID = #model_id OR m.ModelName LIKE '%' + model_names.value + '%';
OK, so I managed to get this working, following the advice given by Kevin Suchlicki re. fn_Split.
I have made this function even more complex than i intended to, but in order to help others out in a similar situation, here is my final solution:
DECLARE #CategoryID int = 1
DECLARE #ManufacturerID int = 3
DECLARE #ModelName varchar(100) = 'iPhone 5s 16GB'
DECLARE #ModelID int = 1840
DECLARE #Carrier varchar(10) = NULL
DECLARE #Colour varchar(10) = NULL
SELECT
I.*
FROM
(
SELECT
DISTINCT M.*
FROM
Models AS M
JOIN
dbo.fn_Split(#ModelName,' ') AS N
ON M.ModelID = #ModelID OR lower(M.ModelName) LIKE '%'+ Lower(N.value) + '%'
WHERE
M.CategoryID = #CategoryID AND M.ManufacturerID = #ManufacturerID
) AS A
LEFT OUTER JOIN
Items AS I ON A.ModelID = I.ModelID
WHERE
I.Barred <> 1
AND I.Locked <> 1
AND I.Ber <> 1
AND I.Condition = 'Working'
AND (LOWER(I.Colour) = LOWER(ISNULL(#Colour, I.Colour)) OR I.Colour IS NULL)
AND (LOWER(I.Carrier) = LOWER(ISNULL(#Carrier, I.Carrier)) OR I.Carrier IS NULL)
I will now create this as a stored procedure to complete the job.
For reference, HERE is a link to the fn_Split function.

Resources