Select certain rows with respect to their min max values - sql-server

In T-SQL (MSSql 2008R2) I Would like to select certain rows from a table or set of results;
StoreId StoreName BrochureId PageId Rank Distance
43561 X 1627 11608 73 598.10
43561 X 1627 11591 68 598.10
43561 X 1627 11615 41 598.10
43827 Y 1727 21708 75 1414.69
43827 Y 1727 21591 62 1414.69
43827 Y 1727 21615 44 1414.69
43919 Z 1827 31809 77 2487.35
43919 Z 1827 31591 60 2487.35
43919 Z 1827 31615 39 2487.35
Would like to select only rows with lowest distance and with the highest rank, as such;
StoreId StoreName BrochureId PageId Rank Distance
43561 X 1627 11608 73 598.10
43827 Y 1727 21708 75 1414.69
43919 Z 1827 31809 77 2487.35
Thank you for your help.

You can use ROW_NUMBER for this.
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY StoreName ORDER BY Distance, [Rank] DESC) Rn
FROM
Table1
) t
WHERE Rn = 1
there are other ranking functions you can use as well.. for example, if you use RANK instead of ROW_NUMBER here, you can include ties in your result as well.
SQL Ranking Functions

Use a correlated subquery in your where clause... assuming your table name is MyTable, something like this should get what you want:
SELECT [StoreId], [StoreName], [BrochureId], [PageId], [Rank], [Distance]
FROM MyTable m
WHERE [Rank] = (SELECT MAX([Rank]) FROM MyTable x WHERE x.StoreId = m.StoreId)
OR [Distance] = (SELECT MIN([Distance]) FROM MyTable y WHERE y.StoreId = m.StoreId)
(note, I enclosed the column names in square brackets because "Rank" is a reserved SQL Keyword)

Related

Looking for a special way to join on SQL server

I would like to join two tables but couldn't find any existing joins(I tried left, right, full, cross) to do that.
I want to combine table 1 and table 2 into the table 3.
The data order was based on chronological order, and I would like to see the same order in desired table.
TABLE 1:
Student---- Score1
A------------ 90
A------------ 80
B------------ 85
B------------ 60
C------------ 50
C------------ 40
Table2:
Student---- Score2
A------------ 66
A------------ 70
A------------ 85
B------------ 60
C------------ 40
Table 3: Desired Table
Student---- Score1-----Score2
A------------ 90 ----------- 66
A------------ 80 ----------- 70
A------------null -----------85
B------------ 85 ----------- 60
B------------ 60 ----------- null
C------------ 50 ----------- 40
C------------ 40 ----------- null
Thank you!
Ok, we need as first thing try to find a way to add a positional column to your table at runtime. This can be done with ROW_NUMBER() function:
SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table1
SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table2
This creates a nice Position column in our result:
Student Score1 Position
---------- ----------- --------------------
A 90 1
A 80 2
B 85 1
B 60 2
C 50 1
C 40 2
(6 rows affected)
Student Score2 Position
---------- ----------- --------------------
A 66 1
A 70 2
A 85 3
B 60 1
C 40 1
(5 rows affected)
Now we need to join these two temporary results. Since you want to include all the rows from each table, leaving empty (NULL) the spaces left from non-matching rows. FULL OUTER JOIN comes to the rescue, in all its beauty:
SELECT *
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table1) T1
FULL OUTER JOIN
(SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table2) T2
ON T1.Student = T2.Student AND T1.Position = T2.Position
We get this:
Student Score1 Position Student Score2 Position
---------- ----------- -------------------- ---------- ----------- --------------------
A 90 1 A 66 1
A 80 2 A 70 2
NULL NULL NULL A 85 3
B 85 1 B 60 1
B 60 2 NULL NULL NULL
C 50 1 C 40 1
C 40 2 NULL NULL NULL
(7 rows affected)
Now just select what you are interested in:
SELECT COALESCE(T1.student, T2.student) Student,
T1.score1,
T2.score2
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table1) T1
FULL OUTER JOIN
(SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY Student) Position FROM Table2) T2
ON T1.Student = T2.Student AND T1.Position = T2.Position
And voilĂ :
Student score1 score2
---------- ----------- -----------
A 90 66
A 80 70
A NULL 85
B 85 60
B 60 NULL
C 50 40
C 40 NULL
(7 rows affected)
Be aware though: with many records, this could not be the most efficient way of storing and retrieving your data...
Edit: what follows has been added after answer acceptance
Really important: since a small diatribe is born in comments, let's state the obvious.
The database design proposed by OP has many defect, for first it's based on the assumption that the order of the records in the table will always be the one in which the records have been inserted.
This could not be true and my solution can not work as expected until some more robust way of sorting records is implemented.
Would it be better to add a CreatedAt column to both tables, of type datetime, in which to store record insert date:
ALTER TABLE dbo.Table1 ADD
CreatedAt datetime NOT NULL CONSTRAINT DF_Table1_CreatedAt DEFAULT getdate()
ALTER TABLE dbo.Table2 ADD
CreatedAt datetime NOT NULL CONSTRAINT DF_Table2_CreatedAt DEFAULT getdate()
This could allow to more safely order the records.
The solution would change as follows:
SELECT COALESCE(T1.student, T2.student) Student,
T1.score1,
T2.score2
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY CreatedAt) Position FROM Table1) T1
FULL OUTER JOIN
(SELECT *, ROW_NUMBER() OVER (PARTITION BY Student ORDER BY CreatedAt) Position FROM Table2) T2
ON T1.Student = T2.Student AND T1.Position = T2.Position
Well, technically:
with t1 as (
select *, ord = row_number() over(partition by student order by score1 desc)
from table1
),
t2 as (
select *, ord = row_number() over(partition by student order by score2)
from table1
)
select student = isnull(t1.student, t2.student),
t1.score1,
t2.score2
from t1
full join t2 on t1.student = t2.student and t1.ord = t2.ord;
But I doubt your desire is to order scores in table1 in descending order and scores in table2 in ascending order. So you're going to have to pin that down. Is there a column for time the test was administered? Probably best to order by that.
Better still, do this in your front end software, such as with SSRS, crystal reports, or the like. I say this because I assume this is for a reporting need where the rows don't really represent 'records' anymore.

