SQL Server - How to display master details data in columns - sql-server

I have two tables, to be concise let’s call them TableA and TableB. This is the schema:
TableA
ID – int
Name varchar(50)
TableB
ID – int
TableA_Fk – int
Value varchar(50)
Each record in table A can have at most 9 records in table B. I want to be able to retrieve the data in a columnar form:
TableA-Name, TableB-Value1, … TableB-Value9
Is this possible using queries? Thanks!

You could do something like:
SELECT rank() OVER (ORDER BY tableA_FK) as rank, tableA_fk, value
INTO #temp
FROM TableB b
ORDER BY rank
SELECT a.Name,
CASE WHEN t.rank = 1 THEN t.Value ELSE NULL END AS TableB-Value1,
CASE WHEN t.rank = 2 THEN t.Value ELSE NULL END AS TableB-Value2,
CASE WHEN t.rank = 3 THEN t.Value ELSE NULL END AS TableB-Value3,
.... (etc.)
FROM TableA a
INNER JOIN #temp t ON a.Id = t.tableA_fk
You need Sql Server 2005 or up.
Sorry, but I don't have Sql Server (or the time) to test this well. Hope this gives you an idea and helps.

You will require a LEFT JOIN and a PIVOT table

This should do it, in addition to be DBRM independant.
SELECT A.Name
, SUM(CASE WHEN B.Value = 1 THEN 1 ELSE NULL END) AS B_Value_1
, SUM(CASE WHEN B.Value = 2 THEN 2 ELSE NULL END) AS B_Value_2
, SUM(CASE WHEN B.Value = 3 THEN 3 ELSE NULL END) AS B_Value_3
, SUM(CASE WHEN B.Value = 4 THEN 4 ELSE NULL END) AS B_Value_4
, SUM(CASE WHEN B.Value = 5 THEN 5 ELSE NULL END) AS B_Value_5
, SUM(CASE WHEN B.Value = 6 THEN 6 ELSE NULL END) AS B_Value_6
, SUM(CASE WHEN B.Value = 7 THEN 7 ELSE NULL END) AS B_Value_7
, SUM(CASE WHEN B.Value = 8 THEN 8 ELSE NULL END) AS B_Value_8
, SUM(CASE WHEN B.Value = 9 THEN 9 ELSE NULL END) AS B_Value_9
FROM A
INNER JOIN B ON B.TableA_FK = A.ID
GROUP BY A.Name
ORDER BY A.Name

Related

How to pivot table from multiple column to multiple rows

CodeDt CodeHeader Item Qty Type Remark Attachment
LK4-033502 RK-K-LK4-032438 IA01001023 2.00 TPR002 2 1.jpeg
LK4-033502RK RK-K-LK4-032438 IA01001023RK 2.00 NULL IA01001023 NULL
Above is my data from Sql server table (using 2008 R2). I want to make it one row only. Here is my expected result:
CodeDt CodeHeader Item NewItem Qty
LK4-033502 RK-K-LK4-032438 IA01001023 IA01001023RK 2.00
How can I achieve that? Here is the relation:
Row 1 Item = Row 2 Remark,
Row 1 Code DT = Row 2 CodeDt+'RK'
There are several solutions
1) Using JOIN. It assumes that Type field is null for rows with CodeDt+'RK'
select
a.CodeDt, a.CodeHeader, a.Item, b.Item, a.Qty
from
myTable a
join myTable b on a.Item = b.Remark
where
a.Type is not null
2) Conditional aggregation
select
max(case when rn = 1 then CodeDt end)
, CodeHeader
, max(case when rn = 1 then Item end)
, max(case when rn = 2 then Item end)
, max(case when rn = 1 then Qty end)
from (
select
*, rn = row_number() over (partition by CodeHeader order by CodeDt)
from
myTable
) t
group by CodeHeader

Order By with Case Statements Not Working as Expected

