T-SQL: Query across tables converting rows to columns? - sql-server

SQL Server 2012.
Straight to the point, this is what I'm trying to do:
User table
id username fullname
---- -------- ----------------------
1 test0001 Test User #1
2 test0002 Test User #2
3 test0003 Test User #3
4 test0004 Test User #4
Flags table
id name description
--- ---------- -------------------------------------------
1 isActive true if user is currently active
2 isAdmin true if user can do Admin things
3 canEdit true if user can can edit
UserFlags table
user flag
---- ----
1 1
1 2
1 3
2 1
2 3
3 1
(user = FK to user.id, flag = FK to flag.id)
Desired result
userId username isActive isAdmin canEdit
------ -------- -------- ------- -------
1 test0001 1 1 1
2 test0002 1 0 1
3 test0003 1 0 0
4 test0004 0 0 0
In short I want to convert each flag in the flags table into a column with the name field used as the column header. Then I want a row for each user, with a boolean in each column indicating whether they have that particular flag.
This needs to be able to adapt - e.g. if another flag is added, the result of the query should have another column with that flag's name as its title.
I'd prefer to do this in a view, but I'd be OK with a table-valued function.
I haven't done anything like this before so I'm not even sure where to start - I can do a full join on the tables and end up with a row per user per flag, but I then want to fold that all down into a single row per user.
EDIT One of the key points is "able to adapt" - the best scenario would be a query that automatically pulls in all currently defined flags from the flags table when building the response. Having to edit the query isn't necessarily bad, but consider the instance where an admin is allowed to add a new flag to the system. It's easy to INSERT a new flag, it's much harder to autonomously edit a stored query to reflect that. If that's simply not possible to do, then an explanation as to why would be helpful. Thanks!

You can use pivot as below:
Select * from (
Select u.id, u.username, f.[name] from #user u
left join #userflags uf
on uf.[user] = u.id
left join #flags f
on uf.flag = f.id
) a
pivot (count([name]) for [name] in ([isActive],[isAdmin],[canEdit])) p
Output as below:
+----+----------+----------+---------+---------+
| id | username | isActive | isAdmin | canEdit |
+----+----------+----------+---------+---------+
| 1 | test0001 | 1 | 1 | 1 |
| 2 | test0002 | 1 | 0 | 1 |
| 3 | test0003 | 1 | 0 | 0 |
| 4 | test0004 | 0 | 0 | 0 |
+----+----------+----------+---------+---------+
Demo
Updated my query if you have dynamic list of flags as below:
Declare #cols1 varchar(max)
Declare #query nvarchar(max)
Select #cols1 = stuff((select distinct ','+QuoteName([name]) from #flags for xml path('')),1,1,'')
Select #query = ' Select * from (
Select u.id, u.username, f.[name] from #user u
left join #userflags uf
on uf.[user] = u.id
left join #flags f
on uf.flag = f.id
) a
pivot (count([name]) for [name] in (' + #cols1 + ')) p '
Exec sp_executesql #query

If you dont like pivot function like me; you can use the SUM IIF method like this
SELECT u.id
, username
, SUM(IIF(flag = 1, 1, 0)) AS isActive
, SUM(IIF(flag = 2, 1, 0)) AS isAdmin
, SUM(IIF(flag = 3, 1, 0)) AS canEdit
FROM User u
LEFT JOIN UserFlags uf ON uf.[user] = u.id
GROUP BY u.id
, username

You can do this with a case statement and subquery or with a pivot. They both essentially do the same thing. This includes the DDL for the tables I made for testing.
declare #user_tbl table (
id int,
username nvarchar(max)
)
INSERT #user_tbl VALUES
(1,'test0001'),
(2,'test0002'),
(3,'test0003'),
(4,'test0004')
declare #userflags_tbl table(
userid int,
flag int
)
INSERT #userflags_tbl VALUES
(1,1),
(1,2),
(1,3),
(2,1),
(2,3),
(3,1)
declare #flags_tbl table(
id int
)
INSERT #flags_tbl VALUES
(1),
(2),
(3)
SELECT
userid,
username,
MAX(isActive) AS isActive,
MAX(isAdmin) AS isAdmin,
MAX(canEdit) AS canEdit
FROM (
SELECT
user_tbl.id AS userid,
user_tbl.username,
CASE
WHEN userflags_tbl.flag = 1 THEN 1
ELSE 0
END AS isActive,
CASE
WHEN userflags_tbl.flag = 2 THEN 1
ELSE 0
END AS isAdmin,
CASE
WHEN userflags_tbl.flag = 3 THEN 1
ELSE 0
END AS canEdit
FROM #user_tbl user_tbl
LEFT JOIN #userflags_tbl userflags_tbl ON
user_tbl.id = userflags_tbl.userid
LEFT JOIN #flags_tbl flags_tbl ON
userflags_tbl.flag = flags_tbl.id
)tbl
GROUP BY
userid,
username
SELECT
userid,
username,
ISNULL([1],0) AS isActive,
REPLACE(ISNULL([2],0),2,1) AS isAdmin,
REPLACE(ISNULL([3],0),3,1) AS canEdit
FROM (
SELECT
user_tbl.id AS userid,
user_tbl.username,
userflags_tbl.flag
FROM #user_tbl user_tbl
JOIN #userflags_tbl userflags_tbl ON
user_tbl.id = userflags_tbl.userid
JOIN #flags_tbl flags_tbl ON
userflags_tbl.flag = flags_tbl.id
) tbl
PIVOT (
MAX(flag)
FOR flag IN ([1],[2],[3])
) AS PivotTable

