Duplicating Results when using Subquery, need to filter out Nulls and only show value if value exists - sql-server-2012

(Edited) I am having a problem with duplicating results when using a Count() Function and then Grouping by a column that changes from Null to having a value. See Below:
Here is a sample table of data:
t_order_table
Line# Order User_Assigned
1 12345 Null
2 12345 Null
3 12345 Null
4 12345 Null
5 12345 Null
6 12345 Null
7 12345 Null
8 12345 Null
9 12345 Null
1 11223 Null
2 11223 Null
3 11223 Null
Here is the script to report on the table(below):
Select Order,
(Select Count(Line#) from t_order_table ord
Where User_Assigned is null
and o.Order = ord.Order) as 'Open Lines',
(Select Count(Line#) from t_order_table ord
Where User_Assigned is not null
and o.Order = ord.Order) as 'Picked Lines',
(Select User_Assigned from t_order_table ord
Where o.Order = ord.Order) as 'User Assigned'
from t_order_table o
Group By Order, User_Assigned
Which Returns:
Order Open Lines Picked Lines User_Assigned
12345 9 0 Null
11223 3 0 Null
As a worker logs into the system, he gets assigned to the lines, one by one as he picks the order. After he picks 3 lines, the table would look like this:
t_order_table
Line# Order User_Assigned
1 12345 Chris
2 12345 Chris
3 12345 Chris
4 12345 Null
5 12345 Null
6 12345 Null
7 12345 Null
8 12345 Null
9 12345 Null
1 11223 Null
2 11223 Null
3 11223 Null
and the report would look like this:
Order Open Lines Picked Lines User_Assigned
12345 9 3 Null
12345 9 3 Chris
11223 3 0 Null
The Result has a duplicated record because the SubSelect Query for 'User_Assigned' found a null and a user, so there is two records. I would like there to be some logic that checks if a user is assigned, it returns the user, else returns null. I can't figure out how to do that. Thanks for the help!!!!

This seems to give you what you need.
set transaction isolation level read uncommitted;
declare #OrderTable as table
(
[Line] tinyint,
[Order] int,
[User_Assigned] nvarchar(50)
);
insert into #OrderTable
(
[Line],
[Order],
[User_Assigned]
)
values
(1, 12345, 'Chris'),
(2, 12345, 'Chris'),
(3, 12345, 'Chris'),
(4, 12345, null),
(5, 12345, null),
(6, 12345, null),
(7, 12345, null),
(8, 12345, null),
(9, 12345, null),
(1, 11223, null),
(2, 11223, null),
(3, 11223, null);
select
o.[Order],
sum(iif(o.User_Assigned is null, 1, 0)) as [OpenLines],
sum(iif(o.User_Assigned is not null, 1, 0)) as [PickedLines],
max(o.User_Assigned) as [User_Assigned]
from #OrderTable as [o]
group by
o.[Order]
Returns:
Order OpenLines PickedLines User_Assigned
11223 3 0 NULL
12345 6 3 Chris

Related

Using recursive CTE to generate hierarchy results ordered by depth without the use of heiarchyid

I would like to query hierarchy results ordered by depth first without the use of SQL's heiarchyid built in function. Essentially, I am hoping to accomplish the depth ordering without any fancy functions.
I have provided a temp table below that contains these records:
Id
p_Id
order1
name1
1
null
1
josh
2
null
2
mary
3
null
3
george
4
1
1
joe
5
1
2
jeff
6
2
1
marg
7
2
2
moore
8
2
3
max
9
3
1
gal
10
3
2
guy
11
4
1
tod
12
4
2
ava
13
9
1
ron
14
9
2
bill
15
9
100
pat
where p_Id is the id of the parent record, and order1 is essentially just the ordering of which the depth first output should be displayed. To show why my query does not fully work, I made the order1 of the last record 100 instead of say, 3. However this should not ultimately matter since 100 and 3 both come after the previous order1 value, 2.
An example of a correct result table is shown below:
Id
p_Id
order1
name1
Descendants
1
null
1
josh
josh
4
1
1
joe
josh/joe
11
4
1
tod
josh/joe/tod
12
4
2
ava
josh/joe/ava
5
1
2
jeff
josh/jeff
2
null
2
mary
mary
6
2
1
marg
mary/marg
7
2
2
moore
mary/moore
8
2
3
max
mary/max
3
null
3
george
george
9
3
1
gal
george/gal
13
9
1
ron
george/gal/ron
15
9
2
bill
george/gal/bill
14
9
100
pat
george/gal/pat
10
3
2
guy
george/guy
Where an example of my results are shown below:
Id
p_Id
order1
name1
Descendants
levels
1
null
1
josh
josh
.1
4
1
1
joe
josh/joe
.1.1
11
4
1
tod
josh/joe/tod
.1.1.1
12
4
2
ava
josh/joe/ava
.1.1.2
5
1
2
jeff
josh/jeff
.1.2
2
null
2
mary
mary
.2
6
2
1
marg
mary/marg
.2.1
7
2
2
moore
mary/moore
.2.2
8
2
3
max
mary/max
.2.3
3
null
3
george
george
.3
9
3
1
gal
george/gal
.3.1
13
9
1
ron
george/gal/ron
.3.1.1
15
9
100
pat
george/gal/pat
.3.1.100
14
9
2
bill
george/gal/bill
.3.1.2
10
3
2
guy
george/guy
.3.2
where I have created a levels column that essentially concatenates the order1 values and separates them with a period. This almost returns the correct results, but due to the fact that I am ordering by this string (of numbers and periods), the levels value of .3.1.100 will come before .3.1.2 , which is not what the desired output should look like. I am sure there is a different method to return the correct depth order. See below for the code that generates a temp table, and the code that I used to generate the incorrect output that I have so far.
if object_id('tempdb..#t1') is not null drop table #t1
CREATE TABLE #t1 (Id int, p_Id int, order1 int, name1 varchar(150))
INSERT into #t1 VALUES
(1, null, 1, 'josh'),
(2, null, 2, 'mary'),
(3, null, 3, 'george'),
(4, 1, 1, 'joe'),
(5, 1, 2, 'jeff'),
(6, 2, 1, 'marg'),
(7, 2, 2, 'moore'),
(8, 2, 3, 'max'),
(9, 3, 1, 'gal'),
(10, 3, 2, 'guy'),
(11, 4, 1, 'tod'),
(12, 4, 2, 'ava'),
(13, 9, 1, 'ron'),
(14, 9, 2, 'bill'),
(100, 9, 100, 'pat');
select * from #t1
-- Looking to generate heiarchy results ordered by depth --
; with structure as (
-- Non-recursive term.
-- Select the records where p_Id is null
select p.Id,
p.p_Id,
p.order1,
p.name1,
cast(p.name1 as varchar(64)) as Descendants,
cast(concat('.', p.order1) as varchar(150)) as levels
from #t1 p
where p.p_Id is null
union all
-- Recursive term.
-- Treat the records from previous iteration as parents.
-- Stop when none of the current records have any further sub records.
select c.Id,
c.p_Id,
c.order1,
c.name1,
cast(concat(p.Descendants, '/', c.name1) as varchar(64)) as Descendants,
cast(concat(p.levels, '.', c.order1) as varchar(150)) as levels
from #t1 c -- c being the 'child' records
inner join structure p -- p being the 'parent' records
on c.p_Id = p.Id
)
select *
from structure
order by replace(levels, '.', '') asc
Take II. As pointed out by OP my original answer fails for more than 10 children. So what we can do (OP's suggestion) is pad the values out with zeros to a constant length. But what length? We need to take the largest number of children under a node and add this to the largest value or order, so for the example provided this is 100 + 3, and then take the length of that (3) and pad every order with zeros to 3 digits long. This means we will always be ordering as desired.
declare #PadLength int = 0;
select #PadLength = max(children)
from (
select len(convert(varchar(12),max(order1)+count(*))) children
from #t1
group by p_Id
) x;
-- Looking to generate heiarchy results ordered by depth --
with structure as (
-- Non-recursive term
-- Select the records where p_Id is null
select
p.Id [Id]
, p.p_Id [ParentId]
, p.order1 [OrderBy]
, p.name1 [Name]
, cast(p.name1 as varchar(64)) Descendants
, concat('.', right(replicate('0',#Padlength) + convert(varchar(12),p.order1), #PadLength)) Levels
from #t1 p
where p.p_Id is null
union all
-- Recursive term
-- Treat the records from previous iteration as parents.
-- Stop when none of the current records have any further sub records.
select
c.Id,
c.p_Id,
c.order1,
c.name1,
cast(concat(p.Descendants, '/', c.name1) as varchar(64)),
concat(p.levels, '.', right(replicate('0',#Padlength) + convert(varchar(12),c.order1), #PadLength))
from #t1 c -- c being the 'child' records
inner join structure p on c.p_Id = p.Id -- p being the 'parent' records
)
select *
from structure
order by replace(levels, '.', '') asc;
Note: This answer fails in the case when there are more than 10 children under a particular node. Leaving for interest.
So this issue you have run into is that you are ordering by a string not a number. So the string 100 comes before the string 2. But you need to order by a string to take care of the hierarchy, so one solution is to replace order1 with row_number() based on the order1 column while its still a number and use the row_number() to build your ordering string.
So you replace:
cast(concat(p.levels, '.', c.order1) as varchar(150)) as levels
with
cast(concat(p.levels, '.', row_number() over (order by c.Order1)) as varchar(150))
giving a full query of
with structure as (
-- Non-recursive term.
-- Select the records where p_Id is null
select p.Id,
p.p_Id,
p.order1,
p.name1,
cast(p.name1 as varchar(64)) as Descendants,
cast(concat('.', p.order1) as varchar(150)) as levels
from #t1 p
where p.p_Id is null
union all
-- Recursive term.
-- Treat the records from previous iteration as parents.
-- Stop when none of the current records have any further sub records.
select c.Id,
c.p_Id,
c.order1,
c.name1,
cast(concat(p.Descendants, '/', c.name1) as varchar(64)) as Descendants,
cast(concat(p.levels, '.', row_number() over (order by c.Order1)) as varchar(150))
from #t1 c -- c being the 'child' records
inner join structure p -- p being the 'parent' records
on c.p_Id = p.Id
)
select *
from structure
order by replace(levels, '.', '') asc;
Which returns the desired results.
Note: good question, well written.

Conditional join based on value of column

I have a table of addresses with a column for Canadian provinces and another column for US States as well as a column for country. We support other countries but we treat Canadian and US different.
We have a separate table the is a list of codes, there is a table id and a code value. So the code table id for Canadian provinces is say 5 and has the 13 values for all the provinces and territories. The US states are in the same table but with a code table is say 6 and has all 50 states.
I have been asked to write a report that would reference the province or state and I am struggling with a way to change the code table id used in the join to get the province or state description depending on the country code.
Table structure for address (simplified):
CNTRY_CD (value 3 is Canada, 22 is US, others exist but only these two are linked tothe code table)
PROV_CD (only has values when country is 3, zero otherwise)
STATE_CD (only has values when country is 22, zero otherwise)
Table structure for code tables (simplified):
TABLE_ID (provinces are table 5 and states are table 6)
CODE (for table 5 there are 13 values, for table 6 there 50 values)
DESC (the name of the province or state depending on above)
If you need additional details just let me know.
EDIT
Sample data follows:
ADDR_ID
PROV_CD
STATE_CD
01
3
NULL
02
NULL
25
03
NULL
NULL
TABLE_ID
CODE
DESC
5
3
Manitoba
5
4
Ontario
6
25
Michigan
6
26
Montana
Report
JURISDICTION
DESC
COUNT
Canada
Manitoba
123
US
Michigan
321
Other
Other
5
What has been causing me the most trouble is that the table id is not in the data - it is known only by the column the data comes from. So if the column is PROV_CD then I know to use code table id 5, if STATE_CD then I know to use code table id 6 but the actual data does not contain the code table id. Hope that makes sense.
This is the closest I have been so far:
Here is redacted source data:
CLIENT_ID ADDR_ID CNTRY_CSN CDN_PROV_CSN US_STE_CSN ADDR_L1_TXT PVST_NM
821 72301 104 0 0 International line 1 NULL
821 72302 148 0 1 NULL NULL
821 72303 221 0 14 NULL NULL
821 72304 36 9 0 NULL NULL
821 72305 0 0 0 NULL NULL
821 72306 221 0 44 NULL NULL
821 72307 36 9 0 NULL NULL
821 72308 0 0 0 NULL NULL
821 72309 0 0 0 NULL NULL
821 72310 0 0 0 NULL NULL
822 1481 36 9 0 NULL NULL
822 1482 36 0 0 NULL NULL
Here is redacted SQL:
SELECT CLIENT_ID, ADDR_ID, CNTRY_CSN, CD_EDESC
FROM CLNT_ADDR
LEFT OUTER JOIN TXSCT
ON CD_TBL_ID =
CASE CNTRY_CSN
WHEN 36 THEN 10
WHEN 148 THEN 538
WHEN 221 THEN 12
ELSE NULL
END
AND CSN =
CASE
WHEN CDN_PROV_CSN > 0 THEN CDN_PROV_CSN
WHEN US_STE_CSN > 0 THEN US_STE_CSN
ELSE NULL
END
WHERE CLIENT_ID IN (821, 822)
WITH UR;
Here is the result I get:
CLIENT_ID ADDR_ID CNTRY_CSN CD_EDESC
821 72301 104 NULL
821 72302 148 Aguascalientes
821 72302 148 Aguascalientes
821 72303 221 Idaho
821 72304 36 Ontario
821 72305 0 NULL
821 72306 221 Texas
821 72307 36 Ontario
821 72308 0 NULL
821 72309 0 NULL
821 72310 0 NULL
822 1481 36 Ontario
822 1482 36 NULL
Address id 72302 is repeated, don't know why.
Try this as is.
Two ways to resolve it - with 2 left joins (comment out all lines until the end starting from the one with JOIN and uncomment 2 lines with LEFT JOIN, comment out the line with C.DESC and uncomment one with COALESCE) or 1 inner join (as is below).
WITH
ADDRESS
(
CNTRY_CD --(value 3 is Canada, 22 is US, others exist but only these two are linked tothe code table)
, PROV_CD --(only has values when country is 3, zero otherwise)
, STATE_CD --(only has values when country is 22, zero otherwise)
)
AS
(
VALUES
( '3', 'P1', '')
, ( '3', 'P2', '')
, ('22', '', 'S1')
, ('22', '', 'S2')
)
, CODE
(
TABLE_ID --(provinces are table 5 and states are table 6)
, CODE --(for table 5 there are 13 values, for table 6 there 50 values)
, DESC --(the name of the province or state depending on above) )
)
AS
(
VALUES
(5, 'P1', 'Canadian Province1')
, (5, 'P2', 'Canadian Province2')
-- Just to show, that the same code may be for different countries
-- but the statement works correctly
, (5, 'S1', 'Canadian Province3')
, (6, 'S1', 'US State1')
, (6, 'S2', 'US State2')
, (6, 'P1', 'US State3')
)
SELECT
A.*
--, COALESCE (C.DESC, U.DESC) AS DESC
, C.DESC
FROM ADDRESS A
--LEFT JOIN CODE C ON C.CODE = A.PROV_CD AND (C.TABLE_ID, A.CNTRY_CD) = (5, '3')
--LEFT JOIN CODE U ON U.CODE = A.STATE_CD AND (U.TABLE_ID, A.CNTRY_CD) = (6, '22')
JOIN CODE C ON
(C.CODE = A.PROV_CD AND (C.TABLE_ID, A.CNTRY_CD) = (5, '3'))
OR (C.CODE = A.STATE_CD AND (C.TABLE_ID, A.CNTRY_CD) = (6, '22'))
The result is:
CNTRY_CD
PROV_CD
STATE_CD
DESC
3
P1
Canadian Province1
3
P2
Canadian Province2
22
S1
US State1
22
S2
US State2
If it's not what you want, then please, edit your questions with your sample data in the form used in the WITH clause and show the result desired.

If join returns null then join on default row

I am trying to join 2 tables on a row, and if that row is null, then join that row to the default row.
Table 1: Events
EventID EventName
----------- -------------
1 January
2 February
3 March
4 April
Table 2: Menus
MenuID EventID MenuVersion
---------- ----------- ---------------
1 1
2 3 2
3 4 4
4 4
What I have Tried
SELECT * FROM Events
LEFT JOIN Menus
ON Events.EventID = Menus.EventID
Output I'm Getting
EventID EventName MenuID EventID MenuVersion
----------- ------------- --------- ---------- ---------------
1 January
2 February
3 March 2 3 2
4 April 3 4 4
The default row of the Menus table in this case is the row with the highest MenuID and a null EventID.
Output I want
EventID EventName MenuID EventID MenuVersion
----------- ------------- --------- ---------- ---------------
1 January 4 4
2 February 4 4
3 March 2 3 2
4 April 3 4 4
Cross apply the default row and use its values when no row is left joined on.
DECLARE #Events TABLE (EventId INT, EventName VARCHAR(12));
DECLARE #Menus TABLE (MenuId INT, EventId INT, MenuVersion INT);
INSERT INTO #Events (EventId, EventName)
VALUES
(1, 'January'),
(2, 'February'),
(3, 'March'),
(4, 'April');
INSERT INTO #Menus (MenuId, EventId, MenuVersion)
VALUES
(1, null, 1),
(2, 3, 2),
(3, 4, 4),
(4, null, 4);
SELECT E.EventId, E.EventName, COALESCE(M.MenuId, D.MenuId) MenuId, M.EventId, COALESCE(M.MenuVersion, D.MenuVersion) MenuVersion
FROM #Events E
LEFT JOIN #Menus M ON M.EventID = E.EventID
CROSS APPLY (SELECT TOP 1 * FROM #Menus WHERE EventId IS NULL ORDER BY MenuId DESC) D;
Returns as requested:
EventId EventName MenuId EventId MenuVersion
1 January 4 NULL 4
2 February 4 NULL 4
3 March 2 3 2
4 April 3 4 4
Note: If you set out your questions like this in future with the DDL/DML statements you'll get a much faster response because it saves people from having to type it all in.

Using PIVOT with 2 related tables

I am working in SQL Server (SSMS) with two tables like:
prop_ppl:
id_numb id_pers cdate val
1 4 NULL NULL
2 2 2018-12-12 250
3 1 2018-12-01 250
4 3 2018-12-11 500
5 6 2018-01-01 500
6 5 2018-12-12 480
ppl:
id_perc name
1 John
2 Derek
3 Mia
4 Chris
5 Ann
6 Dave
Then i need to get the table like this:
name
id_numb value
for these tables it should be, when its nececcary to find all values for ppl with date 2018/12/12:
Derek Ann
2 250 0
6 0 NULL
Code:
CREATE TABLE ppl(
id_perc smallint PRIMARY KEY,
name varchar(50) NOT NULL
)
CREATE TABLE prop_ppl(
id_numb int IDENTITY(1,1) PRIMARY KEY,
id_perc smallint NOT NULL,
cdate smalldatetime,
val int
)
INSERT INTO dbo.ppl (id_perc, name)
VALUES (1, 'John'), (2, 'Derek'), (3, 'Mia'), (4, 'Chris'), (5, 'Ann'),
(6, 'Dave')
INSERT INTO dbo.prop_ppl (id_perc, cdate, val)
VALUES (4, NULL,NULL), (2,'20181212', 250), (1, '20181201', 250),
(3, '20181211',500), (6,'20180101', 500), (5, '20181212', 480)
Then i try to use:
SELECT *
FROM (
SELECT name,id_numb,val
FROM prop_ppl
JOIN ppl ON prop_ppl.id_perc = ppl.id_perc
WHERE cdate='20181212'
)
PIVOT(
SUM(val)
FOR [name] in ('Derek','Ann')
)
but an error appears
"Incorrect syntax near the keyword "PIVOT"."
maybe it's possible to do without PIVOT..
Also, I would like to understand how this script could be applied to arbitrary parameters (and not just to Derek & Ann).
It's easy to handle with CTE :
With CTE as (
SELECT
name,
id_numb,
val
FROM prop_ppl
JOIN ppl ON prop_ppl.id_perc = ppl.id_perc
WHERE cdate='20181212')
Select
*
from CTE
PIVOT(SUM(val) FOR [name] in ([Derek],[Ann])) as Col

INSERT a RANK into a SQL table

I have a table that has the following columns:
NAME (VARCHAR)
FISHING (INT)
SAILING (INT)
NAVIGATION (INT)
SALARY (NUMERIC)
This table has 9 rows of data. I used ALTER to add a column that would hold the rank of the person's salary. It is called SALARY_RANK. My idea was to populate that column with a SELECT statement that would fill that rank. I tried to use INSERT with the below SELECT statement to fill that column but it ends up creating 9 new rows where all except the new column are NULL.
What is the best way to go about this?
This is the SQL I have written for the new column:
ALTER TABLE #CODY_CREW
ADD SALARY_RANK INT;
INSERT INTO #CODY_CREW (SALARY_RANK)
SELECT
DENSE_RANK() OVER (ORDER BY SALARY) AS 'SALARY_RANK'
FROM #CODY_CREW
This is what happens when I run that:
NAME FISHING SAILING NAVIGATION SALARY SALARY_RANK
-------------------------------------------------- ----------- ----------- ----------- --------------------------------------- -----------
Amy 3 5 1 46000 NULL
Bill 1 2 5 43000 NULL
Carl 3 4 2 47000 NULL
Dan 4 3 1 36000 NULL
Eva 4 2 2 43000 NULL
Fred 1 3 4 55000 NULL
Greg 3 1 5 68000 NULL
Henry 5 4 2 64000 NULL
Ida 3 3 3 60000 NULL
NULL NULL NULL NULL NULL 1
NULL NULL NULL NULL NULL 2
NULL NULL NULL NULL NULL 2
NULL NULL NULL NULL NULL 3
NULL NULL NULL NULL NULL 4
NULL NULL NULL NULL NULL 5
NULL NULL NULL NULL NULL 6
NULL NULL NULL NULL NULL 7
NULL NULL NULL NULL NULL 8
(18 rows affected)
You should be using UPDATE instead of INSERT query. Try like below.
UPDATE C1
SET C1.SALARY_RANK = C2.SALARY_RANK
FROM #CODY_CREW C1
JOIN (SELECT
DENSE_RANK() OVER (ORDER BY SALARY) AS 'SALARY_RANK', Name
FROM #CODY_CREW) C2
ON C1.Name = C2.Name
In SQL Server, you can use an updatable CTE or subquery:
WITH toupdate AS (
SELECT cc.*, DENSE_RANK() OVER (ORDER BY SALARY) AS NEW_SALARY_RANK
FROM #CODY_CREW cc
)
UPDATE toupdate
SET SALARY_RANK = NEW_SALARY_RANK;
A JOIN is not necessary.