How can I select individual rows related to multiple Ids? - sql-server

I need to select related Ids from a table based on a list of provided Ids - effectively an Adjacency List problem. I have a working query for a single Id, but it is frankly inelegant at best even though it works! I would welcome suggestions for improvements and for ways to move the single Id solution to a multiple Id solution.
I have a database table like so:
CREATE TABLE [BookingLines]
(
[BookingLineId] BIGINT NOT NULL IDENTITY (138, 1),
[BookingId] BIGINT NOT NULL,
---- Additional Columns Redacted for brevity
[ContractNumber] INT NOT NULL DEFAULT 0,
[ContractSubNumber] DECIMAL NOT NULL DEFAULT 0,
---- Additional Columns Redacted for brevity
);
There will be records in this table, and in some cases there will be 1 or more pairs of records relating to the same Booking Id. The differentiation is in the ContractSubNumber column, where one value in the pair will be n.0 and the other n.1. So if there were three consecutive pairs, the Contract SubNumbers would be:
LineId BookingId SubNumber
1 1 0.0
2 1 0.1
3 1 1.0
4 1 1.1
5 1 2.0
6 1 2.1
I may need to start from the Line Id representing either of the sub numbers, and collect the opposing one. So, if I am starting from LineId 1, I need to retrieve LineId 2 being the related row. I can do this on a single Id using multiple sub selects, like this:
SELECT BookingLineId
FROM
(
SELECT BookingLineId
FROM BookingLines
WHERE BookingId = 1
AND FLOOR(ContractSubNumber) =
(
SELECT FLOOR(ContractSubNumber)
FROM BookingLines
WHERE BookingId = 1 AND BookingLineId = (1)
)
)
WHERE BookingLineId <> 1;
This works correctly, returning the value 2 in this case.
How can I make this more elegant and efficient?
How can I rewrite this to return the opposing values of all Ids in a specified list e.g.
WHERE BookingId = 1 AND BookingLineId IN (1,3,5))
and have it return the result 2,4,6?
All suggestions gratefully received.
EDIT
I have corrected the typo in the SQL provided in the original question, and using the framework proposed by #McNets this is the solution I went for:
SELECT BL.BookingLineId
FROM BookingLines BL
INNER JOIN BookingLines ABL ON ABL.BookingId = BL.BookingId
AND ABL.BookingLineId IN (22, 24, 26)
AND FLOOR(BL.ContractSubNumber) = FLOOR(ABL.ContractSubNumber)
WHERE BL.BookingId = 3 AND BL.BookingLineId NOT IN (22,24,26);
I am very grateful for the contributions and for the final answer. Thanks guys!

