Find rows containing delimited words within nvarchar parameter - sql

I have a procedure that selects an offset of rows from a table:
SELECT * --table contains ID and Name columns
FROM Names
ORDER BY ID
OFFSET #Start ROWS
FETCH NEXT #Length ROWS ONLY
In addition to #Start and #Length parameters, the procedure also receives #SearchValue NVARCHAR(255) parameter. #SearchValue contains a string of values delimited by a space, for example '1 ik mi' or 'Li 3'.
What I need is to query every record containing all of those values. So, if the #SearchValue is '1 ik mi', it should return any records that contain all three values: '1', 'mi', and 'ik'. Another way to understand this is by going here, searching the table (try searching 00 eer 7), and observing the filtered results.
I have the freedom to change the delimiter or run some function (in C#, in my case) that could format an array of those words.
Below are our FAILED attempts (we didn't try implementing it with OFFSET yet):
Select ID, Name
From Names
Where Cast(ID as nvarchar(255)) in (Select value from string_split(#SearchValue, ' ')) AND
Name in (Select value from string_split(#SearchValue, ' '))
SELECT ID, Name
FROM Names
WHERE #SearchValueLIKE '% ' + CAST(ID AS nvarchar(20)) + ' %' AND
#SearchValueLIKE '% ' + Name + ' %';
We used Microsoft docs on string_split for the ideas above.
Tomorrow, I will try to implement this solution, but I'm wondering if there's another way to do this in case that one doesn't work. Thank you!

Your best bet will be to use a FULL TEXT index. This is what they're built for.
Having said that you can work around it.. BUT! You're going to be building a query to do it. You can either build the query in C# and fire it at the database, or build it in the database. However, you're never going to be able to optimise the query very well because users being users could fire all sorts of garbage into your search that you'll need to watch out for, which is obviously a topic for another discussion.
The solution below makes use of sp_executesql, so you're going to have to watch out for SQL injection (before someone else picks apart this whole answer just to point out SQL injection):
DROP TABLE #Cities;
CREATE TABLE #Cities(id INTEGER IDENTITY PRIMARY KEY, [Name] VARCHAR(100));
INSERT INTO #Cities ([Name]) VALUES
('Cooktown'),
('South Suzanne'),
('Newcastle'),
('Leeds'),
('Podunk'),
('Udaipur'),
('Delhi'),
('Murmansk');
DECLARE #SearchValue VARCHAR(20) = 'ur an rm';
DECLARE #query NVARCHAR(1000);
SELECT #query = COALESCE(#query + '%'' AND [Name] LIKE ''%', '') + value
FROM (Select value from string_split(#SearchValue, ' ')) a;
SELECT #query = 'SELECT * FROM #Cities WHERE [Name] LIKE ''%' + #query + '%''';
EXEC sp_executesql #query;

Related

SSMS - MS SQL Sever Query option set ON/OFF to display all columns in Shortdate format?

In SSMS, for MS SQL Server 2008 or newer versions, is there a general query option or something like that, something to set ON or OFF before launching the query, in order to view all DATE columns as Shortdate (only date, without time)?
Something like SET ANSI_NULLS { ON | OFF } ?
Because I often use 'select * from table', or different approaches like that, and inside tables are many columns and the DATE columns are in different places, and I don't want every time to check where these columns are and to explicitly use CONVERT or CAST only on them, to display them properly.
Thank you for any suggestion.
Yeah I will solve such situation from interface end only.
Also saying like,
Because I often use 'select * from table', or different approaches
this is itself bad,you can't have your own way or approaches.
Nonetheless in sql we can do something like this,
USE AdventureWorks2012
GO
--proc parameter
DECLARE #tablename VARCHAR(50) = 'Employee'
DECLARE #table_schema VARCHAR(50) = 'HumanResources'
--local variable
DECLARE #Columnname VARCHAR(max) = ''
DECLARE #Sql VARCHAR(max) = ''
SELECT #Columnname = #Columnname + CASE
WHEN DATA_TYPE = 'date'
OR DATA_TYPE = 'datetime'
THEN 'cast(' + QUOTENAME(COLUMN_NAME) + ' as date)'
ELSE QUOTENAME(COLUMN_NAME)
END + ',' + CASE
WHEN DATA_TYPE = 'date'
OR DATA_TYPE = 'datetime'
THEN 'cast(' + QUOTENAME(COLUMN_NAME) + ' as date)'
ELSE QUOTENAME(COLUMN_NAME)
END + ','
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #tablename
AND TABLE_SCHEMA = #table_schema
ORDER BY ORDINAL_POSITION
SET #Columnname = STUFF(#Columnname, len(#Columnname), 1, '')
--set #Columnname=stuff(#Columnname,1,1,'')
--PRINT #Columnname
SET #Sql = 'select ' + #Columnname + ' from ' + #table_schema + '.' + #tablename + ''
--PRINT #Sql
EXEC (#Sql)
it can be further improve as per requirement.Also please use sp_executeSql
you can customize case condition.
There is no "magic" display format button or function in SSMS no. When you execute a query, SSMS will display that column in the format that is appropriate for that data type; for a datetime field that will include the time.
If you don't want to include the time, then you have to either CAST or CONVERT the individual column(s), or format the data appropriately in your presentation layer (for example, if you're using Excel then dd/MM/yyyy may be appropriate).
If all your columns have '00:00:00.000' at the end of their value, the problem isn't the display format, it's your data type choice. Clearly, the problem isn't that SSMS is returning a time for a date**time** column, it's that you've declare a column as a datetime when it should have been a date. You can change the datatype of a column using ALTER. For example:
USE Sandbox;
Go
CREATE TABLE TestTable (ID smallint IDENTITY(1,1), DateColumn datetime);
INSERT INTO TestTable (DateColumn)
VALUES ('20180201'),('20180202'),('20180203'),('20180204'),('20180205');
SELECT *
FROM TestTable;
GO
ALTER TABLE TestTable ALTER COLUMN DateColumn date;
GO
SELECT *
FROM TestTable;
GO
DROP TABLE TestTable;
TL;DR: SSMS displays data in an appropriate format for the data you have. If you don't like it, you have to supply an alternate format for it to display for each appropriate column. If the issue is your data, change the data type.
Edit: I wanted to add a little more to this.
This question is very much akin to also asking "I would like to be able to run queries where decimals only return the integer part of the value. Can this be done automagically?". So, the value 9.1 would return 9, but also, the value 9.999999999 would return 9.
Now, I realise that you "might" be thinking "Numbers aren't anything like dates", but really, they are. At the end of the (especially in data) a date is just a number (hell, a datetime time in SQL Server is stored as the number of days after 1900-01-01, and the time is a decimal of that number, so 43136.75 is actually 2018-02-07 18:00:00.000).
Now that we're talking in numbers, does it seems like a good idea to you to have all your decimals returned as their FLOOR value? I imagine the answer is "no". Imagine if you were doing some kind of accounting, and only summing the values of transactions using the FLOOR value. You could be losing 1,000's (or more) of £/$/€'s.
Think of the old example of the people who stole money from payments which contained values of less than a penny. The amount they stole was a huge amount, however, not one individual theft had a value >= $0.01. The same principle really rules here; precision is very important and if your column has that precision it should be there for a reason.
The same is true for dates. If you are storing times with dates, and the time isn't relevant for that specific query, change your query; having a setting to ignore times (or decimal points) is, in all honestly, just a bad idea.
I don't think that there is an option like this in SSMS. The best thing I am coming up with is to create views of the tables and this way you can do a
select convert(date, <date column>)
one time and they will appear as just dates in the views.

Passing Multiple Values to Variable in Linked Server Connection String

I have the following query, which pulls data from an Oracle DB into SQL Server 2005:
SELECT *
FROM (
SELECT *
FROM OPENQUERY(LINKEDSERVERNAME, 'SELECT FOO, BAR, FROM TABLE
WHERE ID IN(' + #IDs + '
')) AS TMP
WHERE SOME_ID IN
(SELECT DISTINCT ID
FROM LOCALTABLE);
The runtime, however, is very long, as the query from the linked server results in a large number of rows. I am only interested in a small number of these rows, however the criteria limiting my query are held in the destination database.
Via another post on SO, I see I could potentially use a variable in dynamic sql that looks like:
DECLARE #IDs AS NVARCHAR(100);
SET #IDs = (SELECT ID FROM LOCALTABLE)
DECLARE #sql AS NVARCHAR(3000);
SET #sql = 'SELECT * FROM OPENQUERY(LINKEDSERVERNAME, ''SELECT FOO, BAR, FROM TABLE
WHERE ID IN(' + #IDs + '))'
EXEC sp_executesql #sql
However, I obviously cannot assign more than one value to the variable, and so the result set only contains results for the final ID placed in #IDs.
What is the best strategy for accomplishing this task for all distinct IDs in the local table?
Anup Shah has already pointed out what is wrong in his comment. Your SELECT assignment will only ever put one value into your variable. You need a way to convert your table results to a CSV style for the IN statement. Pinal Dave has a good post which shows a well known technique for doing this with XML PATH.
http://blog.sqlauthority.com/2009/11/25/sql-server-comma-separated-values-csv-from-table-column/
Worth noting that SELECT #var = #var + var FROM table IS NOT a valid way of doing this, although it may appear to work in some cases.
James

Return multiple columns as single comma separated row in SQL Server 2005

I'm curious to see if this is possible.
I have a table or this could be specific to any old table with data. A simple SELECT will return the columns and rows as a result set. What I'm trying to find out if is possible to return rows but rather than columns, the columns concatenated and are comma separated. So expected amount of rows returned but only one varchar column holding comma separated results of all the columns just like a CSV file.
Thanks.
[UPDATE]
Here is a bit more detail why I'm asking. I don't have the option to do this on the client, this is a task I'm trying to do with SSIS.
Scenario: I have a table that is dynamically created in SSIS but the column names change each time it's built. The original package uses BCP to grab the data and put it into a flat file but due to permissions when run as a job BCP can't create the flat file at the required destination. We can't get this changed either.
The other issue is that with SSIS 2005, using the flat files destination, you have to map the column name from the input source which I can't do because the column names keep changing.
I've written a script task to grab all the data from the original tables and then use stream writer to write to the CSV but I have to loop through each row then through each column to produce the string built up of all the columns. I want to measure performance of this concatenation of columns on sql server against a nasty loop with vb.net.
If I can get sql to produce a single column for each row I can just write a single line to the text file instead of iterating though each column to build the row.
I Think You Should try This
SELECT UserName +','+ Password AS ColumnZ
FROM UserTable
Assuming you know what columns the table has, and you don't want to do something dynamic and crazy, you can do this
SELECT CONCAT(ColumnA, ',', ColumnB) AS ColumnZ
FROM Table
There is a fancy way to this using SQL Server's XML functions, but for starters could you just cast the contents of the columns you care about as varchar and concatenate them with commas?
SELECT cast(colA as varchar)+', '+cast(colB as varchar)+', '+cast(colC as varchar)
FROM table
Note, that this will get tripped up if any of your contents have a comma or double quotes in them, in which case you can also use a replace function on each cast to escape them.
This could stand to be cleaned up some, but you can do this by using the metadata stored in sys.objects and sys.columns along with dynamic SQL. Note that I am NOT a fan of dynamic SQL, but for reporting purposes it shouldn't be too much of a problem.
Some SQL to create test data:
if (object_id('test') is not null)
drop table test;
create table test
(
id uniqueidentifier not null default newId()
,col0 nvarchar(255)
,col1 nvarchar(255)
,col2 nvarchar(255)
,col3 nvarchar(255)
,col4 nvarchar(255)
);
insert into test (col0,col1,col2,col3,col4)
select 'alice','bob','charlie','dave','emily'
union
select 'abby','bill','charlotte','daniel','evan'
A stored proc to build CSV rows:
-- emit the contents of a table as a CSV.
-- #table_name: name of a permanent (in sys.objects) table
-- #debug: set to 1 to print the generated query
create procedure emit_csv(#table_name nvarchar(max), #debug bit = 0)
as
declare #object_id int;
set nocount on;
set #object_id = object_id(#table_name);
declare #name nvarchar(max);
declare db_cursor cursor for
select name
from sys.columns
where object_id = #object_id;
open db_cursor;
fetch next from db_cursor into #name
declare #query nvarchar(max);
set #query = '';
while ##FETCH_STATUS = 0
begin
-- TODO: modify appended clause to escape commas in addition to trimming
set #query = #query + 'rtrim(cast('+#name+' as nvarchar(max)))'
fetch next from db_cursor into #name;
-- add concatenation to the end of the query.
-- TODO: Rearrange #query construction order to make this unnecessary
if (##fetch_status = 0)
set #query = #query + ' + '','' +'
end;
close db_cursor;
deallocate db_cursor;
set #query = 'select rtrim('+#query+') as csvrow from '+#table_name;
if #debug != 0
begin
declare #newline nvarchar(2);
set #newline = char(13) + char(10)
print 'Generated SQL:' + #newline + #query + #newline + #newline;
end
exec (#query);
For my test table, this generates the query:
select
rtrim(rtrim(cast(id as nvarchar(max)))
+ ','
+rtrim(cast(col0 as nvarchar(max)))
+ ','
+rtrim(cast(col1 as nvarchar(max)))
+ ','
+rtrim(cast(col2 as nvarchar(max)))
+ ','
+rtrim(cast(col3 as nvarchar(max)))
+ ','
+rtrim(cast(col4 as nvarchar(max))))
as csvrow
from test
and the result set:
csvrow
-------------------------------------------------------------------------------------------
EEE16C3A-036E-4524-A8B8-7CCD2E575519,alice,bob,charlie,dave,emily
F1EE6C84-D6D9-4621-97E6-AA8716C0643B,abby,bill,charlotte,daniel,evan
Suggestions
Modify the cursor loop to escape commas
Make sure that #table_name refers to a valid table (if object_id(#table_name) is null) in the sproc
Some exception handling would be good
Set permissions on this so that only the account that runs the report can execute it. String concatenation in dynamic SQL can be a big security hole, but I don't see another way to do this.
Some error handling to ensure that the cursor gets closed and deallocated might be nice.
This can be used for any table that is not a #temp table. In that case, you'd have to use sys.objects and sys.columns from tempdb...
select STUFF((select ','+ convert(varchar,l.Subject) from tbl_Student B,tbl_StudentMarks L
where B.Id=L.Id FOR XML PATH('')),1,1,'') Subject FROM tbl_Student A where A.Id=10

T-SQL Newline String Concatenation For Exec()

So, I've come across a pretty annoying problem with T-SQL... Essentially, in a table, there are several T-SQL statements. I want a stored procedure to efficiently grab these rows and concatenate them to a variable. Then, execute the concatenated lines with EXEC(#TSQL)
The problem is, string concatenation with newlines seem to be stripped when calling Exec...
For example something like:
declare #sql nvarchar(max) = ''
select #sql += char(13) + char(10) + [sql] from [SqlTable]
exec(#sql) -- Won't always do the right thing
print #sql -- Looks good
I don't want to butcher the code with a Cursor, is there any way around this? Thanks!
Edit:
Okay, so it looks like the issue is in fact only with the GO statement, for example:
declare #test nvarchar(max) = 'create table #test4(i int) ' + char(10) + char(13) + 'GO' + char(10) + char(13) +'create table #test5(i int)'
exec(#test)
I guess this go will have to go (no pun intended) I just really didn't want to have to try and parse it in fear of special cases blowing up the whole thing.
A select statement without order by is free to return results in any order.
You'd have to specify the order in which your SQL snippets make sense:
select #sql += char(13) + char(10) + [sql]
from [SqlTable]
order by
SnippetSequenceNr
As #Bort suggested in the comments, if the snippets are stand-alone SQL, you can separate them with a semicolon. Carriage returns, newlines, tabs and spaces are all the same in T-SQL: they're whitespace.
select #sql += [sql] + ';'
from [SqlTable]
order by
SnippetSequenceNr
Just get rid of the GO "statements". As noted by others you also might need to ensure the string is constructing in the correct statement sequence. Using += is probably not the best idea, though I'm not sure about the dynamic sql idea in the first place. It might actually be more appropriate to use a cursor here.
Sam F,
Your method didn't work with FOR XML method of string concatenation (if you wanted to create a "line" delimited list, based on values found in different rows of a table). However, replace the char(13) with SPACE(13), then it works great.
SELECT PackageNote = SUBSTRING((SELECT (SPACE(13) + char(10) + PackageDescription)
FROM POPackageNote PN2
WHERE PN1.PurchaseOrderNumber = PN2.PurchaseOrderNumber
ORDER BY POPackageNoteID, PackageDescription
FOR XML PATH( '' )
), 3, 1000 )
FROM POPackageNote PN1
WHERE (PurchaseOrderNumber = #PurchaseOrderNumber)
GROUP BY PurchaseOrderNumber

Dynamically search columns for given table

I need to create a search for a java app I'm building where users can search through a SQL database based on the table they're currently viewing and a search term they provide. At first I was going to do something simple like this:
SELECT * FROM <table name> WHERE CAST((SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '<table name>')
AS VARCHAR) LIKE '%<search term>%'
but that subquery returns more than one result, so then I tried to make a procedure to loop through all the columns in a given table and put any relevant fields in a results table, like this:
CREATE PROC sp_search
#tblname VARCHAR(4000),
#term VARCHAR(4000)
AS
SET nocount on
SELECT COLUMN_NAME
INTO #tempcolumns
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #tblname
ALTER TABLE #tempcolumns
ADD printed BIT,
num SMALLINT IDENTITY
UPDATE #tempcolumns
SET printed = 0
DECLARE #colname VARCHAR(4000),
#num SMALLINT
WHILE EXISTS(SELECT MIN(num) FROM #tempcolumns WHERE printed = 0)
BEGIN
SELECT #num = MIN(num)
FROM #tempcolumns
WHERE printed = 0
SELECT #colname = COLUMN_NAME
FROM #tempcolumns
WHERE num = #num
SELECT * INTO #results FROM #tblname WHERE CAST(#colname AS VARCHAR)
LIKE '%' + #term + '%' --this is where I'm having trouble
UPDATE #tempcolumns
SET printed = 1
WHERE #num = num
END
SELECT * FROM #results
GO
This has two problems: first is that it gets stuck in an infinite loop somehow, and second I can't select anything from #tblname. I tried using dynamic sql as well, but I don't know how to get results from that or if that's even possible.
This is for an assignment I'm doing at college and I've gotten this far after hours of trying to figure it out. Is there any way to do what I want to do?
You need to only search columns that actually contain strings, not all columns in a table (which may include integers, dates, GUIDs, etc).
You shouldn't need a #temp table (and certainly not a ##temp table) at all.
You need to use dynamic SQL (though I'm not sure if this has been part of your curriculum so far).
I find it beneficial to follow a few simple conventions, all of which you've violated:
use PROCEDURE not PROC - it's not a "prock," it's a "stored procedure."
use dbo. (or alternate schema) prefix when referencing any object.
wrap your procedure body in BEGIN/END.
use vowels liberally. Are you saving that many keystrokes, never mind time, saying #tblname instead of #tablename or #table_name? I'm not fighting for a specific convention but saving characters at the cost of readability lost its charm in the 70s.
don't use the sp_ prefix for stored procedures - this prefix has special meaning in SQL Server. Name the procedure for what it does. It doesn't need a prefix, just like we know they're tables even without a tbl prefix. If you really need a prefix there, use another one like usp_ or proc_ but I personally don't feel that prefix gives you any information you don't already have.
since tables are stored using Unicode (and some of your columns might be too), your parameters should be NVARCHAR, not VARCHAR. And identifiers are capped at 128 characters, so there is no reason to support > 257 characters for #tablename.
terminate statements with semi-colons.
use the catalog views instead of INFORMATION_SCHEMA - though the latter is what your professor may have taught and might expect.
CREATE PROCEDURE dbo.SearchTable
#tablename NVARCHAR(257),
#term NVARCHAR(4000)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'SELECT * FROM ' + #tablename + ' WHERE 1 = 0';
SELECT #sql = #sql + '
OR ' + c.name + ' LIKE ''%' + REPLACE(#term, '''', '''''') + '%'''
FROM
sys.all_columns AS c
INNER JOIN
sys.types AS t
ON c.system_type_id = t.system_type_id
AND c.user_type_id = t.user_type_id
WHERE
c.[object_id] = OBJECT_ID(#tablename)
AND t.name IN (N'sysname', N'char', N'nchar',
N'varchar', N'nvarchar', N'text', N'ntext');
PRINT #sql;
-- EXEC sp_executesql #sql;
END
GO
When you're happy that it's outputting the SELECT query you're after, comment out the PRINT and uncomment the EXEC.
You get into an infinite loop because EXISTS(SELECT MIN(num) FROM #tempcolumns WHERE printed = 0) will always return a row even if there are no matches - you need to EXISTS (SELECT * .... instead
To use dynamic SQL, you need to build up a string (varchar) of the SQL statement you want to run, then you call it with EXEC
eg:
declare #s varchar(max)
select #s = 'SELECT * FROM mytable '
Exec (#s)