I'm trying to sort a report in the order that the end user wants to work it.
First, it should be sorted by the following scenarios:
String_Field is populated, but Comment_Date is null
String_Field is not populated
Both String_Field and Comment_Date are populated
And then it should be sorted by the aging of Task_Date.
I've added the following to my ORDER BY:
(case when max(table1.comment_date) is null then 2 else 1 end) DESC,
(case when table1.string_field is null then 2 else 1 end) ASC,
max(table2.task_date) ASC
And then I added them to my SELECT also:
(case when max(table1.comment_date) is null then 2 else 1 end) as sort1
(case when table1.string_field is null then 2 else 1 end) as sort2
max(table2.task_date) as task_date
What I'm getting, and cannot figure out for the life of me why, is this (edited to show the high/low at each change):
I've tried casting, moving the aggregation around, and adding the values together, and I still end up with those 2s stuck in the middle of some invisible split in the 1s.
If it makes any difference, I come up with the values for string_field via a CTE Union Query, and comment_date is joined to string_field in a second CTE, before the second CTE (table1) is left joined to the table that contains task_date (table2). You can have a string_field without a comment_date, but never a comment_date without a string_field. There must be a task_date for the entry to appear in the report. The report is grouped by string_field.
I'm on SQL Server 2008 R2.
Thanks in advance for your help!
Edited to add Quasi-SQL:
WITH
old_new_string AS (
SELECT
tablea.old_string_field,
tablea.string_field,
'A' as match_type
FROM
tablea
WHERE
tablea.old_string_field is not null
UNION ALL
SELECT
B1.string_field AS old_string_field,
B2.string_field AS string_field,
'B1' as match_type
FROM
tableb B1
INNER JOIN tableb B2 ON B1.string_field <> B2.string_field AND B1.id1 = B2.id1
WHERE
B1.id1 is not null
UNION ALL
SELECT
B1.string_field AS old_string_field,
B2.string_field AS string_field,
'B2' as match_type
FROM
tableb B1
INNER JOIN tableb B2 ON B1.string_field <> B2.string_field AND B1.id1 = B2.id2
WHERE
B1.id1 is not null),
table1 AS (
SELECT TOP 100 PERCENT /* Enables order by to improve join performance */
old_new_string.old_string_field,
old_new_string.string_field,
old_new_string.match_type,
comment_date = (SELECT max(comment_table.comment_date) FROM comment_table WHERE old_new_string.string_field = comment_table.string_field and comment_table.comment_code = '001')
FROM
old_new_string
ORDER BY
old_new_string.old_string_field)
SELECT
tablez.string_field as old_string_field,
max(table2.task_date) as task_date,
min(cast(getdate() - table2.task_date as bigint)) as Aging
table1.string_field as new_string_field,
max(table1.match_type) as match_type,
max(table1.comment_date) as comment_date,
sort_order1 = (case when max(table1.comment_date) is null then 2 else 1 end),
sort_order2 = (case when table1.string_field is null then 2 else 1 end)
FROM
tablez
INNER JOIN table2 ON tablez.string_field = table2.string_field
LEFT JOIN table1 on tablez.string_field = table1.old_string_field
Where
table2.task_date IS NOT NULL
Group By
tablez.string_field, table1.new_string_field
ORDER BY
(case when max(table1.comment_date) is null then 2 else 1 end) DESC,
(case when table1.string_field is null then 2 else 1 end) ASC,
max(table2.task_date) ASC
ORDER BY
case when max(table1.comment_date) is null then 2 else 0 end
+ case when table1.string_field is null then 0 else 1 end DESC
or
ORDER BY CASE
WHEN max(table1.comment_date) is null and table1.string_field is not null
THEN 0
WHEN table1.string_field is null
THEN 1
WHEN max(table1.comment_date) is not null and table1.string_field is not null
THEN 2
ELSE 3 END

Counts on Date Range and Individual Days

