I'm writing a query against a table A where I want to find out if a row in table A has fields that are null. The thing is that these fields are dynamic and are found in another table B.
Normally you would write,
Table A ....
WHERE A.myField1 IS NOT NULL AND A.myField2 IS NOT NULL
But in this case I want to do
table A ....
WHERE (some columns in table **A** specified in table **B**) IS NOT NULL
Is it possible to do this?
If I understand your question correctly, you may try to generate dynamic SQL statement and execute this statement:
-- Tables
CREATE TABLE #TableA (
MyField1 int,
MyField2 int,
MyField3 int,
MyField4 int,
MyField5 int,
MyField6 int
)
CREATE TABLE #TableB (
FieldName nvarchar(50)
)
INSERT INTO #TableB
(FieldName)
VALUES
('MyField1'),
('MyField2'),
('MyField3')
-- Declarations
DECLARE
#stm nvarchar(max),
#err int
-- Statement generation
SET #stm = N''
SELECT #stm = #stm +
N'AND (' +
[FieldName] +
N' IS NOT NULL) '
FROM #TableB
SET #stm =
N'SELECT * FROM #TableA WHERE ' +
STUFF(#stm, 1, 4, N'')
-- Execution
PRINT #stm
EXEC #err = sp_executesql #stm
IF #err = 0
PRINT 'OK'
ELSE
PRINT 'Error'
Generated statement:
SELECT *
FROM #TableA
WHERE (MyField1 IS NOT NULL) AND (MyField2 IS NOT NULL) AND (MyField3 IS NOT NULL)
Related
I have a query that will return only columns with values. How do I add that to a function so I can use that with any query? Would it be a function in the where clause.
create table test1
(
s_no int not null,
name varchar(10) not null,
address varchar(10) null,
emailid varchar(100) null
)
insert into test1 (s_no, name)
values (1,'A'),(2,'B'),(3,'C')
declare #column_list varchar(8000),
#counter int
set #column_list = ''
set #counter = 0
while (Select max(colid) from syscolumns where id = object_id('test1') and isnullable= 0) > #counter
begin
select #counter = min(colid)
from syscolumns
where id = object_id('test1')
and isnullable = 0
and colid > #counter
select #column_list = #column_list + ',' + (Select name from syscolumns where id = object_id('test1') and isnullable= 0 and colid = #counter)
end
select #column_list = SUBSTRING(#column_list, 2, len(#column_list))
declare #sql varchar(8000)
select #sql = 'select ' + #column_list + ' from test1'
print #sql
exec (#sql)
SELECT * FROM [dbo].[test1]
I guess you could make a stored procedure where you provide the table name as parameter, and then build your query like you are doing already
create procedure ShowOnlyFilledColumns (#tablename varchar(100)) as
begin
set nocount on
declare #column_list varchar(8000),
#counter int
set #column_list = ''
set #counter = 0
while (Select max(colid) from syscolumns where id = object_id(#tablename) and isnullable= 0) > #counter
begin
select #counter = min(colid) from syscolumns where id = object_id(#tablename) and isnullable= 0 and colid > #counter
select #column_list = #column_list + ',' + (Select name from syscolumns where id = object_id(#tablename) and isnullable= 0 and colid = #counter)
end
select #column_list = SUBSTRING(#column_list,2,len(#column_list))
declare #sql varchar(8000)
select #sql = 'select ' + #column_list + ' from ' + #tablename
--print #sql
exec (#sql)
end
and use it like this
exec ShowOnlyFilledColumns 'test1'
See the complete example in this DBFiddle
EDIT: The OP asked how he can add joins on this
There are a few tricks to join with a stored procedure, for example in these answers
However, this won't work on this solution, because it requires to create a temp table to store the result of the procedure.
The trick looks like this
-- create a temporary table to store the results of the procedure
CREATE TABLE #Temp (
s_no int not null,
name varchar(10) not null,
address varchar(10) null,
emailid varchar(100) null
)
-- call the procedure and store the result in the temporary table
INSERT INTO #Temp
exec ShowOnlyFilledColumns 'test1'
-- now I can query the temp table, and join on it and write a where clause, and I can do whatever I want
select * from #Temp
Now, this won't work in this case, because the stored procedure can return different columns every time you run it, and to make the insert into #Temp exec ShowOnlyFilledColumns 'test1' work, the table #Temp must have the same number and type of columns as the procedure returns. And you just don't know that.
I need to reorder columns in the final SELECT statement in a stored procedure. Column orders needs to be fetched from another table.
I have a solution based on dynamic SQL. Is there any better way to do it? I have around 100 columns to return with millions of rows for an Excel export. Is there any other performance optimized solution other than a dynamic query?
Please find sample code below for my current solution.
IF OBJECT_ID( 'tempdb..#TempColumns') IS NOT NULL
BEGIN
DROP TABLE #TempColumns
END
IF OBJECT_ID( 'tempdb..#TempColumnsOrder') IS NOT NULL
BEGIN
DROP TABLE #TempColumnsOrder
END
CREATE TABLE #TempColumns
(
ID INT IDENTITY,
FirstName VARCHAR(MAX),
LastName VARCHAR(MAX),
Gender VARCHAR(MAX)
)
INSERT INTO #TempColumns
VALUES ('ABC', 'DEF', 'MALE'), ('PR', 'ZA', 'FEMALE'), ('ERT', 'GFG', 'MALE')
CREATE TABLE #TempColumnsOrder
(
ID INT IDENTITY,
ColumnName VARCHAR(MAX),
ColumnOrder INT
)
INSERT INTO #TempColumnsOrder
VALUES ('FirstName', 3), ('LastName', 2), ('Gender', 1)
SELECT * FROM #TempColumns
SELECT * FROM #TempColumnsOrder
DECLARE #script VARCHAR(MAX)
SELECT #script = 'SELECT '
SELECT #script = #script + ColumnName + ','
FROM #TempColumnsOrder
ORDER BY ColumnOrder
PRINT #script
SELECT #script = SUBSTRING(RTRIM(#script), 1, LEN(RTRIM(#script)) - 1)
SELECT #script = #script + ' FROM #TempColumns'
EXEC (#script)
IF OBJECT_ID( 'tempdb..#TempColumns') IS NOT NULL
BEGIN
DROP TABLE #TempColumns
END
IF OBJECT_ID( 'tempdb..#TempColumnsOrder') IS NOT NULL
BEGIN
DROP TABLE #TempColumnsOrder
END
Thanks for reply, Is there any better way in Dynamic SQL other than what i did?
You can eliminate the unsupported string concatenation you are using, and modernize and simply the code:
DROP TABLE IF EXISTS #TempColumns
DROP TABLE IF EXISTS #TempColumnsOrder
CREATE TABLE #TempColumns
(
ID INT IDENTITY,
FirstName VARCHAR(MAX),
LastName VARCHAR(MAX),
Gender VARCHAR(MAX)
)
INSERT INTO #TempColumns
Values('ABC','DEF','MALE'),('PR','ZA','FEMALE'),('ERT','GFG','MALE')
CREATE TABLE #TempColumnsOrder
(
ID INT IDENTITY,
ColumnName VARCHAR(MAX),
ColumnOrder INT
)
INSERT INTO #TempColumnsOrder
Values('FirstName',3), ('LastName',2), ('Gender',1)
SELECT * FROM #TempColumns
SELECT * FROM #TempColumnsOrder
DECLARE #script VARCHAR(MAX) = concat(
'SELECT ',
(select STRING_AGG(QUOTENAME(ColumnName),', ') WITHIN GROUP (ORDER BY ColumnOrder)
FROM #TempColumnsOrder),
' FROM #TempColumns')
print #script
EXEC (#script)
DROP TABLE IF EXISTS #TempColumns
DROP TABLE IF EXISTS #TempColumnsOrder
SELECT #script = #script + ColumnName + ',' FROM #TempColumnsOrder
ORDER BY ColumnOrder
The behavior of aggregate string concatenation with the above technique is not guaranteed. The actual behavior is plan-dependent so you may not get the desired results.
In SQL Server 2017 and Azure SQL Database, STRING_AGG is the proper method:
SELECT STRING_AGG(ColumnName, ',') WITHIN GROUP(ORDER BY ColumnOrder)
FROM #TempColumnsOrder;
In older SQL Versions like SQL Server 2012, the best method is with XML PATH():
SELECT #script = #script +
STUFF((SELECT ',' + ColumnName
FROM #TempColumnsOrder
ORDER BY ColumnOrder
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'),1,1,'');
See this answer for details about how the above query works.
I want to run:
select Mycalculatefunction('((3*4)-3)*5')
select ('((3*4)-3)*5')
OUTPUT: ((3*4)-3)*5 wrong (not int value)
My desired output is: 45
I defined a stored procedure:
create PROCEDURE dbo.Eval
(#exp varchar(MAX))
AS
SET NOCOUNT ON
DECLARE #SQLString NVARCHAR(MAX)
SET #SQLString = 'SELECT '+#exp
EXEC sp_executesql #SQLString
I call it:
exec dbo.Eval '((3*4)-3)*5'
How can I do in this process is the trigger?
Your SP is vulnerable to injection. F.e. I pass exec dbo.Eval '1;DROP TABLE some_table;'. Better use xml.query:
CREATE PROCEDURE dbo.Eval
#formula nvarchar(max)
AS
DECLARE #sql nvarchar(max)
SELECT #sql = N'
DECLARE #x xml = ''''
SELECT CAST(#x.query('''+#formula+''') as nvarchar(max))'
EXEC sp_executesql #sql
Then
EXEC dbo.Eval '((3*4)-3)*5'
Output:
45
Triggers part (as there were no info about your tables, just general explanation, I add full batch with comments):
--Create table that will store Formulas
CREATE TABLE Formulas (
ID int IDENTITY(1,1) NOT NULL,
Formula nvarchar(max) NULL,
CONSTRAINT PK_ID PRIMARY KEY (ID)
)
GO
--Create table to store results of the formulas
CREATE TABLE Results (
T1_ID int NOT NULL,
Result int NULL
)
GO
--Linked by ID
ALTER TABLE Results ADD CONSTRAINT FK_Formulas_Results FOREIGN KEY (T1_ID)
REFERENCES Formulas (ID)
GO
--Create a Table Valued Parameter
CREATE TYPE FormulaResults AS TABLE (
ID int NOT NULL,
Formula nvarchar(max) NULL
)
GO
--Create a procedure to do the count
CREATE PROCEDURE dbo.GetResults
#TVP FormulaResults READONLY
AS
DECLARE #sql nvarchar(max)
SELECT #sql = N'DECLARE #x xml = '''' '
SELECT #sql = #sql + 'SELECT '+CAST(ID as nvarchar(max))+' as ID, CAST(#x.query('''+Formula+''') as nvarchar(max)) UNION ALL '
FROM #TVP
SELECT #sql = LEFT(#sql,LEN(#sql)-LEN('UNION ALL '))
EXEC sp_executesql #sql
GO
--Create a trigger that will count formula after insert and update
CREATE TRIGGER GetResultsTrigger
ON Formulas
AFTER INSERT, UPDATE
AS
DECLARE #FormulaTVP AS FormulaResults
DECLARE #Results TABLE(
T1_ID int NOT NULL,
Result int NULL
)
INSERT INTO #FormulaTVP
SELECT *
FROM inserted
INSERT INTO #Results
EXEC dbo.GetResults #FormulaTVP
MERGE Results r
USING #Results s
ON r.T1_ID = s.T1_ID
WHEN NOT MATCHED THEN
INSERT VALUES (s.T1_ID, s.Result)
WHEN MATCHED THEN
UPDATE SET Result = s.Result;
After that run:
INSERT INTO [Formulas] VALUES
('1+3'),('2+2*8')
SELECT [ID],
[Formula]
FROM [Test].[dbo].[Formulas]
SELECT [T1_ID],
[Result]
FROM [Test].[dbo].[Results]
Output:
ID Formula
1 1+3
2 2+2*8
T1_ID Result
1 4
2 18
image 1
ALTER FUNCTION [dbo].[Calculate]
( #expression AS VARCHAR(MAX)
)
RETURNS xml
AS
BEGIN
-- routine body goes here, e.g.
-- SELECT 'Navicat for SQL Server'
DECLARE #result xml
declare #x xml=''
--I can not pass as a parameter
select #result=#x.query(('(4*3)*5-10'))
return #result;
END
i call this function:
SELECT CAST(CAST(CAST(dbo.[Calculate]('How do I pass parameters') AS XML) AS VARCHAR(100)) AS DECIMAL(4,2))
Output:
50.00
select #result=#x.query('sql:variable("#expression")')
result: '2+2' :(((
Requirement: To write stored Procedure(s) such that the values passed in stored procedures are matched against the values in columns in the table and then arranged with highest to lowest matching number of attributes.Then are inserted in a dynamically created temporary table inside the stored procedure.
Problem:
I have say 15-20 attributes that are matched against to confirm the suggestions made in response to record search. Basically, There is a table that stores Patients information and multiple parameters may be passed into the stored procedure to search through so that a Temporary table is created that suggests records in decreasing order of matching attributes.
To frame the basic structure, I tried with 3 attributes and respective stored procedures to match them which in turn are collectively called from a calling procedure based on the input parameter list which in turn creates the required temporary table.
Here is the SQL code(Just to give the gist of what I have tried so far):
But as a matter of fact it is, I realize this is way too naive to be used in a real time application which may require 80-90% of accuracy.So, what exactly can replace this technique for better efficiency?
Create Procedure NameMatch
(
#Name nvarchar(20),
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name = #Name)
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name =' + #Name
set #temp = 0.1*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
Create Procedure AgeMatch
(
#Name nvarchar(20),
#Age int,
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name =#Name and Age = + #Age)
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name = ' + #Name + ' and Age = '+ #Age
set #temp = 0.2*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
Create Procedure Nationality
(
#Name nvarchar(20),
#Age int,
#Nation nvarchar(10),
#PercentContribution nvarchar(4) OUT, #PatientName nvarchar(20) out
)
As
declare #temp int
DECLARE #Query nvarchar(500)
if Exists(select Name from dbo.PatientDetails where Name = #Name and Age = #Age and Nationality = #Nation )
Begin
set #PatientName = #Name
set #query = 'select * from dbo.PatientDetails where Name = ' + #Name + ' and Age = '+ #Age + ' and Nationality = ' + #Nation
set #temp = 0.3*100
set #PercentContribution = #temp + '%'
Execute(#query)
Return
End
create procedure CallingProcedure
(
#Name nvarchar(20),
#Age int = null,
#Nation nvarchar(10)= null
)
As
declare #PercentMatch nvarchar(4)
Begin
create table #results(PatientName nvarchar(30), PercentMatch nvarchar(4))
if(#Nation IS NOT NULL)
Insert into #results exec Nationality #Nation, #Name output, #PercentMatch output
else if(#Age is not Null)
Insert into #results exec AgeMatch #Age, #Name output, #PercentMatch output
else
Insert into #results exec NameMatch #Name, #Name output, #PercentMatch output
End
Setting aside nuances of stored procedure syntax, given parameters 1-n that if not null should match columns 1-n and the results sorted by highest number of matches first, a dynamic query is not needed - plain SQL can do it.
select *
from patient
where #param1 is null or column1 = #param1
or #param2 is null or column2 = #param2
...
or #paramN is null or columnN = #paramN
order by if(column1 = #param1, 1, 0)
+ if(column2 = #param2, 1, 0)
...
+ if(columnN = #paramN, 1, 0) desc
I have this Sql puzzle i'm using Sql server 2005
the following query works fine
DECLARE #query VARCHAR(200)
DECLARE #colname varchar(50)
CREATE TABLE #temp (ID INT IDENTITY PRIMARY KEY , value VARCHAR(50))
INSERT INTO #temp VALUES ('first')
SET #colname = 'myCol'
SET #query = 'SELECT value AS' + #colname + ' FROM #temp'
EXEC(#query)
DROP TABLE #temp
BUT
if i do this
SET #colname = (SELECT value FROM tablename WHERE id = 12) --valid selection
or
SELECT #colname = value FROM tablename WHERE id = 12 --valid selection
I don't get any result , i get a message saying :
(1 row(s) affected)
AND ERROR Message:
Msg 102, Level 15, State 1, Line 1
Incorrect syntax near '1'.
Any body knows what is going on with this thing Thanks
As per the comments you need to guard against the value being NULL because nothing was assigned or the column name being an invalid SQL Server identifier.
You can do the former by checking ##ROWCOUNT after the assignment and the second by using QUOTENAME
DECLARE #query VARCHAR(200)
DECLARE #colname VARCHAR(50)
CREATE TABLE #temp
(
ID INT IDENTITY PRIMARY KEY,
value VARCHAR(50)
)
INSERT INTO #temp
VALUES ('first')
SELECT #colname = QUOTENAME(value)
FROM tablename
WHERE id = 12
IF ##ROWCOUNT <> 1
BEGIN
RAISERROR('Unexpected rowcount',16,1)
RETURN
END
SET #query = 'SELECT value AS ' + #colname + ' FROM #temp'
EXEC(#query)
DROP TABLE #temp
Your #temp table has identity column and your query only insert to the #temp table once..
So you only have ID = 1.
ID = 12 is not exists.
This query will be give the result.
DECLARE #query VARCHAR(200)
DECLARE #colname varchar(50)
CREATE TABLE #temp (ID INT IDENTITY PRIMARY KEY , value VARCHAR(50))
INSERT INTO #temp VALUES ('first')
SELECT #colname = value FROM #temp WHERE id = 1
SET #query = 'SELECT value AS' + #colname + ' FROM #temp'
EXEC(#query)
DROP TABLE #temp