As far as there is no information about AgencyBookingLines and no sample data I cannot set up a fiddle example, but I think you can move the AgencyBookingLines subquery to the ON clause.
SELECT BL.BookingLineId
FROM BookingLines BL
INNER JOIN AgencyBookingLines ABL
ON ABL.BookingId = BL.BookinId
AND ABL.BookingLineId = 1
AND FLOOR(BL.ContractSubNumber) = FLOOR(ABL.ContractSubNumber
WHERE BL.BookingId = 1
AND BL.BookingLineId <> 1;
--
-- AND BL.BookingLineId IN (2,4,6);

Will it sub numbers always *.0 & *.1. Then you could try the below
SELECT oppo.*
FROM AgencyBookingLines AS main
INNER JOIN AgencyBookingLines AS oppo ON
oppo.BookingId = main.BookingId
AND oppo.SubNumber <> main.SubNumber
AND FLOOR(oppo.SubNumber) = FLOOR(main.SubNumber)
WHERE main.BookingId = 1
AND main.LineId IN (1,3,5)

Related

Is there a way you can produce an output like this in T-SQL

I have a column which I translate the values using a case statements and I get numbers like this below. There are multiple columns I need to produce the result like this and this is just one column.
How do you produce the output as a whole like this below.
The 12 is the total numbers counting from top to bottom
49 is the Average.
4.08 is the division 49/12.
1 is how many 1's are there in the output list above. As you can see there is only one 1 in the output above
8.33% is the division and percentage comes from 1/12 * 100
and so on. Is there a way to produce this output below?
drop table test111
create table test111
(
Q1 nvarchar(max)
);
INSERT INTO TEST111(Q1)
VALUES('Strongly Agree')
,('Agree')
,('Disagree')
,('Strongly Disagree')
,('Strongly Agree')
,('Agree')
,('Disagree')
,('Neutral');
SELECT
CASE WHEN [Q1] = 'Strongly Agree' THEN 5
WHEN [Q1] = 'Agree' THEN 4
WHEN [Q1] = 'Neutral' THEN 3
WHEN [Q1] = 'Disagree' THEN 2
WHEN [Q1] = 'Strongly Disagree' THEN 1
END AS 'Test Q1'
FROM test111
I have to make a few assumptions here, but it looks like you want to treat an output column like a column in a spreadsheet. You have 12 numbers. You then have a blank "separator" row. Then a row with the number 12 (which is the count of how many numbers you have). Then a row with the number 49, which is the sum of those 12 numbers. Then the 4.08 row, which is rougly the average, and so on.
Some of these outputs can be provided by cube or rollup, but neither is a complete solution.
If you wanted to produce this output directly from TSQL, you would need to have multiple select statements and combine the results of all of those statements using union all. First you would have a select just to get the numbers. Then you would have a second select which outputs a "blank". Then another select which is providing a count. Then another select which is providing a sum. And so on.
You would also no longer be able to output actual numbers, since a "blank" is not a number. Visually it's best represented as an empty string. But now your output column has to be of datatype char or varchar.
You also have to make sure rows come out in the correct order for presentation. So you need a column to order by. You would have to add some kind of ordering column "manually" to each of the select statements, so when you union them all together you can tell SQL in what order the output should be provided.
So the answer to "can it be done?" is technically "yes". But if you think seems like a whole lot of laborious and inefficient TSQL work, you'd be right.
The real solution here is to change your approach. SQL should not be concerned with "output formatting". What you should do is just return the actual data (your 12 numbers) from SQL, and then do all of the additional presentation (like adding a blank row, adding a count row, etc), in the code of the program that is calling SQL to get that data.
I must say, this is one of the strangest T-SQL requirements I've seen, and is really best left to the presentation layer.
It is possible using GROUPING SETS though. We can use it to get an extra rollup row that aggregates the whole table.
Once you have the rollup, you need to unpivot the totalled row (identified by GROUPING() = 1) to get your final result. We can do this using CROSS APPLY.
This is impossible without a row-identifier. I have added ROW_NUMBER, but any primary or unique key will do.
WITH YourTable AS (
SELECT
ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rn,
CASE WHEN [Q1] = 'Strongly Agree' THEN 5
WHEN [Q1] = 'Agree' THEN 4
WHEN [Q1] = 'Neutral' THEN 3
WHEN [Q1] = 'Disagree' THEN 2
WHEN [Q1] = 'Strongly Disagree' THEN 1
END AS TestQ1
FROM test111
),
RolledUp AS (
SELECT
rn,
TestQ1,
grouping = GROUPING(TestQ1),
count = COUNT(*),
sum = SUM(TestQ1),
avg = AVG(TestQ1 * 1.0),
one = COUNT(CASE WHEN TestQ1 = 1 THEN 1 END),
onePct = COUNT(CASE WHEN TestQ1 = 1 THEN 1 END) * 1.0 / COUNT(*)
FROM YourTable
GROUP BY GROUPING SETS(
(rn, TestQ1),
()
)
)
SELECT v.TestQ1
FROM RolledUp r
CROSS APPLY (
SELECT r.TestQ1, 0 AS ordering
WHERE r.grouping = 0
UNION ALL
SELECT v.value, v.ordering
FROM (VALUES
(NULL , 1),
(r.count , 2),
(r.sum , 3),
(r.avg , 4),
(r.one , 5),
(r.onePct, 6)
) v(value, ordering)
WHERE r.grouping = 1
) v
ORDER BY
v.ordering,
r.rn;
db<>fiddle

Select greater value than 0 if it exists, leave 0 if it is the only option

EDIT: I apologize for phrasing my question first time the wrong way ... I hope this one will make it clearer:
I query the DB for quantities of a certain item (in my case item_id = 321) on all possible locations he might be having quantities on.
I am getting the following result:
item_location_id item_def_loc_name qty_per_location item_id
8 A08 0.00 321
962 POL_113 30.00 321
5 A05 60.00 321
From these three given results (might be more or less in some cases) where we have one line containing zero qty result and couple others with greater quantities - I am aiming for the first "qty_per_location" that is greater than zero but lesser than the next quantity (in this example it is 30).
But for other items that have only one location assigned to them, query will be returning only one result line with qty_per_location = 0 - and I need it to be displayed because there is no greater quantities.
So if I have three quantities on stock for current item, like in the example below, the one with qty = 30 would be the desired one. But if I have only one location with 0 qty on it - then I should get zero as a result.
Hi once again - I ended up doing some fiddling and ended up using this as a possible solution:
DECLARE #QTY as decimal(5,2);
SELECT #QTY = (SELECT SUM(qty_per_location)
FROM [asm].[dbo].[mdta_item-item_def_loc]
where item_id = 321
group by item_id)
SELECT CASE (#QTY) WHEN 0 THEN
(Select top 1 IsNull(qty_per_location, 0) as qty
from [asm].[dbo].[mdta_item-item_def_loc] -- change this to your table name
Where qty_per_location = 0 and item_id = 321
Order by qty_per_location)
ELSE
(Select top 1 IsNull(qty_per_location, 0) as qty
from [asm].[dbo].[mdta_item-item_def_loc] -- change this to your table name
Where qty_per_location > 0 and item_id = 321
Order by qty_per_location)
END
But, the trouble with this script is that I am unable to get [qty_per_location] to be displayed as header / column name above the qty result - I am getting "(No column name)". Also I am not able to get [item_location_id] column to show up beside [qty_per_location] as well ...
Please share your thoughts.
Thank you all!
This query will return the first quantity value in the table or 0 if there aren't any rows with positive value in the table:
Select IsNull(firstQty.qty_per_location, 0) as qty_per_location,
tbl.item_location_id
from mdta_item-item_def_loc tbl
left outer join (
select top 1 tbl2.item_id, tbl2.qty_per_location, tbl2.item_location_id
from mdta_item-item_def_loc tbl2
Where tbl2.qty_per_location > 0 and tbl2.item_id = 321
Order by tbl2.qty_per_location
) firstQty on tbl.item_id = firstQty.item_id and tbl.item_location_id = firstQty.item_location_id
Where tbl.item_id = 321
If you sort the query on the qty_per_location, you can get the lowest or the highest value too.
I want to thank to all people of good will, that helped me solve this SQL puzzle of mine by spending their time to point me on my way :)
This is what I adopted and ended up using, that satisfied my requirement:
DECLARE #QTY as decimal(5,2);
SET #QTY = 0;
IF #QTY < (SELECT SUM(qty_per_location)
FROM [asm].[dbo].[mdta_item-item_def_loc]
where item_id = 321
group by item_id)
Select top 1 qty_per_location, [item_def_loc_name]
from [dbo].[mdta_item-item_def_loc]
inner join [dbo].[mdta_item_def_loc] on
[dbo].[mdta_item_def_loc].[item_def_loc_id] = [dbo].[mdta_item-item_def_loc].[item_location_id]
where item_id = 321
order by qty_per_location desc
ELSE
Select top 1 qty_per_location, [item_def_loc_name]
from [dbo].[mdta_item-item_def_loc]
inner join [dbo].[mdta_item_def_loc] on
[dbo].[mdta_item_def_loc].[item_def_loc_id] = [dbo].[mdta_item-item_def_loc].[item_location_id]
where item_id = 321
Best regards y'all!

