How to group and order dependent results by dates in MySQL - mysql

I have a database of units which have expiry dates. Some of the units are top level units and have sub-assemblies attached. Sub-assemblies also have expiry dates. I'm trying to create a listing that lists the top-level units ordered by their expiration dates and list all relevant sub-assemblies under them.
The ordering should be done in two different ways:
Query which lists top-level units and their sub-assemblies according to expiry date of the parent item.
Query which lists top-level units and their sub-assemblies ordered by the first expiry date of relevant sub-assembly under them.
Here's example of the master table:
ITEM | NAME | UID | INSTALLED_TO_UID | EXPIRY
AAA | Top_level_unit_1 | 1 | | 2018-03-06
BBB | Sub_assy_1 | 75 | 1 | 2019-06-11
AAA | Top_level_unit_2 | 2 | | 2018-08-12
CCC | Sub_assy_2 | 26 | 1 | 2020-02-05
DDD | Sub_assy_3 | 59 | 2 | 2019-11-11
EEE | Sub_assy_4 | 41 | 2 | 2019-10-30
FFF | Sub_assy_5 | 11 | 1 | 2018-04-10
I am running these now in nested queries (second query inside foreach loop), but i am sure there is better and more efficient way of doing this. At least for case 1.
SELECT *
FROM master
WHERE ITEM = AAA
ORDER
BY EXPIRY ASC
And inside result loop for each result:
SELECT *
FROM master
WHERE INSTALLED_TO_UID = (UID from parent query)
The result should be like:
ITEM | NAME | UID | INSTALLED_TO_UID | EXPIRY
AAA | Top_level_unit_1 | 1 | | 2018-03-06
BBB | Sub_assy_1 | 75 | 1 | 2019-06-11
CCC | Sub_assy_2 | 26 | 1 | 2020-02-05
FFF | Sub_assy_5 | 11 | 1 | 2018-04-10
AAA | Top_level_unit_2 | 2 | | 2018-08-12
DDD | Sub_assy_3 | 59 | 2 | 2019-11-11
EEE | Sub_assy_4 | 41 | 2 | 2019-10-30
For case 2 i don't have a clue yet...
Summary: I would like to have only one query to group and order the results instead of relying to another query in result loop. Secondly i would like to figure out a query to order the results by first expiring child item grouped by parent items

You need to use an outer self join to find the parent record (for query 1) or the first-of-children record (for query 2). Then you can use coalesce to decide which value to take (either from the joined table or the main one) for setting the order.
Query 1:
SELECT record.*
FROM master record
LEFT JOIN master parent
ON record.installed_to_uid = parent.uid
AND parent.installed_to_uid is null
ORDER BY COALESCE(parent.expiry, record.expiry),
COALESCE(parent.uid, record.uid),
COALESCE(record.installed_to_uid,-1),
record.expiry
Query 2:
SELECT record.*
FROM master record
LEFT JOIN (SELECT installed_to_uid, MIN(expiry) expiry
FROM master
WHERE installed_to_uid IS NOT NULL
GROUP BY installed_to_uid) first
ON COALESCE(record.installed_to_uid, record.uid) = first.installed_to_uid
ORDER BY COALESCE(first.expiry, record.expiry),
COALESCE(first.installed_to_uid, record.uid),
COALESCE(record.installed_to_uid,-1),
record.expiry
See it run on sqlfiddle

Although there are surely more efficient solutions, why not start with this:
SELECT *
FROM master
WHERE INSTALLED_TO_UID = (
SELECT UID
FROM master
WHERE ITEM = AAA
)
?

Related

Mysql - Compare int field with comma separated field from another table