SQL Convert each date range into each day row

I have one simple requirement. Below is my sql table.
ID Cname StartDate EndDate Value
1 x 01/15/2015 01/20/2015 50
2 x 01/17/2015 01/22/2015 60
3 y 02/15/2015 02/20/2015 40
4 y 02/17/2015 02/22/2015 80
I have date range and I want to convert this each date range into each day row. Along with that whenever there is a overlap of dates it adds the value.
Below is the sample output for more clarification.
Cname date value
x 1/15/2015 60
x 1/16/2015 60
x 1/17/2015 110
x 1/18/2015 110
x 1/19/2015 110
x 1/20/2015 110
x 1/21/2015 60
x 1/22/2015 60
y 2/15/2015 40
y 2/16/2015 40
y 2/17/2015 120
y 2/18/2015 120
y 2/19/2015 120
y 2/20/2015 120
y 2/21/2015 80
y 2/22/2015 80
Any help would be appreciated.
You can use the technique described here, in order to generate a date range for each interval of your table. Then simply group by Cname and date to get the desired result set:
;WITH natural AS
(
SELECT ROW_NUMBER() OVER (ORDER BY [object_id]) - 1 AS val
FROM sys.all_objects
)
SELECT m.Cname, d = DATEADD(DAY, natural.val, m.StartDate),
SUM(value) AS value
FROM mytable AS m
INNER JOIN natural ON natural.val <= DATEDIFF(DAY, m.StartDate, m.EndDate)
GROUP BY Cname, DATEADD(DAY, natural.val, m.StartDate)
ORDER BY Cname, d
The CTE is used to create a tally table. The numbers of this table are then used to add 1,2,3, ... days to StartDate until EndDate is reached.
If you group by Cname, [Date], then SUM will return the required value since it will add any overlapping records within each Cname partition.
SQL Fiddle Demo

Removing Duplicates of two columns in a query

