MySQL order by mixed ASC/DESC in the same column - mysql

I am trying to sort product bin locations from a database by both ASC and DESC order in the same column, to allow a warehouse picker to weave through the warehouse isles to pick product. In other words, when a picker gets a batch of orders to pick from the warehouse, the system needs to start them at the front of isle 1, then order picks going down the isle to the end. Then it would jump them over to the end of isle 2 (instead of to the beginning), and they would work their way toward the front of isle 2, then start at the front of isle 3 and so on.
The bin locations are in the format: ISLE - BAY - SHELF - SLOT/BIN
Here is an example data table of bin locations to pick:
1-0-A-01
1-1-D-06
1-2-E-10
1-2-E-11
1-10-A-01
2-1-D-02
2-1-C-12
2-5-F-01
3-5-A-12
3-6-D-01
4-5-A-02
4-5-A-03
4-5-B-10
I need to do a SQL query and pull the locations and order them like this:
1-0-A-01
1-1-D-06
1-2-E-10
1-2-E-11
1-10-A-01
2-5-F-01
2-1-D-02
2-1-C-12
3-5-A-12
3-6-D-01
4-5-B-10
4-5-A-03
4-5-A-02
Is it possible to do this with just a SQL query?

Yes, this can be done within a SQL query, though the syntax is non-trivial.
You'd first need expressions to "split" the ISLE-BAY-SHELF into separate components, and then you use those expressions in an ORDER BY clause.
For MySQL
Some example expressions, put into the SELECT list just so we can see what they return:
SELECT SUBSTRING_INDEX('1-10-A-01','-',1)+0 AS ISLE
, SUBSTRING_INDEX(SUBSTRING_INDEX('1-10-A-01','-',2),'-',-1)+0 AS BAY
, SUBSTRING_INDEX(SUBSTRING_INDEX('1-10-A-01','-',3),'-',-1) AS SHELF
, SUBSTRING_INDEX('1-10-A-01','-',-1)+0 AS `SLOT/BIN`
These expressions are based on the assumption that there will always be three dashes, and always in the format numeric-numeric-whatever-numeric.
Given the sample data, we could check if the ISLE component is even or odd, and then order the BAY either ascending or descending based on that. But that's probably not what you want, if one aisle is skipped, if we skipped aisle 2 entirely, and did just aisles 1 and 3.
CREATE TABLE ibss (ibss VARCHAR(20));
INSERT INTO ibss (ibss) VALUES
('1-0-A-01')
,('1-1-D-06')
,('1-2-E-10')
,('1-2-E-11')
,('1-10-A-01')
,('2-5-F-01')
,('2-1-D-02')
,('2-1-C-12')
,('3-5-A-12')
,('3-6-D-01')
,('4-5-B-10')
,('4-5-A-03')
,('4-5-A-02');
SELECT i.ibss
, SUBSTRING_INDEX(i.ibss,'-',1)+0 AS ISLE
, SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',2),'-',-1)+0 AS BAY
, SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',3),'-',-1) AS SHELF
, SUBSTRING_INDEX(i.ibss,'-',-1)+0 AS `SLOT/BIN`
, (SUBSTRING_INDEX(i.ibss,'-',1)+0) MOD 2 AS odd_or_even_isle
, IF((SUBSTRING_INDEX(i.ibss,'-',1)+0) MOD 2
,SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',2),'-',-1)+0,NULL
) AS odd_bay
, IF((SUBSTRING_INDEX(i.ibss,'-',1)+0) MOD 2
,NULL,SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',2),'-',-1)+0
) AS even_bay
FROM ibss i
ORDER BY -- ascending by ISLE
SUBSTRING_INDEX(i.ibss,'-',1)+0 ASC
-- ascending by BAY if ISLE is odd
, IF((SUBSTRING_INDEX(i.ibss,'-',1)+0) MOD 2
,SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',2),'-',-1)+0,NULL
) ASC
-- descending by BAY if ISLE is even
, IF((SUBSTRING_INDEX(i.ibss,'-',1)+0) MOD 2
,NULL,SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',2),'-',-1)+0
) DESC
-- ascending by shelf
, SUBSTRING_INDEX(SUBSTRING_INDEX(i.ibss,'-',3),'-',-1)
-- ascending by SLOT/BIN
, SUBSTRING_INDEX(i.ibss,'-',-1)+0
Again, the ascending/descending ordering by BAY is going to depend on whether ISLE is even or odd, not on whether this is an alternating aisle. (This behavior might be desirable if you want the pickers moving the same direction down the aisles, and not in opposite directions.) To get the order changed based on an "aisle change", then we'd need to add some additional logic.
ibss ISLE BAY SHELF SLOT/BIN odd_or_even_isle odd_bay even_bay
--------- ------ ------ ------ -------- ---------------- ------- ----------
1-0-A-01 1 0 A 1 1 0 (NULL)
1-1-D-06 1 1 D 6 1 1 (NULL)
1-2-E-10 1 2 E 10 1 2 (NULL)
1-2-E-11 1 2 E 11 1 2 (NULL)
1-10-A-01 1 10 A 1 1 10 (NULL)
2-5-F-01 2 5 F 1 0 (NULL) 5
2-1-C-12 2 1 C 12 0 (NULL) 1
2-1-D-02 2 1 D 2 0 (NULL) 1
3-5-A-12 3 5 A 12 1 5 (NULL)
3-6-D-01 3 6 D 1 1 6 (NULL)
4-5-A-02 4 5 A 2 0 (NULL) 5
4-5-A-03 4 5 A 3 0 (NULL) 5
4-5-B-10 4 5 B 10 0 (NULL) 5

