mysql - dynamic query depending on multiple grouped other values - mysql

In my application there are products and a product-variant-group which has a defined set of properties which a product of this group must declare and the combination of the properties is unique across that product-variant-group.
Example screen from amazon:
In the image the first select menu has all values obviously. The next select menu depends on the previously selected value, and so on.
Those defined group properties have a unique priority assigned to it which in the following derived table equals the property itself.
For a given property/priority and given list of of property/priority-value pairs. I want to retrieve its possible values.
The priorities of the value pairs must be smaller then the given priority.
public String[] getProductVariantGroupValues(int productVariantGroupId, int priority, Map<Integer, String> prevValues);
An example makes it much clearer:
I have an sql statement which lists all product-variant-group properties that related products have defined:
+---------+----------+-------- +
| product | priority | value |
+---------+----------+---------+
| 1 | 1 | Black |
| 1 | 2 | 38 |
| 1 | 3 | Dots |
| 2 | 1 | Black |
| 2 | 2 | 38 |
| 2 | 3 | Stripes |
| 3 | 1 | Yellow |
| 3 | 2 | 40 |
| 3 | 3 | Stripes |
+---------+----------+---------+
Other view for understanding *(priority is arbitrary just for understanding, with this view this would be trivial)*:
+---------+--------+--------+---------+
| product | value1 | value2 | value3 |
+---------+--------+--------+---------+
| 1 | Black | 38 | Dots |
| 2 | Black | 38 | Stripes |
| 3 | Yellow | 40 | Stripes |
+---------+---------------------------+
Call the above method with priority = 3 and prevValues = {(1, Black), (2, 38)} should result in following result array: {Dots, Stripes}.
If black is selected for property/priority 1 and 38 is selected for property/priority 2 the only possible following values for property/priority 3 are {Dots, Stripes}
The example is simplified and an arbitrary number of properties/priority should be supported. The query must be created dynamically to support arbitrary number of lower priority values.
Maybe I should just use the second table appraoch with a fixed set of properties which would make the unique constraint and this query very simple.

If I understand the question correctly, you have a value for priority 1 and for priority 2 and want to get all priority 3 values that match. The following query gets the products:
select product
from t
group by product
having max(case when priority = 1 and value = 'Black' then 1 else 0 end) = 1 and
max(case when priority = 2 and value = 40 then 1 else 0 end) = 1
To get the priority 3 values requires or a clever select statement (assuming priorities are not repeated for a product):
select product, max(case when priority = 3 then value end)
from t
group by product
having max(case when priority = 1 and value = 'Black' then 1 else 0 end) = 1 and
max(case when priority = 2 and value = 40 then 1 else 0 end) = 1
These queries are to give you an idea of how to construct the queries in your code. The generalization should be pretty straightforward.

Related

SQL - get records from many-to-many relations by the user itself -OR- his group

I have two database tables, one as the main table and the other as the relation table.
The first table is a table of contents and the second table is a table that connects to users or groups.
Some data may also be modified in this second table.
I'm not sure about the structure and performance.
for example, we have User Id 160 which is under group id 7
So for the first, we have a post Table.
id | title | content | cover | status
------------------------------------------------
1 | first | content 1 | /img/... | 1
2 | second | content 2 | /img/... | 1
3 | another | content 3 | /img/... | 1
4 | four | content 4 | /img/... | 1
5 | five | content 5 | /img/... | 1
and for the second we have a post_rel Table:
id | group_id | user_id | post_id | title | cover | sort | status
---------------------------------------------------------------------------
1 | 7 | NULL | 1 | g title | img/... | 1 | 1
2 | NULL | 160 | 1 | u title | NULL | 2 | 1 *** selected for user_id
3 | 7 | NULL | 2 | NULL | img/... | 6 | 0
4 | NULL | 160 | 2 | NULL | img/... | 4 | 1 *** selected for user_id
5 | NULL | 160 | 3 | some | img/... | 3 | 1 *** selected for user_id
6 | 7 | NULL | 4 | NULL | img/... | 9 | 1 *** selected for group_id
7 | NULL | 165 | 5 | NULL | img/... | 5 | 0
This is the basic query we have.
select
`post_rel`.`title` as `custom_title`,
`post_rel`.`cover` as `custom_cover`,
`post_rel`.`group_id`,
`post_rel`.`user_id`,
`post`.*
from
`post`
inner join `post_rel` on `post`.`id` = `post_rel`.`post_id`
where
`post`.`status` = 1
and `post_rel`.`status` = 1
and (
`post_rel`.`user_id` = 160
or (
`post_rel`.`group_id` = 7
and `post_rel`.`post_id` not in (
select
`post_rel`.`post_id`
from
`post_rel`
where
`post_rel`.`user_id` = 160
)
)
)
order by
`post_rel`.`sort` asc
So, what you think about the basic query? Especially in the subquery, won't performance drop in a large table? Is it possible to write a better and simpler query or change the structure?
Edit: this is sqlfiddle example of my code and structure http://sqlfiddle.com/#!9/ed9d4b/1
I would change it to use "not exists" instead of "not in" and would use aliases so I could pull it off like so:
select
b.`title` as `custom_title`,
b.`cover` as `custom_cover`,
b.`group_id`,
b.`user_id`,
a.*
from
`post` a
inner join `post_rel` b on a.`id` = b.`post_id`
where
a.`status` = 1
and b.`status` = 1
and (
b.`user_id` = 160
or (
b.`group_id` = 7
and not exists (
select
'x'
from
`post_rel` c
where
c.`user_id` = 160 and c.`post_id`=b.`post_id`
)
)
)
order by
b.`sort` asc
Typically when managing users and group, there's this notion of an exception user who directly can get assigned to assets just like the whole group. This seems to be an example of that.
From a modeling-only perspective, there are 2 ways to deal with that:
Ensure that every user exists in a group and that you only assign assets to groups. For the exception user, create a group. You could even enforce that every user belongs to only one group. This way your post_rel table deals with only groups. Unfortunately, the relationship between group and user is not understood well enough to weigh in appropriately.
Driven only by the need to eliminate null values towards a good model which also reduces overhead, the other option is to use name value pairs and allows the User and Group to exist in the same field with another field besides it, denoting Group or User.
These are the SQL Fiddle:
NOT EXISTS version: http://sqlfiddle.com/#!9/1af8cf/2
NOT IN version: http://sqlfiddle.com/#!9/1af8cf/1
Some reading on nulls https://dev.mysql.com/doc/refman/5.6/en/data-size.html
Specifically:
Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.

