Grab one record per ID with multiple Lead values - sql

I have a table like this:
ID | Val | Quantity
----------------------
1 | A | 11
1 | B | 15
1 | B | 19
1 | Z | 45
2 | D | 4
2 | E | 25
2 | F | 13
2 | Y | 2
3 | G | 10
3 | H | 15
3 | I | 19
I want to select the top record for each ID ordered by VAL, Quantity AND add the next 2 Val/Quantity within the sort as columns to that row. My expected output look like this:
ID | Val | Quantity | VAL2 | Quantity2 | VAL3 | Quantity3
-------------------------------------------------------------------
1 | A | 11 | B | 15 | B | 19
2 | B | 15 | D | 4 | E | 25
3 | C | 19 | G | 10 | H | 15
I've almost done it using lead, but I don't know how to get rid of the rest of the records in my data-set, as I only want the top.
SELECT ID,
VAL,
Quantity,
lead(VAL,1) over (order by VAL, Quantity ASC) as Val2,
lead(Quantity,1) over (order by VAL, Quantity ASC) as Quantity2,
lead(VAL,2) over (order by VAL, Quantity ASC) as Val3,
lead(Quantity,2) over (order by VAL, Quantity ASC) as Quantity3,
FROM MY_TABLE
order by VAL, Quantity ASC
How can I only select the top record for each ID, while maintaining the lead records? Or is there a more elegant/efficient way to do this?

From your question it seems the expected output should actually be:
ID VAL QUANTITY VAL2 QUANTITY2 VAL3 QUANTITY3
1 A 11 B 15 B 19
2 D 4 E 25 F 13
3 G 10 H 15 I 19
You can get this result with a CTE which generates the LEAD values, as well as a ROW_NUMBER for each set of values. You can then select the first row for each ID from the CTE:
WITH CTE AS (
SELECT ID,
Val, Quantity,
LEAD(Val) OVER (PARTITION BY ID ORDER BY Val, Quantity) AS Val2,
LEAD(Quantity) OVER (PARTITION BY ID ORDER BY Val, Quantity) AS Quantity2,
LEAD(Val, 2) OVER (PARTITION BY ID ORDER BY Val, Quantity) AS Val3,
LEAD(Quantity, 2) OVER (PARTITION BY ID ORDER BY Val, Quantity) AS Quantity3,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Val, Quantity) AS rn
FROM MY_TABLE
)
SELECT ID, Val, Quantity, Val2, Quantity2, Val3, Quantity3
FROM CTE
WHERE rn = 1
Demo on SQLFiddle

You may use ROW_NUMBER to define the order of the rows within the same ID
with t as (
select ID, VAL, QUANTITY,
row_number() over (partition by ID order by VAL, QUANTITY) as rn
from tab)
select *
ID V QUANTITY RN
---------- - ---------- ----------
1 A 11 1
1 B 15 2
1 B 19 3
1 Z 45 4
2 D 4 1
...
In the next step use PIVOT to get the best three values in one row.
with t as (
select ID, VAL, QUANTITY,
row_number() over (partition by ID order by VAL, QUANTITY) as rn
from tab)
select *
from t
PIVOT
(MAX(VAL) as VAL, MAX(QUANTITY) as QUANTITY FOR RN IN (1 as "COL1" ,2 as "COL2",3 as "COL3")
)
ID C COL1_QUANTITY C COL2_QUANTITY C COL3_QUANTITY
---------- - ------------- - ------------- - -------------
1 A 11 B 15 B 19
2 D 4 E 25 F 13
3 G 10 H 15 I 19
If the standar pivot column naming is not fine, simple add a next query and rename the columns.
Note that this query return the same result as apternative approach base on multiple LEAD columns, but you get a bit better flexibility if you plan to vary the number of traced columns.

Related

Select with limited join