I'm having to re-write a project that was done using a combination of SQL queries and Query-of-Queries in ColdFusion. There were dozens of queries referencing the original SQL Query results set, but it wasn't abstracted to work for different events. So I would like to improve it by moving most of the counting into SQL. I got the first 6 counts they need working (not sure if in the most optimal way). But, in addition to those, I need to be able to do a break down not just on the overall date range, but also for each individual day in that date range for the unique counts.
So far the query is:
SELECT Count(CASE
WHEN type IN ( 1, 3, 4, 5, 9 ) THEN barcode
ELSE NULL
END) AS total_scans,
Count(CASE
WHEN type IN ( 2, 8 ) THEN barcode
ELSE NULL
END) AS total_creds,
Count(barcode) AS total_scans,
Count(DISTINCT CASE
WHEN type IN ( 1, 3, 4, 5, 9 ) THEN barcode
ELSE NULL
END) AS unique_scans,
Count(DISTINCT CASE
WHEN type IN ( 2, 8 ) THEN barcode
ELSE NULL
END) AS unique_creds,
Count(DISTINCT barcode) AS unique_scans
FROM (SELECT c.id,
a.barcode,
d.type,
c.location,
Datepart(mm, a.scan_time) AS scan_month,
Datepart(dd, a.scan_time) AS scan_day,
Datepart(hour, a.scan_time) AS scan_hour,
Datepart(minute, a.scan_time) AS scan_min
FROM [scan_11pc_gate_entries] AS a
INNER JOIN scan_units AS b
ON a.scanner = b.id
INNER JOIN scan_gates AS c
ON b.gate = c.id
INNER JOIN [scan_11pc_gate_allbarcodes] AS d
ON a.barcode = d.barcode
WHERE ( c.id IN (SELECT id
FROM scan_gates
WHERE ( event_id = 21 )) )
AND ( a.valid IN ( 1, 8 ) )
AND a.scan_time >= '20110808'
AND a.scan_time <= '20110814') data
Well I see plenty of other room for improvement / optimization here, but to satisfy the immediate requirement:
SELECT d, Count(CASE WHEN type IN ( 1, 3, 4, 5, 9 ) THEN barcode
ELSE NULL END) AS total_scans,
Count(CASE WHEN type IN ( 2, 8 ) THEN barcode
ELSE NULL END) AS total_creds,
Count(barcode) AS total_scans,
Count(DISTINCT CASE WHEN type IN ( 1, 3, 4, 5, 9 ) THEN barcode
ELSE NULL END) AS unique_scans,
Count(DISTINCT CASE WHEN type IN ( 2, 8 ) THEN barcode
ELSE NULL END) AS unique_creds,
Count(DISTINCT barcode) AS unique_scans
FROM (SELECT c.id, -- is this column necessary?
a.barcode,
d.type,
c.location, -- is this column necessary?
d = DATEADD(DAY, DATEDIFF(DAY, 0, a.scan_time), 0)
FROM [scan_11pc_gate_entries] AS a
INNER JOIN scan_units AS b
ON a.scanner = b.id
INNER JOIN scan_gates AS c
ON b.gate = c.id
AND c.event_id = 21 -- join criteria,
-- shouldn't be an extra IN clause
INNER JOIN [scan_11pc_gate_allbarcodes] AS d
ON a.barcode = d.barcode
WHERE ( a.valid IN ( 1, 8 ) )
AND a.scan_time >= '20110808'
AND a.scan_time <= '20110814') data
GROUP BY d
ORDER BY d;
Note that >= and <= is the same as BETWEEN, and unless scan_time is a DATE column (or guaranteed to always be at midnight), the approach you're using isn't safe. Better to say:
AND a.scan_time >= '20110808'
AND a.scan_time < '20110815'
More info here:
What do BETWEEN and the devil have in common?
EDIT
Understanding that this is based on the information I've pieced together from comments and questions, which is far from a complete picture of your environment and workload, an indexed view you may find useful would be:
CREATE VIEW dbo.myview
WITH SCHEMABINDING
AS
SELECT c.event_id,
a.barcode,
[type] = CASE WHEN d.[type] IN (2,8) THEN 'c' ELSE 's' END,
scan_date = DATEADD(DAY, DATEDIFF(DAY, 0, a.scan_time), 0),
total = COUNT_BIG(*)
FROM dbo.scan_11pc_gate_entries AS a
INNER JOIN dbo.can_units AS b
ON a.scanner = b.id
INNER JOIN dbo.scan_gates AS c
ON b.gate = c.id
INNER JOIN dbo.scan_11pc_gate_allbarcodes AS d
ON a.barcode = d.barcode
WHERE (a.valid IN (1,8))
AND d.[type] IN (2,3,4,5,8,9)
GROUP BY
c.event_id,
a.barcode,
CASE WHEN d.[type] IN (2,8) THEN 'c' ELSE 's' END,
DATEADD(DAY, DATEDIFF(DAY, 0, a.scan_time), 0);
GO
CREATE UNIQUE CLUSTERED INDEX x
ON dbo.myview(event_id, barcode, [type], scan_date);
Now you can write a query that does something like this:
SELECT [date] = CONVERT(CHAR(8), scan_date, 112),
SUM(CASE WHEN [type] = 's' THEN total ELSE 0 END) AS total_scans,
SUM(CASE WHEN [type] = 'c' THEN total ELSE 0 END) AS total_creds,
COUNT(CASE WHEN [type] = 's' THEN 1 END) AS unique_scans,
COUNT(CASE WHEN [type] = 'c' THEN 1 END) AS unique_creds
FROM dbo.myview WITH (NOEXPAND) -- in case STD Edition
WHERE event_id = 21
AND scan_date BETWEEN '20110808' AND '20110814'
GROUP BY scan_date
UNION ALL
SELECT [date] = 'weekly',
SUM(CASE WHEN [type] = 's' THEN total ELSE 0 END) AS total_scans,
SUM(CASE WHEN [type] = 'c' THEN total ELSE 0 END) AS total_creds,
COUNT(DISTINCT CASE WHEN [type] = 's' THEN barcode END) AS unique_scans,
COUNT(DISTINCT CASE WHEN [type] = 'c' THEN barcode END) AS unique_creds
FROM dbo.myview WITH (NOEXPAND) -- in case STD Edition
WHERE event_id = 21
AND scan_date BETWEEN '20110808' AND '20110814'
ORDER BY [date];
This is all completely untested as your schema is a little bit cumbersome to try and create a complete repro of your system, but hopefully this gives you a general idea to work from...