I have two tables in a MySQL database like this:
User:
userid |userid | Username | Plan(VARCHAR) | Status |
-----------+------------+--------------+---------------+---------+
1 | 1 | John | 1,2,3 |1 |
2 | 2 | Cynthia | 1,2 |1 |
3 | 3 | Charles | 2,3,4 |1 |
Plan: (planid is primary key)
planid(INT) | Plan_Name | Cost | status |
-------------+----------------+----------+--------------+
1 | Tamil Pack | 100 | ACTIVE |
2 | English Pack | 100 | ACTIVE |
3 | SportsPack | 100 | ACTIVE |
4 | KidsPack | 100 | ACTIVE |
OUTPUT
id |userid | Username | Plan | Planname |
---+-------+----------+------------+-------------------------------------+
1 | 1 | John | 1,2,3 |Tamil Pack,English Pack,SportsPack |
2 | 2 | Cynthia | 1,2 |Tamil Pack,English Pack |
3 | 3 | Charles | 2,3,4 |English Pack,Sportspack, Kidspack |
Since plan id in Plan table is integer and the user can hold many plans, its stored as comma separated as varchar, so when i try with IN condition its not working.
SELECT * FROM plan WHERE find_in_set(plan_id,(select user.planid from user where user.userid=1))
This get me the 3 rows from plan table but i want the desired output as above.
How to do that.? any help Please
A rewrite off your query what should work is as follows..
Query
SELECT
all columns you need
, GROUP_CONCAT(Plan.Plan_Name ORDER BY Plan.planid) AS Planname
FROM
Plan
WHERE
FIND_IN_SET(Plan.plan_id,(
SELECT
User.Plan
FROM
user
WHERE User.userid = 1
)
)
GROUP BY
all columns what are in the select (NOT the GROUP_CONCAT function)
You also can use FIND_IN_SET on the ON clause off a INNER JOIN.
One problem is that the join won't ever use indexes.
Query
SELECT
all columns you need
, GROUP_CONCAT(Plan.Plan_Name ORDER BY Plan.planid) AS Planname
FROM
User
INNER JOIN
Plan
ON
FIND_IN_SET(Plan.id, User.Plan)
WHERE
User.id = 1
GROUP BY
all columns what are in the select (NOT the GROUP_CONCAT function)
Like i said in the comments you should normalize the table structures and add the table User_Plan whats holds the relations between the table User and Plan.

Properly SQL query

I need to skip results with high price per day. I've got a table like this:
+------+-------------+-------+
| days | return_date | value |
+------+-------------+-------+
| 2 | 2017-12-27 | 15180 |
| 3 | 2017-12-28 | 14449 |
| 4 | 2017-12-29 | 13081 |
| 5 | 2017-12-30 | 11203 |
| 6 | 2017-12-31 | 9497 |
| 6 | 2017-12-31 | 9442 |
+------+-------------+-------+
How can I print only the lowest price for 6 days (9442 in this example).
We can use a GROUP BY clause and an aggregate function. For example:
SELECT t.days
, t.return_date
, MIN(t.value) AS min_value
FROM mytable t
GROUP
BY t.days
, t.return_date
This doesn't really "skip" rows. It accesses all the rows that satisfy the conditions in the WHERE clause (in this example, every row in the table). Then MySQL collapses rows into groups (in this example, rows with identical values of days and return_date get put into a group. The MIN(t.value) aggregate function selects out the minimum (lowest) value out of the group.
The query above is just an example of one approach of satisfying a particular specification.

Only return an ordered subset of the rows from a joined table