Making changes to multiple records based on change of single record with SQL

I have a table of food items. They have a "Position" field that represents the order they should appear in on a list (listID is the list they are on, we don't want to re-order items on another list).
+--id--+--listID--+---name---+--position--+
| 1 | 1 | cheese | 0 |
| 2 | 1 | chips | 1 |
| 3 | 1 | bacon | 2 |
| 4 | 1 | apples | 3 |
| 5 | 1 | pears | 4 |
| 6 | 1 | pie | 5 |
| 7 | 2 | carrots | 0 |
| 8,9+ | 3,4+ | ... | ... |
+------+----------+----------+------------+
I want to be able to say "Move Pears to before Chips" which involves setting the position of Pears to position 1, and then incrementing all the positions inbetween by 1. so that my resulting Table look like this...
+--id--+--listID--+---name---+--position--+
| 1 | 1 | cheese | 0 |
| 2 | 1 | chips | 2 |
| 3 | 1 | bacon | 3 |
| 4 | 1 | apples | 4 |
| 5 | 1 | pears | 1 |
| 6 | 1 | pie | 5 |
| 7 | 2 | carrots | 0 |
| 8,9+ | 3,4+ | ... | ... |
+------+----------+----------+------------+
So that all I need to do is SELECT name FROM mytable WHERE listID = 1 ORDER BY position and I'll get all my food in the right order.
Is it possible to do this with a single query? Keep in mind that a record might be moving up or down in the list, and that the table contains records for multiple lists, so we need to isolate the listID.
My knowledge of SQL is pretty limited so right now the only way I know of to do this is to SELECT id, position FROM mytable WHERE listID = 1 AND position BETWEEN 1 AND 5 then I can use Javascript (node.js) to change position 5 to 1, and increment all others +1. Then UPDATE all the records I just changed.
It's just that anytime I try to read up on SQL stuff everyone keeps saying to avoid multiple queries and avoid doing syncronous coding and stuff like that.
Thanks
This calls for a complex query that updates many records. But a small change to your data can change things so that it can be achieved with a simple query that modifies just one record.
UPDATE my_table set position = position*10;
In the old days, the BASIC programming language on many systems had line numbers, it encouraged spagetti code. Instead of functions many people wrote GOTO line_number. Real trouble arose if you numbered the lines sequentially and had to add or delete a few lines. How did people get around it? By increment lines by 10! That's what we are doing here.
So you want pears to be the second item?
UPDATE my_table set position = 15 WHERE listId=1 AND name = 'Pears'
Worried that eventually gaps between the items will disappear after multiple reordering? No fear just do
UPDATE my_table set position = position*10;
From time to time.
I do not think this can be conveniently done in less than two queries, which is OK, there should be as few queries as possible, but not at any cost. The two queries would be like (based on what you write yourself)
UPDATE mytable SET position = 1 WHERE listID = 1 AND name = 'pears';
UPDATE mytable SET position = position + 1 WHERE listID = 1 AND position BETWEEN 2 AND 4;
I've mostly figured out my problem. So I've decided to put an answer here incase anyone finds it helpful.
I can make use of a CASE statement in SQL. Also by using Javascript beforehand to build my SQL query I can change multiple records.
This builds my SQL query:
var sql;
var incrementDirection = (startPos > endPos)? 1 : -1;
sql = "UPDATE mytable SET position = CASE WHEN position = "+startPos+" THEN "+endPos;
for(var i=endPos; i!=startPos; i+=incrementDirection){
sql += " WHEN position = "+i+" THEN "+(i+incrementDirection);
}
sql += " ELSE position END WHERE listID = "+listID;
If I want to move Pears to before Chips. I can set:
startPos = 4;
endPos = 1;
listID = 1;
My code will produce an SQL statement that looks like:
UPDATE mytable
SET position = CASE
WHEN position = 4 THEN 1
WHEN position = 1 THEN 2
WHEN position = 2 THEN 3
WHEN position = 3 THEN 4
ELSE position
END
WHERE listID = 1
I run that code and my final table will look like:
+--id--+--listID--+---name---+--position--+
| 1 | 1 | cheese | 0 |
| 2 | 1 | chips | 2 |
| 3 | 1 | bacon | 3 |
| 4 | 1 | apples | 4 |
| 5 | 1 | pears | 1 |
| 6 | 1 | pie | 5 |
| 7 | 2 | carrots | 0 |
| 8,9+ | 3,4+ | ... | ... |
+------+----------+----------+------------+
After that, all I have to do is run SELECT name FROM mytable WHERE listID = 1 ORDER BY position and the output will be as follows::
cheese
pears
chips
bacon
apples
pie

Flag the row with the max value in field B based on name field in Field A

I am able to find the row with the maximum value for a Value field in an given set of records with the same name using
Select Name, Max(Value) from table group by Name, Value
which returns to me the record with the highest value but I am looking to turn this into an update so I can
Flag the record with the highest value in a IsMaxValue
For each record in the Name, Value group store the highest value found in a 'MaxValue' field
Simple select version is here:
http://sqlfiddle.com/#!9/ccd32/5
with fields ready for updates as per above if it is possible.
I believe this statement is what you might be looking for:
update maxvalues
join (
Select Color, Max(`Value`) max_value
from MaxValues
group by Color
) a on maxvalues.color = a.color and value = a.max_value
set ismaxrecord = '1', maxrecordid = a.max_value;
Sample SQL Fiddle
Given your sample data the table would look like below after the update:
| Color | Value | IsMaxRecord | MaxRecordID |
|--------|-------|-------------|-------------|
| Orange | 1 | | 0 |
| Orange | 2 | | 0 |
| Orange | 3 | 1 | 3 |
| Black | 30 | 1 | 30 |
| Black | 20 | | 0 |
| Black | 10 | | 0 |

Using MIN() in SET statement MySQL

I am using MySQL. Lets call a table that I have as Inventory which looks is below:
+----+--------+--------+-------------+----------+
| ID | Price1 | Price2 | TargetPrice | Quantity |
+----+--------+--------+-------------+----------+
| 1 | 12 | 1 | | 0 |
| 2 | 3 | 3 | 3 | 2 |
| 3 | | 4 | | 0 |
| 4 | 2 | 2 | 2 | 2 |
| 5 | 5 | 45 | 5 | 1 |
+----+--------+--------+-------------+----------+
Now, I need to update the TargetPrice to minimum of Price1 and Price2 for any row whose Quantity is 0
I have tried:
UPDATE Inventory SET
TargetPrice= MIN(Price1,Price2)
WHERE Quantity >0
However, MySQL complains about the usage of MIN() function. I know it is expecting MIN() to work on the data contained inside column, rather than taking MIN() of two columns of a specified row.
Anyway to achieve this other than cursors?
EDIT:
Price1 and Price2 can be null or 0 and in all these cases, it should be treated as infinity so that the other price gets to be minimum when compared against it.
Use LEAST instead of MIN:
UPDATE Inventory
SET TargetPrice = LEAST(Price1,Price2)
WHERE Quantity = 0
MIN is an aggregate function operating on a rowset, whereas LEAST operates on the list of arguments passed.
EDIT:
UPDATE Inventory
SET TargetPrice = LEAST(COALESCE(Price1, Price2), COALESCE(Price2, Price1))
WHERE Quantity = 0
You can use COALESCE to handle NULL values.
EDIT2:
You can use NULLIF to handle 0 values:
UPDATE Inventory
SET TargetPrice = LEAST(COALESCE(NULLIF(Price1,0), Price2),
COALESCE(NULLIF(Price2,0), Price1))
WHERE Quantity = 0

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