I have two tables: products and products_prices.
products table:
id
name
user_id
1
Headphones
1
2
Phone
1
products_prices table:
id
product_id
price
time
1
1
10
1
2
1
15
2
3
1
20
3
4
2
10
4
5
2
15
5
6
2
20
6
I have a simple query:
SELECT * FROM products WHERE (user_id = 1) LIMIT 1 OFFSET 1
So I need to get limited rows from products table with only two prices values from table product_prices ordered by time for each row in products.
(I need to get product with two latest prices).
This is example of what I want to get:
id
user_id
name
curr_price
prev_price
2
1
Phone
20
15
And example of my query:
select products.*,
(SELECT price FROM products_prices WHERE product_id = products.id ORDER BY time asc LIMIT 1 OFFSET 0) as curr_price,
(SELECT price FROM products_prices WHERE product_id = products.id ORDER BY time asc LIMIT 1 OFFSET 1) as prev_price
from "products"
where (products."user_id" = 1)
limit 1 offset 1
Is it possible to do it without subqueries?
Not sure I find any of these easier to read...
0th approach using window functions and a CTE Demo
With products as (SELECT 1 ID, 'Headphones' name, 1 user_id UNION ALL
SELECT 2 ID, 'Phone' name, 1 user_id ),
products_Prices as (SELECT 1 ID, 1 Product_ID, 10 price, 1 time UNION ALL
SELECT 2 ID, 1 Product_ID, 15 price, 2 time UNION ALL
SELECT 3 ID, 1 Product_ID, 20 price, 3 time UNION ALL
SELECT 4 ID, 2 Product_ID, 33 price, 4 time UNION ALL
SELECT 5 ID, 2 Product_ID, 22 price, 5 time UNION ALL
SELECT 6 ID, 2 Product_ID, 11 price, 6 time),
STEP1 as (
SELECT P.ID, P.Name, P.user_ID,
price as CurrentPrice, lead(price) over (partition by P.ID order by time desc) Prev_Price, time,
row_number() over (Partition by P.ID order by time Desc) RN
FROM Products P
LEFT JOIN Products_Prices Z
on Z.Product_ID = P.ID)
SELECT Id, Name, User_ID, CurrentPRice, PRev_Price
From Step1 where RN = 1
Giving us:
+----+------------+---------+--------------+------------+
| id | name | user_id | currentprice | prev_price |
+----+------------+---------+--------------+------------+
| 1 | Headphones | 1 | 20 | 15 |
| 2 | Phone | 1 | 11 | 22 |
+----+------------+---------+--------------+------------+
1st approach using analytics and a CTE: note I changed price numbers to show variance.
DEMO
With products as (SELECT 1 ID, 'Headphones' name, 1 user_id UNION ALL
SELECT 2 ID, 'Phone' name, 1 user_id ),
products_Prices as (SELECT 1 ID, 1 Product_ID, 10 price, 1 time UNION ALL
SELECT 2 ID, 1 Product_ID, 15 price, 2 time UNION ALL
SELECT 3 ID, 1 Product_ID, 20 price, 3 time UNION ALL
SELECT 4 ID, 2 Product_ID, 33 price, 4 time UNION ALL
SELECT 5 ID, 2 Product_ID, 22 price, 5 time UNION ALL
SELECT 6 ID, 2 Product_ID, 11 price, 6 time),
STEP1 as (SELECT P.ID, P.Name, P.user_ID, PP.price, row_number() over (partition by PP.product_ID order by time desc) RN
FROM Products P
LEFT JOIN products_prices PP
on P.ID = PP.Product_ID)
SELECT ID, Name, User_ID, max(case when RN = 1 then Price end) as Current_price, max(case when RN=2 then price end) as Last_price
FROM STEP1
WHERE RN <=2
GROUP BY ID, name, User_ID
Giving us:
+----+------------+---------+---------------+------------+
| id | name | user_id | current_price | last_price |
+----+------------+---------+---------------+------------+
| 2 | Phone | 1 | 11 | 22 |
| 1 | Headphones | 1 | 20 | 15 |
+----+------------+---------+---------------+------------+
Option 2 using lateral.
demo
With products as (SELECT 1 ID, 'Headphones' name, 1 user_id UNION ALL
SELECT 2 ID, 'Phone' name, 1 user_id ),
products_Prices as (SELECT 1 ID, 1 Product_ID, 10 price, 1 time UNION ALL
SELECT 2 ID, 1 Product_ID, 15 price, 2 time UNION ALL
SELECT 3 ID, 1 Product_ID, 20 price, 3 time UNION ALL
SELECT 4 ID, 2 Product_ID, 33 price, 4 time UNION ALL
SELECT 5 ID, 2 Product_ID, 22 price, 5 time UNION ALL
SELECT 6 ID, 2 Product_ID, 11 price, 6 time)
SELECT P.ID, P.Name, P.user_ID, PP.price, time
FROM Products P
LEFT JOIN lateral (SELECT Product_ID, Price, time
FROM Products_Prices Z
WHERE Z.Product_ID = P.ID
ORDER BY Time Desc LIMIT 2) PP
on TRUE
ORDER BY TIME DESC;
Givng us : (unpivoted) and using the row number logic above we could pivot.
+----+------------+---------+-------+------+
| id | name | user_id | price | time |
+----+------------+---------+-------+------+
| 2 | Phone | 1 | 11 | 6 |
| 2 | Phone | 1 | 22 | 5 |
| 1 | Headphones | 1 | 20 | 3 |
| 1 | Headphones | 1 | 15 | 2 |
+----+------------+---------+-------+------+