I have a select * query which gives lots of row and lots of columns of results. I have an issue with duplicates of one column A when given the same value of another column B that I would like to only include one of.
Basically I have a column that tells me the "name" of object and another that tells me the "number". Sometimes I have an object "name" with more than one entry for a given object "number". I only want distinct "numbers" within a "name" but I want the query to give the entire table when this is true and not just these two columns.
Name Number ColumnC ColumnD
Bob 1 93 12
Bob 2 432 546
Bob 3 443 76
This example above is fine
Name Number ColumnC ColumnD
Bob 1 93 12
Bob 2 432 546
Bill 1 443 76
Bill 2 54 1856
This example above is fine
Name Number ColumnC ColumnD
Bob 1 93 12
Bob 2 432 546
Bob 2 209 17
This example above is not fine, I only want one of the Bob 2's.
Try it if you are using SQL 2005 or above:
With ranked_records AS
(
select *,
ROW_NUMBER() OVER(Partition By name, number Order By name) [ranked]
from MyTable
)
select * from ranked_records
where ranked = 1
If you just want the Name and number, then
SELECT DISTINCT Name, Number FROM Table1
If you want to know how many of each there are, then
SELECT Name, Number, COUNT(*) FROM Table1 GROUP BY Name, Number
By using a Common Table Expression (CTE) and the ROW_NUMBER OVER PARTION syntax as follows:
WITH
CTE AS
(
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY Name, Number ORDER BY Name, Number) AS R
FROM
dbo.ATable
)
SELECT
*
FROM
CTE
WHERE
R = 1
WITH
CTE AS
(
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY Plant, BatchNumber ORDER BY Plant, BatchNumber) AS R
FROM dbo.StatisticalReports WHERE dbo.StatisticalReports. \!"FermBatchStartTime\!" >= DATEADD(d,-90, getdate())
)
SELECT
*
FROM
CTE
WHERE
R = 1
ORDER BY dbo.StatisticalReports.Plant, dbo.StatisticalReports.FermBatchStartTime

t-sql grouping rows between ranges

I have this query that returns Quantity based discount list
with cte([item code],price,discount,rngLow,rngHigh,id) as
(
select 'xxx-xxxxxxx' as [item code],l.t$pric,l.t$disc,lqanp=l.t$qanp,hqanp=h.t$qanp,id = row_number() over(partition by h.t$qanp order by h.t$qanp) from EdiCatalog l
join ediCatalog h on l.comno=h.comno and l.t$cpls=h.t$cpls and l.t$cuno=h.t$cuno and h.t$item=l.t$item and l.t$qanp < h.t$qanp
where l.comno=#comno and l.t$cpls=#cpls and l.t$cuno=#cuno
)
select * from cte
returning result set
How do I transform the result set to this
You can start with this:
SELECT *
FROM cte a
WHERE rngHigh=(
SELECT MIN(rngHigh)
FROM cte b
WHERE b.rngLow=a.rngLow
)
Which will give you this result set:
discount rngLow rngHigh
40 1 9
68 9 23
73 23 47
75 47 299
#Frazz,
Here is what I have now
with little alteration to what you suggested..
SELECT rngLow=case rngLow
when 1 then rnglow
else rnglow+1 end,rngHigh,discount,id
FROM cte a
WHERE rngHigh=(
SELECT MIN(rngHigh)
FROM cte b
WHERE b.rngLow=a.rngLow
)

How to call columns without using Group By clause

I am new to sql, i had one doubt
select t1.PayeeCode,t1.PayeeName,t1.PayeeIFSCCode,t1.grossAmount as ga,t2.Deduction
as da,(t1.grossAmount-t2.Deduction) as SubAmount from (
(select PayeeCode,PayeeName,PayeeIFSCCode,PayeeBankAcNo,sum(PayeeAmount) as
grossAmount from tblPayees where AccountType='g' group by payeeCode, PayeeName,PayeeIFSCCode,PayeeBankAcNo) t1
inner join
(select PayeeCode,sum(PayeeAmount) as deduction from tblPayees where
AccountType='d' group by payeeCode) t2
on t1.PayeeCode=t2.PayeeCode
)
curent result
PayeeCode payeename payeeifsccode ga da subamount type
--------------------------------------------------------------
p1 x 123 1300 1400 100 g
p1 z 34 450 550 100 g
p1 y 35 150 150 0 d
p2 z 45 150 100 50 d
expected result:
PayeeCode payeename payeeifsccode ga da subamount type
--------------------------------------------------------------
p1 x 123 1750 1950 200 g
p1 y 35 300 250 50 d
Here this is the Column 'tblPayees.PayeeName' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
I know here why eeror is occured but , i dont want to group by payeeName, i wnat only payeecode.How to do that?
help me please
The problem is that you either need to include the column in group by clause or use an aggregate in select list like MIN, MAX, etc. otherwise the database engine doesn't know what to do with it i.e. what value from the group to select.
Now are you sure you cannot group on both columns? Can you have different PayeeName for the same PayeeCode? If not, you can include it in group by:
select PayeeCode, PayeeName from tblPayees group by PayeeCode, PayeeName
If PayeeName can vary for the same PayeeCode then you need to tell the database engine which value you want to select by means of aggregate function:
select PayeeCode, Max(PayeeName) as PayeeName from tblPayees group by PayeeCode
It is even possible to concatenate the PayeeName values in a comma-separated list, but this is out of scope of this question I guess.

Resources