Union and order by - sql

Consider a table like
tbl_ranks
--------------------------------
family_id | item_id | view_count
--------------------------------
1 10 101
1 11 112
1 13 109
2 21 101
2 22 112
2 23 109
3 30 101
3 31 112
3 33 109
4 40 101
4 51 112
4 63 109
5 80 101
5 81 112
5 88 109
I need to generate a result set with the top two(2) rows for a subset of family ids (say, 1,2,3 and 4) ordered by view count.
I'd like to do something like
select top 2 * from tbl_ranks where family_id = 1 order by view_count
union all
select top 2 * from tbl_ranks where family_id = 2 order by view_count
union all
select top 2 * from tbl_ranks where family_id = 3 order by view_count
union all
select top 2 * from tbl_ranks where family_id = 4 order by view_count
but, of course, order by isn't valid in a union all context in this manner. Any suggestions? I know I could run a set of 4 queries, store the results into a temp table and select the contents of that temp as the final result, but I'd rather avoid using a temp table if possible.
Note: in the real app, the number of records per family id is indeterminate, and the view_counts are also not fixed as they appear in the above example.

You can try something like this
DECLARE #tbl_ranks TABLE(
family_id INT,
item_id INT,
view_count INT
)
INSERT INTO #tbl_ranks SELECT 1,10,101
INSERT INTO #tbl_ranks SELECT 1,11,112
INSERT INTO #tbl_ranks SELECT 1,13,109
INSERT INTO #tbl_ranks SELECT 2,21,101
INSERT INTO #tbl_ranks SELECT 2,22,112
INSERT INTO #tbl_ranks SELECT 2,23,109
INSERT INTO #tbl_ranks SELECT 3,30,101
INSERT INTO #tbl_ranks SELECT 3,31,112
INSERT INTO #tbl_ranks SELECT 3,33,109
INSERT INTO #tbl_ranks SELECT 4,40,101
INSERT INTO #tbl_ranks SELECT 4,51,112
INSERT INTO #tbl_ranks SELECT 4,63,109
INSERT INTO #tbl_ranks SELECT 5,80,101
INSERT INTO #tbl_ranks SELECT 5,81,112
INSERT INTO #tbl_ranks SELECT 5,88,109
SELECT *
FROm (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY family_id ORDER BY view_count DESC) MyOrder
FROM #tbl_ranks
) MyOrders
WHERE MyOrder <= 2

If you're using SQL Server 2005 or later, you can take advantage of analytic functions:
SELECT * FROM (
SELECT rank() OVER (PARTITION BY family_id ORDER BY view_count) AS RNK, * FROM ...
)
WHERE RNK <= 2
ORDER BY ...

SELECT tro.*
FROM family
CROSS APPLY
(
SELECT TOP 2 *
FROM tbl_ranks tr
WHERE tr.family_id = family.id
ORDER BY
view_count DESC
) tro
WHERE family.id IN (1, 2, 3, 4)
If you don't have an actual family table, you can construct it using a set of unions or a recursive CTE:
WITH family AS
(
SELECT 1 AS id
UNION ALL
SELECT 2 AS id
UNION ALL
SELECT 3 AS id
UNION ALL
SELECT 4 AS id
)
SELECT tro.*
FROM family
CROSS APPLY
(
SELECT TOP 2 *
FROM tbl_ranks tr
WHERE tr.family_id = family.id
ORDER BY
view_count DESC
) tro
WHERE family.id IN (1, 2, 3, 4)
Make sure you have an index on tbl_ranks (family_id, viewcount).
This will be efficient if you have lots of ranks per family, since analytic functions like ROW_NUMBER will not use the TOP method if used with PARTITION BY.

Use:
SELECT *
FROM (select *,
ROW_NUMBER() OVER (PARTITION BY family_id ORDER BY view_count DESC) 'rank'
from tbl_ranks) x
WHERE x.rank <= 2
ORDER BY ...
The rationale is to assign a ranking, and then filter based on it.

