I need to show values from one column in a SQL tabe into 4 columns according its top 4 values.
Example current table:
----------------------------------
ID | Name | Amount
1 | Test | 80
1 | Test2| 70
1 | Test3| 40
1 | Any | 25
1 | Any1 | 15
1 | Any2 | 12
1 | Any3 | 5
2 | TS1 | 70
2 | TS2 | 55
2 | TS3 | 30
2 | TS4 | 19
2 | Any | 11
--------------------------
Example expected SELECT Query result:
----------------------------------
ID | Col1 | Col2 | Col3 | Col4
1 | 80 | 70 | 40 | 25
2 | 70 | 55 | 30 | 19
----------------------------------
The issue here is to group the top 4 amount in 4 columns not considering names, just the numbers.
Is there some way to reach this result in table like that?
Please try this pseudocode. Where data retrieves as per given ordering in sample input or storing position in database.
SELECT t.id
, MAX(CASE WHEN row_num = 1 THEN t.amount END) col1
, MAX(CASE WHEN row_num = 2 THEN t.amount END) col2
, MAX(CASE WHEN row_num = 3 THEN t.amount END) col3
, MAX(CASE WHEN row_num = 4 THEN t.amount END) col4
FROM (SELECT id
, amount
, ROW_NUMBER() OVER (PARTITION BY id) row_num
FROM test) t
WHERE t.row_num <= 4
GROUP BY t.id;
Please check from url https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=8d3b9a3da33f9177f2bb6957ef08e21b
Also you can use amount in ORDER BY clause in descending order as per expected result if needed. Try this pseudocode then.
SELECT t.id
, MAX(CASE WHEN row_num = 1 THEN t.amount END) col1
, MAX(CASE WHEN row_num = 2 THEN t.amount END) col2
, MAX(CASE WHEN row_num = 3 THEN t.amount END) col3
, MAX(CASE WHEN row_num = 4 THEN t.amount END) col4
FROM (SELECT id
, amount
, ROW_NUMBER() OVER (PARTITION BY id ORDER BY amount DESC) row_num
FROM test) t
WHERE t.row_num <= 4
GROUP BY t.id;
FIRST STEP: Create Ranking (1 to 4) with order by DESC amount as TOP 4 will be highest to lowest
Second Step: USE MAX() on ranking to get all the rankings in one row rather than diagonally
WITH data as (
SELECT
ID,
AMOUNT,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY AMOUNT DESC) RANKING
FROM [TABLE NAME]
)
SELECT
ID,
MAX(CASE WHEN RANKING = 1 THEN AMOUNT ELSE NULL END) COL_1,
MAX(CASE WHEN RANKING = 2 THEN AMOUNT ELSE NULL END) COL_2,
MAX(CASE WHEN RANKING = 3 THEN AMOUNT ELSE NULL END) COL_3,
MAX(CASE WHEN RANKING = 4 THEN AMOUNT ELSE NULL END) COL_4
FROM data
GROUP BY 1
Alternatively you can use window functionality to achieve this result.
Below is the query written in postgresql
select
id,col1,col2,col3,col4
from
(
SELECT
tbl.id
,tbl.amount AS col1 -- 1st
,LEAD(tbl.amount, 1) OVER (ORDER BY id ASC) as col2 --2nd
,LEAD(tbl.amount, 2) OVER (ORDER BY id ASC) as col3 --3rd
,LEAD(tbl.amount, 3) OVER (ORDER BY id ASC) as col4 --4th
,ROW_NUMBER() OVER (PARTITION BY id order by amount desc) rn
FROM
(
SELECT
id
, amount
, ROW_NUMBER() OVER (PARTITION BY id order by amount desc) rn
FROM <TABLE_NAME>
) tbl
WHERE 1=1 and tbl.rn <= 4 -- 4: variable
)tbl2
where 1=1 and tbl2.rn =1 -- 1: fixed
;
LEAD(column, n) returns column's value at the row n rows aer the current row
Related
I have a mysql table that stores all customer orders as follows (simplified for question):
+----------+---------------+------+
| Field | Type | Null |
+----------+---------------+------+
| id (pk) | int(10) | NO |
| cust_id | int(10) | NO |
| total | decimal(10,2) | NO |
| created | datetime) | NO |
+----------+---------------+------+
In one query, I wish to get each user's first ever order and the order total and their most recent order and that order total
So that I should have results like:
+----------+------------------+---------------+------------------+---------------+
| cust_id | first_ord_total | first_ord_date| last_ord_total | last_ord_date |
+----------+------------------+---------------+------------------+---------------+
| 123 | 150.48 | 2018-03-01 | 742.25 | 2020-05-19 |
| 456 | 20.99 | 2019-08-01 | 67.22 | 2020-09-17 |
| 789 | 259.99 | 2019-01-01 | 147.15 | 2020-08-31 |
+----------+------------------+---------------+------------------+---------------+
I seem to be able to get the first and last order dates using MIN and MAX but I can't link it back to also give the order total from that same order/record
I know this is possible but I'm struggling to get it right
If your version of MySql supports window functions, with MIN(), MAX() and FIRST_VALUE():
select distinct cust_id,
first_value(total) over (partition by cust_id order by created) first_order_total,
min(created) over (partition by cust_id) first_order_date,
first_value(total) over (partition by cust_id order by created desc) last_order_total,
max(created) over (partition by cust_id) last_order_date
from customers
Without window functions, use a query that retutns the first and the last order dates of each customer and join it to the table where you use conditional aggregation:
select c.cust_id,
max(case when c.created = t.min_created then c.total end) first_order_total,
max(case when c.created = t.min_created then c.created end) first_order_date,
max(case when c.created = t.max_created then c.total end) last_order_total,
max(case when c.created = t.max_created then c.created end) last_order_date
from customers c
inner join (
select cust_id, min(created) min_created, max(created) max_created
from customers
group by cust_id
) t on t.cust_id = c.cust_id and c.created in (t.min_created, t.max_created)
group by c.cust_id
On MySQL 8+, ROW_NUMBER comes in handy here:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BY created) rn_first,
ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BY created DESC) rn_last
FROM orders
)
SELECT
cust_id,
MAX(CASE WHEN rn_first = 1 THEN total END) AS first_ord_total,
MAX(CASE WHEN rn_first = 1 THEN created END) AS first_ord_date,
MAX(CASE WHEN rn_last = 1 THEN total END) AS last_ord_total,
MAX(CASE WHEN rn_last = 1 THEN created END) AS last_ord_date
FROM cte
GROUP BY
cust_id;
The stategy here is to use ROW_NUMBER, per each customer, to identify the first and last transaction records, in a CTE. Then, we aggregate by customer to find the first and last total amounts and dates.
I have a table like so
id | status | data | date
----|---------|--------|-------
1 | START | a4c | Jan 1
2 | WORKING | 2w3 | Dec 29
3 | WORKING | 2d3 | Dec 29
4 | WORKING | 3ew | Dec 26
5 | WORKING | 5r5 | Dec 23
6 | START | 2q3 | Dec 22
7 | WORKING | 32w | Dec 20
8 | WORKING | 9k5 | Dec 10
and so on...
What I am trying to do, is to get the number of 'WORKING' rows between two 'START' i.e.
id | status | count | date
----|---------|--------|-------
1 | START | 4 | Jan 1
6 | START | 2 | Dec 22
and so on ...
I am using MySql 5.7.28.
Highly appreciate any help/suggestion!
date is unusable in the example, try using id as an ordering column instead
select id, status,
(select count(*)
from mytable t2
where t2.id > t.id and t2.status='WORKING'
and not exists (select 1
from mytable t3
where t3.id > t.id and t3.id < t2.id and status='START')
) count,
date
from mytable t
where status='START';
Fiddle
Assuming id is safe then you can do this by finding the next id for each block (and assigning some dummy values) then grouping by next id
drop table if exists t;
create table t
(id int,status varchar(20), data varchar(3),date varchar(10));
insert into t values
( 1 , 'START' , 'a4c' , 'Jan 1'),
( 2 , 'WORKING' , '2w3' , 'Dec 29'),
( 3 , 'WORKING' , '2d3' , 'Dec 29'),
( 4 , 'WORKING' , '3ew' , 'Dec 26'),
( 5 , 'WORKING' , '5r5' , 'Dec 23'),
( 6 , 'START' , '2q3' , 'Dec 22'),
( 7 , 'WORKING' , '32w' , 'Dec 20'),
( 8 , 'WORKING' , '9k5' , 'Dec 10');
SELECT MIN(ID) ID,
'START' STATUS,
SUM(CASE WHEN STATUS <> 'START' THEN 1 ELSE 0 END) AS OBS,
Max(DATE) DATE
FROM
(
select t.*,
CASE WHEN STATUS = 'START' THEN DATE ELSE '' END AS DT,
COALESCE(
(select t1.id from t t1 where t1.STATUS = 'START' and t1.id > t.id ORDER BY T1.ID limit 1)
,99999) NEXTID
from t
) S
GROUP BY NEXTID;
+------+--------+------+--------+
| ID | STATUS | OBS | DATE |
+------+--------+------+--------+
| 1 | START | 4 | Jan 1 |
| 6 | START | 2 | Dec 22 |
+------+--------+------+--------+
2 rows in set (0.00 sec)
This is a form of gaps-and-islands problem -- which is simpler in MySQL 8+ using window functions.
In older versions, probably the most efficient method is to accumulate a count of starts to define groupings for the rows. You can do this using variables and then aggregate:
select min(id) as id, 'START' as status, sum(status = 'WORKING') as num_working, max(date) as date
from (select t.*, (#s := #s + (t.status = 'START')) as grp
from (select t.* from t order by id asc) t cross join
(select #s := 0) params
) t
group by grp
order by min(id);
Here is a db<>fiddle.
SELECT id, status, `count`, `date`
FROM ( SELECT #count `count`,
id,
status,
`date`,
#count:=(#status=status)*#count+1,
#status:=status
FROM test,
( SELECT #count:=0, #status:='' ) init_vars
ORDER BY id DESC
) calculations
WHERE status='START'
ORDER BY id
> Since I am still in design/development I can move to MySQL 8 if that makes it easier for this logic? Any idea how this could be done with Windows functions? – N0000B
WITH cte AS ( SELECT id,
status,
`date`,
SUM(status='WORKING') OVER (ORDER BY id DESC) workings
FROM test
ORDER BY id )
SELECT id,
status,
workings - COALESCE(LEAD(workings) OVER (ORDER BY id), 0) `count`,
`date`
FROM cte
WHERE status='START'
ORDER BY id
fiddle
I would like to have a column showing the rank (highest amount being #1) of this result set. Can this be done somehow?
Here is the query to produce this result:
SELECT user_names.user_name,city.city,state.state,SUM(events_full.amount) AS total
FROM user_names,city,state,events_full
WHERE user_names.user_id=events_full.user_id
AND city.city_id=events_full.city_id
AND state.state_id=events_full.state_id
AND events_full.season_id=13
AND amount > 0
Group By user_names.user_name
I've got a hunch that you actually use MariaDB.
(based on your comment in a deleted answer)
Then you could try to add a DENSE_RANK over the SUM to your SELECT
DENSE_RANK() OVER (ORDER BY SUM(events_full.amount) DESC) AS Ranking
A simplyfied example:
create table test
(
col1 int,
col2 int
);
insert into test values
(1,1),(1,2),(1,3),
(2,1),(2,2),(2,3),(2,4),
(3,1),(3,5),
(4,1),(4,2);
select col1
, sum(col2) tot
, dense_rank() over (order by sum(col2) desc) rnk
from test
group by col1
order by rnk
col1 | tot | rnk
---: | --: | --:
2 | 10 | 1
1 | 6 | 2
3 | 6 | 2
4 | 3 | 3
db<>fiddle here
In MySql 5.7 it can be emulated via variables
For example:
select *
from
(
select col1, total
, case
when total = #prev_tot
and #prev_tot := total
then #rnk
when #prev_tot := total
then #rnk := #rnk + 1
end as rnk
from
(
select col1
, sum(col2) as total
from test
group by col1
order by total desc
) q1
cross join (select #rnk:=0, #prev_tot:=0) v
) q2
order by rnk;
col1 | total | rnk
---: | ----: | :--
2 | 10 | 1
1 | 6 | 2
3 | 6 | 2
4 | 3 | 3
db<>fiddle here
I'd like to get the Date & ID which corresponds to the lowest and Largest Time, respectively the extreme rows in the table below with ID 5 & 4.
Please note the following:
Dates are stored as values in ms
The ID reflects the Order By Date ASC
Below I have split the Time to make it clear
* indicates the two rows to return.
Values should be returns as columns, i.e: SELECT minID, minDate, maxID, maxDate FROM myTable
| ID | Date | TimeOnly |
|----|---------------------|-----------|
| 5 | 14/11/2019 10:01:29 | 10:01:29* |
| 10 | 15/11/2019 10:01:29 | 10:01:29 |
| 6 | 14/11/2019 10:03:41 | 10:03:41 |
| 7 | 14/11/2019 10:07:09 | 10:07:09 |
| 11 | 15/11/2019 12:01:43 | 12:01:43 |
| 8 | 14/11/2019 14:37:16 | 14:37:16 |
| 1 | 12/11/2019 15:04:50 | 15:04:50 |
| 9 | 14/11/2019 15:04:50 | 15:04:50 |
| 2 | 13/11/2019 18:10:41 | 18:10:41 |
| 3 | 13/11/2019 18:10:56 | 18:10:56 |
| 4 | 13/11/2019 18:11:03 | 18:11:03* |
In earlier versions of MySQL, you can use couple of inline queries. This is a straight-forward option that could be quite efficient here:
select
(select ID from mytable order by TimeOnlylimit 1) minID,
(select Date from mytable order by TimeOnly limit 1) minDate,
(select ID from mytable order by TimeOnly desc limit 1) maxID,
(select Date from mytable order by TimeOnly desc limit 1) maxDate
One option for MySQL 8+, using ROW_NUMBER with pivoting logic:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY TimeOnly) rn_min,
ROW_NUMBER() OVER (ORDER BY Date TimeOnly) rn_max
FROM yourTable
)
SELECT
MAX(CASE WHEN rn_min = 1 THEN ID END) AS minID,
MAX(CASE WHEN rn_min = 1 THEN Date END) AS minDate
MAX(CASE WHEN rn_max = 1 THEN ID END) AS maxID,
MAX(CASE WHEN rn_max = 1 THEN Date END) AS maxDate
FROM cte;
Here is an option for MySQL 5.7 or earlier:
SELECT
MAX(CASE WHEN pos = 1 THEN ID END) AS minID,
MAX(CASE WHEN pos = 1 THEN Date END) AS minDate
MAX(CASE WHEN pos = 2 THEN ID END) AS maxID,
MAX(CASE WHEN pos = 2 THEN Date END) AS maxDate
FROM
(
SELECT ID, Date, 1 AS pos FROM yourTable
WHERE TimeOnly = (SELECT MIN(TimeOnly) FROM yourTable)
UNION ALL
SELECT ID, Date, 2 FROM yourTable
WHERE TimeOnly = (SELECT MAX(TimeOnly) FROM yourTable)
) t;
This second 5.7 option uses similar pivoting logic, but instead of ROW_NUMBER is uses subqueries to identify the min and max records. These records are brought together using a union, along with an identifier to keep track of which record be min/max.
You could simply do this:
SELECT minval.ID, minval.Date, maxval.ID, maxval.Date
FROM (
SELECT ID, Date
FROM t
ORDER BY CAST(Date AS TIME)
LIMIT 1
) AS minval
CROSS JOIN (
SELECT ID, Date
FROM t
ORDER BY CAST(Date AS TIME) DESC
LIMIT 1
) AS maxval
If you want two rows then change CROSS JOIN query to a UNION ALL query.
Demo on db<>fiddle
I need to to get the max price of the 33% cheapests products. My idea is like this. Of course, this code is just an example. I need to use subqueries.
select max((select price from products order by preco limit 33% )) as result from products
For example
product_id price
1 10
2 50
3 100
4 400
5 900
6 8999
I need I query that returns 50, since 33% of the rows are 2, and the max value of the 2(33%) of the rows is 50.
In MySQL 8+, you would use window functions:
select avg(precio)
from (select p.*, row_number() over (order by precio) as seqnum,
count(*) over () as cnt
from products p
) p
where seqnum <= 0.33 * cnt;
Obviously there are multiple approaches to this but here is how I would do it.
Simply get a count on the table. This will let me pick the max price of the cheapest 33% of products. Let's say it returned n records. Third of that would be n/3. Here you can either round up or down but needs to be rounded in case of a fraction.
Then my query would be something like SELECT * FROM products ORDER BY price ASC LIMIT 1 OFFSET n/3. This would return me a single record with minimal calculations and look ups on MySQL side.
For MySQL versions under MySQL 8.0 you can use MySQL's user variables to simulate/emulate a ROW_NUMBER()
Query
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
Result
| product_id | price | ROW_NUMBER |
| ---------- | ----- | ---------- |
| 1 | 10 | 1 |
| 2 | 50 | 2 |
| 3 | 100 | 3 |
| 4 | 400 | 4 |
| 5 | 900 | 5 |
| 6 | 8999 | 6 |
When we get the ROW_NUMBER we can use that in combination with ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33));
Which works like this
(SELECT COUNT(*) FROM t) => Counts and returns 6
(SELECT COUNT(*) FROM t) * 0.33) Calculates 33% from 6 which is 1.98 and returns it
CEIL(..) Return the smallest integer value that is greater than or equal to 1.98 which is 2 in this case
ROW_NUMBER <= 2 So the last filter is this.
Query
SELECT
a.product_id
, a.price
FROM (
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
) AS a
WHERE
ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33));
Result
| product_id | price |
| ---------- | ----- |
| 1 | 10 |
| 2 | 50 |
see demo
To get get the max it's just as simple as adding ORDER BY a.price DESC LIMIT 1
Query
SELECT
a.product_id
, a.price
FROM (
SELECT
t.product_id
, t.price
, (#ROW_NUMBER := #ROW_NUMBER + 1) AS ROW_NUMBER
FROM
t
CROSS JOIN (SELECT #ROW_NUMBER := 0) AS init_user_variable
ORDER BY
t.price ASC
) AS a
WHERE
ROW_NUMBER <= CEIL(((SELECT COUNT(*) FROM t) * 0.33))
ORDER BY
a.price DESC
LIMIT 1;
Result
| product_id | price |
| ---------- | ----- |
| 2 | 50 |
see demo
If your version supports window functions, you can use NTILE(3) to divide the rows into three groups ordered by price. The first group will contain (about) "33%" of lowest prices. Then you just need to select the MAX value from that group:
with cte as (
select price, ntile(3) over (order by price) as ntl
from products
)
select max(price)
from cte
where ntl = 1
Demo
Prior to MySQL 8.0 I would use a temprary table with an AUTO_INCREMENT column:
create temporary table tmp (
rn int auto_increment primary key,
price decimal(10,2)
);
insert into tmp(price)
select price from products order by price;
set #max_rn = (select max(rn) from tmp);
select price
from tmp
where rn <= #max_rn / 3
order by rn desc
limit 1;
Demo