How to order an already ordered subquery - sql

Creating this table:
CREATE TABLE #Test (id int, name char(10), list int, priority int)
INSERT INTO #Test VALUES (1, 'One', 1, 1)
INSERT INTO #Test VALUES (2, 'Two', 2, 1)
INSERT INTO #Test VALUES (3, 'Three', 3, 2)
INSERT INTO #Test VALUES (4, 'Four', 4, 1)
INSERT INTO #Test VALUES (5, 'THREE', 3, 1)
and ordering it by, list and priority:
SELECT * FROM #Test ORDER BY list, priority
1 | One | 1 | 1
2 | Two | 2 | 1
5 | THREE | 3 | 1
3 | Three | 3 | 2
4 | Four | 4 | 1
However I want to step through rows one by one selecting the top one for each list ordered by priority, and start over when I get to the end.
For example with this simpler table:
1 | One | 1 | 1
2 | Two | 2 | 1
3 | Three | 3 | 1
4 | Four | 4 | 1
and this query:
SELECT TOP 1 * FROM #Test ORDER BY (CASE WHEN list>#PreviousList THEN 1 ELSE 2 END)
If #PreviousList is the list for the previous row I got, then this will select the next row and gracefully jump to the top when I have selected the last row.
But there are rows that will have the same list only ordered by priority - like my first example:
1 | One | 1 | 1
2 | Two | 2 | 1
5 | THREE | 3 | 1
3 | Three | 3 | 2
4 | Four | 4 | 1
Here id=3 should be skipped because id=5 have the same list ordering and a better priority. The only way I can think of doing this is simply by first order the entire list by list and priority, and then run the order by that goes through the rows one by one, like this:
SELECT TOP 1 * FROM (
SELECT * FROM #Test ORDER BY list, priority
) ORDER BY (CASE WHEN list>#PreviousList THEN 1 ELSE 2 END)
But of course I cannot order by an already ordered subquery and get the error:
The ORDER BY clause is invalid in views, inline functions, derived tables,
subqueries, and common table expressions, unless TOP or FOR XML is also
specified.
Are there any ways and can get past this problem or get the query down to a single query and order by?

Another possible solution is to use a subquery to select the min priority grouped by list and join it back to the table for the rest of the details
SELECT T2.*
FROM (SELECT MIN(priority) as priority, list
FROM #Test
GROUP BY list) AS T1
INNER JOIN #Test T2 ON T1.list = T2.list AND T1.priority = T2.priority
ORDER BY T1.list, T1.priority

I want to step through rows one by one selecting the top one for each
list ordered by priority, and start over when I get to the end.
You can use the built in ROW_NUMBER function that is designed for these scenarios with OVER(PARTITION BY name ORDER BY priority) to do this directly:
WITH CTE
AS
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY name ORDER BY priority) AS RN
FROM #Test
)
SELECT *
FROM CTE
WHERE RN = 1;
Live DEMO
The ranking number rn generated by ROW_NUMBER() OVER(PARTITION BY name ORDER BY priority) will rank each group of rows that has the same name ordered by priority then when you filtered by WHERE rn = 1 it will remove all the duplicate with the same name and left only the first priority.

SELECT TOP 1 * FROM (
SELECT * FROM #Test
) ORDER BY (CASE WHEN list>#PreviousList THEN 1 ELSE 2 END)
Try this, because Order By is not allowed in CTE.

Perhaps I am missing the requirement that makes this harder than I realize, but what about a nice simple join to select highest priority for the list. To scale, performance would require an index on list.
select t.*
, ttop.id as firstid
from #test t
JOIN #test ttop on ttop.id = (SELECT TOP 1 ID
FROM #TEST tbest
WHERE t.list = tbest.list order by priority)
and ttop.id = t.id -- this does the trick!

Related

Get the min of one column but select multiple columns