ORA-00937: Not a single-group group function - Query error

Error: ORA-00937: Not a single-group group function
Query:
select count(*) todas,
sum(case when i.prioridade = 1 then 1 else 0 end) urgente,
sum(case when i.prioridade = 2 then 1 else 0 end) alta,
sum(case when i.prioridade = 3 then 1 else 0 end) normal,
sum(case when i.prioridade = 4 then 1 else 0 end) baixa,
(select count(*)
from GMITEMOS i
inner join GMCTLSLA c on c.os = i.cd_numero_os and c.item = i.item
where i.situacao in ('A', 'I', 'P')
and c.ordem = 99999
) naoAvaliados,
sum(case when i.situacao = 'P' then 1 else 0 end) pendentes,
sum(case when i.situacao = 'A' or i.situacao = 'I' then 1 else 0 end) iniciados
from GMITEMOS i
where i.situacao in ('A', 'I', 'P')
and exists (select 1
from GMCTLSLA c
where c.os = i.cd_numero_os
and c.item = i.item)
The error is ocurring here:
(select count(*)
from GMITEMOS i
inner join GMCTLSLA c on c.os = i.cd_numero_os and c.item = i.item
where i.situacao in ('A', 'I', 'P')
and c.ordem = 99999
) naoAvaliados
Can someone tell why is it happening?
You may have fixed it with max but that's not why it's happening and is a little bit hacky. Your problem is that your sub-query, which translates into a single column is not an aggregate query, min, max, sum etc and so needs to be included in a group by clause. You fixed this by wrapping it in max as the maximum of a single value will always be constant.
However, as your sub-query is, itself, an analytic query and will only ever return one row the obvious thing to do is to use a cartesian join to add it to your query. In the explicit join syntax this is known as the cross join.
select count(*) todas
, sum(case when i.prioridade = 1 then 1 else 0 end) urgente
, sum(case when i.prioridade = 2 then 1 else 0 end) alta
, sum(case when i.prioridade = 3 then 1 else 0 end) normal
, sum(case when i.prioridade = 4 then 1 else 0 end) baixa
, naoAvaliados
, sum(case when i.situacao = 'P' then 1 else 0 end) pendentes
, sum(case when i.situacao = 'A' or i.situacao = 'I' then 1 else 0 end) iniciados
from GMITEMOS i
cross join (select count(*) as naoAvaliados
from GMITEMOS j
inner join GMCTLSLA k
on k.os = j.cd_numero_os
and k.item = j.item
where j.situacao in ('A', 'I', 'P')
and k.ordem = 99999
)
where i.situacao in ('A', 'I', 'P')
and exists (select 1
from GMCTLSLA c
where c.os = i.cd_numero_os
and c.item = i.item
)
The cartesian join has a bad reputation as it multiples the number of rows on one side of the join by the number of rows on the other. It does, however, have it's uses, especially in this sort of case.
It's happening because the subquery itself is a scalar result, not a group function. As you have apparently found, you can fix it by substituting a group function that yields an equivalent result to your subquery.
In merge statement, if you are getting this error than simple use the group by and it will resolve the issue.
merge into table1 tb1
using
(select a.id,a.ac_no,sum(a.qy) as qyt,sum(a.amt) as sum_amt from
table2 a, table1 b
where a.id=b.id
and a.id = '1234'
and a.date = '08Oct2014'
and a.ac_no in (123, 234, 345)
and a.ac_no = b.ac_no
group by a.ac_no,a.id
)qry
on (qry.id=tb1.id and qry.ac_no=tb1.ac_no )
when matched then
update set qy=qry.qy,amt = qry.sum_amt;

