Join with dynamic pivot (version 2) - sql

I have some tables with data:
Category
CategoryID CategoryName
1 Home
2 Contact
3 About
Position
PositionID PositionName
1 Main menu
2 Left menu
3 Right menu
...(new row can be added later)
CategoryPosition
CPID CID PID COrder
1 1 1 1
2 1 2 2
3 1 3 3
4 2 1 4
5 2 3 5
How can I make a table like this:
CID CName MainMenu LeftMenu RightMenu
1 Home 1 2 3
2 Contact 4 0 5
3 About 0 0 0
And if a new Category or Position row is added later, the query should reflect the change automatically, e.g:
CID CName MainMenu LeftMenu RightMenu BottomMenu
1 Home 1 2 3 0
2 Contact 4 0 5 0
3 About 0 0 0 0
4 News 0 0 0 0

The following dynamic query seems to work:
declare #columnlist nvarchar(4000)
select #columnlist = IsNull(#columnlist + ', ', '') + '[' + PositionName + ']'
from #Position
declare #query nvarchar(4000)
select #query = '
select *
from (
select CategoryId, CategoryName, PositionName,
IsNull(COrder,0) as COrder
from #Position p
cross join #Category c
left join #CategoryPosition cp
on cp.pid = p.PositionId
and cp.cid = c.CategoryId
) pv
PIVOT (max(COrder) FOR PositionName in (' + #columnlist + ')) as Y
ORDER BY CategoryId, CategoryName
'
exec sp_executesql #query
Some clarification:
The #columnlist contains the dymamic field list, built from the Positions table
The cross join creates a list of all categories and all positions
The left join seeks the corresponding COrder
max() selects the highest COrder per category+position, if there is more than one
PIVOT() turns the various PositionNames into separate columns
P.S. My table names begin with #, because I created them as temporary tables. Remove the # to refer to a permanent table.
P.S.2. If anyone wants to try his hands at this, here is a script to create the tables in this question:
set nocount on
if object_id('tempdb..#Category') is not null drop table #Category
create table #Category (
CategoryId int identity,
CategoryName varchar(50)
)
insert into #Category (CategoryName) values ('Home')
insert into #Category (CategoryName) values ('Contact')
insert into #Category (CategoryName) values ('About')
--insert into #Category (CategoryName) values ('News')
if object_id('tempdb..#Position') is not null drop table #Position
create table #Position (
PositionID int identity,
PositionName varchar(50)
)
insert into #Position (PositionName) values ('Main menu')
insert into #Position (PositionName) values ('Left menu')
insert into #Position (PositionName) values ('Right menu')
--insert into #Position (PositionName) values ('Bottom menu')
if object_id('tempdb..#CategoryPosition') is not null
drop table #CategoryPosition
create table #CategoryPosition (
CPID int identity,
CID int,
PID int,
COrder int
)
insert into #CategoryPosition (CID, PID, COrder) values (1,1,1)
insert into #CategoryPosition (CID, PID, COrder) values (1,2,2)
insert into #CategoryPosition (CID, PID, COrder) values (1,3,3)
insert into #CategoryPosition (CID, PID, COrder) values (2,1,4)
insert into #CategoryPosition (CID, PID, COrder) values (2,3,5)

Since PIVOT requires a static list of columns, I think a dynamic-sql-based approach is really all that you can do: http://www.simple-talk.com/community/blogs/andras/archive/2007/09/14/37265.aspx

As mentioned by several posters, dynamic SQL using the PIVOT command is the way to go. I wrote a stored proc named pivot_query.sql awhile back that has been very handy for this purpose. It works like this:
-- Define a query of the raw data and put it in a variable (no pre-grouping required)
declare #myQuery varchar(MAX);
set #myQuery = '
select
cp.cid,
c.CategoryName,
p.PositionName,
cp.COrder
from
CategoryPosition cp
JOIN Category c
on (c.CategoryId = cp.cid)
JOIN Position p
on (p.PositionId = cp.pid)';
-- Call the proc, passing the query, row fields, pivot column and summary function
exec dbo.pivot_query #myQuery, 'CategoryName', 'PositionName', 'max(COrder) COrder'
The full syntax of the pivot_query call is:
pivot_query '<query>', '<field list for each row>', '<pivot column>', '<aggregate expression list>', '[<results table>]', '[<show query>]'
it is explained more in the comments at the top of the source code.
A couple of advantages of this proc are that you can specify multiple summary functions like max(COrder),min(COrder) etc. and it has the option to store the output in a table in case you want to join the summary data up with other information.