I have a table as following:
ID NAME AMOUNT
______________________
1 A 3
1 B 4
2 C 18
4 I 2
4 P 9
And I want the min(Amount) for each ID but I still want to display its Name. So I want this:
ID NAME min(AMOUNT)
______________________
1 A 3
2 C 18
4 I 2
ID's can occur multiple times, Names too. I tried this:
SELECT ID, NAME, min(AMOUNT) FROM TABLE
GROUP BY ID
But of course its an error because I have to
GROUP BY ID, NAME
But then I get
ID NAME AMOUNT
______________________
1 A 3
1 B 4
2 C 18
4 I 2
4 P 9
And I understand why, it looks for the min(AMOUNT) for each combination of ID + NAME. So my question is basically, how can I select multiple column (ID, NAME, AMOUNT) and get the minimum for only one column, still displaying the others?
Im new to SQL but I cant seem to find an answer..
If you are using PostgreSQL, SQL Server, MySQL 8.0 and Oracle then try the following with window function row_number().
in case you have one id with similar amount then you can use dense_rank() instead of row_number()
Here is the demo.
select
id,
name,
amount
from
(
select
*,
row_number() over (partition by id order by amount) as rnk
from yourTable
) val
where rnk = 1
Output:
| id | name | amount |
| --- | ---- | ------ |
| 1 | A | 3 |
| 2 | C | 18 |
| 4 | I | 2 |
Second Option without using window function
select
val.id,
t.name,
val.amount
from myTable t
join
(
select
id,
min(amount) as amount
from myTable
group by
id
) val
on t.id = val.id
and t.amount = val.amount
You did not specify your db vendor. If it is luckily Postgres, the problem can be also solved without nested subquery using proprietary distinct on clause:
with t(id,name,amount) as (values
(1, 'A', 3),
(1, 'B', 4),
(1, 'W', 3),
(2, 'C', 18),
(4, 'I', 2),
(4, 'P', 9)
)
select distinct on (id, name_of_min) id
, first_value(name) over (partition by id order by amount) as name_of_min
, amount
from t
order by id, name_of_min
Just for widening knowledge. I don't recommend using proprietary features. first_value is standard function but to solve problem in simple query is still not enough. #zealous' answer is perfect.
In many databases, the most efficient method uses a correlated subquery:
select t.*
from t
where t.amount = (select min(t2.amount) from t t2 where t2.id = t.id);
In particular, this can take advantage of an index on (id, amount).

SQL select a row X times and insert into new

I am trying to migrate a bunch of data from an old database to a new one, the old one used to just have the number of alarms that occurred on a single row. The new database inserts a new record for each alarm that occurs. Here is a basic version of how it might look. I want to select each row from Table 1 and insert the number of alarm values as new rows into Table 2.
Table 1:
| Alarm ID | Alarm Value |
|--------------|----------------|
| 1 | 3 |
| 2 | 2 |
Should go into the alarm table as the below values.
Table 2:
| Alarm New ID | Value |
|--------------|----------|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 2 |
I want to create a select insert script that will do this, so the select statement will bring back the number of rows that appear in the "Value" column.
A recursive CTE can be convenient for this:
with cte as (
select id, alarm, 1 as n
from t
union all
select id, alarm, n + 1
from cte
where n < alarm
)
select row_number() over (order by id) as alarm_id, id as value
from cte
order by 1
option (maxrecursion 0);
Note: If your values do not exceed 100, then you can remove OPTION (MAXRECURSION 0).
Replicate values out with a CTE.
DECLARE #T TABLE(AlarmID INT, Value INT)
INSERT #T VALUES
(1,3),
(2,2)
;WITH ReplicateAmount AS
(
SELECT AlarmID, Value FROM #T
UNION ALL
SELECT R.AlarmID, Value=(R.Value - 1)
FROM ReplicateAmount R
INNER JOIN #T T ON R.AlarmID = T.AlarmID
WHERE R.Value > 1
)
SELECT
AlarmID = ROW_NUMBER() OVER( ORDER BY AlarmID),
Value = AlarmID --??
FROM
ReplicateAmount
ORDER BY
AlarmID
This answers your question. I would think the query below would be more useful, however, you did not include usage context.
SELECT
AlarmID,
Value
FROM
ReplicateAmount
ORDER BY
AlarmID
Rather than using an rCTE, which is recursive (as the name suggests) and will fail at 100 rows, you can use a Tally table, which tend to be far faster as well:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3)
SELECT ROW_NUMBER() OVER (ORDER BY V.AlarmID,T.I) AS AlarmNewID,
V.AlarmID
FROM (VALUES(1,3),(2,2))V(AlarmID,AlarmValue)
JOIN Tally T ON V.AlarmValue >= T.I;

SQL - Calculate next row based on previous in the same column

I have spent hours trying to solve this with loops, the lag function but it doesn't solve my problem. I have a table where the first row of a particular field is populated, the next row is calculated based on a subtraction of the previous row of data from 2 columns, the next row is then based on the result of this. The example is below of the original table and the result set:
a b a b
502.5 33.85 502.5 33.85
25.46 468.65 25.46
20.83 443.19 20.83
133.07 422.36 133.07
144.65 289.29 144.65
144.65 144.64 144.65
I have tried several different methods with stored procedures and can get the 2nd row result set but I can't get it to continue and calculate the rest of the fields, it's easy in excel but not so in SQL. Any suggestions?
If your RDBMS supports windowed aggregate functions:
Assuming you have an id or some such thing that is determining the order of your rows (as you indicated there is a first).
You can use the max() over() (in this case min() works instead of max() as well) and sum() over() windowed aggregate functions
select
id
, max(a) over (order by id) - (sum(b) over (order by id) - b) as a
, b
from t
rextester demo: http://rextester.com/MGKM17497
returns:
+----+--------+--------+
| id | a | b |
+----+--------+--------+
| 1 | 502,50 | 33,85 |
| 2 | 468,65 | 25,46 |
| 3 | 443,19 | 20,83 |
| 4 | 422,36 | 133,07 |
| 5 | 289,29 | 144,65 |
| 6 | 144,64 | 144,65 |
+----+--------+--------+
In case, as I saw data before editing )
This solution also assumes that you have id column and order depends on this column
with t(id, a, b) as(
select 1, 502.5, 33.85 union all
select 2, 25.46, null union all
select 3, 20.83, null union all
select 4, 133.07, null union all
select 5, 144.65, null union all
select 6, 144.65, null
)
select case when id = 1 then a else b end as a, case when id = 1 then (select b from t order by id offset 0 rows fetch next 1 rows only) else a end as b from (
select id, a, lag((select a from t order by id offset 0 rows fetch next 1 rows only)-s) over(order by id) as b from (
select id, a, sum(case when b is null then a else b end ) over(order by id) s
from t
) tt
) ttt