SQL: Reverse tree traversal on single result

My table looks like this:
id | name | type_id | desc | parent_id
1 | Foo | 1 | Foo | NULL
2 | Bar | 2 | Bar | 1
3 | FB | 2 | FB | 1
4 | Foo1 | 1 | Foo1 | NULL
5 | Bar1 | 2 | Bar1 | 4
6 | FB1 | 2 | FB1 | 4
And I want to provide an ID of the lowest node, returning everything up to the highest node in a single row (There is other data that I'm returning along with this).
For example, I want to provide ID 3, and the results to look like so:
xxxxx (other data) | id | name | type_id | desc | parent_id | id | name | type_id | desc | parent_id
xxxxxxx | 3 | FB | 2 | FB | 1 | 1 | Foo | 1 | Foo | NULL
Unfortunately, I haven't found anything that can work for me. I have a CTE but it goes top down and each node is its own row:
WITH RECURSIVE cte AS (
select T.*
from table as T
where T.id = 3
union all
select T.*
from table as T
inner join cte as C
on T.parent_id = C.id
)
SELECT * FROM cte
When I do this, I only get one result:
id | name | type_id | desc | parent_id
3 | FB | 2 | FB | 1
Any help would be appreciated, thanks!
The logic of the common-table expression looks good; it generates one row for the original id, and then one row per parent. To pivot the resulting rows to columns, you can then use conditional aggregation - this requires that you decide in advance the maximum number of levels. For two levels, this would be:
with recursive cte as (
select t.*, 1 lvl
from table as t
where t.id = 3
union all
select t.*, c.lvl + 1
from table as t
inner join cte as c on t.parent_id = c.id
)
select
max(id) filter(where lvl = 1) id,
max(name) filter(where lvl = 1) name,
max(type_id) filter(where lvl = 1) type_id,
max(descr) filter(where lvl = 1) descr,
max(parent_id) filter(where lvl = 1) parent_id,
max(id) filter(where lvl = 2) id2,
max(name) filter(where lvl = 2) name2,
max(type_id) filter(where lvl = 2) type_id2,
max(descr) filter(where lvl = 2) descr2,
max(parent_id) filter(where lvl = 2) parent_id2,
from cte
You might also want to consider accumating the rows as an array of json objects:
with recursive cte as (
select t.*, 1 lvl
from table as t
where t.id = 3
union all
select t.*, c.lvl + 1
from table as t
inner join cte as c on t.parent_id = c.id
)
select jsonb_agg(to_jsonb(c) order by lvl) res
from cte c
I have used Oracle 11g using Pivot, Row_number and hierarchical queries to solve this.
Demo
WITH CTE1 AS (SELECT A.*, LEVEL AS LVL FROM TABLE1 A
START WITH ID IN (2,3)
CONNECT BY PRIOR PARENT_ID = ID)
select * from (
select x.*, row_number() over (order by id desc) rn from (
SELECT DISTINCT ID, NAME, TYPE_ID, DESCRIPTION, PARENT_ID FROM CTE1 ORDER BY ID DESC) x) y
pivot
( min(id) ID, min(name) name, min(type_id) type_id,
min(description) description, min(parent_id) for rn in (1, 2, 3)
);

Is there a way to get first row of a group in postgres based on Max(date)

Input :
id name value1 value2 date
1 A 1 1 2019-01-01
1 A 2 2 2019-02-15
1 A 3 3 2019-01-15
1 A 1 1 2019-07-13
2 B 1 2 2019-01-01
2 B 1 3 2019-02-15
2 B 2 1 2019-07-13
3 C 2 4 2019-02-15
3 C 1 2 2019-01-01
3 C 1 9 2019-07-13
3 C 3 1 2019-02-15
Expected Output :
id name value1 value2 date
1 A 1 Avg(value2) 2019-07-13
2 B 2 Avg(value2) 2019-07-13
3 C 1 Avg(value2) 2019-07-13
You can use window functions. rank() over() can be used to identify the first record in each group, and avg() over() will give you a window average of value2 in each group:
select id, name, value1, avg_value2 value2, date
from (
select
t.*,
avg(value2) over(partition by id, name) avg_value2,
rank() over(partition by id, name order by date desc) rn
from mytable t
) t
where rn = 1
sort your data in the right way, use the window function row_number() as identifier and select the first entry of every partition.
with temp_data as
(
select
row_number() over (partition by debug.tbl_data.id order by debug.tbl_data.date desc) as index,
*,
avg(debug.tbl_data.value2)over (partition by debug.tbl_data.id) as data_avg
from debug.tbl_data
order by id asc, debug.tbl_data.date desc
)
select
*
from temp_data
where index = 1
You seem to want the most common value of value1. In statistics, this is called the "mode". You can do this as:
select id, name,
mode() within group (order by value1) as value1_mode,
avg(value2),
max(date)
from t
group by id, name;

SQL Server Query group by analytic

I have a table like this
Id scid name namesuffix nameId namesuffixid fullname
--------------------------------------------------------
1 1 a a 100 100 a
2 1 a b 100 101 ab
3 1 b c 101 102 abc
4 1 c d 102 103 abcd
5 2 e e 104 104 e
6 2 e f 104 105 ef
7 2 f g 105 106 efg
8 3 i i 107 107 i
9 3 i j 107 108 ij
10 3 j k 108 109 ijk
11 3 k l 109 110 ijkl
12 3 l m 110 111 ijklm
for each scid (group by scid)
select firstRow fullName
Last row fullName
Expected output
id scid fullname
-------------------
1 1 a
4 1 abcd
5 2 e
7 2 efg
8 3 i
12 3 ijklm
I tried first_value and last_value analytic functions, but the rows are repeating, didn't get expected result.
Any help appreciated.
Another option is to use ROW_NUMBER() and COUNT
select
id, scid, fullname
from (
select
*, row_number() over (partition by scid order by id) rn
, count(*) over (partition by scid) cnt
from
myTable
) t
where
rn = 1
or rn = cnt
You could use FIRST_VALUE and LAST_VALUE as you proposed:
SELECT scid,
FIRST_VALUE(id) OVER(PARTITION BY scid ORDER BY id
ROWS UNBOUNDED PRECEDING) AS id,
FIRST_VALUE(fullname) OVER(PARTITION BY scid ORDER BY id
ROWS UNBOUNDED PRECEDING) AS fullname
FROM tab_name
UNION
SELECT scid,
LAST_VALUE(id) OVER(PARTITION BY scid ORDER BY id
RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS id,
LAST_VALUE(fullname) OVER(PARTITION BY scid ORDER BY id
RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS fullname
FROM tab_name
ORDER BY scid, id;
Demo
There are other ways to do this without window funtions:
select t.*
from t join
(select min(id) as min_id, max(id) as max_id
from t
group by sc_id
) tt
on t.id in (min_id, max_id);
I only suggest this because there are many ways to do what you want. If performance is an issue, you may want to experiment with different methods.

How to select ranges in a range of record in oracle

If I have a table like this
Number Status
------ ------
1 A
2 A
3 A
4 U
5 U
6 A
7 U
8 U
9 A
10 A
What query can I use to group the range into ranges where Status = A?
Range Count Status
----- ----- ------
1-3 3 A
6-6 1 A
9-10 2 A
My query is
select min(number) || '--' || max(number), count(*), Status
from table
where Status = 'A'
group by Status
Range Count Status
----- ----- ------
1-10 6 A
This is a nice way, fancy name "Tabibitosan method" given by Aketi Jyuuzou.
SQL> WITH data AS
2 (SELECT num - DENSE_RANK() OVER(PARTITION BY status ORDER BY num) grp,
3 status,
4 num
5 FROM t
6 )
7 SELECT MIN(num)
8 ||' - '
9 || MAX(num) range,
10 COUNT(*) cnt
11 FROM data
12 WHERE status='A'
13 GROUP BY grp
14 ORDER BY grp
15 /
RANGE CNT
------ ----------
1 - 3 3
6 - 6 1
9 - 10 2
SQL>
Note It is better to use DENSE_RANK to avoid duplicates.
Table
SQL> SELECT * FROM t ORDER BY num;
NUM S
---------- -
1 A
1 A
2 A
2 A
3 A
4 U
5 U
6 A
7 U
8 U
9 A
NUM S
---------- -
10 A
12 rows selected.
There are duplicates for num = 1.
Using DENSE_RANK:
SQL> WITH data AS
2 (SELECT num - DENSE_RANK() OVER(PARTITION BY status ORDER BY num) grp,
3 status,
4 num
5 FROM t
6 )
7 SELECT MIN(num)
8 ||' - '
9 || MAX(num) range,
10 COUNT(*) cnt
11 FROM data
12 WHERE status='A'
13 GROUP BY grp
14 ORDER BY grp
15 /
RANGE CNT
------ ----------
1 - 3 5
6 - 6 1
9 - 10 2
SQL>
Using ROW_NUMBER:
SQL> WITH DATA AS
2 (SELECT num - ROW_NUMBER() OVER(PARTITION BY status ORDER BY num) grp,
3 status,
4 num
5 FROM t
6 )
7 SELECT MIN(num)
8 ||' - '
9 || MAX(num) range,
10 COUNT(*) cnt
11 FROM data
12 WHERE status='A'
13 GROUP BY grp
14 ORDER BY grp
15 /
RANGE CNT
------ ----------
2 - 3 2
1 - 2 2
1 - 6 2
9 - 10 2
SQL>
So, in case of duplicates, the ROW_NUMBER query would give incorrect results. You should use DENSE_RANK.
SQL Fiddle
Oracle 11g R2 Schema Setup:
create table x(
num_ number,
status_ varchar2(1)
);
insert into x values(1,'A');
insert into x values(2,'A');
insert into x values(3,'A');
insert into x values(4,'U');
insert into x values(5,'U');
insert into x values(6,'A');
insert into x values(7,'U');
insert into x values(8,'U');
insert into x values(9,'A');
insert into x values(10,'A');
Query 1:
select min(num_) || '-' || max(num_) range_, status_,
count(1) count_
from
(
select num_, status_,
num_ - row_number() over (order by status_, num_) y --gives a group number to each groups, which have same status over consecutive records.
from x
)
where status_ = 'A'
group by y, status_
order by range_
Results:
| RANGE_ | STATUS_ | COUNT_ |
|--------|---------|--------|
| 1-3 | A | 3 |
| 6-6 | A | 1 |
| 9-10 | A | 2 |