First of all, you should rightfully split out these data elements into their own columns in the table. Having done that, this problem becomes trivial and allows you to even sort by shelf/bin:
SELECT isle,
(IF(MOD(isle/2)=1,1,-1) * bay) AS baysort,
bay,
shelf,
bin
FROM table
ORDER BY
isle ASC,
baysort ASC,
shelf ASC,
bin ASC
Note here I am making calculate baysort column which basically makes bay a negative value for even numbered isles.
You can obviously discard the baysort value in your application (or simply move this to a sorting condition instead of select - I used select here so you can visually see what is happening).

Related

adding a column with sequence data to table based on repeating events

I'm learning MySQL (via mode) and am approaching the Advanced section. I have a dataset that contains events that recur and I want to add a column that identifies the first, second, third, fourth, etc (no limit) occurrence of the events for that day.
CustomerID ActivityType Day Sequence
Adam Inquiry 1 1
Barb Inquiry 1 2
Adam Inquiry 1 3
Charlie Inquiry 1 4
Barb Order 1 5
Charlie Inquiry 1 6
Adam Inquiry 1 7
Barb Order 1 8
I've searched here for problems that seem similar, to help focus my learning but I don't see anything quite the same.
My desired output would be the same as above but with an added column that shows the sequence of recurring combinations of customer and activity, like this:
CustomerID ActivityType Day Sequence Recur
Adam Inquiry 1 1 1
Barb Inquiry 1 2 1
Adam Inquiry 1 3 2
Charlie Inquiry 1 4 1
Barb Order 1 5 1
Charlie Inquiry 1 6 2
Adam Inquiry 1 7 3
Barb Order 1 8 2
In MySQL 8.0, you can simply use window function ROW_NUMBER() for that purpose:
SELECT
t.*,
ROW_NUMBER() OVER(PARTITION BY customerID, ActivityType ORDER BY Sequence) recur
FROM mytable t
Note: As commented by Strawberry, storing that information in a additional column does not make a lot of sense, because it can be complicated to maintain: for example, if your table is updated, you will potentially need to recompute the whole column. You would better compute the information on the fly when querying the table.

use a transaction database to calculate the probability of an item appearing in a future transaction using R or SQL

I have a database of transactions like in the table below
user_id order_id order_number product_name n
<int> <int> <int> <fctr> <int>
1 11878590 3 Pistachios 1
1 11878590 3 Soda 1
1 12878790 4 Yogurt 1
1 12878790 4 Cheddar Popcorn 1
1 12878790 4 Cinnamon Toast Crunch 1
2 12878791 11 Milk Chocolate Almonds 1
2 12878791 11 Half & Half 1
2 12878791 11 String Cheese 1
11 12878792 19 Whole Milk 1
11 12878792 19 Pistachios 1
11 12878792 19 Soda 1
11 12878792 19 Paper Towel Rolls 1
The table has multiple users who each have multiple transactions. Some users only have 3 transactions, other users have 15, etc. This is all in one table.
I'm trying to calculate a transition matrix for a markov model. I want to find the probability that an item will be in a new basket given that it was present in the previous basket of transactions.
I want my final table to look something like this
user_id product_name probability_present probability_absent
1 Soda .5 .5
1 Pistachios .5 .5
I'm having trouble figuring out how to get the data into a form so that I can calculate the probabilities and specifically coming up with a way to compare all of the t,t-1 combinations.
I have code that I've written to get things into this form, but I'm stuck at this point. I've written my code using the dplyr R package, but I could translate something in SQL into the R code. I can post my code in R if it will be helpful, but it is pretty simple at this point as I just had to do a few joins to get the table into this shape.
What else do I have to do to get the table/values that I'm trying to calculate?
This seems to give you the desired probabilities:
SELECT user_id,
product_name,
COUNT(DISTINCT order_number) / COUNT(*) AS prob_present,
1 - COUNT(DISTINCT order_number) / COUNT(*) AS prob_absent
FROM tbl
WHERE user_id = 1
GROUP BY user_id, product_name;
Or at least it gives you the numbers you have. If this is not right, please provide a slightly more complex example dataset.

