I have 2 tables say:
Orders:
id | Name | Amount | Date
1 | ABC | 100 | 2020-10-01
2 | XYZ | 200 | 2020-10-01
3 | MNO | 250 | 2020-11-01
Order_details:
id | Item | Qty
1 | A | 2
1 | B | 1
1 | C | 3
2 | X | 1
3 | A | 4
Now I want to fetch the data using the date on which the order was made.
Say if I want to fetch the data of 2020-10-01 the output should be something like this:
id | Name | Amount | Date | Item1 | Qty | Item2 | Qty ...
1 | ABC | 100 | 2020-10-01 | A | 2 | B | 1
2 | XYZ | 200 | 2020-10-01 | X | 1
I tried to fetch it using a subquery, but I was not sure how to print that data.
Thanks in advance!
You can use Conditional Aggregation within Dynamic Pivot Statement which works even for DB version 5.5 :
SET SESSION group_concat_max_len = 18446744073709551615;
SET #sql = NULL;
SET #date = '2020-10-01';
SELECT GROUP_CONCAT(
DISTINCT
CONCAT(
'MAX(CASE WHEN rn = ', rn,' THEN Item END ) AS Item', rn,
', MAX(CASE WHEN rn = ', rn,' THEN Qty END ) AS Qty'
)
)
INTO #sql
FROM (
SELECT *, #rn := IF(#i = id, #rn + 1, 1) AS rn, #i := id
FROM Order_details
JOIN (SELECT #i := 0, #rn := 0) i
ORDER BY id, Item
) od;
SET #sql = CONCAT('SELECT o.id, o.name, o.amount, o.date,',#sql,
' FROM Orders o
JOIN (
SELECT *, #rn := IF(#i = id, #rn + 1, 1) AS rn, #i := id
FROM Order_details
JOIN (SELECT #i := 0, #rn := 0) i
ORDER BY id, Item
) od
ON od.id = o.id
WHERE o.date = "',#date,'"
GROUP BY o.id, o.name, o.amount, o.date
ORDER BY o.id');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
where parameter value might be updated within the second line ( SET #date = '2020-10-01'; ) . Btw, the function GROUP_CONCAT() has an upper length limit(for the parameter group_concat_max_len with default value of 1024) that might be updated(upto the max value of 18446744073709551615) for the current session for the cases the table has multiple distinct items, so having lots of columns.
Demo
For a fixed maximum number of items per orders, you can use window functions and conditional aggregation:
select o.*,
max(case when od.rn = 1 then item end) item1,
max(case when od.rn = 1 then qty end) qty1,
max(case when od.rn = 2 then item end) item2,
max(case when od.rn = 2 then qty end) qty2,
max(case when od.rn = 3 then item end) item3,
max(case when od.rn = 3 then qty end) qty3
from orders o
inner join (
selet od.*, row_number() over(partition by id order by item) rn
from order_details od
) od on od.id = o.id
group by o.id
You can extend the select clause with more conditional expressions to handle more than 3 items per order.
Note that window functions are available in MySQL 8.0 only.
Related
I am struggling to make a GROUP BY contiguous blocks, I've used the following two for references:
- GROUP BY for continuous rows in SQL
- How can I do a contiguous group by in MySQL?
- https://gcbenison.wordpress.com/2011/09/26/queries-that-group-tables-by-contiguous-blocks/
The primary idea that I am trying to encapsulate periods with a start and end date of a given state. A complexity unlike other examples is that I'm using a date per room_id as the indexing field (rather than a sequential id).
My table:
room_id | calendar_date | state
Sample data:
1 | 2016-03-01 | 'a'
1 | 2016-03-02 | 'a'
1 | 2016-03-03 | 'a'
1 | 2016-03-04 | 'b'
1 | 2016-03-05 | 'b'
1 | 2016-03-06 | 'c'
1 | 2016-03-07 | 'c'
1 | 2016-03-08 | 'c'
1 | 2016-03-09 | 'c'
2 | 2016-04-01 | 'b'
2 | 2016-04-02 | 'a'
2 | 2016-04-03 | 'a'
2 | 2016-04-04 | 'a'
The objective:
room_id | date_start | date_end | state
1 | 2016-03-01 | 2016-03-03 | a
1 | 2016-03-04 | 2016-03-05 | b
1 | 2016-03-06 | 2016-03-09 | c
2 | 2016-04-01 | 2016-04-01 | b
2 | 2016-04-02 | 2016-04-04 | c
The two attempts I've made at this:
1)
SELECT
rooms.row_new,
rooms.state_new,
MIN(rooms.room_id) AS room_id,
MIN(rooms.state) AS state,
MIN(rooms.date) AS date_start,
MAX(rooms.date) AS date_end,
FROM
(
SELECT #r := #r + (#state != state) AS row_new,
#state := state AS state_new,
rooms.*
FROM (
SELECT #r := 0,
#state := ''
) AS vars,
rooms_vw
ORDER BY room_id, date
) AS rooms
WHERE room_id = 1
GROUP BY row_new
ORDER BY room_id, date
;
This is very close to working, but when I print out row_new it starts to jump (1, 2, 3, 5, 7, ...)
2)
SELECT
MIN(rooms_final.calendar_date) AS date_start,
MAX(rooms_final.calendar_date) AS date_end,
rooms_final.state,
rooms_final.room_id,
COUNT(*)
FROM (SELECT
rooms.date,
rooms.state,
rooms.room_id,
CASE
WHEN rooms_merge.state IS NULL OR rooms_merge.state != rooms.state THEN
#rownum := #rownum+1
ELSE
#rownum
END AS row_num
FROM rooms
JOIN (SELECT #rownum := 0) AS row
LEFT JOIN (SELECT rooms.date + INTERVAL 1 DAY AS date,
rooms.state,
rooms.room_id
FROM rooms) AS rooms_merge ON rooms_merge.calendar_date = rooms.calendar_date AND rooms_merge.room_id = rooms.room_id
ORDER BY rooms.room_id, rooms.calendar_date
) AS rooms_final
GROUP BY rooms_final.state, rooms_final.row_num
ORDER BY room_id, calendar_date;
For some reason this is returning some null room_id's results as well as generally inaccurate.
Working with variables is a bit tricky. I would go for:
SELECT r.state_new, MIN(r.room_id) AS room_id, MIN(r.state) AS state,
MIN(r.date) AS date_start, MAX(r.date) AS date_end
FROM (SELECT r.*,
(#grp := if(#rs = concat_ws(':', room, state), #grp,
if(#rs := concat_ws(':', room, state), #grp + 1, #grp + 1)
)
) as grp
FROM (SELECT r.* FROM rooms_vw r ORDER BY ORDER BY room_id, date
) r CROSS JOIN
(SELECT #grp := 0, #rs := '') AS params
) AS rooms
WHERE room_id = 1
GROUP BY room_id, grp
ORDER BY room_id, date;
Notes:
Assigning a variable in one expression and using it in another is unsafe. MySQL does not guarantee the order of evaluation of expressions.
In more recent versions of MySQL, you need to do the ORDER BY in a subquery.
In the most recent versions, you can use row_number(), greatly simplifying the calculation.
Thanks to #Gordon Linoff for giving me insights to get to this answer:
SELECT
MIN(room_id) AS room_id,
MIN(state) AS state,
MIN(date) AS date_start,
MAX(date) AS date_end
FROM
(
SELECT
#r := #r + IF(#state <> state OR #room_id <> room_id, 1, 0) AS row_new,
#state := state AS state_new,
#room_id := room_id AS room_id_new,
tmp_rooms.*
FROM (
SELECT #r := 0,
#room_id := 0,
#state := ''
) AS vars,
(SELECT * FROM rooms WHERE room_id IS NOT NULL ORDER BY room_id, date) tmp_rooms
) AS rooms
GROUP BY row_new
order by room_id, date
;
I want to write a query that will update duplicates per group:
INPUT
+-------+-------+
| group | name |
+-------+-------+
| 1 | name1 |
| 1 | name1 |
| 1 | name1 |
| 1 | name2 |
| 2 | name1 |
| 2 | name1 |
| 3 | name1 |
| 3 | name2 |
+-------+-------+
OUTPUT
+-------+----------------+
| group | name |
+-------+----------------+
| 1 | name1 |
| 1 | name1 - Copy 1 |
| 1 | name1 - Copy 2 |
| 1 | name2 |
| 2 | name1 |
| 2 | name1 - Copy 1 |
| 3 | name1 |
| 3 | name2 |
+-------+----------------+
There is something like that here Renaming the duplicate data in sql but my problem is how to deal with groups.
It is not so important how to name this duplicates but it will be cool if I can do it as specified in my example.
If you have a primary key id column, then try this:
update (
select `group`, name, min(id) as min_id
from test
group by `group`, name
) x
join test t using (`group`, name)
set t.name =
case when t.id <> x.min_id
then concat(t.name, ' - Copy ', t.id - x.min_id)
else t.name
end
;
Demo: http://rextester.com/AWEX77086
Here is another way, which is probably slower, but will guarantee consecutive copy numbers.
update (
select l.id, count(*) as copy_nr
from test l
join test r
on r.group = l.group
and r.name = l.name
and r.id < l.id
group by l.id
) x
join test t using (id)
set t.name = concat(t.name, ' - Copy ', x.copy_nr);
Demo: http://rextester.com/NWSF57017
Try the below, replace Groups, with Group.
Ok, I was pointed out that this is a MySQL question, so the below wont work for MySQL, but only for t-sql.
SELECT Groups,
CASE WHEN Duplicate > 1
THEN Name + ' - Copy ' + CONVERT(VARCHAR(10), Duplicate)
ELSE Name
END AS Name
FROM
(
SELECT Groups,
Name,
ROW_NUMBER() OVER(PARTITION BY Name, Groups ORDER BY Name ) As Duplicate
FROM TableName
) AS Data
ORDER BY Groups
This is untested, but you can similate the LAG function seen in many other RDBMS' with variables (more info here).
The idea is you store the group and name fields and compare against them before updating them.
SET #RowNumber = 0;
SET #PreviousGroup = NULL;
SET #PreviousName = NULL;
SELECT
#PreviousGroup AS PreviousGroup,
#PreviousName AS PreviousName,
CASE
WHEN #PreviousGroup = `group` AND #PreviousName = `name` THEN (#RowNumber := #RowNumber + 1)
ELSE #RowNumber := 0
END AS `Counter`,
CASE
WHEN #PreviousGroup = `group` AND #PreviousName = `name` THEN CONCAT(`name`,'- Copy ',#RowNumber)
ELSE `name`
END AS `Name`,
#PreviousGroup := `group` AS RawGroup,
#PreviousName := `name` AS RawName
FROM
tbl1
ORDER BY
`group` ASC,
`name` ASC;
You can do this using variables. I would recommend:
set #i = 0;
set #gn := '';
update t
set name = concat_ws(' - Copy ', name,
nullif(if(#gn = concat_ws(':', group, name), #i := #i + 1,
if(#gn := concat_ws(':', group, name), #i := 1, #i := 1)
), 0)
order by t.group, name;
I'm performing a query that looks like this:
SELECT a.transactionID,a.customerID,b.value
FROM adjustments a
INNER JOIN change b
on a.transactionID = b.transactionID
and a.event_date = b.event_date
and a.event_id = b.event_id
WHERE comment LIKE 'TRANSFER'
ORDER BY a.transactionID;
this query brings the following result:
transactionID | customerID | value
------------------------------------
TRANSFER-001 | CUSTA | -200
TRANSFER-001 | CUSTB | 200
TRANSFER-002 | CUSTC | -150
TRANSFER-002 | CUSTD | 0
TRANSFER-003 | CUSTA | 0
TRANSFER-003 | CUSTC | 150
I need to change this query to bring a list that ignore those cases where the sum of value is 0 for the same transactionID and also, group the customerID and values as following:
transactionID | customerID_A | value_A | customerID_B | value_B
------------------------------------------------------------------
TRANSFER-002 | CUSTC | -150 | CUSTD | 0
TRANSFER-003 | CUSTA | 0 | CUSTC | 150
Can you give any advise about how to solve this?
Try this :
SELECT * FROM (
SELECT a.transactionID,a.customerID,b.value
FROM adjustments a
INNER JOIN change b
on a.transactionID = b.transactionID
and a.event_date = b.event_date
and a.event_id = b.event_id
WHERE comment LIKE 'TRANSFER'
)M
GROUP BY transactionID,customerID,value
HAVING SUM(value) <> 0
ORDER BY transactionID;
SELECT distinct a.transactionID,a.customerID,b.value
FROM adjustments a
INNER JOIN change b
on a.transactionID = b.transactionID
and a.event_date = b.event_date
and a.event_id = b.event_id
WHERE comment LIKE 'TRANSFER'
ORDER BY a.transactionID;
Try distinct at the beginning it will eliminate all the duplicate values.
If I understand correctly, you want conditional aggregation. However, you need to pivot the customers and there is no column for doing that. You can enumerate the customers for each transaction using variables, and use that to pivot the first two customers on the transaction:
SELECT transactionId,
MAX(CASE WHEN seqnum = 1 THEN customerId END) as customer_A,
MAX(CASE WHEN seqnum = 1 THEN value END) as value_A,
MAX(CASE WHEN seqnum = 2 THEN customerId END) as customer_B,
MAX(CASE WHEN seqnum = 2 THEN value END) as value_B
FROM (SELECT a.transactionID, a.customerID, b.value,
(#rn := if(#t = a.transactionID, #rn + 1,
if(#t := a.transactionID, 1, 1)
)
) as seqnum
FROM adjustments a INNER JOIN
change c
ON a.transactionID = c.transactionID AND
a.event_date = c.event_date AND
a.event_id = c.event_id CROSS JOIN
(SELECT #rn := 0, #t := '') params,
WHERE comment LIKE 'TRANSFER'
ORDER BY a.transactionID, b.value DESC
) t
GROUP BY transactionId
HAVING SUM(value) <> 0;
I have in my Moodle db table for every session sessid and timestart. The table looks like this:
+----+--------+------------+
| id | sessid | timestart |
+----+--------+------------+
| 1 | 3 | 1456819200 |
| 2 | 3 | 1465887600 |
| 3 | 3 | 1459839600 |
| 4 | 2 | 1457940600 |
| 5 | 2 | 1460529000 |
+----+--------+------------+
How to get for every session the first date from the timestamps in SQL?
You can easy use this:
select sessid,min(timestart) FROM mytable GROUP by sessid;
And for your second question, something like this:
SELECT
my.id,
my.sessid,
IF(my.timestart = m.timestart, 'yes', 'NO' ) AS First,
my.timestart
FROM mytable my
LEFT JOIN
(
SELECT sessid,min(timestart) AS timestart FROM mytable GROUP BY sessid
) AS m ON m.sessid = my.sessid;
Try this.
SELECT
*
FROM
tbl
WHERE
(sessid, timestart) IN (
SELECT tbl2.sessid, MIN(tbl2.timestart)
FROM tbl tbl2
WHERE tbl.sessid = tbl2.sessid
);
Query
select sessid, min(timestart) as timestart
from your_table_name
group by sessid;
Just an other perspective if you need even the id.
select t.id, t.sessid, t.timestart from
(
select id, sessid, timestart,
(
case sessid when #curA
then #curRow := #curRow + 1
else #curRow := 1 and #curA := sessid end
) as rn
from your_table_name t,
(select #curRow := 0, #curA := '') r
order by sessid,id
)t
where t.rn = 1;
I got table orders and order_comments. Each order can have from 0 to n comments. I would like to get list of all orders with their comments in a sepcific order.
Table orders:
order_id | order_nr
1 | 5252
4 | 6783
5 | 6785
Table order_comments
id_order_comments | order_fk | created_at | email | content
1 | 4 | 2015-01-12 | jack | some text here
2 | 5 | 2015-01-13 | marta | some text here
3 | 5 | 2015-01-14 | beata | some text here
4 | 4 | 2015-01-16 | julia | some text here
As a result, I would like to get 1 row for each order. Comments should be shown in separate columns, starting from the oldest comment. So desired output in this case is:
order_id | 1_comment_created_at | 1_comment_author | 1_comment_content | 2_comment_created_at | 2_comment_author | 2_comment_content
1 | NULL | NULL | NULL | NULL | NULL | NULL
4 | 2015-01-12 | jack | some text here | 2015-01-16 | Julia | some text here
5 | 2015-01-13 | marta | some text here | 2015-01-14 | beata | some text here
I found this: MySQL - Rows to Columns - but I cannot use 'create view'.
I found this: http://dev.mysql.com/doc/refman/5.5/en/while.html - but I cannot create procedure in this db.
What I got:
SELECT #c := (SELECT count(*) FROM order_comments GROUP BY order_fk ORDER BY count(*) DESC LIMIT 1);
SET #rank=0;
SET #test=0;
SELECT
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.created_at END AS created,
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.author END AS author,
CASE WHEN #test < #c AND temp.comment_id = #test THEN temp.content END AS content
/*But I cannot set #test as +1. And I cannot name column with variable - like CONCAT(#test, '_created')*/
FROM (
SELECT #rank := #rank +1 AS comment_id, created_at, author, content
FROM order_comments
WHERE order_fk = 4
ORDER BY created_at
) AS temp
Problem: I would like to search more than 1 order. I should get orders with no comments too.
What can I do?
You can use variables for this type of pivot, but the query is a bit more complicated, because you need to enumerate the values for each order:
SELECT o.order_id,
MAX(case when rank = 1 then created_at end) as created_at_1,
MAX(case when rank = 1 then email end) as email_1,
MAX(case when rank = 1 then content end) as content_1,
MAX(case when rank = 2 then created_at end) as created_at_2,
MAX(case when rank = 2 then email end) as email_2,
MAX(case when rank = 2 then content end) as content_2,
FROM orders o LEFT JOIN
(SELECT oc.*,
(#rn := if(#o = order_fk, #rn + 1,
if(#o := order_fk, 1, 1)
)
) as rank
FROM order_comments oc CROSS JOIN
(SELECT #rn := 0, #o := 0) vars
ORDER BY order_fk, created_at
) oc
ON o.order_id = oc.order_fk
GROUP BY o.order_id;