Related

what to use instead of union to join same results based on two where clauses

I have two queries that work as expected for example
Query 1
select Name,ID,Product,Question
from table 1
where Id= 9 and ProductID=30628
table output
Name | ID | Product | QUestion
0659e103-b33d-4603 |12356|Apple | is it picked up?
0659e103-b33d-4603 |12456|Apple |Available in store?
0659e103-b33d-4603 |12458|Apple |confirm order?
query 2
select Name,ID,Product,Question
from table 1
where Id= 9 and TypeID=2
table output
Name | ID | Product | QUestion
0659e103-b33d-4603 |12347|Apple | Problem at store?
as you can see in query 1 i use a ProductID and in query 2 i use a TypeID these two values gives me different out puts
so i used a union to join both as follows
select Name,ID,Product,Question
from table 1
where Id= 9 and ProductID=30628
union
select Name,ID,Product,Question
from table 1
where Id= 9 and TypeID=2
which i get the desired output
Name | ID | Product | QUestion
0659e103-b33d-4603 |12356|Apple | is it picked up?
0659e103-b33d-4603 |12456|Apple |Available in store?
0659e103-b33d-4603 |12458|Apple |confirm order?
0659e103-b33d-4603 |12347|Apple | Problem at store?
is their a better way to do this because my query will grow and i would not like to repeat the same thing over again. is their a better way to optimize the query?
NOte i can not use ProductID and TypeID on the same line because they do not result in accurate results
You could use OR since you are querying the same table.
SELECT Name
,ID
,Product
,Question
FROM TABLE1
WHERE (
Id = 9
AND ProductID = 30628
)
OR (
Id = 9
AND TypeID = 2
)
If you have a growing number of OR conditions you could use a temp table/variable and inner join to profit from a set based operation.
The inner join will only return matching rows.
CREATE TABLE #SomeTable(Id INT NOT NULL, ProductID INT NULL, TypeID INT NULL)
-- Insert all conditions you want to match.
INSERT INTO #SomeTable(Id, ProductID, TypeId)
VALUES (9, 30628, NULL)
, (9, NULL, 2)
SELECT Name
,ID
,Product
,Question
FROM TABLE1 x
INNER JOIN #SomeTable y ON
x.ID = y.ID -- Since ID is Not null in the temp table
AND (y.ProductID IS NULL OR y.ProductID = x.ProductID)
AND (y.TypeID IS NULL OR y.TypeID = x.TypeID)
You can use cas-when clause with a self join.
Case-when something like this:
SELECT t1_2.Name,
t1_2.ID,
t1_2.Product,
t1_2.Question,
(CASE WHEN (t1.Id= 9 and t1.ProductID=30628) THEN ID
WHEN (t1.Id= 9 and t1.TypeID=2) THEN ID
ELSE NULL) AS IDcalc
FROM table_1 t1 LEFT JOIN table_1 t1_2
ON t1.ID = t1_2.ID
WHERE (CASE WHEN (t1.Id= 9 and t1.ProductID=30628) THEN ID
WHEN (t1.Id= 9 and t1.TypeID=2) THEN ID
ELSE NULL) IS NOT NULL
You can use any table in the join.
In comparison of query performance the OR is much better until you have only one table, if you have more tables, then you should use temp table or case-when in your query.

select resultset of counts by array param in postgres

