Update a sorting index column to move items - mysql

If I have the following table & data to allow us to use the sort_index for sorting:
CREATE TABLE `foo` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`bar_id` INT(11) DEFAULT NULL,
`sort_index` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `foo` (`bar_id`, `sort_index`) VALUES
(1,1),(1,2),(1,3),(1,4),
(2,1),(2,2),(2,3),(2,4),(2,5);
I want to be able to do the following in the most efficient manner:
Move a foo entry to a given position (scoped by the bar_id)
Ensure that the sort_index is always 1 indexed and has no gaps
You should be able to move items to the beginning and end of the list and rule #2 should still be applied
It should be done entirely in queries and as few as possible (as the sets could be very large and looping over them doing individual UPDATEs is not ideal)
To clarify what I'm trying to do, lets assume the table was empty so we have the following data:
id | bar_id | sort_index
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 1 | 4
5 | 2 | 1
6 | 2 | 2
7 | 2 | 3
8 | 2 | 4
9 | 2 | 5
Then if we were to do the following moves
Foo 1 to sort_index 3
Foo 7 to sort_index 1
Foo 5 to sort_index 5
We should get the following data:
id | bar_id | sort_index
1 | 1 | 3
2 | 1 | 1
3 | 1 | 2
4 | 1 | 4
5 | 2 | 5
6 | 2 | 2
7 | 2 | 1
8 | 2 | 3
9 | 2 | 4
And SELECT * FROM foo ORDER BY bar_id, sort_index; gives us:
id | bar_id | sort_index
2 | 1 | 1
3 | 1 | 2
1 | 1 | 3
4 | 1 | 4
7 | 2 | 1
6 | 2 | 2
8 | 2 | 3
9 | 2 | 4
5 | 2 | 5

