Convert Bit field table to table - sql-server

I have a table showing locations with a BIT column for each tool in use at each location:
CREATE TABLE dbo.[ToolsSelected] (
[LocationID] NVARCHAR(40) NOT NULL,
[Tool1] INTEGER DEFAULT 0 NOT NULL,
[Tool2] INTEGER DEFAULT 0 NOT NULL,
[Tool3] INTEGER DEFAULT 0 NOT NULL,
[Tool4] INTEGER DEFAULT 0 NOT NULL,
PRIMARY KEY ([LocationID])
);
LocID Tool1 Tool2 Tool3 Tool4
----- ----- ----- ----- -----
AZ 0 1 1 0
NY 1 0 1 1
I need to convert this to a table by LocationID indicating which tools at which locations:
CREATE TABLE dbo.[ByLocation] (
[LocationID] NVARCHAR(40) NOT NULL,
[Tool] NVARCHAR(40) NOT NULL, -- Column title of ToolsSelected table
PRIMARY KEY ([LocationID], [Tool])
);
LocID Tool
----- -----
AZ Tool2
AZ Tool3
NY Tool1
NY Tool3
NY Tool4
The idea is that each location can select the tools they need, I then need to query the tools table to get details (versions, etc) for each tool selected. Each location is unique; each tool is unique. Is there a way to do this or a much better implementation?