SQL Join one-to-many tables, selecting only most recent entries

This is my first post - so I apologise if it's in the wrong seciton!
I'm joining two tables with a one-to-many relationship using their respective ID numbers: but I only want to return the most recent record for the joined table and I'm not entirely sure where to even start!
My original code for returning everything is shown below:
SELECT table_DATES.[date-ID], *
FROM table_CORE LEFT JOIN table_DATES ON [table_CORE].[core-ID] = table_DATES.[date-ID]
WHERE table_CORE.[core-ID] Like '*'
ORDER BY [table_CORE].[core-ID], [table_DATES].[iteration];
This returns a group of records: showing every matching ID between table_CORE and table_DATES:
table_CORE date-ID iteration
1 1 1
1 1 2
1 1 3
2 2 1
2 2 2
3 3 1
4 4 1
But I need to return only the date with the maximum value in the "iteration" field as shown below
table_CORE date-ID iteration Additional data
1 1 3 MoreInfo
2 2 2 MoreInfo
3 3 1 MoreInfo
4 4 1 MoreInfo
I really don't even know where to start - obviously it's going to be a JOIN query of some sort - but I'm not sure how to get the subquery to return only the highest iteration for each item in table 2's ID field?
Hope that makes sense - I'll reword if it comes to it!
--edit--
I'm wondering how to integrate that when I'm needing all the fields from table 1 (table_CORE in this case) and all the fields from table2 (table_DATES) joined as well?
Both tables have additional fields that will need to be merged.
I'm pretty sure I can just add the fields into the "SELECT" and "GROUP BY" clauses, but there are around 40 fields altogether (and typing all of them will be tedious!)
Try using the MAX aggregate function like this with a GROUP BY clause.
SELECT
[ID1],
[ID2],
MAX([iteration])
FROM
table_CORE
LEFT JOIN table_DATES
ON [table_CORE].[core-ID] = table_DATES.[date-ID]
WHERE
table_CORE.[core-ID] Like '*' --LIKE '%something%' ??
GROUP BY
[ID1],
[ID2]
Your example field names don't match your sample query so I'm guessing a little bit.
Just to make sure that I have everything you’re asking for right, I am going to restate some of your question and then answer it.
Your source tables look like this:
table_core:
table_dates:
And your outputs are like this:
Current:
Desired:
In order to make that happen all you need to do is use a subquery (or a CTE) as a “cross-reference” table. (I used temp tables to recreate your data example and _ in place of the - in your column names).
--Loading the example data
create table #table_core
(
core_id int not null
)
create table #table_dates
(
date_id int not null
, iteration int not null
, additional_data varchar(25) null
)
insert into #table_core values (1), (2), (3), (4)
insert into #table_dates values (1,1, 'More Info 1'),(1,2, 'More Info 2'),(1,3, 'More Info 3'),(2,1, 'More Info 4'),(2,2, 'More Info 5'),(3,1, 'More Info 6'),(4,1, 'More Info 7')
--select query needed for desired output (using a CTE)
; with iter_max as
(
select td.date_id
, max(td.iteration) as iteration_max
from #table_dates as td
group by td.date_id
)
select tc.*
, td.*
from #table_core as tc
left join iter_max as im on tc.core_id = im.date_id
inner join #table_dates as td on im.date_id = td.date_id
and im.iteration_max = td.iteration
select *
from
(
SELECT table_DATES.[date-ID], *
, row_number() over (partition by table_CORE date-ID order by iteration desc) as rn
FROM table_CORE
LEFT JOIN table_DATES
ON [table_CORE].[core-ID] = table_DATES.[date-ID]
WHERE table_CORE.[core-ID] Like '*'
) tt
where tt.rn = 1
ORDER BY [core-ID]