I've been searching for this and it seems like it should be something simple, but apparently not so much. I want to return a resultSet within PostgreSQL 9.4.x using an array parameter so:
| id | count |
--------------
| 1 | 22 |
--------------
| 2 | 14 |
--------------
| 14 | 3 |
where I'm submitting a parameter of {'1','2','14'}.
Using something (clearly not) like:
SELECT id, count(a.*)
FROM tablename a
WHERE a.id::int IN array('{1,2,14}'::int);
I want to test it first of course, and then write it as a storedProc (function) to make this simple.
Forget it, here is the answer:
SELECT a.id,
COUNT(a.id)
FROM tableName a
WHERE a.id IN
(SELECT b.id
FROM tableName b
WHERE b.id = ANY('{1,2,14}'::int[])
)
GROUP BY a.id;
You can simplify to:
SELECT id, count(*) AS ct
FROM tbl
WHERE id = ANY('{1,2,14}'::int[])
GROUP BY 1;
More:
Check if value exists in Postgres array
To include IDs from the input array that are not found I suggest unnest() followed by a LEFT JOIN:
SELECT id, count(t.id) AS ct
FROM unnest('{1,2,14}'::int[]) id
LEFT JOIN tbl t USING (id)
GROUP BY 1;
Related:
Preserve all elements of an array while (left) joining to a table
If there can be NULL values in the array parameter as well as in the id column (which would be an odd design), you'd need (slower!) NULL-safe comparison:
SELECT id, count(t.id) AS ct
FROM unnest('{1,2,14}'::int[]) id
LEFT JOIN tbl t ON t.id IS NOT DISTINCT FROM id.id
GROUP BY 1;

Select N rows avoiding duplicates on a non-key, non-index field

Using T-SQL, how can I select n rows of a non-key, non-index column and avoid duplicate results?
Example table:
ID_ | state | customer | memo
------------------------------------------
1 | abc | 123 | memo text xyz
2 | abc | 123 | memo text abc
3 | abc | 456 | memo text def
4 | abc | 456 | memo text rew
5 | abc | 789 | memo text yte
6 | def | 123 | memo text hrd
7 | def | 432 | memo text dfg
I want to select, say, 2 memos for state 'abc' but the returned memos should not be for the same customer.
memo
----
memo text xyz
memo text def
PS: The only select condition available is state (eg: where state = 'abc')
I have managed to do this in a very inefficient way
SELECT top 2 MAX(memo)
FROM table
WHERE state = 'abc'
GROUP BY customer
This works fine for small sample size, but the production table has over 1 billion rows.
You can try using the following query, in your actual database size. Not sure of the performance in database table with billion rows. So you can do the test yourself.
SELECT memo
FROM (SELECT memo,
ROW_NUMBER() OVER (PARTITION BY customer ORDER BY (SELECT 0)) AS RN
FROM table1 WHERE state = 'abc') T
WHERE RN = 1
You can check the SQL FIDDLE
EDIT: Adding a non-clustered index on state and customer including memo will tremendously improve the performance.
CREATE NONCLUSTERED INDEX [custom_index] ON table
(
[state] ASC,
[customer] ASC
)
INCLUDE ( [memo]) WITH (SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [DATA]
A way to get that n distinct value for state/customer is to get an ID for every group
SELECT MIN(ID_) ID
FROM Table1
GROUP BY State, customer
(MIN can be substituted by MAX, it's just a way to get one of the values)
then JOIN that to the table adding the other condition
WITH getID AS (
SELECT MIN(ID_) ID
FROM Table1
GROUP BY State, customer
)
SELECT TOP 2
t.ID_, t.State, t.Customer, t.memo
FROM table1 t
INNER JOIN getID g ON t.ID_ = g.ID
WHERE t.state = 'abc'
SQLFiddle demo
if your version of SQLServer doesn't support WITH the CTE can become a subquery
SELECT TOP 2
t.ID_, t.State, t.Customer, t.memo
FROM table1 t
INNER JOIN (SELECT MIN(ID_) ID
FROM Table1
GROUP BY State, customer
) g ON t.ID_ = g.ID
WHERE t.state = 'abc'
Another way is to use CROSS APPLY to get the distinct ID
SELECT TOP 2
t.ID_, t.State, t.Customer, t.memo
FROM table1 t
CROSS APPLY (SELECT TOP 1
ID_
FROM table1 t1
WHERE t1.State = t.State AND t1.Customer = t.Customer) c
WHERE t.state = 'abc'
AND c.ID_ = t.ID_;
SQLFiddle demo

MSSQL join tables w bit

I have three tables :
1) UserTable
UserId UserName
1 Mike
2 John
3 Jennifer
2) FormName
fID fName
1 edit
2 cafe
3 backoffice
3)User to form
fId UserId Allowed(bit)
1 1 0
2 1 1
3 1 1
2 2 1
3 2 0
The first table is the user table with user informations.
The second table is the form table where it stores form names of application
The third table is user level table where it says which user is allowed to open which form .
I want to create sql query where I can see all information in a single table like :
UserId USerName Edit Cafe BackOffice
1 mike 0 1 1
2 john 1 1 0
I think it is possbile with SQL Fiddle and Pivot but I am having hard time to figure the right code out .
You can use the PIVOT function, but you have to cast the allowed column to something other than a bit datatype. For example, the below casts it to an int:
select userid, username,
coalesce(Edit, 0) Edit,
coalesce(Cafe, 0) Cafe,
coalesce(BackOffice, 0) BackOffice
from
(
select u.userid,
u.username,
f.fname,
cast(allowed as int) allowed
from usertable u
inner join user_form uf
on u.userid = uf.userid
inner join formname f
on uf.fid = f.fid
) d
pivot
(
max(allowed)
for fname in (Edit, Cafe, BackOffice)
) piv;
See SQL Fiddle with Demo