mysql: specific item to be first and then to sort the rest chronologically to end then chronologically from beginning

Lets say I have the following table:
friends
_______
id name
1 johnny
2 tam
3 slick
4 mat
5 Rhanda
6 Tommy
7 ike
8 Spencer
9 Alan
I want to get all the friends list but I want the id 5 to be the first item in the list. I want the next item to follow chronologically until the end. Then the list start from the beginning until all results have been returned. So it should end like this...
friends
_______
id name
5 Rhanda
6 Tommy
7 ike
8 Spencer
9 Alan
1 johnny
2 tam
3 slick
4 mat
I have found and tried this but as you can imagine it's only returning the first item and then ordering the rest from 1 up. I have searched and worked on this for 2 days now and really could use some help. Any suggestions?
You can use:
order by (id >= 5) desc, id
This puts all ids 5 or greater first. Then it sorts each of the parts in ascending order.
MySQL treats boolean expressions as integers in an integer context, with 0 for false and 1 for true. So, to put the true values first, desc is needed.
You can add an expression to order clause like this:
select id, name from table
order by
case when id >= 5 then 0 else 1 end
, id

Compare rows and get percentage

I found it hard to find a fitting title. For simplicity let's say I have the following table:
cook_id cook_rating
1 2
1 1
1 3
1 4
1 2
1 2
1 1
1 3
1 5
1 4
2 5
2 2
Now I would like to get an output of 'good' cooks. A good cook is someone who has a rating of at least 70% of 1, 2 or 3, but not 4 or 5.
So in my example table, the cook with id 1 has a total of 10 ratings, 7 of which have type 1, 2 and 3. Only three have type 4 or 5. Therefore the cook with id 1 would be a 'good' cook, and the output should be the cook's id with the number of good ratings.
cook_id cook_rating
1 7
The cook with id 2, however, doesn't satisfy my condition, therefore should not be listed at all.
select cook_id, count(cook_rating) - sum(case when cook_rating = 4 OR cook_rating = 5 then 1 else 0 end) as numberOfGoodRatings from cook
where cook_rating in (1,2,3,4,5)
group by cook_id
order by numberOfGoodRatings desc
However, this doesn't take into account the fact that there might be more 4 or 5 than good ratings, resulting in negative outputs. Plus, the requirement of at least 70% is not included.
You can get this with a comparison in your HAVING clause. If you must have just the two columns in the result set, this can be wrapped as a sub-select select cook_id, positive_ratings FROM (...)
SELECT
cook_id,
count(cook_rating < 4 OR cook_rating IS NULL) as positive_ratings,
count(*) as total_ratings
FROM cook
GROUP BY cook_id
HAVING (positive_ratings / total_ratings) >= 0.70
ORDER BY positive_ratings DESC
Edit Note that count(cook_rating < 4) is intended to only count rows where the rating is less than 4. The MySQL documentation says that count will only count non-null rows. I haven't tested this to see if it equates FALSE with NULL but I would be surprised it it doesn't. Worst case scenario we would need to wrap that in an IF(cook_rating < 4, 1,NULL).
I suggest you change a little your schema to make this kind of queries trivial.
Suppose you add 5 columns to your cook table, to simply count the number of each ratings :
nb_ratings_1 nb_ratings_2 nb_ratings_3 nb_ratings_4 nb_ratings_5
Updating such a table when a new rating is entered in DB is trivial, just as would be recomputing those numbers if having redundancy makes you nervous. And it makes all filterings and sortings fast and easy.

MySQL delete except one