SELECT First Group

Problem Definition
I have an SQL query that looks like:
SELECT *
FROM table
WHERE criteria = 1
ORDER BY group;
Result
I get:
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1
B | 2 | 1
B | 3 | 1
Expected Result
However, I would like to limit the results to only the first group (in this instance, A). ie,
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1
What I've tried
Group By
SELECT *
FROM table
WHERE criteria = 1
GROUP BY group;
I can aggregate the groups using a GROUP BY clause, but that would give me:
group | value
-------------
A | 0
B | 2
or some aggregate function of EACH group. However, I don't want to aggregate the rows!
Subquery
I can also specify the group by subquery:
SELECT *
FROM table
WHERE criteria = 1 AND
group = (
SELECT group
FROM table
WHERE criteria = 1
ORDER BY group ASC
LIMIT 1
);
This works, but as always, subqueries are messy. Particularly, this one requires specifying my WHERE clause for criteria twice. Surely there must be a cleaner way to do this.
You can try following query:-
SELECT *
FROM table
WHERE criteria = 1
AND group = (SELECT MIN(group) FROM table)
ORDER BY value;
If your database supports the WITH clause, try this. It's similar to using a subquery, but you only need to specify the criteria input once. It's also easier to understand what's going on.
with main_query as (
select *
from table
where criteria = 1
order by group, value
),
with min_group as (
select min(group) from main_query
)
select *
from main_query
where group in (select group from min_group);
-- this where clause should be fast since there will only be 1 record in min_group
Use DENSE_RANK()
DECLARE #yourTbl AS TABLE (
[group] NVARCHAR(50),
value INT,
criteria INT
)
INSERT INTO #yourTbl VALUES ( 'A', 0, 1 )
INSERT INTO #yourTbl VALUES ( 'A', 1, 1 )
INSERT INTO #yourTbl VALUES ( 'B', 2, 1 )
INSERT INTO #yourTbl VALUES ( 'B', 3, 1 )
;WITH cte AS
(
SELECT i.* ,
DENSE_RANK() OVER (ORDER BY i.[group]) AS gn
FROM #yourTbl AS i
WHERE i.criteria = 1
)
SELECT *
FROM cte
WHERE gn = 1
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1

SQL Server: Select only one row of rows that has the same ID on some coulmn

I have a table that has 3 columns:
- ID
- FROM
- TO
And i have data like that
-----------------------
ID | FROM | TO
1 | 2 | 1
2 | 5 | 1
3 | 7 | 1
4 | 2 | 1
5 | 2 | 1
6 | 9 | 1
7 | 3 | 1
8 | 4 | 1
9 | 5 | 1
I would like to create a query that selects all rows where TO = 1 and i don't want to display rows that was previously retrieved, for example i have multiple rows where FROM = 2 and TO = 1, i just need to retrieve that row only once.
My table doesn't really look like this but i am giving a small example because my aim is to collect all FROM numbers but without any redundancy.
use distinct keyword
select distinct m.from,m.to from mytable as m;
Use DISTINCT
SELECT DISTINCT from,to FROM yourTable WHERE to = 1
You just have to group by the columns you want to display:
select [from] from mytable group by [from]
If you want to see how many froms you have all you have to do is:
select [from], count(*) from mytable group by [from]
You could use distinct but it would slower than group by but require more memory.
Please read here if you want an explanation on the difference between group by and distinct:
Huge performance difference when using group by vs distinct
Not sure what exactly you meant select distinct [FROM] from TableName where [TO] = 1
OR
may be you need single row for every distinct [FROM] value for given [TO] ?
;with cte as (
select ID, [FROM], [TO],
rn = row_number() over (partition by [FROM] order by ID)
from TableName
where [TO] = 1
)
select ID, [FROM], [TO]
from cte
where rn=1