We've just been given the following code as a solution for a complicated search query in a new application provided by offshore developers. I'm skeptical of the use of dynamic SQL because I could close the SQL statement using '; and then excute a nasty that will be performed on the database!
Any ideas on how to fix the injection attack?
ALTER procedure [dbo].[SearchVenues] --'','',10,1,1,''
#selectedFeature as varchar(MAX),
#searchStr as varchar(100),
#pageCount as int,
#startIndex as int,
#searchId as int,
#venueName as varchar(100),
#range int,
#latitude varchar(100),
#longitude varchar(100),
#showAll int,
#OrderBy varchar(50),
#SearchOrder varchar(10)
AS
DECLARE #sqlRowNum as varchar(max)
DECLARE #sqlRowNumWhere as varchar(max)
DECLARE #withFunction as varchar(max)
DECLARE #withFunction1 as varchar(max)
DECLARE #endIndex as int
SET #endIndex = #startIndex + #pageCount -1
SET #sqlRowNum = ' SELECT Row_Number() OVER (ORDER BY '
IF #OrderBy = 'Distance'
SET #sqlRowNum = #sqlRowNum + 'dbo.GeocodeDistanceMiles(Latitude,Longitude,' + #latitude + ',' + #longitude + ') ' +#SearchOrder
ELSE
SET #sqlRowNum = #sqlRowNum + #OrderBy + ' '+ #SearchOrder
SET #sqlRowNum = #sqlRowNum + ' ) AS RowNumber,ID,RecordId,EliteStatus,Name,Description,
Address,TotalReviews,AverageFacilityRating,AverageServiceRating,Address1,Address2,Address3,Address4,Address5,Address6,PhoneNumber,
visitCount,referalCount,requestCount,imgUrl,Latitude,Longitude,
Convert(decimal(10,2),dbo.GeocodeDistanceMiles(Latitude,Longitude,' + #latitude + ',' + #longitude + ')) as distance
FROM VenueAllData '
SET #sqlRowNumWhere = 'where Enabled=1 and EliteStatus <> 3 '
--PRINT('#sqlRowNum ='+#sqlRowNum)
IF #searchStr <> ''
BEGIN
IF (#searchId = 1) -- county search
BEGIN
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and Address5 like ''' + #searchStr + '%'''
END
ELSE IF(#searchId = 2 ) -- Town search
BEGIN
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and Address4 like ''' + #searchStr + '%'''
END
ELSE IF(#searchId = 3 ) -- postcode search
BEGIN
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and Address6 like ''' + #searchStr + '%'''
END
IF (#searchId = 4) -- Search By Name
BEGIN
IF #venueName <> ''
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and ( Name like ''%' + #venueName + '%'' OR Address like ''%'+ #venueName+'%'' ) '
ELSE
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and ( Name like ''%' + #searchStr + '%'' OR Address like ''%'+ #searchStr+'%'' ) '
END
END
IF #venueName <> '' AND #searchId <> 4
SET #sqlRowNumWhere = #sqlRowNumWhere + ' and ( Name like ''%' + #venueName + '%'' OR Address like ''%'+ #venueName+'%'' ) '
set #sqlRowNum = #sqlRowNum + ' ' + #sqlRowNumWhere
--PRINT(#sqlRowNum)
IF #selectedFeature <> ''
BEGIN
DECLARE #val1 varchar (255)
Declare #SQLAttributes varchar(max)
Set #SQLAttributes = ''
Declare #tempAttribute varchar(max)
Declare #AttrId int
while (#selectedFeature <> '')
BEGIN
SET #AttrId = CAST(SUBSTRING(#selectedFeature,1,CHARINDEX(',',#selectedFeature)-1) AS Int)
Select #tempAttribute = ColumnName from Attribute where id = #AttrId
SET #selectedFeature = SUBSTRING(#selectedFeature,len(#AttrId)+2,len(#selectedFeature))
SET #SQLAttributes = #SQLAttributes + ' ' + #tempAttribute + ' = 1 And '
END
Set #SQLAttributes = SUBSTRING(#SQLAttributes,0,LEN(#SQLAttributes)-3)
set #sqlRowNum = #sqlRowNum + ' and ID in (Select VenueId from '
set #sqlRowNum = #sqlRowNum + ' CachedVenueAttributes WHERE ' + #SQLAttributes + ') '
END
IF #showAll <> 1
set #sqlRowNum = #sqlRowNum + ' and dbo.GeocodeDistanceMiles(Latitude,Longitude,' + #latitude + ',' + #longitude + ') <= ' + convert(varchar,#range )
set #withFunction = 'WITH LogEntries AS (' + #sqlRowNum + ')
SELECT * FROM LogEntries WHERE RowNumber between '+ Convert(varchar,#startIndex) +
' and ' + Convert(varchar,#endIndex) + ' ORDER BY ' + #OrderBy + ' ' + #SearchOrder
print(#withFunction)
exec(#withFunction)
As an aside, I would not use EXEC; rather I would use sp_executesql. See this superb article, The Curse and Blessings of Dynamic SQL, for the reason and other info on using dynamic sql.
See this answer.
Also, these:
Am I immune to SQL injections if I use stored procedures?
Avoiding SQL Injection in SQL query with Like Operator using parameters?
Can I protect against SQL Injection by escaping single-quote and surrounding user input with single-quotes?
Here's an optimized version of the query above that doesn't use dynamic SQL...
Declare #selectedFeature as varchar(MAX),
#searchStr as varchar(100),
#pageCount as int,
#startIndex as int,
#searchId as int,
#venueName as varchar(100),
#range int,
#latitude varchar(100),
#longitude varchar(100),
#showAll int,
#OrderBy varchar(50),
#SearchOrder varchar(10)
Set #startIndex = 1
Set #pageCount = 50
Set #searchStr = 'e'
Set #searchId = 4
Set #OrderBy = 'Address1'
Set #showAll = 1
--Select dbo.GeocodeDistanceMiles(Latitude,Longitude,#latitude,#longitude)
DECLARE #endIndex int
SET #endIndex = #startIndex + #pageCount -1
;
WITH LogEntries as (
SELECT
Row_Number()
OVER (ORDER BY
CASE #OrderBy
WHEN 'Distance' THEN Cast(dbo.GeocodeDistanceMiles(Latitude,Longitude,#latitude,#longitude) as varchar(10))
WHEN 'Name' THEN Name
WHEN 'Address1' THEN Address1
WHEN 'RecordId' THEN Cast(RecordId as varchar(10))
WHEN 'EliteStatus' THEN Cast(EliteStatus as varchar(10))
END) AS RowNumber,
RecordId,EliteStatus,Name,Description,
Address,TotalReviews,AverageFacilityRating,AverageServiceRating,Address1,Address2,Address3,Address4,Address5,Address6,PhoneNumber,
visitCount,referalCount,requestCount,imgUrl,Latitude,Longitude,
Convert(decimal(10,2),dbo.GeocodeDistanceMiles(Latitude,Longitude,#latitude,#longitude)) as distance
FROM VenueAllData
where Enabled=1 and EliteStatus <> 3
And
(
(Address5 like #searchStr + '%' And #searchId = 1) OR
(Address4 like #searchStr + '%' And #searchId = 2) OR
(Address6 like #searchStr + '%' And #searchId = 3) OR
(
(
#searchId = 4 And
(Name like '%' + #venueName + '%' OR Address like '%'+ #searchStr+'%')
)
)
)
And
ID in (
Select VenueID
From CachedVenueAttributes
--Extra Where Clause for the processing of VenueAttributes using #selectedFeature
)
And
(
(#showAll = 1) Or
(#showAll <> 1 and dbo.GeocodeDistanceMiles(Latitude,Longitude,#latitude,#longitude) <= convert(varchar,#range ))
)
)
SELECT * FROM LogEntries
WHERE RowNumber between #startIndex and #endIndex
ORDER BY CASE #OrderBy
WHEN 'Distance' THEN Cast(Distance as varchar(10))
WHEN 'Name' THEN Name
WHEN 'Address1' THEN Address1
WHEN 'RecordId' THEN Cast(RecordId as varchar(10))
WHEN 'EliteStatus' THEN Cast(EliteStatus as varchar(10))
END
The only thing I haven't fixed is the selection from CachedVenueAttributes that seems to build up a where statement in a loop. I think I might put this in a table valued function, and refactor it in isolation to the rest of the procedure.
I like dynamic SQL for search.
Where I have used it in the past I have used .Net prepared statements with any user generated string being passed in as a parameter NOT included as text in the SQL.
To run with the existing solution you can do a number of thing to mitigate risk.
White list input, validate input so that it can only contain a-zA-Z0-9\w (alpha numerics and white space) (bad if you need to support unicode chars)
Execute any dynamic sql as a restricted user. Set owner of stored proc to a user which has only read access to the tables concerned. deny write to all tables ect. Also when calling this stored proc you may need to do it with a user with similar restrictions on what they can do, as it appares MS-SQL executes dynamic sql within a storedproc as the calling user not the owner of the storedproc.
I've realized that this is a really old post, but when doing things like:
AND
(
(#showAll = 1)
OR (#showAll <> 1
AND dbo.GeocodeDistanceMiles(Latitude,Longitude,#latitude,#longitude) <= convert(varchar,#range))
)
... an OPTION(RECOMPILE) will usually help pick a more concise plan, as long as it's not going to be executed a thousand times per second or anything.
Related
I have built a stored procedure that aims to identify duplicates in a table and to display the duplicated rows in a meaningful order. It looks like this:
CREATE PROCEDURE [dbo].[spFindDuplicates]
#tableName nvarchar(255),
#field1 nvarchar(255),
#field2 nvarchar(255) = '1',
#field3 nvarchar(255) = '2',
#field4 nvarchar(255) = '3',
#field5 nvarchar(255) = '4'
AS
BEGIN
DECLARE #query AS nvarchar(MAX);
SET #query = '
SELECT *
FROM ' + #tableName + '
WHERE CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
IN
(
SELECT CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
FROM ' + #tableName + '
GROUP BY CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
HAVING COUNT(*) > 1
)
ORDER BY ' + #field1 + ', ' + #field2 + ', ' + #field3 + ', ' + #field4 + ', ' + #field5
EXECUTE(#query);
END
GO
--Example:
EXEC spFindDuplicates #tableName = 'someRandomTable', #field1 = 'firstField', #field2 = 'secondField', #field3 = 'thirdField'
As you can see, I can use at most 5 different fields that I concatenate in order for me to get a key used to determine whether we have a duplicate or not. Please note that I use the CAST function to be able to concatenate fields with various datatypes (varchar, int, dates, etc.).
When I execute the above stored procedure with 5 different fields, it works fine. But I would like to be able to run it with a variable number of fields (from 1 to 5), which is why I provided default values for #field2 to #field5.
But when I execute it with the above example (3 fields provided), I get the following error message:
A column has been specified more than once in the order by list. Columns in the order by list must be unique.
QUESTION: How can I keep ordering the resulting table without getting an error?
BONUS QUESTION: If you find a dynamic way to use that stored procedure with any number of fields (4, 17, or whatever), that'd be even more useful to me.
Like I said in the comments, injection is a huge problem here, and you need to consider it. Saying "Let's consider I don't mind about injection" is naïve and you need to change that attitude. Always make your SQL safe; then there are no excuses and chances for your application being compromised.
As what you are after, I suspect this achieves the goal. There's no need for the subquery to scan your table with an IN here, you can make use of COUNT and the OVER clause within a CTE.
CREATE PROCEDURE [dbo].[FindDuplicates] --I've removed te sp prefix, as sp_ is reserved by MS
#tableName sysname,
#field1 sysname,
#field2 sysname = NULL,
#field3 sysname = NULL,
#field4 sysname = NULL,
#field5 sysname = NULL
AS BEGIN
DECLARE #query AS nvarchar(MAX);
SET #query = N'WITH CTE AS(' + NCHAR(10) +
N' SELECT *' + NCHAR(10) +
N' COUNT(*) OVER (PARTITION BY ' + STUFF(CONCAT(N',' + QUOTENAME(#field1),N',' + QUOTENAME(#field2),N',' + QUOTENAME(#field3),N',' + QUOTENAME(#field4),N',' + QUOTENAME(#field5)),1,1,N'') + N' AS RowCount' + NCHAR(10) +
N' FROM ' + QUOTENAME(#tableName) + N')' + NCHAR(10) +
N'SELECT *' + NCHAR(10) +
N'FROM CTE' + NCHAR(10) +
N'WHERE RowCount > 1' + NCHAR(10) +
N'ORDER BY ' + STUFF(CONCAT(N',' + QUOTENAME(#field1),N',' + QUOTENAME(#field2),N',' + QUOTENAME(#field3),N',' + QUOTENAME(#field4),N',' + QUOTENAME(#field5)),1,1,N'') + N';';
PRINT #query;
--EXEC sys.sp_executesql #query; --Uncomment to rrun the actual query
END
GO
For the command you gave us EXEC dbo.FindDuplicates #tableName = 'someRandomTable', #field1 = 'firstField', #field2 = 'secondField', #field3 = 'thirdField';, this returns the SQL:
WITH CTE AS(
SELECT *
COUNT(*) OVER (PARTITION BY [firstField],[secondField],[thirdField] AS RowCount
FROM [someRandomTable])
SELECT *
FROM CTE
WHERE RowCount > 1
ORDER BY [firstField],[secondField],[thirdField];
Which, I believe gives you the behaviour you are after.
Edited the code to check if the column list exists on the sys.columns there by making sure we get only the columns which are appropriate.
CREATE FUNCTION dbo.fn_SplitString
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
ALTER PROCEDURE [dbo].[spFindDuplicates]
#tableName nvarchar(255),
#columnlist nvarchar(max)
AS
BEGIN
DECLARE #query AS nvarchar(MAX);
SET #columnlist = (SELECT STUFF((SELECT ','+'['+[name]+']'
FROM SYS.columns
WHERE object_id = object_id(#tableName)
AND [Name] IN
(
SELECT Item
FROM dbo.fn_SplitString(#columnlist,',')
)
FOR XML PATH('')
)
,1,1,''))
PRINT #columnlist
SET #query = 'SELECT * FROM (SELECT '+CAST(#columnlist AS NVARCHAR(MAX))+'
FROM '+CAST(#tableName AS nvarchar(MAX))+'
GROUP BY '+CAST(#columnlist AS NVARCHAR(MAX))+'
HAVING COUNT(*) > 1)Res1
ORDER BY '+#columnlist
EXEC SP_EXECUTESQL #query;
END
GO
Aim: I want to query three tables in total and display each line separately.
I only need to display results from TblA and TblF as TblProperty is the parent table so whilst we might search using it I don't need it's data.
i.e. A user might search for a Postcode however a user might only search for a rating in TblA.
I've provided two pieces of code. The first is a cut down version, I think this might help guide both the reader and myself to the solution. The second code is the full version. (I need to add some quotenames etc.. but whilst I'm testing I'm after getting the main part working)
The main point: If I have a one result from TblA and one result from TblF I want two lines of data not one returned.
Using:
SQL Server Management Studio 2012
Query:
I'm looking to get a fresh pair of eyes at this stage. Maybe I need to search both tables first and then the property or look to create a temporary table?
Code 1:
USE DB
DECLARE #QUERY NVARCHAR(MAX) = ''
DECLARE #QUERYSTRING NVARCHAR(MAX) = ''
DECLARE #sTypeOfUtility NVARCHAR(MAX) = '2'
SET #QUERY =
'SELECT
p.ID AS ID,
p.UPRN AS UPRN,
COALESCE(a.OverallRiskCategory,''0'') AS RiskType2,
COALESCE(f.RiskRating,''0'') AS RiskType3,
COALESCE(a.TypeOfUtility,'''') + COALESCE(f.TypeOfUtility,'''') AS TypeOfUtility
FROM TblProperty AS p'
SET #QUERY = #QUERY + ' INNER JOIN TblA AS a on a.UPRN = p.UPRN'
SET #QUERY = #QUERY + ' INNER JOIN TblFAS f on f.FIREUPRN = p.UPRN'
IF #sTypeOfUtility = '2'
SET #QUERYSTRING = #QUERYSTRING + ' AND a.TypeOfUtility LIKE ''%' + LTRIM(RTRIM(#sTypeOfUtility)) + '%'''
IF #sTypeOfUtility = '3'
SET #QUERYSTRING = #QUERYSTRING + ' AND f.TypeOfUtility LIKE ''%' + LTRIM(RTRIM(#sTypeOfUtility)) + '%'''
SET #QUERY = LTRIM(RTRIM(#QUERY)) + ' WHERE 1 = 1 ' + LTRIM(RTRIM(#QUERYSTRING)) + ' ORDER BY typeofutility DESC'
EXECUTE(#QUERY)
Code 2 (Fullcode so far but with only two tables):
USE [DB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
ALTER PROCEDURE [dbo].[spGridSearch]
#sRiskRating NVARCHAR(50),#sUPRN NVARCHAR(20),
#sPostcode VARCHAR(20), #sPropertyName NVARCHAR(50) ,
#sStreet NVARCHAR(50), #sTypeOfUtility NVARCHAR(10),
#sDateFrom DATETIME, #sDateTo DATETIME
AS
BEGIN
DECLARE #QUERY NVARCHAR(MAX) = ''
DECLARE #QUERYSTRING NVARCHAR(MAX) = ''
SET #QUERY =
'SELECT
p.ID AS ID,
p.UPRN AS UPRN,
COALESCE(a.OverallRiskCategory,''0'') AS OverallRiskCategory,
COALESCE(a.TypeOfUtility,''0'') AS TypeOfUtility,
COALESCE(a.SurveyDate,'''') AS SurveyDate, COALESCE(a.ItemRef, '''') AS ItemRef,
COALESCE(a.NextSurveyDue,'''') AS NextSurveyDue ,
COALESCE(a.Recommendations,''NO DATA'') AS Recommendations,
COALESCE(a.StatusOfIssue,''0'') As StatusOfIssue
FROM TblProperty AS p '
SET #QUERY = #QUERY + ' LEFT JOIN TblA AS a on p.UPRN = a.UPRN '
IF #sRiskRating <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND a.OverallRiskCategory LIKE ''%' + LTRIM(RTRIM(#sRiskRating)) + '%'''
IF #sTypeOfUtility <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND a.TypeOfUtility LIKE ''%' + LTRIM(RTRIM(#sTypeOfUtility)) + '%'''
--IF #sDateFROM <> '2050-01-01' AND #sDateTO <> '2050-01-01'
--SET #QUERYSTRING = #QUERYSTRING + ' AND a.SurveyDate BETWEEN ' + #sDateFrom + ' AND ' + #sDateTo
IF #sUPRN <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND p.UPRN LIKE ''%' + LTRIM(RTRIM(#sUPRN)) + '%'''
IF #sPostcode <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND p.Postcode LIKE ''%' + LTRIM(RTRIM(#sPostcode)) + '%'''
IF #sPropertyName <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND p.BuildingNo LIKE ''%' + LTRIM(RTRIM(#sPropertyName)) + '%'''
IF #sStreet <> '1234xyz'
SET #QUERYSTRING = #QUERYSTRING + ' AND p.Street LIKE ''%' + LTRIM(RTRIM(#sStreet)) + '%'''
IF LEN(LTRIM(RTRIM(#QUERYSTRING))) > 5
--Remove last as we dont need it
--SET #QUERYSTRING = LEFT(#QUERYSTRING, NULLIF(LEN(#QUERYSTRING)-1,-1))
SET #QUERY = LTRIM(RTRIM(#QUERY)) + ' WHERE 1 = 1 ' + LTRIM(RTRIM(#QUERYSTRING))
EXECUTE(#QUERY)
END
References:
http://support.sas.com/documentation/cdl/en/sqlproc/62086/HTML/default/viewer.htm#a001361784.htm
http://www.w3schools.com/sql/sql_join.asp
As I mentioned in the comments you should use the UNION ALL statement to get all results from both tables. This requires that both select have the same column count and columns should have the same datatype
You query would basically look like this:
SELECT
...
FROM TblProperty AS p
INNER JOIN TblA AS a on a.UPRN = p.UPRN
UNION ALL
SELECT
...
FROM TblProperty AS p
INNER JOIN TblFAS f on f.FIREUPRN = p.UPRN
I would also recommend to use use sp_executesql and named parameters like this named parameters in sp_executesql
I have a procedure that I working on and I don't know what's going wrong. I have reviewed all other sites and could not find the issue that I'm having
I want to create procedure that has a dynamic where clause base on a combination of bits being sent to the procedure. I don't want to have to create a bunch of similar procedures because they have slightly different conditions.
I'm placing the below query into a cursor then looping through the cursor. Please help.
CREATE PROCEDURE [dbo].[procContainTest] (
#USE_A BIT,
#USE_B BIT,
#ValueA VARCHAR(50),
#ValueB VARCHAR(50),
#USERID VARCHAR(50)
)
AS
DECLARE #TEMP_Col1 INT,
#TEMP_Col2 INT,
#TEMP_Col3 VARCHAR(50),
#TEMP_Col4 VARCHAR(50),
#TEMP_Col5 VARCHAR(50),
#POINT_ONE NVARCHAR(50),
#POINT_TWO NVARCHAR(50)
SET #TRIGGER = 0
WHILE #TRIGGER = 0
BEGIN
-- F2 Booking Term
IF #USE_A = 1
AND #USE_B = 1
BEGIN
SET #POINT_ONE = 'ColName2'
SET #POINT_TWO = 'ColName3'
END
-- F6 Booking Term
IF #USE_A = 0
AND #USE_B = 1
BEGIN
SET #POINT_ONE = 'ColName1'
SET #POINT_TWO = 'ColName2'
END
DECLARE INNER_CURSOR CURSOR
FOR
SELECT TOP 1 TEMP_Col1 INT,
TEMP_Col2,
TEMP_Col3,
#TEMP_Col4,
#TEMP_Col5
FROM TEMP_Table
WHERE #POINT_ONE = + '''' + #ValueA + ''''
AND #POINT_TWO = + '''' + #ValueB + ''''
AND USERID = #USERID
ORDER BY LENGTH
l
You can put your Select Statmement in a variable like:
declare #YourSelectStatement nvarchar(max)
set #YourSelectStatement = ' SELECT TOP 1 TEMP_Col1 INT,
TEMP_Col2,
TEMP_Col3,
FROM TEMP_Table
WHERE ' + #POINT_ONE + '=' + #ValueA + '
AND ' + #POINT_TWO + '=' + #ValueB + '
AND USERID = ' + #USERID + '
ORDER BY LENGTH'
sp_executesql(#YourSelectStatement)
this is probably help you:
SELECT
id, first, last, email, notes
FROM
My_Table
WHERE
CASE ''''+#column_name_variable+''''
WHEN ''''+column_1+''''=1 THEN column_1
WHEN ''''+column_2+''''=2 THEN column_2
...
ELSE 'not null'
END IS NOT NULL
I would avoid the use of dynamic SQL altogether. You can eliminate your IF statements and embed the same logid in your SELECT statement like this:
SELECT TOP 1
TEMP_Col1,
TEMP_Col2,
TEMP_Col3,
TEMP_Col4,
TEMP_Col5
FROM TEMP_Table
WHERE (#USE_A = 1 AND #USE_B = 1 AND ColName2 = '''' + #ValueA + '''' AND ColName3 = '''' + #ValueB + '''')
OR (#USE_A = 0 AND #USE_B = 1 AND ColName1 = '''' + #ValueA + '''' AND ColName2 = '''' + #ValueB + '''')
I am using SQL Server 2008. I use to take the script of my data from SQL table using Tasks --> Generate Scripts option.
Here is my problem:
Let's say I have 21,000 records in Employee table. When I take the script of this table, it takes the insert script for all 21000 records. What is the solution if I want to take only the script of 18000 records from the table?
Is there any solution using SQL query or from the tasks wizard?
Thanks in advance...
Create a new View where you select your desired rows from your Employee table e.g. SELECT TOP 21000...
Then simply script that View instead of the Table.
In case the views are not an option for you I wrote the following code based on the Aaron Bertrand's answer here that will give the insert statement for a single record in the db.
CREATE PROCEDURE dbo.GenerateSingleInsert
#table NVARCHAR(511), -- expects schema.table notation
#pk_column SYSNAME, -- column that is primary key
#pk_value NVARCHAR(10) -- change data type accordingly
AS
BEGIN
SET NOCOUNT ON;
DECLARE #cols NVARCHAR(MAX), #vals NVARCHAR(MAX),
#valOut NVARCHAR(MAX), #valSQL NVARCHAR(MAX);
SELECT #cols = N'', #vals = N'';
SELECT #cols = #cols + ',' + QUOTENAME(name),
#vals = #vals + ' + '','' + ' + 'ISNULL('+REPLICATE(CHAR(39),4)+'+RTRIM(' +
CASE WHEN system_type_id IN (40,41,42,43,58,61) -- dateteime and time stamp type
THEN
'CONVERT(CHAR(8), ' + QUOTENAME(name) + ', 112) + '' ''+ CONVERT(CHAR(14), ' + QUOTENAME(name) + ', 14)'
WHEN system_type_id IN (35) -- text type
THEN
'REPLACE(CAST(' + QUOTENAME(name) + 'as nvarchar(MAX)),'+REPLICATE(CHAR(39),4)+','+REPLICATE(CHAR(39),6)+')'
ELSE
'REPLACE(' + QUOTENAME(name) + ','+REPLICATE(CHAR(39),4)+','+REPLICATE(CHAR(39),6)+')'
END
+ ')+' + REPLICATE(CHAR(39),4) + ',''null'') + '
FROM sys.columns WHERE [object_id] = OBJECT_ID(#table)
AND system_type_id <> 189 -- can't insert rowversion
AND is_computed = 0; -- can't insert computed columns
SELECT #cols = STUFF(#cols, 1, 1, ''),
#vals = REPLICATE(CHAR(39),2) + STUFF(#vals, 1, 6, '') + REPLICATE(CHAR(39),2) ;
SELECT #valSQL = N'SELECT #valOut = ' + #vals + ' FROM ' + #table + ' WHERE '
+ QUOTENAME(#pk_column) + ' = ''' + RTRIM(#pk_value) + ''';';
EXEC sp_executesql #valSQL, N'#valOut NVARCHAR(MAX) OUTPUT', #valOut OUTPUT;
SELECT SQL = 'INSERT ' + #table + '(' + #cols + ') SELECT ' + #valOut;
END
I took the above code and wrapped it the following proc that will use the where clause you give it to select which insert statements to create
CREATE PROCEDURE dbo.GenerateInserts
#table NVARCHAR(511), -- expects schema.table notation
#pk_column SYSNAME, -- column that is primary key
#whereClause NVARCHAR(500) -- the where clause used to parse down the data
AS
BEGIN
declare #temp TABLE ( keyValue nvarchar(10), Pos int );
declare #result TABLE ( insertString nvarchar(MAX) );
declare #query NVARCHAR(MAX)
set #query =
'with qry as
(
SELECT ' + #pk_column + ' as KeyValue, ROW_NUMBER() over(ORDER BY ' + #pk_column + ') Pos
from ' + #table + '
' + #whereClause + '
)
select * from qry'
insert into #temp
exec sp_sqlexec #query
Declare #i int, #key nvarchar(10)
select #i = count(*) from #temp
WHILE #i > 0 BEGIN
select #key = KeyValue from #temp where Pos = #i
insert into #result
exec [dbo].[GenerateSingleInsert] #table, #pk_column, #key
set #i = #i - 1
END
select insertString from #result
END
Calling it could look like the following. You pass in the table name, the table primary key and the where clause and you should end up with your insert statements.
set #whereClause = 'where PrettyColorsId > 1000 and PrettyColorsID < 5000'
exec [dbo].GenerateInserts 'dbo.PrettyColors', 'PrettyColorsID', #whereClause
set #whereClause = 'where Color in (' + #SomeValues + ')'
exec [dbo].GenerateInserts 'dbo.PrettyColors', 'PrettyColorsID', #whereClause
I have the following function:
CREATE FUNCTION fGetTransactionStatusLog
(
#TransactionID int
)
RETURNS varchar(8000) AS
BEGIN
declare StatusChanges cursor for
select NewStatusID, FirstName + ' ' + LastName AS UserName, Stamp, CAST(Notes AS varchar(8000)) AS Notes
from TransactionStatusChanges tsc
left join Users us ON tsc.UserID = us.UserID
where TransactionID = #TransactionID ORDER BY StatusNum
declare #output varchar(8000)
declare #NewStatusID char(2)
declare #UserName varchar(255)
declare #Stamp datetime
declare #Notes varchar(8000)
set #output = ''
OPEN StatusChanges
FETCH NEXT FROM StatusChanges INTO #NewStatusID, #UserName, #Stamp, #Notes
WHILE ##FETCH_STATUS = 0
BEGIN
set #output = #output + RTRIM(CAST(#Stamp AS varchar(30))) + ': ' + #NewStatusID + ' by ' + #UserName + CHAR(13) + CHAR(10)
IF #Notes IS NOT NULL
BEGIN
set #output = #output + '---' + #Notes + CHAR(13) + CHAR(10)
END
FETCH NEXT FROM StatusChanges INTO #NewStatusID, #UserName, #Stamp, #Notes
END
CLOSE StatusChanges
DEALLOCATE StatusChanges
RETURN #output
END
Now, that function returns exactly what I want for the Transactions that don't have any Notes in any records...
For transaction that have at least one record in TransactionStatusChanges with a non-NULL Notes field, I get NULL.
I don't quite get it, since I AM checking that #Notes is not NULL before concatting it.
Any ideas?
NOTE: I'm using varchar(8000) because I can't use text inside Functions.
One of these is NULL
set #output = #output + RTRIM(CAST(#Stamp AS varchar(30))) + ': ' + #NewStatusID + ' by ' + #UserName + CHAR(13) + CHAR(10)
Also, you can make your code simpler by using ISNULL or COALESCE to handle columns which contain NULLs
CREATE FUNCTION fGetTransactionStatusLog
(
#TransactionID int
)
RETURNS varchar(8000) AS
BEGIN
declare #output AS varchar(8000)
select #output = ISNULL(#output, '')
+ ISNULL(RTRIM(CAST(Stamp AS varchar(30))), '<NULL>')
+ ISNULL(NewStatusID, '<NULL>') + ' by '
+ ISNULL(FirstName + ' ' + LastName, '<NULL>') + CHAR(13) + CHAR(10)
+ ISNULL('---' + Notes + CHAR(13) + CHAR(10), '')
from TransactionStatusChanges tsc
left join Users us ON tsc.UserID = us.UserID
where TransactionID = #TransactionID ORDER BY StatusNum
RETURN #output
END