Sql Server,use a subquery to find value in one table that is not in another

I am comparing an original table in SQL server to an Update table. I'm trying to find how many "First Numbers" have changed. As they do change in this system. But, this query seems to bring back "First Numbers" that that are equal to both tables. What am I doing wrong?
select *
from
tblBlue
where
Exists (Select 'x'
From tblRed
Where tblRed.FirstNumber != tblBlue.FirstNumber
and tblRed.ID = tblBlue.ID)
Example data:
tblRed
ID FirstNumber
1 10
2 20
3 30
4 40
tblBlue
1 5
2 20
3 35
4 40
I would expect to see:
1 5
3 35
Your query should work (see example at SQL Fiddle.) Could you post example data for which it's returning the wrong results?
A slightly clearer way to write it:
select *
from tblBlue new
join tblRed old
on new.ID = red.ID
where new.FirstNumber <> old.FirstNumber
Easier solution: use a left join
SELECT r.*
FROM tblRed r
LEFT JOIN tblBlue b ON b.ID = r.ID AND b.FirstNumber = r.FirstNumber
WHERE b.ID IS NULL
This will return records in tblRed that satisfy one of two conditions: 1) the ID isn't even found in tblBlue, i.e. a new record. or 2) the IDs were found, but the numbers have changed. Because if both the ID is the same and the FirsTNumber is the same, then b.ID will not be NULL, thus a match, and you can exclude it from the resultset of different values.