SQL Joins : in, out, shake it all about

I am performing the follwing sql to return a data where there is a match of both dob and address in tables1 & 2.
select table1.dob
, table1.address
, sum(case when person_status in ('A','B','C') then 1 else 0 end) as 'ABC_count'
, sum(case when person_status in ('D','E') then 1 else 0 end) as 'DE_Count'
, sum(case when person_status in ('F','G') then 1 else 0 end) as 'FG_Count'
from table1
inner join table2
on (table1.dob = table2.dob and table1.address = table2.address)
where table1.dob > #myDate
group by table1.dob, table1.address
order by table1.dob, table1.address
However I now want to return the data from table1 when there is no match in table2 and only that data, I thought simply changing inner join to left outer would perform what I required, it does not.
Thanks!
If there is no match in the join, the field from the second table are NULL, so you have to check for a NULL value in table2. Assuming dob is NOT NULL in table2, this should solve your problem:
select table1.dob
, table1.address
, sum(case when person_status in ('A','B','C') then 1 else 0 end) as 'ABC_count'
, sum(case when person_status in ('D','E') then 1 else 0 end) as 'DE_Count'
, sum(case when person_status in ('F','G') then 1 else 0 end) as 'FG_Count'
from table1
left outer join table2
on (table1.dob = table2.dob and table1.address = table2.address)
where table1.dob > #myDate and table2.dob is null
group by table1.dob, table1.address
order by table1.dob, table1.address
In this case thre's not a join, you should use NOT EXISTS function.
In my opinion LEFT JOIN is much more cleaner and you should go with that if there is no big difference between the performance of LEFT JOIN and NOT EXISTS. #JNK said "EXISTS and NOT EXISTS are ordinarily faster than joins or other operators like IN because they short circuit - the first time they get a hit they move on to the next entry", but my understanding is that NOT EXISTS and NOT IN are usually expensive as sql server has to go through all the records in the lookup table to make sure that the entry in fact does NOT EXIST, so i dont know how the short circuit would work
You could also use the EXCEPT keyword here.
select table1.dob
, table1.address
, sum(case when person_status in ('A','B','C') then 1 else 0 end) as 'ABC_count'
, sum(case when person_status in ('D','E') then 1 else 0 end) as 'DE_Count'
, sum(case when person_status in ('F','G') then 1 else 0 end) as 'FG_Count'
from table1
where table1.dob > #myDate
EXCEPT
select table1.dob
, table1.address
, sum(case when person_status in ('A','B','C') then 1 else 0 end) as 'ABC_count'
, sum(case when person_status in ('D','E') then 1 else 0 end) as 'DE_Count'
, sum(case when person_status in ('F','G') then 1 else 0 end) as 'FG_Count'
from table1
inner join table2
on (table1.dob = table2.dob and table1.address = table2.address)
where table1.dob > #myDate
That would get you all of the records in the first query that are not in the second query.

Resources