Here is the answer to the immediate question, given only 4 tools columns:
SELECT LocID = LocationID, Tool
FROM
(
SELECT LocationID, Tool = 'Tool1' FROM dbo.ToolsSelected WHERE Tool1 = 1
UNION ALL
SELECT LocationID, Tool = 'Tool2' FROM dbo.ToolsSelected WHERE Tool2 = 1
UNION ALL
SELECT LocationID, Tool = 'Tool3' FROM dbo.ToolsSelected WHERE Tool3 = 1
UNION ALL
SELECT LocationID, Tool = 'Tool4' FROM dbo.ToolsSelected WHERE Tool4 = 1
) AS x
ORDER BY LocID, Tool;
With 40 columns, you could do the same thing, but along with the desire to generate this dynamically:
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'';
SELECT #sql += '
UNION ALL
SELECT LocationID, Tool = ''' + name + '''
FROM dbo.ToolsSelected WHERE ' + name + ' = 1'
FROM sys.columns WHERE [object_id] = OBJECT_ID('dbo.ToolsSelected')
AND name LIKE 'Tool[0-9]%';
SELECT #sql = N'SELECT LocID = LocationID, Tool
FROM
(' + STUFF(#sql, 1, 17, '') + '
) AS x ORDER BY LocID, Tool;';
PRINT #sql;
-- EXEC sp_executesql #sql;
*BUT*
Storing these as separate columns is a recipe for disaster. So when you add Tool41, Tool42 etc. you have to change the schema then change all your code that passes the column names and 1/0 via parameters etc. Why not represent these as simple numbers, e.g.
CREATE TABLE dbo.LocationTools
(
LocID NVARCHAR(40),
ToolID INT
);
So in the above case you would store:
LocID Tool
----- ----
AZ 2
AZ 3
NY 1
NY 3
NY 4
Now when you pass in the checkboxes they've selected, presumably from the front end you are receiving two values, such as:
LocID: "NY"
Tools: "Tool1, Tool5, Tool26"
If that's about right, then you can populate the table when a user creates or changes their choice, first using a split function to break up the comma-separated list dictated by the checkboxes:
CREATE FUNCTION dbo.SplitTools
(
#ToolList NVARCHAR(MAX)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT ToolID = y.i.value('(./text())[1]', 'int')
FROM
(
SELECT x = CONVERT(XML,
'<i>' + REPLACE(REPLACE(#List, ',', '</i><i>'), 'Tool', '')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
(You forgot to tell us which version of SQL Server you are using - if 2008 or above you could use a table-valued parameter as an alternative to a split function.)
Then a procedure to handle it:
CREATE PROCEDURE dbo.UpdateLocationTools
#LocID NVARCHAR(40),
#Tools NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
-- in case they had previously selected tools
-- that are no longer selected, clear first:
DELETE dbo.LocationTools WHERE LocID = #LocID;
INSERT dbo.LocationTools(LocID, ToolID)
SELECT #LocID, ToolID
FROM dbo.SplitTools(#Tools);
END
GO
Now you can add new tool #s without changing schema or code, since your list of checkboxes could also be generated from your data - assuming you have a dbo.Tools table or want to add one. This table could also be used for data integrity purposes (you could put a foreign key on dbo.LocationTools.ToolID).
And you can generate your desired query very simply:
SELECT LocID, Tool = 'Tool' + CONVERT(VARCHAR(12), ToolID)
FROM dbo.LocationTools
ORDER BY LocID, ToolID;
No redundant data, no wide tables with unmanageable columns, and a proper index can even help you search for, say, all locations using Tool3 efficiently...

Related

How to get 2 sub strings as 2 columns from single column of table

[{"key":"Mobile","value":"9100617634"},{"key":"Email","value":"balajirao1#ziaff.in"}]
The above one represents the value in one column of a table.
I want 2 columns named as mobile and email where those having values as 9000617634,balajirao#ziraff, respectively .
How to get in sql server.
see the below one.
mobile Email
------ ------------
9100617634 balajirao1#ziaff.in
U can Use Pivot Function to convert the row values to column,
Below is the Static Code for implementation
SELECT * FROM(
SELECT
columns_name
FROM
table_name
) M
PIVOT (MAX(Key) FOR table_name IN (mobile ,Email))AS P ;
Well that was fun doing it the hard way :)
--initial string
declare #s1 varchar(1000) = (select '[{"key":"Mobile","value":"9100617634"},{"key":"Email","value":"balajirao1#ziaff.in"}]')
--selecting first part and second part of string
declare #mobile varchar(100) = (select right(left(#s1,charindex('}',#s1)-1),len(left(#s1,charindex('}',#s1))) - charindex(':',left(#s1,charindex('}',#s1)))-1))
declare #mail varchar(100) = (select right(#s1,charindex(':',reverse(#s1))-1))
--getting rid of extra characters
set #mobile = (right(#mobile, len(#mobile) - charindex(':',#mobile)))
set #mail = (left(#mail, len(#mail) - charindex('}',reverse(#mail))))
--getting rid of double quotes
set #mobile = replace(#mobile,'"','')
set #mail = replace(#mail,'"','')
--selecting data
select
#mobile as Mobile,
#mail as Mail
Result is as following:
Mobile Mail
9100617634 balajirao1#ziaff.in
SELECT
case when [MOBILE/PHONE] like '[a-z]%' then null else [MOBILE/PHONE] end as [MOBILE/PHONE],
case when ISNUMERIC(email)=0 then EMAIL end as EMAIL
FROM
(
select
REPLACE(SUBSTRING(RIGHT(right(left(AC.Communication,charindex('}',AC.Communication)),len(left(AC.Communication,charindex('}',AC.Communication))) - charindex(':',left(AC.Communication,charindex('}',AC.Communication)))),LEN(right(left(AC.Communication,charindex('}',AC.Communication)),len(left(AC.Communication,charindex('}',AC.Communication))) - charindex(':',left(AC.Communication,charindex('}',AC.Communication)))))-CHARINDEX(':',right(left(AC.Communication,charindex('}',AC.Communication)),len(left(AC.Communication,charindex('}',AC.Communication))) - charindex(':',left(AC.Communication,charindex('}',AC.Communication)))))),2,CHARINDEX(']',AC.Communication)),'"}','') AS [MOBILE/PHONE],
REPLACE(REPLACE(LEFT(right(AC.Communication,charindex(':',reverse(AC.Communication))),LEN(right(AC.Communication,charindex(':',reverse(AC.Communication))))-charindex('}',reverse(right(AC.Communication,charindex(':',reverse(AC.Communication)))))),':"',' '),'"','') AS EMAIL
from DimAccountContact as AC

Queries with Dynamic Parameters - better ways?

I have the following stored procedure that is quite extensive because of the dynamic #Name parameter and the sub query.
Is there a better more efficient way to do this?
CREATE PROCEDURE [dbo].[spGetClientNameList]
#Name varchar(100)
AS
BEGIN
SET NOCOUNT ON;
SELECT
*
FROM
(
SELECT
ClientID,
FirstName + ' ' + LastName as Name
FROM
Client
) a
where a.Name like '%' + #Name + '%'
Shamelessly stealing from two recent articles by Aaron Bertrand:
Follow-up #1 on leading wildcard seeks - Aaron Bertrand
One way to get an index seek for a leading %wildcard - Aaron Bertrand
The jist is to create something that we can use that resembles a trigram (or trigraph) in PostgreSQL.
Aaron Bertrand also includes a disclaimer as follows:
"Before I start to show how my proposed solution would work, let me be absolutely clear that this solution should not be used in every single case where LIKE '%wildcard%' searches are slow. Because of the way we're going to "explode" the source data into fragments, it is likely limited in practicality to smaller strings, such as addresses or names, as opposed to larger strings, like product descriptions or session abstracts."
test setup: http://rextester.com/IIMT54026
Client table
create table dbo.Client (
ClientId int not null primary key clustered
, FirstName varchar(50) not null
, LastName varchar(50) not null
);
insert into dbo.Client (ClientId, FirstName, LastName) values
(1, 'James','')
, (2, 'Aaron','Bertrand')
go
Function used by Aaron Bertrand to explode string fragments (modified for input size):
create function dbo.CreateStringFragments(#input varchar(101))
returns table with schemabinding
as return
(
with x(x) as (
select 1 union all select x+1 from x where x < (len(#input))
)
select Fragment = substring(#input, x, len(#input)) from x
);
go
Table to store fragments for FirstName + ' ' + LastName:
create table dbo.Client_NameFragments (
ClientId int not null
, Fragment varchar(101) not null
, constraint fk_ClientsNameFragments_Client
foreign key(ClientId) references dbo.Client
on delete cascade
);
create clustered index s_cat on dbo.Client_NameFragments(Fragment, ClientId);
go
Loading the table with fragments:
insert into dbo.Client_NameFragments (ClientId, Fragment)
select c.ClientId, f.Fragment
from dbo.Client as c
cross apply dbo.CreateStringFragments(FirstName + ' ' + LastName) as f;
go
Creating trigger to maintain fragments:
create trigger dbo.Client_MaintainFragments
on dbo.Client
for insert, update as
begin
set nocount on;
delete f from dbo.Client_NameFragments as f
inner join deleted as d
on f.ClientId = d.ClientId;
insert dbo.Client_NameFragments(ClientId, Fragment)
select i.ClientId, fn.Fragment
from inserted as i
cross apply dbo.CreateStringFragments(i.FirstName + ' ' + i.LastName) as fn;
end
go
Quick trigger tests:
/* trigger tests --*/
insert into dbo.Client (ClientId, FirstName, LastName) values
(3, 'Sql', 'Zim')
update dbo.Client set LastName = 'unknown' where LastName = '';
delete dbo.Client where ClientId = 3;
--select * from dbo.Client_NameFragments order by ClientId, len(Fragment) desc
/* -- */
go
New Procedure:
create procedure [dbo].[Client_getNameList] #Name varchar(100) as
begin
set nocount on;
select
ClientId
, Name = FirstName + ' ' + LastName
from Client c
where exists (
select 1
from dbo.Client_NameFragments f
where f.ClientId = c.ClientId
and f.Fragment like #Name+'%'
)
end
go
exec [dbo].[Client_getNameList] #Name = 'On Bert'
returns:
+----------+----------------+
| ClientId | Name |
+----------+----------------+
| 2 | Aaron Bertrand |
+----------+----------------+
I guess search operation on Concatenated column wont take Indexes sometimes. I got situation like above and I replaced the Concatenated search with OR which gave me better performance most of the times.
Create Non Clustered Indexes on FirstName and LastName if not present.
Check the performance after modifying the above Procedure like below
CREATE PROCEDURE [dbo].[spGetClientNameList]
#Name varchar(100)
AS
BEGIN
SET NOCOUNT ON;
SELECT
ClientID,
FirstName + ' ' + LastName as Name
FROM
Client
WHERE FirstName LIKE '%' + #Name + '%'
OR LastName LIKE '%' + #Name + '%'
END
And do check execution plans to verify those Indexes are used or not.
The problem really comes down to having to compute the column (concat the first name and last name), that pretty much forces sql server into doing a full scan of the table to determine what is a match and what isn't. If you're not allowed to add indexes or alter the table, you'll have to change the query around (supply firstName and lastName separately). If you are, you could add a computed column and index that:
Create Table client (
ClientId INT NOT NULL PRIMARY KEY IDENTITY(1,1)
,FirstName VARCHAR(100)
,LastName VARCHAR(100)
,FullName AS FirstName + ' ' + LastName
)
Create index FullName ON Client(FullName)
This will at least speed your query up by doing index seeks instead of full table scans. Is it worth it? It's difficult to say without looking at how much data there is, etc.
where a.Name like '%' + #Name + '%'
This statement never can use index. In this situation it's beter to use Full Text Search
if you can restrict your like to
where a.Name like #Name + '%'
it will use index automaticaly. Moreover you can use REVERSE() function to index statement like :
where a.Name like '%' + #Name

Dynamic Pivot with varying columns

I have a POA Code dynamic pivot that pulls data from a DX temp table and inserts the data into a temp POA table.
The issue I'm having is that there is a possibility of up to 35 different columns that can be returned. Depending on the month there could be 15 columns (POA1...POA15) or there could be all 35 columns (POA1...POA35). I join this dynamic pivot temp table on another patient table. My problem is, I need to show all 35 columns even if some of the columns do not exist in the temp POA table.
--Pivot DX POA Codes
DECLARE #POANAME VARCHAR(40)
SELECT #POAName = '##tmpPOA'
DECLARE #colsPOA NVARCHAR(2000)
SELECT #colsPOA = STUFF((SELECT DISTINCT TOP 100 PERCENT
'],[' + 'POA' + CAST(Dx.RowNum AS NVARCHAR)
FROM #tmpDX DX
ORDER BY '],[' + 'POA' + CAST(Dx.RowNum AS NVARCHAR)
FOR XML PATH ('')
),1,2,'') + ']'
DECLARE #queryPOA NVARCHAR(4000)
SET #queryPOA = 'N
SELECT
EncObjID,
'+
#colsPOA
+' INTO ' + POAName + '
FROM
(SELECT
Dx.EncObjID
,''POA'' + Dx.RowNum AS RowNum
,Dx.POAMne
FROM #tmpDx Dx
) p
PIVOT
(
MIN([POAMne])
FOR RowNum IN
( ' + #colsPOA + ' )
) AS pvt'
EXECUTE(#queryPOA)
I'm receiving an Invalid Column Name in my patient query because some of the columns don't exist in ##tmpPOA. I thought about creating a temp table called #tmpDxPOA and doing an insert (Insert Into #tmpDxPOA select * from ##tmpPOA), but that doesn't work (I receive a Column Name or number of supplied values does not match error).
Any thoughts on how to create all 35 columns even if there isn't any data? I don't care if they're null, I just need to have those place holders in the main patient query and it doesn't help that the number of columns returned varies every month.
With the help of #mxix I was able to come up with the following:
DECLARE #POASQL NVARCHAR(MAX)
SET #POASQL = N'INSERT INTO #tmpPOAFinal (EncObjID,'+#colsPOA+') SELECT * FROM ##tmpPOA'
EXECUTE(#POASQL)
I put this after the EXECUTE(#queryPOA) in my main query.
In order for this to work with Dynamic SQL the rows/colums need to exists more than zero times. Whether it be for one or more patient. I would try to fan out the number of POA possibilities right off the bat and then left outer join to get the actual values back.
IF OBJECT_ID('tempdb..#tmpPOA') IS NOT NULL DROP TABLE #tmpPOA
CREATE TABLE #tmpPOA (POA varchar(10))
IF OBJECT_ID('tempdb..#tmpPatient') IS NOT NULL DROP TABLE #tmpPatient
CREATE TABLE #tmpPatient (Patient varchar(15))
INSERT INTO #tmpPatient VALUES ('ABC123'),('ABC456'),('ABC789')
DECLARE #POAFlag as INT = 0
WHILE #POAFlag <36
BEGIN
INSERT INTO #tmpPOA
VALUES('POA' +CONVERT(varchar,#POAFlag))
SET #POAFlag = #POAFlag + 1
END
SELECT * FROM #tmpPOA
CROSS JOIN #tmpPatient
This should fan out all of the possibilities of the 35DXCodes for you to get their POA flag.

TSQL - Dynamic Column Name in Dynamic SQL?

I have a "table A" with the following structure:
Act_Code ACT_TYPE_1 ACT_TYPE_2 ACT_TYPE_3 ACT_TYPE_4
-------- -------- ----------- ---------- -----------
ACT1 A NULL NULL NULL
ACT2 NULL B NULL NULL
ACT3 NULL NULL C NULL
ACT4 NULL NULL NULL D
ACT1 A NULL NULL NULL
As you can see, the ACT_TYPE data will always store into the field name which refers to the last digit of "Act_Code" fields:
Eg.
When Act_Code = "ACT1", the Act_Type is stored in field "ACT_TYPE_1"
When Act_Code = "ACT2", the Act_Type is stored in field "ACT_TYPE_2"
and so on...
Now, I want to read the data from the above "Table A" and insert into "Table B" which is having the following structure:
ACT ACT_TYPE
---- --------
1 A
2 B
3 C
4 D
Question:
*1. How can I add "dynamic column name" inside a select query?*
For example,
INSERT INTO Table_B (ACT, ACT_TYPE)
SELECT RIGHT(Act_Code,1), ## FROM Table_A
How can I handle the dynamic column name as per the symbol "##" above?
I've tried:
SET #sql = 'INSERT INTO Table_B (ACT,ACT_TYPE) '
SET #sql = #sql + 'SELECT RIGHT(Act_Code,1), '
SET #sql = #sql + '''ACT_TYPE_'' + RIGHT(Act_Code,1) FROM Table_A'
EXEC (#sql)
But it doesn't work!
Please help, thanks very much!
declare #Foo as Table ( Voot varchar(10), Plevny1 varchar(10), Plevny2 varchar(10) )
insert into #Foo ( Voot, Plevny1, Plevny2 ) values ( 'Thing1', 'a', 'A' ), ( 'Thing2', 'b', 'B' )
select SubString( Voot, 6, 1 ) as Vootette,
case SubString( Voot, 6, 1 )
when '1' then Plevny1
when '2' then Plevny2
else NULL end as Plevny
from #Foo
Depending on your specific requirements you may need to parse a multiple digit integer from the controlling column's value, may want to handle a default output value, may want to check for the "other" values in the row being NULL, ... .
Looks like you can do that without dynamic SQL:
insert Table_B
(Act, Act_Type)
select case Act_Code
when 'ACT1' then 1
when 'ACT2' then 2
when 'ACT3' then 3
when 'ACT4' then 4
end
, coalesce(ACT_TYPE_1, ACT_TYPE_2, ACT_TYPE_3, ACT_TYPE_4)

TSQL - A join using full-text CONTAINS

I currently have the following select statement, but I wish to move to full text search on the Keywords column. How would I re-write this to use CONTAINS?
SELECT MediaID, 50 AS Weighting
FROM Media m JOIN #words w ON m.Keywords LIKE '%' + w.Word + '%'
#words is a table variable filled with words I wish to look for:
DECLARE #words TABLE(Word NVARCHAR(512) NOT NULL);
If you are not against using a temp table, and EXEC (and I realize that is a big if), you could do the following:
DECLARE #KeywordList VARCHAR(MAX), #KeywordQuery VARCHAR(MAX)
SELECT #KeywordList = STUFF ((
SELECT '"' + Keyword + '" OR '
FROM FTS_Keywords
FOR XML PATH('')
), 1, 0, '')
SELECT #KeywordList = SUBSTRING(#KeywordList, 0, LEN(#KeywordList) - 2)
SELECT #KeywordQuery = 'SELECT RecordID, Document FROM FTS_Demo_2 WHERE CONTAINS(Document, ''' + #KeywordList +''')'
--SELECT #KeywordList, #KeywordQuery
CREATE TABLE #Results (RecordID INT, Document NVARCHAR(MAX))
INSERT INTO #Results (RecordID, Document)
EXEC(#KeywordQuery)
SELECT * FROM #Results
DROP TABLE #Results
This would generate a query like:
SELECT RecordID
,Document
FROM FTS_Demo_2
WHERE CONTAINS(Document, '"red" OR "green" OR "blue"')
And results like this:
RecordID Document
1 one two blue
2 three red five
If CONTAINS allows a variable or column, you could have used something like this.
SELECT MediaID, 50 AS Weighting
FROM Media m
JOIN #words w ON CONTAINS(m.Keywords, w.word)
However, according to Books Online for SQL Server CONTAINS, it is not supported. Therefore, no there is no way to do it.
Ref: (column_name appears only in the first param to CONTAINS)
CONTAINS
( { column_name | ( column_list ) | * }
,'<contains_search_condition>'
[ , LANGUAGE language_term ]
)

Resources