I've the following database structure:
id idproperty idgbs
1 1 136
2 1 128
3 1 10
4 1 1
5 2 136
6 2 128
7 2 10
8 2 1
9 3 561
10 3 560
11 3 10
12 3 1
13 4 561
14 4 560
15 4 10
16 4 1
17 5 234
18 5 120
19 5 1
20 6 234
21 6 120
22 6 1
Here are the details:
The table refers idproperty with different geographic location. For example:
idgbs
1 refers to United States
10 refers to Alabama with parentid 1 (United States)
128 refers to Alabama Gulf Coast with parentid 10 (Alabama)
136 Dauphin Island with parentid 128 (Alabama Gulf Coast)
So, the structure is:
United States > Alabama > Alabama Gulf Coast > Dauphin Island
I want to delete all entries for idproperty EXCEPT the first with the set of idgbs 136, 128, 10, 1 i.e. leave atleast 1 property in all GBS and delete others.
Also, sometimes it is 4 level of geographic entries, sometimes it is 3 level.
Please share the logic & SQL query to delete all entries except one in every unique GBS.
GBS 1, 10, 128, 136 is one unique, so database should only contain 1 property id with these GBS.
After the query, the table would look like this:
id idproperty idgbs
1 1 136
2 1 128
3 1 10
4 1 1
9 3 561
10 3 560
11 3 10
12 3 1
17 5 234
18 5 120
19 5 1
Rephrasing the question:
I want to keep properties in every root level GBS i.e. there should be only ONE property in Dauphin Island.
Whew... I think I understand what you are after now. I couldn't let this one go ;-)
I had to realize that in the question, you wanted property 2 deleted, because it shared a hierarchy with property 1. Once I realized that, I got the following idea. Basically, we join to an aggregated version of self twice: the first one tells us what our "gbs hierarchy path" is, and the second one matches any previous properties with the same hierarchy. Rows which find that there are no "previous" properties that share their hierarchy are spared, the rest with that hierarchy are deleted. It's possible that this could be further tweaked, but I wanted to share this now. I have tested it with the data you showed, and I got the results you posted.
DELETE
each_row.*
FROM property_gbs AS each_row
JOIN ( SELECT
idproperty,
GROUP_CONCAT(idgbs ORDER BY idgbs DESC SEPARATOR "/") AS idgbs_path
FROM property_gbs
GROUP BY idproperty
) AS mypath
USING(idproperty)
LEFT JOIN ( SELECT
idproperty,
GROUP_CONCAT(idgbs ORDER BY idgbs DESC SEPARATOR "/") AS idgbs_path
FROM property_gbs
GROUP BY idproperty
) AS previous_property
ON mypath.idgbs_path = previous_property.idgbs_path
AND previous_property.idproperty < each_row.idproperty
WHERE previous_property.idproperty
Note that the last line is not a typo, we are just checking if there is a previous property with the same path. If there is, then delete the currently-evaluated row.
Cheers!
note for clarification
The thought here is to associate every row with it's hierarchy, even if it's a row which represents somewhere in the middle of the hierarchy (such as row: {2, 1, 128} in the question). With the first join to the aggregate, each row now "knows" what it's path is (so that row would get "136/128/10/1"). We can then use that value in the second join to find other properties with the same path, but only if they have a LOWER property id. This allows us to check for the existence of a lower-ID property with the same "path", and delete any row which represents a property which does have such a "lower-order path-sibling".
I'm really not sure. But try this.
DELETE a1 FROM table a1, table a2
WHERE a1.id > a2.id
AND a1.idgbs = a2.idgbs
AND a1.idgbs <> 1
if you want to keep the row with the lowest id.
This one was difficult #dang but I enjoyed the challenge.
;With [CTE] as (Select id ,idproperty ,idgbs ,Row_Number() Over(Partition By idgbs order by idproperty Asc) as RN From [TableGBS])
,[CTE2] as (Select * From [CTE] Where RN > 1)
,[CTE3] as (Select idproperty ,count(*) as [Count] From [CTE2] Group by idproperty)
Delete from [TableGBS] Where id in (Select a.id From [CTE] as a Left Join [CTE3] as b on a.idproperty = b.idproperty Where RN > 1 And [Count] > 2);
Since i dont think you can do a delete statement in sqlfiddle here are the rows it will delete showing in a select statement: http://sqlfiddle.com/#!3/08108/40
Edit: I use MySQL linked to Microsoft SQL Server Management Studio so this might not work for you
This technique joins the table against an aggregated version of itself, essentially matching each row in the table to the knowledge of which idproperty is the lowest for it's idgbs, and deletes the row if it does not share that idproperty (i.e. the row with the lowest idproperty, when joined to itself, will not be deleted, but the rest of the rows with that idgbs will).
DELETE
each_row.*
FROM table AS each_row
JOIN (select MIN(idproperty), idgbs FROM table GROUP BY idgbs) as lowest_id
USING(idgbs)
WHERE each_row.idproperty != lowest_id.idproperty;