You only have to tweak your suggested SQL commands a little bit to make it work like you wanted. To bind TOP and ORDER BY you can put the statement inside paranthesis which you select from and give a name (not used here but required).
With the DECLARE and INSERT statements from Adriaan Stander's answer the following
SELECT * FROM (SELECT TOP 2 * FROM #tbl_ranks WHERE family_id = 1 ORDER BY view_count) AS dummy1 UNION ALL
SELECT * FROM (SELECT TOP 2 * FROM #tbl_ranks WHERE family_id = 2 ORDER BY view_count) AS dummy2 UNION ALL
SELECT * FROM (SELECT TOP 2 * FROM #tbl_ranks WHERE family_id = 3 ORDER BY view_count) AS dummy3 UNION ALL
SELECT * FROM (SELECT TOP 2 * FROM #tbl_ranks WHERE family_id = 4 ORDER BY view_count) AS dummy4
gives
family_id item_id view_count
1 10 101
1 13 109
2 21 101
2 23 109
3 30 101
3 33 109
4 40 101
4 63 109

Related

Find first match from the result set of a join between two tables in SQL Server

I have a table #testrades like this:
CREATE TABLE #testtrades
(
TradeID int,
producttype varchar(10),
tradeddate date,
settledate date,
busunit varchar(5),
qty int,
price float,
amount float,
tradeside varchar(1),
buysell varchar(1)
)
INSERT #testtrades
SELECT 1,'equity',getdate(),getdate()+3,'bus1',10,100,1000,'S','B'
INSERT #testtrades
SELECT 2,'equity',getdate(),getdate()+3,'bus1',10,100,950,'C','S'
INSERT #testtrades
SELECT 3,'equity',getdate(),getdate()+3,'bus2',11,100,1000,'S','B'
INSERT #testtrades
SELECT 4,'equity',getdate(),getdate()+3,'bus3',10,100,1200,'S','S'
INSERT #testtrades
SELECT 5,'equity',getdate(),getdate()+3,'bus1',10,100,1200,'C','B'
INSERT #testtrades
SELECT 6,'equity',getdate(),getdate()+3,'bus2',10,100,1000,'C','S'
INSERT #testtrades
SELECT 7,'equity',getdate(),getdate()+3,'bus4',10,100,1000,'C','B'
INSERT #testtrades
SELECT 8,'equity',getdate(),getdate()+3,'bus5',10,100,950,'S','S'
INSERT #testtrades
SELECT 9,'equity',getdate(),getdate()+3,'bus5',10,100,1200,'C','S'
INSERT #testtrades
SELECT 10,'equity',getdate(),getdate()+3,'bus4',14,100,1000,'S','B'
INSERT #testtrades
SELECT 11,'equity',getdate(),getdate()+3,'bus6',10,100,1000,'C','S'
INSERT #testtrades
SELECT 12,'equity',getdate(),getdate()+3,'bus7',10,100,950,'C','B'
INSERT #testtrades
SELECT 13,'equity',getdate(),getdate()+3,'bus7',10,100,1200,'C','S'
INSERT #testtrades
SELECT 14,'equity',getdate(),getdate()+3,'bus7',10,100,1000,'S','S'
INSERT #sideA
SELECT * FROM #testtrades
WHERE tradeside='C'
INSERT #sideB
SELECT * FROM #testtrades
WHERE tradeside='S'
SELECT
ROW_NUMBER() OVER (ORDER BY A.tradeID),
A.TradeID, B.TradeID
FROM
#sideA A
JOIN
#sideB B ON A.buysell = B.buysell
AND A.price = B.price
AND A.Qty = B.Qty
AND ABS(A.Amount - B.Amount) <= 50
Result Set:
id tradeidA tradeidB
--------------------
1 2 8
2 2 14
3 6 8
4 6 14
5 7 1
6 9 4
7 11 8
8 11 14
9 12 1
10 13 4
From the above resultset, I only want to retrieve the first matched rows, like tradeIDA 2 is matched with 8 and 14. I only want to retrieve 2,8. Then 6 is matching with 8 first. but since 8 is already matched with 2, 6 is not eligible. Then 6 is matched with 14 so I want to retrieve that record. Expected resultset looks like below.
id tradeIDA tradeIDB
--------------------
1 2 8
4 6 14
5 7 1
6 9 4
maybe this can give you an idea.
;
WITH CTE
AS ( SELECT ROW_NUMBER() OVER ( ORDER BY A.tradeID ) rownum ,
A.TradeID TradeA ,
B.TradeID TradeB
FROM #sideA A
JOIN #sideB B ON A.buysell = B.buysell
AND A.price = B.price
AND A.Qty = B.Qty
AND ABS(A.Amount - B.Amount) <= 50
),
CTE1
AS ( SELECT rownum ,
TradeA ,
ROW_NUMBER() OVER ( ORDER BY rownum ) ctr
FROM ( SELECT rownum ,
TradeA ,
ROW_NUMBER() OVER ( PARTITION BY TradeA ORDER BY rownum ) ctr
FROM cte
) T
WHERE t.ctr = 1
),
CTE2
AS ( SELECT TradeB ,
ROW_NUMBER() OVER ( ORDER BY rownum ) ctr
FROM ( SELECT rownum ,
TradeB ,
ROW_NUMBER() OVER ( PARTITION BY TradeB ORDER BY rownum ) ctr
FROM cte
) T
WHERE t.ctr = 1
),
CTE3
AS ( SELECT CTE1.TradeA [tradeIDA],
CTE2.TradeB [tradeIDB]
FROM CTE1
JOIN CTE2 ON CTE2.ctr = CTE1.ctr
)
SELECT *
FROM CTE3
Result:
tradeIDA tradeIDB
----------- -----------
2 8
6 14
7 1
9 4
(4 row(s) affected)

SQL Grouping by first digit from sets of record

I need your help in SQL
I have a set of records of Cost center ID below.
what I want to do is to segregate/group them by inserting column to distinguish the category.
as you can see all digits start in 7 is belong to the bold digits.
my expected out is on below image also.
You can as the below:
DECLARE #Tbl TABLE (ID INT)
INSERT INTO #Tbl
VALUES
(735121201),
(735120001),
(5442244),
(735141094),
(735141097),
(4008060),
(735117603),
(40100000),
(735142902),
(735151199),
(4010070)
;WITH TableWithRowId
AS
(
SELECT
ROW_NUMBER() OVER (ORDER BY(SELECT NULL)) RowId,
ID
FROM
#Tbl
), TempTable
AS
(
SELECT T.RowId + 1 AS RowId FROM TableWithRowId T
WHERE
LEFT(T.ID, 1) != 7
), ResultTable
AS
(
SELECT
T.RowId ,
T.ID,
DENSE_RANK() OVER (ORDER BY (SELECT TOP 1 A.RowId FROM TempTable A WHERE A.RowId > T.RowId ORDER BY A.RowId)) AS Flag
FROM TableWithRowId T
)
SELECT * FROM ResultTable
Result:
RowId ID Flag
----------- ----------- ----------
1 735121201 1
2 735120001 1
3 5442244 1
4 735141094 2
5 735141097 2
6 4008060 2
7 735117603 3
8 40100000 3
9 735142902 4
10 735151199 4
11 4010070 4
The following query is similer with NEER's
;WITH test_table(CenterID)AS(
SELECT '735121201' UNION ALL
SELECT '735120001' UNION ALL
SELECT '5442244' UNION ALL
SELECT '735141094' UNION ALL
SELECT '735141097' UNION ALL
SELECT '4008060' UNION ALL
SELECT '735117603' UNION ALL
SELECT '40100000' UNION ALL
SELECT '735142902' UNION ALL
SELECT '735151199' UNION ALL
SELECT '4010070'
),t1 AS (
SELECT *,ROW_NUMBER()OVER(ORDER BY(SELECT 1)) AS rn,CASE WHEN LEFT(t.CenterID,1)='7' THEN 1 ELSE 0 END AS isSeven
FROM test_table AS t
),t2 AS(
SELECT t1.*,ROW_NUMBER()OVER(ORDER BY t1.rn) AS toFilter
FROM t1 LEFT JOIN t1 AS pt ON pt.rn=t1.rn-1
WHERE pt.CenterID IS NULL OR (t1.isSeven=1 AND pt.isSeven=0)
)
SELECT t1.CenterID,x.toFilter FROM t1
CROSS APPLY(SELECT TOP 1 t2.toFilter FROM t2 WHERE t2.rn<=t1.rn ORDER BY rn desc) x
CenterID toFilter
--------- --------------------
735121201 1
735120001 1
5442244 1
735141094 2
735141097 2
4008060 2
735117603 3
40100000 3
735142902 4
735151199 4
4010070 4

Group by values that are in sequence

I have some table like this
row chequeNo
1 15
2 19
3 20
4 35
5 16
and I need to get the result like this
row from to
1 15 16
2 19 20
3 35 35
so I need groups of chequeNo where values would be sequential without any interruptions. chequeNo is unique column. Additionally it should be done with one sql select query, because I haven't permissions to create any sql structures except select queries.
So is it possible?
Would be grateful for any help
You can use Aketi Jyuuzou's technique called Tabibitosan here:
SQL> create table mytable (id,chequeno)
2 as
3 select 1, 15 from dual union all
4 select 2, 19 from dual union all
5 select 3, 20 from dual union all
6 select 4, 35 from dual union all
7 select 5, 16 from dual
8 /
Table created.
SQL> with tabibitosan as
2 ( select chequeno
3 , chequeno - row_number() over (order by chequeno) grp
4 from mytable
5 )
6 select row_number() over (order by grp) "row"
7 , min(chequeno) "from"
8 , max(chequeno) "to"
9 from tabibitosan
10 group by grp
11 /
row from to
---------- ---------- ----------
1 15 16
2 19 20
3 35 35
3 rows selected.
Regards,
Rob.
This should work with Oracle 10 (only tested with Oracle 11)
select group_nr + 1,
min(chequeno) as start_value,
max(chequeno) as end_value
from (
select chequeno,
sum(group_change_flag) over (order by rn) as group_nr
from (
select row_number() over (order by chequeno) as rn,
chequeno,
case
when chequeno - lag(chequeno,1,chequeno) over (order by chequeno) <= 1 then 0
else 1
end as group_change_flag
from foo
) t1
) t2
group by group_nr
order by group_nr
(it should work with any DBMS supporting standard SQL windowing functions, e.g. PostgreSQL, DB2, SQL Server 2012)
Here is a "plain vanilla" approach:
SELECT T1.chequeNo, T2.chequeNo
FROM Table1 AS T1 INNER JOIN Table1 AS T2 ON T2.chequeNo >= T1.chequeNo
WHERE
NOT EXISTS (SELECT T0.chequeNo FROM Table1 T0 WHERE T0.chequeNo IN ((T1.chequeNo-1), (T2.chequeNo+1)))
AND (SELECT COUNT(*) FROM Table1 T0 WHERE T0.chequeNo BETWEEN T1.chequeNo AND T2.chequeNo)=(T2.chequeNo - T1.chequeNo + 1)
ORDER BY 1,2
Please let me know if it's too inefficient for large data sets.
CREATE TABLE YOUR_TABLE (
chequeNo NUMBER PRIMARY KEY
);
INSERT INTO YOUR_TABLE VALUES (15);
INSERT INTO YOUR_TABLE VALUES (19);
INSERT INTO YOUR_TABLE VALUES (20);
INSERT INTO YOUR_TABLE VALUES (35);
INSERT INTO YOUR_TABLE VALUES (16);
SELECT T1.chequeNo "from", T2.chequeNo "to"
FROM
(
SELECT chequeNo, ROW_NUMBER() OVER (ORDER BY chequeNo) RN
FROM (
SELECT chequeNo, LAG(chequeNo) OVER (ORDER BY chequeNo) PREV
FROM YOUR_TABLE
)
WHERE PREV IS NULL OR chequeNo > PREV + 1
) T1
JOIN
(
SELECT chequeNo, ROW_NUMBER() OVER (ORDER BY chequeNo) RN
FROM (
SELECT chequeNo, LEAD(chequeNo) OVER (ORDER BY chequeNo) NEXT
FROM YOUR_TABLE
)
WHERE NEXT IS NULL OR chequeNo < NEXT - 1
) T2
USING (RN);
Result:
from to
---------------------- ----------------------
15 16
19 20
35 35
If we spice things up a little...
INSERT INTO YOUR_TABLE VALUES (17);
INSERT INTO YOUR_TABLE VALUES (18);
...we get:
from to
---------------------- ----------------------
15 20
35 35

In Oracle, how do I get a page of distinct values from sorted results?

I have 2 columns in a one-to-many relationship. I want to sort on the "many" and return the first occurrence of the "one". I need to page through the data so, for example, I need to be able to get the 3rd group of 10 unique "one" values.
I have a query like this:
SELECT id, name
FROM table1
INNER JOIN table2 ON table2.fkid = table1.id
ORDER BY name, id;
There can be multiple rows in table2 for each row in table1.
The results of my query look like this:
id | name
----------------
2 | apple
23 | banana
77 | cranberry
23 | dark chocolate
8 | egg
2 | yak
19 | zebra
I need to page through the result set with each page containing n unique ids. For example, if start=1 and n=4 I want to get back
2
23
77
8
in the order they were sorted on (i.e., name), where id is returned in the position of its first occurrence. Likewise if start=3 and n=4 and order = desc I want
8
23
77
2
I tried this:
SELECT * FROM (
SELECT id, ROWNUM rnum FROM (
SELECT DISTINCT id FROM (
SELECT id, name
FROM table1
INNER JOIN table2 ON table2.fkid = table1.id
ORDER BY name, id)
WHERE ROWNUM <= 4)
WHERE rnum >=1)
which gave me the ids in numerical order, instead of being ordered as the names would be.
I also tried:
SELECT * FROM (
SELECT DISTINCT id, ROWNUM rnum FROM (
SELECT id FROM (
SELECT id, name
FROM table1
INNER JOIN table2 ON table2.fkid = table1.id
ORDER BY name, id)
WHERE ROWNUM <= 4)
WHERE rnum >=1)
but that gave me duplicate values.
How can I page through the results of this data? I just need the ids, nothing from the "many" table.
update
I suppose I'm getting closer with changing my inner query to
SELECT id, name, rank() over (order by name, id)
FROM table1
INNER JOIN table2 ON table2.fkid = table1.id
...but I'm still getting duplicate ids.
You may need to debug it a little, but but it will be something like this:
SELECT * FROM (
SELECT * FROM (
SELECT id FROM (
SELECT id, name, row_number() over (partition by id order by name) rn
FROM table1
INNER JOIN table2 ON table2.fkid = table1.id
)
) WHERE rn=1 ORDER BY name, id
) WHERE rownum>=1 and rownum<=4;
It's a bit convoluted (and I would tend to suspect that it could be simplified) but it should work. You'd can put whatever start and end position you want in the WHERE clause-- I'm showing here with start=2 and n=4 are pulled from a separate table but you could simplify things by using a couple of parameters instead.
SQL> ed
Wrote file afiedt.buf
1 with t as (
2 select 2 id, 'apple' name from dual union all
3 select 23, 'banana' from dual union all
4 select 77, 'cranberry' from dual union all
5 select 23, 'dark chocolate' from dual union all
6 select 8, 'egg' from dual union all
7 select 2, 'yak' from dual union all
8 select 19, 'zebra' from dual
9 ),
10 x as (
11 select 2 start_pos, 4 n from dual
12 )
13 select *
14 from (
15 select distinct
16 id,
17 dense_rank() over (order by min_id_rnk) outer_rnk
18 from (
19 select id,
20 min(rnk) over (partition by id) min_id_rnk
21 from (
22 select id,
23 name,
24 rank() over (order by name) rnk
25 from t
26 )
27 )
28 )
29 where outer_rnk between (select start_pos from x) and (select start_pos+n-1 from x)
30* order by outer_rnk
SQL> /
ID OUTER_RNK
---------- ----------
23 2
77 3
8 4
19 5

tsql - Setting sequential values without looping/cursoring

I need to set a non-unique identifier in a data table. This would be sequential within a group ie. for each group, the ID should start at 1 and rise in incremements of 1 until the last row for that group.
This is illustrated by the table below. "New ID" is the column I need to populate.
Unique ID Group ID New ID
--------- -------- ------
1 1123 1
2 1123 2
3 1124 1
4 1125 1
5 1125 2
6 1125 3
7 1125 4
Is there any way of doing this without looping/cursoring? If looping/cursoring is the only way, what would the most efficient code be?
Thanks
One method is to use ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...) in an UPDATE...FROM statement with a subquery in the FROM clause.
update MyTable set NewID = B.NewID
from
MyTable as A
inner join (select UniqueID, ROW_NUMBER() over (partition by GroupID order by UniqueID) as NewID from MyTable) as B on B.UniqueID = A.UniqueID
MSDN has a good sample to get you started:
You need to utilize a subquery in the FROM clause in order to utilize a windows function (Row_Index())
Partition By tells the server when to reset the row numbers
Order By tells the server which way to order the group's NewID's
I agree with Damien's point in the comments but you don't need a JOIN you can just update the CTE directly.
;WITH cte AS
(
SELECT [New ID],
ROW_NUMBER() OVER (PARTITION BY [Group ID] ORDER BY [Unique ID]) AS _NewID
FROM #T
)
UPDATE cte
SET [New ID] = _NewID
Online Demo
Alternate to RowNumber() if you're on SS 2000
SELECT UniqueID,
GroupID,
(SELECT COUNT(T2.GroupID)
FROM myTable T2
WHERE GroupID <= T1.GroupID) AS NewID
FROM myTable T1
This solution will also work, if you are running an old version of mssql
--Test table:
DECLARE #t table(Unique_ID int, Group_ID int, New_ID int)
--Test data:
INSERT #t (unique_id, group_id)
SELECT 1, 1123 UNION ALL SELECT 2, 1123 UNION ALL SELECT 3, 1124 UNION ALL SELECT 4, 1125 UNION ALL SELECT 5, 1125 UNION ALL SELECT 6, 1125 UNION ALL SELECT 7, 1125
--Syntax:
UPDATE t
SET new_id =
(SELECT count(*)
FROM #t
WHERE t.unique_id >= unique_id and t.group_id = group_id
GROUP BY group_id)
FROM #t t
--Result:
SELECT * FROM #t
Unique_ID Group_ID New_ID
----------- ----------- -----------
1 1123 1
2 1123 2
3 1124 1
4 1125 1
5 1125 2
6 1125 3
7 1125 4
SELECT
UniqueId,
GroupID,
ROW_NUMBER() OVER (PARTITION BY GroupId ORDER BY UniqueId) AS NewIdx
FROM
....