You should be able to do this in a single query: something along the lines of UPDATE foo SET sort_index = sort_index + 1 WHERE bar_id == b AND sort_index < s1 AND sort_index >= s2, where b is the bar_id of the row to be moved, s1 is the current sort_index of that row, and s2 is the the sort_index you want to move it to. Then, you'd just change the sort_index of the row.
You'd probably want to do the two queries inside a transaction. Also, it might speed things up if you created an index on the sort_index using something like CREATE INDEX foo_index ON foo (sort_index).
(By the way, here I'm assuming that you don't want duplicate sort_index values within a given bar_id, and that the relative order of rows should never be changed except explicitly. If you don't need this, the solution is even simpler.)

Related

MySQL re-order row priority field values and also make them sequential

Given the following table:
+--------+-------------------+-----------+
| ID | Name | Priority |
+--------+-------------------+-----------+
| 1 | Andy | 1 |
| 2 | Bob | 2 |
| 3 | David | 8 |
| 4 | Edward | 9 |
| 5 | CHARLES | 15 |
+--------+-------------------+-----------+
I would like to move CHARLES to between Bob and David by Priority value (ignore the alphabetical list, this is just to make the desired result obvious).
(Also note the Priority values may not be sequential)
To do this I need to change CHARLES' current Priority (15) to Bob's Priority+1, and update David and Edward's Priority to Priority+1.
I can DO this if I know two things, the id of CHARLES and the Priority value of the row he must be after (Bob):
UPDATE mytable SET Priority =
IF(ID = :charles_id, :bob_priority + 1,
IF(Priority >= :bob_priority,
Priority + 1, Priority))
The PROBLEM or at least question is, how could I compress the resulting values to 1,2,3,4,5 instead of 1,2,3,9,10 - and do it in one shot?
Oracle has a "pseudo field" which is the index of the row, but I don't know of anything equivalent in MySQL.
The first part of the problem is fairly trivial...
DROP TABLE IF EXISTS priorities;
CREATE TABLE priorities
(ID SERIAL PRIMARY KEY
,Name VARCHAR(12) NOT NULL
,Priority INT NOT NULL
,INDEX(priority)
);
INSERT INTO priorities VALUES
(101,'Andy',1),
(108,'Bob',2),
(113,'David',8),
(124,'Edward',9),
(155,'CHARLES',15);
UPDATE priorities a
JOIN
( SELECT x.id,x.name, #i:=#i+1 priority FROM priorities x, (SELECT #i:=0) vars ORDER BY id) b
ON b.id = a.id
SET a.priority = b.priority;
SELECT * FROM priorities
+-----+---------+----------+
| ID | Name | Priority |
+-----+---------+----------+
| 101 | Andy | 1 |
| 108 | Bob | 2 |
| 113 | David | 3 |
| 124 | Edward | 4 |
| 155 | CHARLES | 5 |
+-----+---------+----------+

Mysql: Conditions on a unique primary key tuple

Short
Is it possible, that if I have a unique primary key tuple (idUser,idRep) that for each idUser n only certain combinations with idRep are possible?
For example that the following groups of keys are allowed:
(n,1);(n,2);(n,3) OR (n,1);(n,6) OR (n,2);(n,5) OR (n,4);(n,3) OR (n,7)
but not the keys (n,1) and (n,7) together?
Long question
I have the following table
idUser | idGroup | idType |
where the primary key is (idUser,idGroup). There are three different values for idGroup: 1,2,3. For almost all users the idType is always the same independent of the group, but there are a few exceptions. Thus the table looks like that
idUser | idGroup | idType
--------------------------
1 | 1 | 43
1 | 2 | 43
1 | 3 | 43
2 | 1 | 22
2 | 2 | 22
3 | 1 | 12
3 | 3 | 12
4 | 2 | 5
4 | 3 | 6
I thought I could avoid the redundancy with a column idRep which contains the information in which groups one user is as follows
1 -> user is in group 1
2 -> user is in group 2
3 -> user is in group 1 and 2 (1+2=3)
4 -> user is in group 3
5 -> user is in group 1 and 3 (4+1=5)
6 -> user is in group 2 and 3 (4+2=6)
7 -> user is in group 1 and 2 and 3 (4+2+1=7)
this would reduce redundancy and the above table would shrink to
idUser | idRep | idType
-------------------------
1 | 7 | 43
2 | 3 | 22
3 | 5 | 12
4 | 2 | 5
4 | 4 | 6
However the disadvantage is that its possible that even if (idUser,idRep) is a primary key there is the possibility of a wrong entry like
idUser | idRep | idType
-------------------------
1 | 7 | 43
1 | 1 | 42
In this case, there is a contradiction about idType for idUser 1 in group 1.
So my question is: Is it possible to say that for a specific idUser n only the following groups of keys are allowed:
(n,1);(n,2);(n,3) OR (n,1);(n,6) OR (n,2);(n,5) OR (n,4);(n,3) OR (n,7)
but not for (n,1) and (n,7)?

mysql (innodb) increment a value to create a hole

say I have a simple tree type table
id(key) | parent | order
============================
1 | 0 | 0
2 | 0 | 1
4 | 2 | 0
5 | 2 | 1
6 | 2 | 2
I want to insert a new node so that it has parent = 2 and order = 1, so it then the table data looks like:
id(key) | parent | order
============================
1 | 0 | 0
2 | 0 | 1
4 | 2 | 0
5 | 2 | 2
6 | 2 | 3
7 | 2 | 1
E.g. existing rows increment their order value.
What's the best way to ensure that existing rows increment their order (non-key field) starting at an existing arbitrary value, to make a hole for my insert statement?
If you have a new row with parent $p and order position $o you can:
UPDATE table SET order = order + 1 WHERE parent = $p AND order >= $o
and then:
INSERT INTO order (id,parent,order) VALUES($id,$p,$o)

Count the rows of a table, while checking if the value is NULL or not and grouping the results by the values of another column

I am stuck trying to find a solution to my silly little problem.
The MySQL table looks as follows:
-- Create a table that will record all AdCamp hits
CREATE TABLE `advertising_campaign_hits` (
`adcamp_hit_id` INT NOT NULL AUTO_INCREMENT,
`adcamp_id` INT,
`customer_id` INT,
`recorded_at` DATETIME,
PRIMARY KEY (`adcamp_hit_id`)
) ENGINE=MyISAM;
Example values would look like this:
a_h_id | a_id | c_id | ...
1 | 1 | 1 | ...
2 | 1 | 2 | ...
3 | 1 | 3 | ...
4 | 1 | 0 | ...
5 | 1 | 0 | ...
6 | 2 | 1 | ...
7 | 2 | 0 | ...
The goal here is to count the number of hits for each of the advertising campaigns, but divide them into two groups of KnownCustomers and UnknownCustomers and then further divide them each by adcamp_id.
So the results I would expect to get are:
adcamp_id | HitsByKnown | HitsByUnknown
1 | 3 | 2
2 | 1 | 1
I am currently stuck in where I can get SQL to give me two separate rows for each of the adcamps, but the results of COUNT(*) list all of my entries.
So what I get is:
adcamp_id | HitsByKnown | HitsByUnknown
1 | 4 | 3
2 | 4 | 3
I can't figure out how to split it all up.
select adcamp_id,sum(if(customer_id>0,1,0)) as HitsByKnown,sum(if(customer_id=0,1,0)) as HitsByUnknown from advertising_campaign_hits group by adcamp_id
or even easier:
select adcamp_id,sum(customer_id!=0) as HitsByKnown,sum(customer_id=0) as HitsByUnknown from advertising_campaign_hits group by adcamp_id

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