I guess you need to select using PIVOT. By default, pivots only select a static list of columns. There are some solutions on the net dealing with dynamic column pivots, such as here and here.

My suggestion would be to return your data as a simple join and let the front end sort it out. There are some things for which SQL is excellent, but this particular problem seems like something that the front end should be doing. Of course, I can't know that without knowing your full situation, but that's my hunch.

Related

How can I update strings within an SQL server table based on a query?

I have two tables A and B. A has an Id and a string with some embedded information for some text and ids from a table C that is not shown
Aid| AString
1 "<thing_5"><thing_6">"
2 "<thing_5"><thing_6">"
Bid|Cid|Aid
1 5 1
2 6 1
3 5 2
4 6 2
I realise this is an insane structure but that is life.
I need to update the strings within A so that instead of having the Cid they have the corresponding Bid (related by the Aid and Bid pairing)
Is this even something I should be thinking of doing in SQL... A has about 300 entries and B about 1200 so not something doing by hand
For clarity I wish for B to remain the same and A to finally look like this
Aid| AString
1 "<thing_1"><thing_2">"
2 "<thing_3"><thing_4">"
This script relies on generating dynamic SQL statements to update the table, then executes those statements.
Taking into account that the cid's are within thing_ and ":
First replaces the cid's using a placeholder ($$$$$$ in this case) to account for the fact that cid's and bid's may overlap (example, changing 3->2 and later 2->1)
Then changes the placeholders to the proper bid
CREATE TABLE #a(aid INT,astr VARCHAR(MAX));
INSERT INTO #a(aid,astr)VALUES(1,'<thing_5"><thing_6">'),(2,'<thing_5"><thing_6">');
CREATE TABLE #rep(aid INT,bid INT,cid INT);
INSERT INTO #rep(bid,cid,aid)VALUES(5,6,1),(6,5,1),(3,5,2),(4,6,2);
DECLARE #cmd NVARCHAR(MAX)=(
SELECT
'UPDATE #a '+
'SET astr=REPLACE(astr,''thing_'+CAST(r.cid AS VARCHAR(16))+'"'',''thing_$$$$$$'+CAST(r.cid AS VARCHAR(16))+'"'') '+
'WHERE aid='+CAST(a.aid AS VARCHAR(16))+';'
FROM
(SELECT DISTINCT aid FROM #a AS a) AS a
INNER JOIN #rep AS r ON
r.aid=a.aid
FOR
XML PATH('')
);
EXEC sp_executesql #cmd;
SET #cmd=(
SELECT
'UPDATE #a '+
'SET astr=REPLACE(astr,''thing_$$$$$$'+CAST(r.cid AS VARCHAR(16))+'"'',''thing_'+CAST(r.bid AS VARCHAR(16))+'"'') '+
'WHERE aid='+CAST(a.aid AS VARCHAR(16))+';'
FROM
(SELECT DISTINCT aid FROM #a AS a) AS a
INNER JOIN #rep AS r ON
r.aid=a.aid
FOR
XML PATH('')
);
EXEC sp_executesql #cmd;
SELECT * FROM #a;
DROP TABLE #rep;
DROP TABLE #a;
Result is:
+-----+----------------------+
| aid | astr |
+-----+----------------------+
| 1 | <thing_6"><thing_5"> |
| 2 | <thing_3"><thing_4"> |
+-----+----------------------+
You could do this with SQL with something like below. It wasn't clear to me how c was related, but you can adjust it as necessary...
create table a (
Aid int null,
AString varchar(25) null)
insert into a values(1,'"<thing_5"><thing_6">"')
insert into a values(2,'"<thing_5"><thing_6">"')
create table b (
Aid int null,
Bid int null,
Cid int null)
insert into b values(1,1,5)
insert into b values(1,2,6)
insert into b values(2,3,5)
insert into b values(2,4,6)
UPDATE Ax
SET Ax.ASTRING = REPLACE(Ax.ASTRING, 'thing_' + cast(cID as varchar(1)),'thing_' + cast(BID as varchar(1)))
FROM A Ax
INNER JOIN Bx
on ax.Aid=bx.Aid
and Ax.AString like '%thing_' + cast(Cid as varchar(1)) + '%'

What is the best way to join between two table which have coma seperated columns

