SQL Server - Dynamic Pivot using existing table - sql-server

I have the following query
SELECT CONCAT(RIGHT('00' + CONVERT(VARCHAR(2),Procedures.SeriesNum),2),'-',RIGHT('0000' + CONVERT(VARCHAR(4),Procedures.ProcNum),4)) AS 'Procedure',
Procedures.Description,
Procedures.CurrentRev,
Procedures.DayToDayLevel,
Procedures.MaxLevel,
Users.Username,
UsersProcedures.RevTrained,
UsersProcedures.LevelTrained
FROM Procedures
CROSS JOIN Users
LEFT JOIN UsersProcedures ON UsersProcedures.Username = Users.Username AND Procedures.SeriesNum = UsersProcedures.SeriesNum AND Procedures.ProcNum = UsersProcedures.ProcNum
Which returns the following results:
However I would like to use a Pivot so that the info for each user (Specifically The Level they were trained at is shown as a value in a column (With the column title being that of the username).
I would like to ignore the CurrentRev, and instead have 1 row for each Revision for each procedure (if no users have been trained to the current revision - whether users have been trained to any previous revisions on that procedure or not, that should be there as an empty row), like below:
I presume I need to use a pivot, although i've never attempted it before, plus the examples i've seen on the web seem to use a static list for the pivoted columns, whereas I want to use all records in the Users table.
I should point out, when it comes to displaying the results in a datagrid, it'll be coloured for clarity.
How would I go about attempting this?
UPDATE: I've got as far as this query
SELECT *
FROM (SELECT CONCAT(RIGHT('00' + CONVERT(VARCHAR(2),Procedures.SeriesNum),2),'-',RIGHT('0000' + CONVERT(VARCHAR(4),Procedures.ProcNum),4)) AS 'Procedure',
Procedures.Description,
Procedures.CurrentRev,
UsersProcedures.RevTrained,
Users.Username,
UsersProcedures.LevelTrained FROM Procedures CROSS JOIN Users LEFT JOIN UsersProcedures ON UsersProcedures.Username = Users.Username AND Procedures.SeriesNum = UsersProcedures.SeriesNum AND Procedures.ProcNum = UsersProcedures.ProcNum) AS Procs
PIVOT
(
MAX(LevelTrained)
FOR Procs.Username
IN(User1,User2,User3)
) AS PivotTable
Which gives me this:
However i'd still like to group the results further similar to my 2nd screenshot. i.e. if at least 1 user has already been trained on a particular revision, then don't show another empty row for that revision as well.
Also if RevTrained is NULL, then show CurrentRev in its place.

OK, here's the finished solution, using a dynamic query to get all the users, and an IF statement to solve the above problems:
If at least 1 user has already been trained on a particular revision, then don't show another empty row for that revision as well.
Also if RevTrained is NULL, then show CurrentRev in its place.
DECLARE #users AS VARCHAR(MAX),#query AS VARCHAR(MAX)
SELECT #users = STUFF((SELECT ',' + CONCAT('[',Username,']') FROM Users FOR XML PATH(''),TYPE).value('.', 'VARCHAR(MAX)'),1,1,'')
SET #query = 'SELECT *
FROM (SELECT CONCAT(RIGHT(''00'' + CONVERT(VARCHAR(2),Procedures.SeriesNum),2),''-'',RIGHT(''0000'' + CONVERT(VARCHAR(4),Procedures.ProcNum),4)) AS ''Procedure Number'',
CASE WHEN UsersProcedures.RevTrained IS NULL THEN Procedures.CurrentRev ELSE UsersProcedures.RevTrained END AS ''Revision'',
Procedures.Description,
Procedures.DayToDayLevel AS ''Maxmimum Training Level'',
Procedures.DayToDayLevel AS ''Training level for Day to Day Usage'',
Users.Username,
UsersProcedures.LevelTrained FROM Procedures CROSS JOIN Users LEFT JOIN UsersProcedures ON UsersProcedures.Username = Users.Username AND Procedures.SeriesNum = UsersProcedures.SeriesNum AND Procedures.ProcNum = UsersProcedures.ProcNum) AS Procs
PIVOT(
MAX(LevelTrained)
FOR Procs.Username
IN('+#users+')) AS PivotTable'
execute(#query);

Related

SQL-Server join issue when filtering on a freetext (vchar) column

I am stuck with a SQL Server view I am trying to create. The view returns a list of resources that are assigned to a project along with a few other details such as contract information.
I'm having issues with the resource_contracts table though because I'm stuck dealing with what is essentially a free-text field.
SELECT DISTINCT
CONCAT(RTRIM(res.first_name),' ', RTRIM(res.surname)) AS fullname ,
res.main_res_id ,
res.resource_id ,
res.resource_typ ,
res.status ,
rel.rel_value ,
asn.booking_project AS project ,
asn.booking_project_descr AS project_descr ,
asn.assignment_position AS position ,
asn.date_from AS commencement_date ,
DATEADD(DAY,1,asn.date_to) AS end_date ,
con.comment_fx
FROM resourcees res
INNER JOIN resource_relations rel
ON
res.main_res_id = rel.resource_id
AND rel.date_to >= CAST(CURRENT_TIMESTAMP AS DATE)
AND res.client = rel.client
LEFT OUTER JOIN resource_relations cc
ON
res.client = cc.client
AND res.resource_id = cc.resource_id
AND cc.rel_attr_id = 'C1'
AND res.date_to BETWEEN cc.date_from AND cc.date_to
AND cc.status = 'N'
INNER JOIN relation_values ar2
ON
cc.rel_value = ar2.dim_value
AND ar2.client = res.client
INNER JOIN assignments asn
ON
res.main_res_id = asn.resource_id
LEFT OUTER JOIN resource_contracts con
ON
con.dim_value = res.main_res_id
AND res.client = con.client
AND con.comment_fx LIKE '%CONAU%'
AND con.date_to_fx >= asn.date_to
WHERE
asn.booking_project = '123456'
ORDER BY
fullname
I guess the above looks fairly large. It's the last join causing the issue for reference.
The resource_contracts table contacts three columns. I hate this setup, but it's outside of my control unfortunately.
date_from_fx = DATETIME
date_to_fx = DATETIME
comment_fx = VCHAR 255
It's used to record contracts date from and date to, and a free text field that could contain anything annoyingly. Sample values might be "CONAU SPP" or "CONSG ABC" etc..
I'm stuck on the comment_fx field above however.
I specifically want to see contracts containing CONAU, or return a NULL value if they do not have one that meets the date require, or do not have a row at all. Unfortunately this logic is getting mixed up any other contract they have such as "CONSG ABC"
No matter what join I apply, I either can see all the resources with the contract required, or duplicate rows with null values and a mix of the non-applicable contracts. I guess I am missing something simple
Ultimately I need to produce a list of resources that are assigned to a project, but do not have the required contract (CONAU), that list will trigger another process that I've already sorted out.
Updated:
Let me show you the data that gets returned if we removed the resource_contracts table causing my issues:
Data result
Apologies, I couldn't format the table into something that resembled a table to paste here.
Here is the data from the contracts table:
contracts table
There's kind of multiple things I want to do here but I'll simplify it into a single one.
I'm trying to send a parameter to the query, 'CONAU' for example. So it will return all resources that do NOT have a valid row containing the string CONAU.
Problem is, they might have other rows like CONSG, or no rows at all.
During my attempts I would often get the wrong rows to show or when using ISNULL in the SELECT part, I would get null rows and duplicated data.
The conditions could be inverted but I'm trying to learn this myself.
SQL fiddle too: http://sqlfiddle.com/#!18/7558f/2
WHERE
asn.booking_project = '123456'
AND con.comment_fx LIKE '%CONAU%'
To the trailing end.

SQL Server 2014 get rows through two relational tables to separate tables using id from a main table

This is hard for me to explain so please feel free to let me know if further clarification is needed. I have 5 tables:
Artwork {artwork_id, artwork_title, artwork_artist_id}
Artwork_Insurance {artwork_id, insurance_id}
Insurance {insurance_id, insurance_name}
Artwork_Exhibit {artwork_id, exhibit_id}
Exhibit {exhibit_id, exhibit_name, exhibit_start_date, exhibit_end_date}
There can be several insurances per artwork, however there can only be one active exhibit per each artwork (determined if the current date is between the start & end dates of the exhibit). My question is how do I retrieve all the artwork rows (with all the artwork table's columns), all the insurances applied to that artwork (values in insurance_name column for that artwork comma separated) as well as the exhibit_name for that artwork if one is currently active otherwise a null value is returned.
I'm pretty limited in my SQL knowledge and much of what I'm hoping to achieve isn't flushed out but here's what I have so far:
select distinct
dbo.Artwork.artwork_id,
dbo.Artwork.artwork_title,
dbo.Artwork.artwork_artist_id,
dbo.Insurance.insurance_name,
dbo.Exhibit.exhibit_name
from
dbo.Artwork_Insurance
left join
dbo.Artwork on dbo.Artwork_Insurance.artwork_id = dbo.Artwork.artwork_id
left join
dbo.Insurance on dbo.Insurance.insurance_id = dbo.Artwork_Insurance.insurance_id
left join
dbo.Artwork_Exhibit on dbo.Artwork_Exhibit.artwork_id = dbo.Artwork.artwork_id
left join
dbo.Exhibit on dbo.Exhibit.exhibit_id = dbo.Artwork_Exhibit.exhibit_id;
However not only does this not return the same number of rows as running select * from dbo.Artwork but the old exhibits aren't filtered out and the insurance names aren't comma separated in the same cell together. I've done some reading on left joins but I don't think I'm using them correctly for what I'm after. I've been working on this for days and could really use some help. Any help appreciated. Thanks!
First of all get comma seperated insurances. It can be done in many ways. I have used for xml. But there other ways using pivot or group + case. And then join with other tables. I have not checked query, but it should work correctly and return your expected output
with cte_insurance as (
select
distinct Artwork_Insurance.artwork_id, stuff((
select
', ' + Insurance.insurance_name
from
Insurance
where
Insurance.insurance_id = Artwork_Insurance.insurance_id
), 1, 2, '') insurance_name
from
Artwork_Insurance
)
select
Artwork.artwork_id, Artwork.artwork_title, Artwork.artwork_artist_id
, cte_insurance.insurance_name, Exhibit.exhibit_name
from
Artwork
left join cte_insurance on Artwork.artwork_id = cte_insurance.artwork_id
left join Artwork_Exhibit on Artwork.artwork_id = Artwork_Exhibit.artwork_id
left join Exhibit
on Artwork_Exhibit.exhibit_id = Exhibit.exhibit_id
and getdate() between Exhibit.exhibit_start_date and Exhibit.exhibit_end_date

STUFF SQL Query in NHibernate, basically trying to replicate MySQLGroupConcat

Quick info on what I am trying to accomplish. I have some history tables and am joining the data into a single row for each history item. The issue I am having is trying to replicate a STUFF(query,1,1,'') in NHibernate. I have all my relationships setup and working and the query with just the joins works fine, just cant figure out how to implement the STUFF with a query into it.
This is the entire query:
SELECT h.*, u.FirstName, u.LastName, eh.*,
STUFF((
SELECT CONCAT(c.Name, ' - ')
FROM SubHistory sh
LEFT JOIN Cust c ON c.CustID = sh.SubCustId
WHERE h.Id = sh.HistoryId
FOR XML PATH ('')), 1, 1, ''
) AS Subs
FROM History h
LEFT JOIN EmailHistory eh ON eh.HistoryId = h.Id
LEFT JOIN Usr u ON u.UsrID = h.UserId
The Result I need is (columns):
H.ID - H.ReportID - H.UserID - (Concatenated Subs) - u.FirstName - u.LastName - eh.Email
I can do this without the STUFF in Nhibernate like so:
IList<History> hist = session.QueryOver<History>(() => historyAlias)
.Left.JoinAlias(() => historyAlias.User, () => usrAlias)
.Left.JoinAlias(() => historyAlias.Email, () => emailAlias)
.List();
I followed the tutorial here http://blog.andrewawhitaker.com/blog/2014/08/15/queryover-series-part-7-using-sql-functions/ for creating the STUFF function, but I do not think it was created with the idea of using it the way I want to and I have been unable to successfully make it work.
So, my question can and how can I implement STUFF the way I want to? Or is there a better approach to getting the info I want?
Database Example for reference:
History Table
ID - ReportID - Name - UserID
SubHistory Table - (Can be many)
ID - HistoryID - SubInfo
UserTable
ID - FirstName - LastName
Cust Table
ID - CustInfo
EmailHistory Table - (Can only have one)
ID - HistoryID - Email
I imagine with enough tinkering, you can eventually get it to work with NHibernate calling STUFF, but it might take a very long time. There might be a faster / easier solution: Create a view called HistoryView that essentially does the query you have at the top. Then, create a new C# class also called HistoryView that has properties that correspond to your view's columns. These HistoryView objects will be readonly, but it will accomplish the goal you're after.
So I ended up actually using a stored procedure call for this. I essentially did what Aron suggested with a View but as a stored proc instead, and created a new object to house the stored procedure returned rows.
How to call a SP in NHibernate:
http://nhibernate.info/doc/nhibernate-reference/querysql.html#sp_query

NHibernate Criteria SQL Inner Join on Sub Select Same Table

I can't for the life of me figure out how to translate the following SQL query using NHibernate's Criteria API:
SELECT r.* from ContentItemVersionRecords as r
INNER JOIN (
SELECT ContentItemId as CID, Max(Number) as [Version]
FROM ContentItemVersionRecords
GROUP BY ContentItemId
) AS l
ON r.ContentItemId = l.CID and r.Number = l.[Version]
WHERE Latest = 0 and Published = 0
The table looks like this:
The result of the SQL query above will return the highlighted records.
The idea is to select the latest version of content items, so I basically need to group by ContentItemId and get the record with the highest Number.
So the result will look like this:
I started out with a detached criteria, but I am clueless as to how to use it in the criteria:
// Sub select for the inner join:
var innerJoin = DetachedCriteria.For<ContentItemVersionRecord>()
.SetProjection(Projections.ProjectionList()
.Add(Projections.GroupProperty("ContentItemId"), "CID")
.Add(Projections.Max("Number"), "Version"));
// What next?
var criteria = session.CreateCriteria<ContentItemVersionRecord>();
Please note that I have to use the Criteria API - I can't use LINQ, HQL or SQL.
Is this at all possible with the Criteria API?
UPDATE: I just came across this post which looks very similar to my question. However, when I apply that as follows:
var criteria = session
.CreateCriteria<ContentItemVersionRecord>()
.SetProjection(
Projections.ProjectionList()
.Add(Projections.GroupProperty("ContentItemId"))
.Add(Projections.Max("Number")))
.SetResultTransformer(Transformers.AliasToBean<ContentItemVersionRecord>());
I get 2 results, which looks promising, but all of the integer properties are 0:
UPDATE 2: I found out that if I supply aliases, it will work (meaning I will get a list of ContentItemVersionRecords with populated objects):
var criteria = session
.CreateCriteria<ContentItemVersionRecord>()
.SetProjection(
Projections.ProjectionList()
.Add(Projections.Max("Id"), "Id")
.Add(Projections.GroupProperty("ContentItemId"), "ContentItemId")
.Add(Projections.Max("Number"), "Number"))
.SetResultTransformer(Transformers.AliasToBean<ContentItemVersionRecord>());
However, I can't use the projected values as the end result - I need to use these results as some sort of input into the outer query, e.g.
SELECT * FROM ContentItemVersionRecord WHERE Id IN ('list of record ids as a result from the projection / subquery / inner join')
But that won't work, since the projection returns 3 scalar values (Id, ContentItemId and Number). If it would just return "Id", then it might work. But I need the other two projections to group by ContentItemId and order by Max("Number").
OK, so in a nutshell, you need to unwind that nested query, and do a group by with a having clause, which is pretty much a where on aggregated values, as in the following HQL:
SELECT civ.ContentItem.Id, MAX(civ.Number) AS VersionNumber
FROM ContentItemVersionRecord civ
JOIN ContentItem ci
GROUP BY civ.ContentItem.Id " +
HAVING MAX(civ.Latest) = 0 AND MAX(civ.Published) = 0
This gives you, for each deleted content items (those have all their latest and published flags to zero on all their content item version records), the maximum version number, i.e. the latest version of each deleted content item.

Updating column based on three tables

I know it's very unprofessional, but it's our business system so I can't change it.
I have three tables: t_posList, t_url, t_type. The table t_posList has a column named URL which is also stored in the table t_url (the ID of the table t_url is not saved in t_posList so I have to find it like posList.Url = t_url.Url).
The column t_posList.status of every data row should be updated to 'non-customer' (it will be a status id but lets keep it simple) if: the ID of t_url can NOT be found in t_type.url_id.
So the query has like two steps: first I have to get all of the data rows where t_posList.Url = t_url.Url. After this I have to check which ID's of the found t_url rows can NOT be found in t_type.url_id.
I really hope you know what I mean. Because our system is very unprofessional and my SQL knowledge is not that good I'm not able to make this query.
EDIT: I tried this:
UPDATE t_poslist SET status = (
SELECT 'non-customer'
FROM t_url, t_type
WHERE url in
(select url from t_url
LEFT JOIN t_type ON t_url.ID = t_type.url_id
WHERE t_type.url_id is null)
)
What about this?
UPDATE p
SET status = 'non-customer'
FROM t_poslist p
INNER JOIN t_url u ON u.url = p.url
WHERE NOT EXISTS
(
SELECT * FROM t_type t WHERE t.url_id = u.ID
)

Resources