TSQL: Get all rows for given ID

I am trying to output all of my database reports into one report. I'm currently using nested select statements to get each line for each ID (the number of ID's is unknown). Now I would like to return all the rows for every ID (e.g. 1-25 if there are 25 rows) in one query. How would I do this?
SELECT (
(SELECT ... FROM ... WHERE id = x) As Col1
(SELECT ... FROM ... WHERE id = x) As Col2
(SELECT ... FROM ... WHERE id = x) As Col3
)
EDIT: Here's an example:
SELECT
(select post_id from posts where report_id = 1) As ID,
(select isnull(rank, 0) from results where report_id = 1 and url like '%www.testsite.com%') As Main,
(select isnull(rank, 0) from results where report_id = 1 and url like '%.testsite%' and url not like '%www.testsite%') As Sub
This will return the rank of a result for the main domain and the sub-domain, as well as the ID for the posts table.
ID Main Sub
--------------------------------------
1 5 0
I'd like to loop through this query and change report_id to 2, then 3, then 4 and carry on until all results are displayed. Nothing else needs to change other than the report_id.
Here's a basic example of what is inside the tables
POSTS
post_id post report_id
---------------------------------------------------------
1 "Hello, I am..." 1
2 "This may take..." 2
3 "Bla..." 2
4 "Bla..." 3
5 "Bla..." 4
RESULTS
result_id url title report_id
--------------------------------------------------------
1 http://... "Intro" 1
2 http://... "Hello!" 1
3 http://... "Question" 2
4 http://... "Help" 3
REPORTS
report_id description
---------------------------------
1 Introductions
2 Q&A
3 Starting Questions
4 Beginner Guides
5 Lectures
The query will want to pull the first post, the first result from the main website (www) and the first result from a subdomain by their report_id. These tables are part of a complicated join structure with many other tables but for these purposes these tables are the only ones that are needed.
I've managed to solve the problem by creating a table, setting variables to take all the contents and insert them in a while loop, then selecting them and dropping the table. I'll leave this open for a bit to see if anyone picks up a better way of doing it because I hate doing it this way.
If you need each report id on its own column, take a look at the PIVOT/UNPIVOT commands.
Here's one way of doing it :
SELECT posts.post_id AS ID,
IsNull(tblMain.Rank, 0) AS Main,
IsNull(tblSub.Rank, 0) AS Sub
FROM posts
LEFT JOIN results AS tblMain ON posts.post_id = tblMain.report_id AND tblMain.url like '%www.testsite.com%'
LEFT JOIN results AS tblSub ON posts.post_id = tblSub.report_id AND tblSub.url like '%.testsite%' and tblSub.url not like '%www.testsite%'
That is one query? You've provided your own answer?
If you mean you want to return a series of 'rows' as, for some reason, 'columns', this ability does exist, but I can't remember the exact name. Possible pivot. But it's a little odd.
see if this is what you are looking
SELECT
CASE WHEN reports.id = 1 THEN reports.Name
ELSE "" AS Col1,
CASE WHEN reports.id = 2 THEN reports.Name
ELSE "" AS Col2
....
FROM reports
Best Regards,
Iordan
Assuming you have a "master" table of IDs (if not I suggest you do so for Foreign Key purposes):
SELECT (
(SELECT ... FROM ... WHERE id = m.ID) As Col1
(SELECT ... FROM ... WHERE id = m.ID) As Col2
(SELECT ... FROM ... WHERE id = m.ID) As Col3
)
FROM MasterIDs m
Depending on how much each report is similar,you may be able to speed that up by moving some of the logic out of the nested statements and into the main body of the query.
Possibly a better way of thinking about this is to alter each report statement to return (ID,value) and do something like:
SELECT
report1.Id
,report1.Value AS Col1
,report2.Value AS Col2
FROM (SELECT Id, ... AS Value FROM ...) report1
JOIN (SELECT Id, ... AS Value FROM ...) report2 ON report1.Id = report2.Id
again, depending on the similarity of your reports you could probably combine these in someway.

Resources