Table1
ID Name Tags
----------------------------------
1 Customer1 Tag1,Tag5,Tag4
2 Customer2 Tag2,Tag6,Tag4,Tag11
3 Customer5 Tag6,Tag5,Tag10
and Table2
ID Name Tags
----------------------------------
1 Product1 Tag1,Tag10,Tag6
2 Product2 Tag2,Tag1,Tag5
3 Product5 Tag1,Tag2,Tag3
what is the best way to join Table1 and Table2 with Tags column?
It should look at the tags column which coma seperated on table 2 for each coma seperated tag on the tags column in the table 1
Note: Tables are not full-text indexed.
The best way is not to have comma separated values in a column. Just use normalized data and you won't have trouble with querying like this - each column is supposed to only have one value.
Without this, there's no way to use any indices, really. Even a full-text index behaves quite different from what you might thing, and they are inherently clunky to use - they're designed for searching for text, not meaningful data. In the end, you will not get much better than something like
where (Col like 'txt,%' or Col like '%,txt' or Col like '%,txt,%')
Using a xml column might be another alternative, though it's still quite a bit silly. It would allow you to treat the values as a collection at least, though.
I don't think there will ever be an easy and efficient solution to this. As Luaan pointed out, it is a very bad idea to store data like this : you lose most of the power of SQL when you squeeze what should be individual units of data into a single cell.
But you can manage this at the slight cost of creating two user-defined functions. First, use this brilliant recursive technique to split the strings into individual rows based on your delimiter :
CREATE FUNCTION dbo.TestSplit (#sep char(1), #s varchar(512))
RETURNS table
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(#sep, #s)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(#sep, #s, stop + 1)
FROM Pieces
WHERE stop > 0
)
SELECT pn AS SplitIndex,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS SplitPart
FROM Pieces
)
Then, make a function that takes two strings and counts the matches :
CREATE FUNCTION dbo.MatchTags (#a varchar(512), #b varchar(512))
RETURNS INT
AS
BEGIN
RETURN
(SELECT COUNT(*)
FROM dbo.TestSplit(',', #a) a
INNER JOIN dbo.TestSplit(',', #b) b
ON a.SplitPart = b.SplitPart)
END
And that's it, here is a test roll with table variables :
DECLARE #A TABLE (Name VARCHAR(20), Tags VARCHAR(100))
DECLARE #B TABLE (Name VARCHAR(20), Tags VARCHAR(100))
INSERT INTO #A ( Name, Tags )
VALUES
( 'Customer1','Tag1,Tag5,Tag4'),
( 'Customer2','Tag2,Tag6,Tag4,Tag11'),
( 'Customer5','Tag6,Tag5,Tag10')
INSERT INTO #B ( Name, Tags )
VALUES
( 'Product1','Tag1,Tag10,Tag6'),
( 'Product2','Tag2,Tag1,Tag5'),
( 'Product5','Tag1,Tag2,Tag3')
SELECT * FROM #A a
INNER JOIN #B b ON dbo.MatchTags(a.Tags, b.Tags) > 0
I developed a solution as follows:
CREATE TABLE [dbo].[Table1](
Id int not null,
Name nvarchar(250) not null,
Tag nvarchar(250) null,
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Table2](
Id int not null,
Name nvarchar(250) not null,
Tag nvarchar(250) null,
) ON [PRIMARY]
GO
get sample data for Table1, it will insert 28000 records
INSERT INTO Table1
SELECT CustomerID,CompanyName, (FirstName + ',' + LastName)
FROM AdventureWorks.SalesLT.Customer
GO 3
sample data for Table2.. i need same tags for Table2
declare #tag1 nvarchar(50) = 'Donna,Carreras'
declare #tag2 nvarchar(50) = 'Johnny,Caprio'
get sample data for Table2, it will insert 9735 records
INSERT INTO Table2
SELECT ProductID,Name, (case when(right(ProductID,1)>=5) then #tag1 else #tag2 end)
FROM AdventureWorks.SalesLT.Product
GO 3
My Solution
create TABLE #dt (
Id int IDENTITY(1,1) PRIMARY KEY,
Tag nvarchar(250) NOT NULL
);
I've create temp table and i will fill with Distinct Tag-s in Table1
insert into #dt(Tag)
SELECT distinct Tag
FROM Table1
Now i need to vertical table for tags
create TABLE #Tags ( Tag nvarchar(250) NOT NULL );
Now i'am fill #Tags table with While, you can use Cursor but while is faster
declare #Rows int = 1
declare #Tag nvarchar(1024)
declare #Id int = 0
WHILE #Rows>0
BEGIN
Select Top 1 #Tag=Tag,#Id=Id from #dt where Id>#Id
set #Rows =##RowCount
if #Rows>0
begin
insert into #Tags(Tag) SELECT Data FROM dbo.StringToTable(#Tag, ',')
end
END
last step : join Table2 with #Tags
select distinct t.*
from Table2 t
inner join #Tags on (',' + t.Tag + ',') like ('%,' + #Tags.Tag + ',%')
Table rowcount= 28000 Table2 rowcount=9735 select is less than 2 second
I use this kind of solution with paths of trees. First put a comma at the very begin and at the very end of the string. Than you can call
Where col1 like '%,' || col2 || ',%'
Some database index the column also for the like(postgres do it partially), therefore is also efficient. I don't know sqlserver.

SSMS Temp Table / Parameter Issue

I have a temp table with 2 columns, each column is a parameter I've declared. I've done so using this sql.
Declare
#SourceKey varchar(40) = '1109'
,#Department Key varchar(1500) = '14,55
The table is then populated using the following sql:
if OBJECT_ID('Tempdb..#Department','U') is not null
drop table #Department
CREATE TABLE #Department
(DepartmentKey int
,BaseTerm varchar(5))
INSERT INTO #Department
SELECT value
,skt.Key from YYY.ParseList(#Department,',')
join #SourceKeyTable skt
on skt.Key = skt.key
If I select * From #Department I get these results:
Department Key | SourceKey
14 | 1109
55 | 1109
Thats what I expect. I then join the temp table to my main query like so
JOIN #Department d
on Table.rKey = d.DepartmentKey
I need to have a temp table to allow for a multi-select in the visual studio report. However, with the department key equal to 14 AND 55 its skewing my results. I need 1 value passed 14 OR 55 not both. But the temp table is neccessary for the multi-select.
Any suggestions on how to pass only 1 value while still having set up mentioned previously?
I'll do my best to answer questions as I might not have explained this question well enough for some.
I reckon you need to parse your list into a temporary table or table variable and then do whatever needs to be done with it.
It's difficult to see from your code exactly what that would involve but the code below should illustrate the idea sufficiently.
I create a table variable. Insert the parsed list values and then cycle through them printing the values to output
--Create a table variable
DECLARE #Departments TABLE(DepartmentOrder int identity, RKey nvarchar(40) NOT NULL)
--Create variables to loop through table variable
DECLARE #DepartmentOrder int
DECLARE #RKey nvarchar(40)
--Populate table variable with the parsed list values
INSERT #Departments (RKey) SELECT RKey from YYY.ParseList(#Department,',')
--Get the first list entry
SELECT #DepartmentOrder = min(DepartmentOrder ) FROM #Departments
--While we've not reached the end
WHILE #DepartmentOrder IS NOT NULL
BEGIN
--Get the Department key for this entry
SET #RKey = (SELECT RKey FROM #Departments WHERE DepartmentOrder = #DepartmentOrder )
--Use the values
PRINT '#DepartmentOrder = '+CONVERT(nvarchar(9),#DepartmentOrder )
PRINT '#RKey = '''+#RKey +''''
--Get the next list entry
SET #DepartmentOrder = (SELECT MIN(DepartmentOrder ) FROM #Departments WHERE DepartmentOrder > #DepartmentOrder )
END

The most elegant way to generate permutations in SQL server

Given a the following table:
Index | Element
---------------
1 | A
2 | B
3 | C
4 | D
We want to generate all the possible permutations (without repetitions) using the elements.
the final result (skipping some rows) will look like this:
Results
----------
ABCD
ABDC
ACBD
ACDB
ADAC
ADCA
...
DABC
DACB
DBCA
DBAC
DCAB
DCBA
(24 Rows)
How would you do it?
After making some perhaps snarky comments, this problem stuck in my brain all evening, and I eventually came up with the following set-based approach. I believe it definitely qualifies as "elegant", but then I also think it qualifies as "kinda dumb". You make the call.
First, set up some tables:
-- For testing purposes
DROP TABLE Source
DROP TABLE Numbers
DROP TABLE Results
-- Add as many rows as need be processed--though note that you get N! (number of rows, factorial) results,
-- and that gets big fast. The Identity column must start at 1, or the algorithm will have to be adjusted.
-- Element could be more than char(1), though the algorithm would have to be adjusted again, and each element
-- must be the same length.
CREATE TABLE Source
(
SourceId int not null identity(1,1)
,Element char(1) not null
)
INSERT Source (Element) values ('A')
INSERT Source (Element) values ('B')
INSERT Source (Element) values ('C')
INSERT Source (Element) values ('D')
--INSERT Source (Element) values ('E')
--INSERT Source (Element) values ('F')
-- This is a standard Tally table (or "table of numbers")
-- It only needs to be as long as there are elements in table Source
CREATE TABLE Numbers (Number int not null)
INSERT Numbers (Number) values (1)
INSERT Numbers (Number) values (2)
INSERT Numbers (Number) values (3)
INSERT Numbers (Number) values (4)
INSERT Numbers (Number) values (5)
INSERT Numbers (Number) values (6)
INSERT Numbers (Number) values (7)
INSERT Numbers (Number) values (8)
INSERT Numbers (Number) values (9)
INSERT Numbers (Number) values (10)
-- Results are iteratively built here. This could be a temp table. An index on "Length" might make runs
-- faster for large sets. Combo must be at least as long as there are characters to be permuted.
CREATE TABLE Results
(
Combo varchar(10) not null
,Length int not null
)
Here's the routine:
SET NOCOUNT on
DECLARE
#Loop int
,#MaxLoop int
-- How many elements there are to process
SELECT #MaxLoop = max(SourceId)
from Source
-- Initialize first value
TRUNCATE TABLE Results
INSERT Results (Combo, Length)
select Element, 1
from Source
where SourceId = 1
SET #Loop = 2
-- Iterate to add each element after the first
WHILE #Loop <= #MaxLoop
BEGIN
-- See comments below. Note that the "distinct" remove duplicates, if a given value
-- is to be included more than once
INSERT Results (Combo, Length)
select distinct
left(re.Combo, #Loop - nm.Number)
+ so.Element
+ right(re.Combo, nm.Number - 1)
,#Loop
from Results re
inner join Numbers nm
on nm.Number <= #Loop
inner join Source so
on so.SourceId = #Loop
where re.Length = #Loop - 1
-- For performance, add this in if sets will be large
--DELETE Results
-- where Length <> #Loop
SET #Loop = #Loop + 1
END
-- Show results
SELECT *
from Results
where Length = #MaxLoop
order by Combo
The general idea is: when adding a new element (say "B") to any string (say, "A"), to catch all permutations you would add B
to all possible positions (Ba, aB), resulting in a new set of strings. Then iterate: Add a new element (C) to each position in a string
(AB becomes Cab, aCb, abC), for all strings (Cba, bCa, baC), and you have the set of permutations. Iterate over each result set with
the next character until you run out of characters... or resources. 10 elements is 3.6 million permutations, roughly 48MB with the above algorithm, and 14 (unique) elements would hit 87 billion permutations and 1.163 terabytes.
I'm sure it could eventually be wedged into a CTE, but in the end all that would be is a glorified loop. The logic
is clearer this way, and I can't help but think the CTE execution plan would be a nightmare.
DECLARE #s VARCHAR(5);
SET #s = 'ABCDE';
WITH Subsets AS (
SELECT CAST(SUBSTRING(#s, Number, 1) AS VARCHAR(5)) AS Token,
CAST('.'+CAST(Number AS CHAR(1))+'.' AS VARCHAR(11)) AS Permutation,
CAST(1 AS INT) AS Iteration
FROM dbo.Numbers WHERE Number BETWEEN 1 AND 5
UNION ALL
SELECT CAST(Token+SUBSTRING(#s, Number, 1) AS VARCHAR(5)) AS Token,
CAST(Permutation+CAST(Number AS CHAR(1))+'.' AS VARCHAR(11)) AS
Permutation,
s.Iteration + 1 AS Iteration
FROM Subsets s JOIN dbo.Numbers n ON s.Permutation NOT LIKE
'%.'+CAST(Number AS CHAR(1))+'.%' AND s.Iteration < 5 AND Number
BETWEEN 1 AND 5
--AND s.Iteration = (SELECT MAX(Iteration) FROM Subsets)
)
SELECT * FROM Subsets
WHERE Iteration = 5
ORDER BY Permutation
Token Permutation Iteration
----- ----------- -----------
ABCDE .1.2.3.4.5. 5
ABCED .1.2.3.5.4. 5
ABDCE .1.2.4.3.5. 5
(snip)
EDBCA .5.4.2.3.1. 5
EDCAB .5.4.3.1.2. 5
EDCBA .5.4.3.2.1. 5
first posted a while ago here
However, it would be better to do it in a better language such as C# or C++.
Just using SQL, without any code, you could do it if you can crowbar yourself another column into the table. Clearly you need to have one joined table for each of the values to be permuted.
with llb as (
select 'A' as col,1 as cnt union
select 'B' as col,3 as cnt union
select 'C' as col,9 as cnt union
select 'D' as col,27 as cnt
)
select a1.col,a2.col,a3.col,a4.col
from llb a1
cross join llb a2
cross join llb a3
cross join llb a4
where a1.cnt + a2.cnt + a3.cnt + a4.cnt = 40
Am I correctly understanding that you built Cartesian product n x n x n x n, and then filter out unwanted stuff? The alternative would be generating all the numbers up to n! and then using factorial number system to map them via element encoding.
Simpler than a recursive CTE:
declare #Number Table( Element varchar(MAX), Id varchar(MAX) )
Insert Into #Number Values ( 'A', '01')
Insert Into #Number Values ( 'B', '02')
Insert Into #Number Values ( 'C', '03')
Insert Into #Number Values ( 'D', '04')
select a.Element, b.Element, c.Element, d.Element
from #Number a
join #Number b on b.Element not in (a.Element)
join #Number c on c.Element not in (a.Element, b.Element)
join #Number d on d.Element not in (a.Element, b.Element, c.Element)
order by 1, 2, 3, 4
For an arbitrary number of elements, script it out:
if object_id('tempdb..#number') is not null drop table #number
create table #number (Element char(1), Id int, Alias as '_'+convert(varchar,Id))
insert #number values ('A', 1)
insert #number values ('B', 2)
insert #number values ('C', 3)
insert #number values ('D', 4)
insert #number values ('E', 5)
declare #sql nvarchar(max)
set #sql = '
select '+stuff((
select char(13)+char(10)+'+'+Alias+'.Element'
from #number order by Id for xml path (''), type
).value('.','NVARCHAR(MAX)'),3,1,' ')
set #sql += '
from #number '+(select top 1 Alias from #number order by Id)
set #sql += (
select char(13)+char(10)+'join #number '+Alias+' on '+Alias+'.Id not in ('
+stuff((
select ', '+Alias+'.Id'
from #number b where a.Id > b.Id
order by Id for xml path ('')
),1,2,'')
+ ')'
from #number a where Id > (select min(Id) from #number)
order by Element for xml path (''), type
).value('.','NVARCHAR(MAX)')
set #sql += '
order by 1'
print #sql
exec (#sql)
To generate this:
select
_1.Element
+_2.Element
+_3.Element
+_4.Element
+_5.Element
from #number _1
join #number _2 on _2.Id not in (_1.Id)
join #number _3 on _3.Id not in (_1.Id, _2.Id)
join #number _4 on _4.Id not in (_1.Id, _2.Id, _3.Id)
join #number _5 on _5.Id not in (_1.Id, _2.Id, _3.Id, _4.Id)
order by 1
This method uses a binary mask to select the correct rows:
;with src(t,n,p) as (
select element, index, power(2,index-1)
from table
)
select s1.t+s2.t+s3.t+s4.t
from src s1, src s2, src s3, src s4
where s1.p+s2.p+s3.p+s4.p=power(2,4)-1
My original post:
declare #t varchar(4) = 'ABCD'
;with src(t,n,p) as (
select substring(#t,1,1),1,power(2,0)
union all
select substring(#t,n+1,1),n+1,power(2,n)
from src
where n < len(#t)
)
select s1.t+s2.t+s3.t+s4.t
from src s1, src s2, src s3, src s4
where s1.p+s2.p+s3.p+s4.p=power(2,len(#t))-1
This is one of those problems that haunts you. I liked the simplicity of my original answer but there was this issue where I was still building all the possible solutions and then selecting the correct ones. One more try to make this process more efficient by only building the solutions that were correct yielded this answer. Add a character to the string only if that character didn't exist in the string. Patindex seemed like the perfect companion for a CTE solution. Here it is.
declare #t varchar(10) = 'ABCDEFGHIJ'
;with s(t,n) as (
select substring(#t,1,1),1
union all
select substring(#t,n+1,1),n+1
from s where n<len(#t)
)
,j(t) as (
select cast(t as varchar(10)) from s
union all
select cast(j.t+s.t as varchar(10))
from j,s where patindex('%'+s.t+'%',j.t)=0
)
select t from j where len(t)=len(#t)
I was able to build all 3.6 million solutions in 3 minutes and 2 seconds. Hopefully this solution will not get missed just because it's not the first.
Current solution using a recursive CTE.
-- The base elements
Declare #Number Table( Element varchar(MAX), Id varchar(MAX) )
Insert Into #Number Values ( 'A', '01')
Insert Into #Number Values ( 'B', '02')
Insert Into #Number Values ( 'C', '03')
Insert Into #Number Values ( 'D', '04')
-- Number of elements
Declare #ElementsNumber int
Select #ElementsNumber = COUNT(*)
From #Number;
-- Permute!
With Permutations( Permutation, -- The permutation generated
Ids, -- Which elements where used in the permutation
Depth ) -- The permutation length
As
(
Select Element,
Id + ';',
Depth = 1
From #Number
Union All
Select Permutation + ' ' + Element,
Ids + Id + ';',
Depth = Depth + 1
From Permutations,
#Number
Where Depth < #ElementsNumber And -- Generate only the required permutation number
Ids Not like '%' + Id + ';%' -- Do not repeat elements in the permutation (this is the reason why we need the 'Ids' column)
)
Select Permutation
From Permutations
Where Depth = #ElementsNumber
Assuming your table is named Elements and has 4 rows, this is as simple as:
select e1.Element + e2.Element + e3.Element + e4.Element
from Elements e1
join Elements e2 on e2.Element != e1.Element
join Elements e3 on e3.Element != e2.Element AND e3.Element != e1.Element
join Elements e4 on e4.Element != e3.Element AND e4.Element != e2.Element AND e4.Element != e1.Element
Way too much rust on my SQL skills, but i took a different tack for a similar problem and thought it worth sharing.
Table1 - X strings in a single field Uno
Table2 - Y strings in a single field Dos
(SELECT Uno, Dos
FROM Table1
CROSS JOIN Table2 ON 1=1)
UNION
(SELECT Dos, Uno
FROM Table1
CROSS JOIN Table2 ON 1=1)
Same principle for 3 tables with an added CROSS JOIN
(SELECT Tres, Uno, Dos
FROM Table1
CROSS JOIN Table2 ON 1=1
CROSS JOIN Table3 ON 1=1)
although it takes 6 cross-join sets in the union.
--Hopefully this is a quick solution, just change the values going into #X
IF OBJECT_ID('tempdb.dbo.#X', 'U') IS NOT NULL DROP TABLE #X; CREATE table #X([Opt] [nvarchar](10) NOT NULL)
Insert into #X values('a'),('b'),('c'),('d')
declare #pSQL NVarChar(max)='select * from #X X1 ', #pN int =(select count(*) from #X), #pC int = 0;
while #pC<#pN begin
if #pC>0 set #pSQL = concat(#pSQL,' cross join #X X', #pC+1);
set #pC = #pC +1;
end
execute(#pSQL)
--or as single column result
IF OBJECT_ID('tempdb.dbo.#X', 'U') IS NOT NULL DROP TABLE #X; CREATE table #X([Opt] [nvarchar](10) NOT NULL)
Insert into #X values('a'),('b'),('c'),('d')
declare #pSQL NVarChar(max)=' as R from #X X1 ',#pSelect NVarChar(Max)=' ',#pJoin NVarChar(Max)='', #pN int =(select count(*) from #X), #pC int = 0;
while #pC<#pN begin
if #pC>0 set #pJoin = concat(#pJoin ,' cross join #X X', #pC+1) set #pSelect = concat(#pSelect ,'+ X', #pC+1,'.Opt ')
set #pC = #pC +1;
end
set #pSQL = concat ('select X1.Opt', #pSelect,#pSQL ,#pJoin)
exec(#pSQL)
create function GeneratePermutations (#string nvarchar(4000))
RETURNS #Permutations
TABLE(
name nVARCHAR(500)
)
AS
begin
declare #SplitedString table(name nvarchar(500))
insert into #SplitedString
select *
from string_split(#string,' ')
declare #CountOfWords as int
set #CountOfWords = (select count(*) from #SplitedString)
;with cte_Permutations (name, level) as (
select convert(nvarchar(500), name), 1 as level from #SplitedString
union all
select convert(nvarchar(500),splited.name+','+cte_Permutations.name),level+1
from #SplitedString splited ,cte_Permutations
where level < #CountOfWords
)
insert into #Permutations
select name
from cte_Permutations
where level = #CountOfWords
order by name
return
end
select *
From (
select 1 id,'a b c' msg
union all
select 2 id,'d e' msg
) p
cross apply dbo.GeneratePermutations(p.msg)

Is there a way to return multiple results with a subquery?

I have need to return multiple results from a subquery and have been unable to figure it out. The end result will produce the persons name across the vertical axis, various actions based on an action category across the horizontal axis. So the end result looking like:
----------
**NAME CATEGORY 1 CATEGORY 2**
Smith, John Action 1, Action 2 Action 1, Action 2, Action 3
----------
Is there a way to do this in a single query?
select
name,
(select action from actionitemtable where actioncategory = category1 and contact = contactid)
from
contact c
inner join actionitemtable a
on c.contactid = a.contactid
If more than one result is returned in that subquery I would like to be able to display it as a single comma separated string, or list of actions, etc.
Thank you.
Microsoft Sql Server 2005 is being used.
I use a User Defined Function for this task. The udf creates a delimited string with all elements matching the parameters, then you call the udf from your select statement such that you pull a delimited list for each record in the recordset.
CREATE FUNCTION dbo.ud_Concat(#actioncategory int, #contactid int)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE #sOutput VARCHAR(8000)
SET #sOutput = ''
SELECT #sOutput = COALESCE(#sOutput, '') + action + ', '
FROM dbo.actionitemtable
WHERE actioncategory=#actioncategory AND contact=#contact
ORDER BY action
RETURN #sOutput
END
SELECT
name,
dbo.ud_Concat(category1, contactid) as contactList
FROM contact c
INNER JOIN actionitemtable a ON c.contactid = a.contactid
you need to give more info about your table structure and how they join to each other.
here is a generic example about combining multiple rows into a single column:
declare #table table (name varchar(30)
,ID int
,TaskID char(3)
,HoursAssigned int
)
insert into #table values ('John Smith' ,4592 ,'A01' ,40)
insert into #table values ('Matthew Jones',2863 ,'A01' ,20)
insert into #table values ('Jake Adams' ,1182 ,'A01' ,100)
insert into #table values ('Matthew Jones',2863 ,'A02' ,50)
insert into #table values ('Jake Adams' ,2863 ,'A02' ,10)
SELECT DISTINCT
t1.TaskID
,SUBSTRING(
replace(
replace(
(SELECT
t2.Name
FROM #Table AS t2
WHERE t1.TaskID=t2.TaskID
ORDER BY t2.Name
FOR XML PATH(''))
,'</NAME>','')
,'<NAME>',', ')
,3,2000) AS PeopleAssigned
FROM #table AS t1
OUTPUT:
TaskID PeopleAssigned
------ --------------------------------------
A01 Jake Adams, John Smith, Matthew Jones
A02 Jake Adams, Matthew Jones
(2 row(s) affected)
This is pretty abstract and complex. My initial reaction was "pivot query", but the more I looked at it (and at the earlier responses) the more I thought: Can you pass this one off to the application team? You return the "base", and they write and apply the procedural code that makes this kind of problem a snap. Sure, you can squeeze it in to SQL, but that doesn't make it the right place to do the work.
According to your query try this:
SELECT [Name],
STUFF(
(
SELECT ' ,' + [Action]
FROM [AactionItemTable]
WHERE [ActionCategory] = category1
AND [Contact] = contactid
FOR XML PATH('')
), 1, 2, ''
) AS [AdditionalData]
FROM [Contact] C
INNER JOIN [ActionItemTable] A
ON C.[ContactId] = A.[ContactId]
Guess this is the simplest way to do what you want.
EDIT: if there is no action in the subquery found, the [AdditionalData] result will be NULL.
You will probably have to create a custom aggregate function. Microsoft has a knowledge base article with sample code here.
If you don't mind using cursors, you can write your own function to do this. Here's an example that will work on the Adventureworks sample DB:
CREATE FUNCTION CommaFunctionSample
(
#SalesOrderID int
)
RETURNS varchar(max)
AS
BEGIN
DECLARE OrderDetailCursor CURSOR LOCAL FAST_FORWARD
FOR
SELECT SalesOrderDetailID
FROM Sales.SalesOrderDetail
WHERE SalesOrderID = #SalesOrderID
DECLARE #SalesOrderDetailID INT
OPEN OrderDetailCursor
FETCH NEXT FROM OrderDetailCursor INTO #SalesOrderDetailID
DECLARE #Buffer varchar(max)
WHILE ##FETCH_STATUS = 0
BEGIN
IF #Buffer IS NOT NULL SET #Buffer = #Buffer + ','
ELSE SET #Buffer = ''
SET #Buffer = #Buffer + CAST(#SalesOrderDetailID AS varchar(12))
FETCH NEXT FROM OrderDetailCursor INTO #SalesOrderDetailID
END
CLOSE OrderDetailCursor
DEALLOCATE OrderDetailCursor
RETURN #Buffer
END
This is how such a function would appear in a select query:
SELECT AccountNumber, dbo.CommaFunctionSample(SalesOrderID)
FROM Sales.SalesOrderHeader