Given a structure like this in a MySQL database
#data_table
(id) | user_id | time | (...)
#relations_table
(id) | user_id | user_coach_id | (...)
we can select all data_table rows belonging to a certain user_coach_id (let's say 1) with
SELECT rel.`user_coach_id`, dat.*
FROM `relations_table` rel
LEFT JOIN `data_table` dat ON rel.`uid` = dat.`uid`
WHERE rel.`user_coach_id` = 1
ORDER BY val.`time` DESC
returning something like
| user_coach_id | id | user_id | time | data1 | data2 | ...
| 1 | 9 | 4 | 15 | foo | bar | ...
| 1 | 7 | 3 | 12 | oof | rab | ...
| 1 | 6 | 4 | 11 | ofo | abr | ...
| 1 | 4 | 4 | 5 | foo | bra | ...
(And so on. Of course time are not integers in reality but to keep it simple.)
But now I would like to query (ideally) only up to an arbitrary number of rows from data_table per distinct user_id but still have those ordered (i.e. newest first). Is that even possible?
I know I can use GROUP BY user_id to only return 1 row per user, but then the ordering doesn't work and it seems kind of unpredictable which row will be in the result. I guess it's doable with a subquery, but I haven't figured it out yet.
To limit the number of rows in each GROUP is complicated. It is probably best done with an #variable to count, plus an outer query to throw out the rows beyond the limit.
My blog on Groupwise Max gives some hints of how to do such.

MySQL: optimize query for scoring calculation

I have a data table that I use to do some calculations. The resulting data set after calculations looks like:
+------------+-----------+------+----------+
| id_process | id_region | type | result |
+------------+-----------+------+----------+
| 1 | 4 | 1 | 65.2174 |
| 1 | 5 | 1 | 78.7419 |
| 1 | 6 | 1 | 95.2308 |
| 1 | 4 | 1 | 25.0000 |
| 1 | 7 | 1 | 100.0000 |
+------------+-----------+------+----------+
By other hand I have other table that contains a set of ranges that are used to classify the calculations results. The range tables looks like:
+----------+--------------+---------+
| id_level | start | end | status |
+----------+--------------+---------+
| 1 | 0 | 75 | Danger |
| 2 | 76 | 90 | Alert |
| 3 | 91 | 100 | Good |
+----------+--------------+---------+
I need to do a query that add the corresponding 'status' column to each value when do calculations. Currently, I can do that adding the following field to calculation query:
select
...,
...,
[math formula] as result,
(select status
from ranges r
where result between r.start and r.end) status
from ...
where ...
It works ok. But when I have a lot of rows (more than 200K), calculation query become slow.
My question is: there is some way to find that 'status' value without do that subquery?
Some one have worked on something similar before?
Thanks
Yes, you are looking for a subquery and join:
select s.*, r.status
from (select s.*
from <your query here>
) s left outer join
ranges r
on s.result between r.start and r.end
Explicit joins often optimize better than nested select. In this case, though, the ranges table seems pretty small, so this may not be the performance issue.

Is there a query in MySQL that would allow variable group numbers and limits akin to this

I've checked out a few of the stackoverflow questions and there are similar questions, but didn't quite put my fingers on this one.
If you have a table like this:
uid cat_uid itm_uid
1 1 4
2 1 5
3 2 6
4 2 7
5 3 8
6 3 9
where the uid column in auto_incremented and the cat_uid references a
category of relevance to filter on and the itm_uid values are the one
we're seeking
I would like to get a result set that contains the following sample results:
array (
0 => array (1 => array(4,5)),
1 => array (2 => array(6,7)),
2 => array (3 => array(8,9))
)
An example issue is - select 2 records from each category (however many categories there may be) and make sure they are the last 2 entries by uid in those categories.
I'm not sure how to structure the question to allow an answer, and any hints on a method for the solution would be welcome!
EDIT:
This wasn't a very clear question, so let me extend the scenario to something more tangible.
I have a set of records being entered into categories and I would like to select, with as few queries as possible, the latest 2 records entered per category, so that when I list out the contents of those categories, I will have at least 2 records per category (assuming that there are 2 or more already in the database). A similar query was in place that selected the last 100 records and filtered them into categories, but for small numbers of categories with some being updated faster than others can lead to having the top 100 not consisting of members from every category, so to try to resolve that, I was looking for a way to select 2 records from each category (or N-records assuming it's the same per-category) and for those 2 records to be the last entered. A date field is available to sort on, but the itm_uid itself could be used to indicate inserted order.
SELECT cat_uid, itm_uid,
IF( #cat = cat_uid, #cat_row := #cat_row + 1, #cat_row := 0 ) AS cat_row,
#cat := cat_uid
FROM my_table
JOIN (SELECT #cat_row := 0, #cat := 0) AS init
HAVING cat_row < 2
ORDER BY cat_uid, uid DESC
You will have two extra columns in the results, just ignore them.
This is the logic:
We sort the table by cat_uid, uid descending, then we start from the top and give each row a "row number" (cat_row) we reset this row number to zero whenever cat_uid changes:
---------------------------------------
| uid | cat_uid | itm_uid | cat_row |
| 45 | 4 | 34 | 0 |
| 33 | 4 | 54 | 1 |
| 31 | 4 | 12 | 2 |
| 12 | 4 | 51 | 3 |
| 56 | 6 | 11 | 0 |
| 20 | 6 | 64 | 1 |
| 16 | 6 | 76 | 2 |
| ... | ... | ... | ... |
---------------------------------------
now if we keep only the rows that have cat_row < 2 we get the results we want:
---------------------------------------
| uid | cat_uid | itm_uid | cat_row |
| 45 | 4 | 34 | 0 |
| 33 | 4 | 54 | 1 |
| 56 | 6 | 11 | 0 |
| 20 | 6 | 64 | 1 |
| ... | ... | ... | ... |
---------------------------------------
This is called an adjacent tree model or a parent-child tree model. It's one of the simplier tree model where there is only 1 pointer or 1 leaf. You would solve your query with a recursion or using a Self Join. Sadly MySQL doesn't support recursive queries, maybe it's working with prepared statements. I want to suggest you an Self Join. With a Self Join you can get all the rows from the right side and the left side with a special condition.
select t1.cat_uid, t2.cat_uid, t1.itm_uid, t2.itm_uid From t1 Inner Join t2 On t1.cat_uid = t2.cat_uid