having a count column from a subquery that's querying the same table

Lets say i have the table:
accounts: id, id2, id3, custId, option1, option2, type
and this same table has ALL accounts with some being "parent" accounts (which can be "solo" accounts with no children) and some being "children" accounts (bad design i know, but that's what I'm working with).
The distinct/composite key for each account is id + id2 + id3 + option1 + option2 + custId.
I want to query a list of "parent" or "solo" accounts with a specific custId and type, which is easily done by:
Select *
From accounts
Where custId = 1 And type = 'foo'
and (option1 = 'solo' Or option2 = 0)
where 'solo' means it's a solo account and has no children and 0 means that's its the first of a line of accounts and therefore its parent.
Then I want to obtain the count of "children" associated to every parent obtained by the result set of the above query. Obviously "solo" accounts won't have children.
For example obtaining the "children" count from a specific "parent" account would be something like (let's say I'm looking for the children of account with id=1, id2=1, id3=1:
Select Count(*)
From accounts
Where id = 1 And id2 = 1 And id3 = 1 And custId = 1
And option1 != 'solo' And option2 != 0)
So how can I combine the two queries in order to obtain the result set of the first with counts for each of its rows?
Example
populating the table we could have:
id id2 id3 custId option1 option2 type
------------------------------------------------------------
1 1 1 1 solo 9 foo
2 2 2 1 solo 9 foo
3 4 4 1 NULL 0 foo
3 4 4 1 NULL 1 foo
3 4 4 1 NULL 2 foo
I want a result set like this:
id id2 id3 custId option1 option2 type children
-------------------------------------------------------------------------
1 1 1 1 solo 9 foo 0
2 2 2 1 solo 9 foo 0
3 4 4 1 NULL 0 foo 2
Basically I would want something like this (i know this is wrong)
Select *,
(Select count(*) from accounts
Where option1 != 'solo' And option2 != 0
And --all 3 ids and custId equal to the current row
) --this is the part i don't know how to do
From accounts
Where custId = 1 And Type = 'foo' And (option1 = 'solo' Or option2 = 0)
My brain is running circles around how to do this. Thank you for your help.
This works for your data:
select a.*
, children = isnull(c.children, 0)
from accounts a
outer apply (select children = count(1)
from accounts c
where a.option1 is null
and a.id = c.id
and a.id2 = c.id2
and a.id3 = c.id3
and a.custId = c.custId
and c.option2 <> 0) c
where (a.option1 = 'solo' or a.option2 = 0)
and a.type = 'foo'
SQL Fiddle with demo.
Edit after comment:
OK, so the main part of the query is the WHERE clause - this determines the three rows that are returned.
Once we have these rows, we need to work out how many children there are - this is what the OUTER APPLY achieves.
Basically, for each non-solo row, we match this to any child rows (i.e. c.option2 <> 0) and get a count of these rows, explicitly returning 0 when there are now matches or it is a 'solo' parent row.
Since we are only matching against non-solo parent rows, we filter these with a.option1 is null, i.e. checking the parent row option1 values before checking for any matches. Due to the nature of the data, c.option1 is null would be fine, too, as both parent and child non-solo rows have null option1 values.
count(1) doesn't mean anything in particular; it's just an arbitrary value to be counted for each row, in this case a constant. You could just as easily use count(*). Back in the day this might have made a difference in query processing but with modern RDBMS optimisers you won't see any differences.
Here's an alternate query that does it a slightly different way:
select a.*
, children = isnull(c.children, 0)
from accounts a
left join (select children = count(*)
, id
, id2
, id3
, custId
from accounts
where option1 is null and option2 <> 0
group by id
, id2
, id3
, custId) c
on a.id = c.id
and a.id2 = c.id2
and a.id3 = c.id3
and a.custId = c.custId
where (a.option1 = 'solo' or a.option2 = 0)
and a.type = 'foo'
SQL Fiddle with demo.
So it uses count(*) and a LEFT JOIN instead of count(1) and OUTER APPLY respectively. OUTER APPLY gets applied to every row outside the query; you can use it for functions, but it can help code be more concise, too, like in this case. Just personal preference for me making these choices.

Resources