How to select row pairs that match certain criteria in mysql - mysql

I have a table of things. Here is a simplified structure:
CREATE TABLE `things` (
`thing_id` int(11) NOT NULL AUTO_INCREMENT,
`thing_group` int(11) NOT NULL,
`thing_status` int(1) NOT NULL DEFAULT '0'
);
There are 2 types of things. Primary, which would have thing_id = thing_group and secondary, which would having a unqiue thing_id but the same thing_group as the primary item.
INSERT INTO `things` (`thing_id`, `thing_group`, `thing_status`) VALUES
(1, 1, 0),
(2, 1, 1),
(3, 3, 1),
(4, 3, 0),
(5, 5, 1),
(6, 5, 1),
(7, 7, 0),
(8, 7, 0),
(9, 9, 1),
(10, 9, 1),
I have thousands of these pairs.
thing_status can be 0 for either the primary or the secondary (or both), but I want to select ONLY a pair (at random) that has thing_status = 1 both for primary and secondary thing.
So from the sample data I provided, it should only return pairs which are either thing_id 5 and 6, or 9 and 10 (at random)
Hard part:
Some things can just have the primary thing only, and no secondary. The query should still return those and treat them equally to things that come in pairs.
Am i better off doing 2 queries or a convoluted single query?

Group your rows by thing_group and select those where the number of rows is the same as the sum of thing_status. Join the resulting set back to the original table on thing_group to obtain the actual rows corresponding to the groups. So:
SELECT
t.thing_id,
t.thing_group
FROM things t
INNER JOIN (
SELECT thing_group
FROM things
GROUP BY thing_group
HAVING COUNT(*) = SUM(thing_status)
ORDER BY RAND()
LIMIT 1
) g ON t.thing_group = g.thing_group

Not so hard, maybe the random part is a bit more tricky:
select *
from things
where
thing_group = (select thing_group
from things
where thing_status = 1
group by thing_group
having count(thing_id) = 2
limit 1)
limit 1

My intuition says that you should use 2 queries with a UNION ALL. But... with MySQL it's not always clear what works and what doesn't.
I believe that this query does what you want though:
SELECT t1.thing_id, t1.group_id
FROM things t1
LEFT JOIN things t2
ON t2.thing_id = t2.thing_group
AND t1.thing_id != t2.thing_id
WHERE (
t1.thing_id = t1.thing_group
AND t2.thing_id IS NULL
) OR (
t1.thing_group = t2.thing_id
AND t1.thing_id != t1.thing_group
)
GROUP BY t1.thing_id, t1.group_id

Related

Problem MYSQL Query join with output want

I has problem with MySql query. I has try many time for query, but still not get what I want. Maybe anyone can help my problem.
This is structure table and what output I want :
This is whats i try, but when #IDPERIODS=2, thats not show i want :
SET #IDPERIODS:=2;
SELECT billing.*
FROM _t_data_user
LEFT JOIN (
SELECT user_id as iduser,
IF(a.id_bill_type=b.id_bill_type,a.id_setting_bill,ifnull(b.id_setting_bill,a.id_setting_bill)) as idsettingbill,
id_user_group as group_user,
IF(a.id_bill_type=b.id_bill_type,a.id_bill_type, ifnull(b.id_bill_type,a.id_bill_type)) as idbilltype,
IF(a.id_bill_type=b.id_bill_type,a.id_period, ifnull(b.id_period,a.id_period)) as period,
IF(a.id_bill_type=b.id_bill_type,a.amount_bill, ifnull(b.amount_bill,a.amount_bill)) as amount_billing
FROM _t_data_user
LEFT JOIN _t_setting_bill_user b ON b.id_group_user=id_user_group and b.id_period=#IDPERIODS
LEFT JOIN _t_setting_bill_user a ON a.id_user=user_id and a.id_period=#IDPERIODS
WHERE IFNULL(a.id_period, b.id_period) = #IDPERIODS
) billing ON iduser = user_id
WHERE period = #IDPERIODS
GROUP BY user_id, idbilltype
This MySql table scheme :
Table structure and sample data:
CREATE TABLE `_t_data_user` (
`user_id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`id_user_group` int(4) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `_t_data_user` (`user_id`, `id_user_group`)
VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 2);
CREATE TABLE `_t_setting_bill_user` (
`id_setting_bill` int(11) unsigned NOT NULL AUTO_INCREMENT,
`id_group_user` int(4) DEFAULT NULL,
`id_user` int(4) DEFAULT NULL,
`id_period` int(4) DEFAULT NULL,
`id_bill_type` int(4) DEFAULT NULL,
`amount_bill` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id_setting_bill`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
INSERT INTO `_t_setting_bill_user`
(`id_setting_bill`, `id_group_user`, `id_user`,
`id_period`, `id_bill_type`, `amount_bill`)
VALUES
(1, 1, 0, 1, 1, 1000),
(2, 1, 0, 1, 2, 500),
(3, 0, 1, 1, 1, 900),
(4, 0, 1, 2, 1, 1000),
(5, 1, 0, 2, 2, 500),
(6, 2, 0, 1, 1, 1100);
This gives you the raw data you want:
SELECT *
FROM
setting_bill_user s
JOIN data_user d
ON
s.id_group_user = d.id_user_group OR
s.id_user = d.user_id
Look:
You just have to choose and alias the columns appropriately
I can't work out why your desired output is missing the 1000 row for user id 1/setting bill 1, but I'm sure you can add some WHERE clause to cover that, whatever the reason may be
It seems that removing a few parts from the subquery will return the result that you're looking for:
SELECT billing.*
FROM _t_data_user
JOIN (
SELECT user_id AS iduser,
IF(a.id_bill_type=b.id_bill_type,a.id_setting_bill,IFNULL(b.id_setting_bill,a.id_setting_bill)) AS idsettingbill,
id_user_group AS group_user,
IF(a.id_bill_type=b.id_bill_type,a.id_bill_type, IFNULL(b.id_bill_type,a.id_bill_type)) AS idbilltype,
IF(a.id_bill_type=b.id_bill_type,a.id_period, IFNULL(b.id_period,a.id_period)) AS period,
IF(a.id_bill_type=b.id_bill_type,a.amount_bill, IFNULL(b.amount_bill,a.amount_bill)) AS amount_billing
FROM _t_data_user t
LEFT JOIN _t_setting_bill_user b ON b.id_group_user=t.id_user_group
LEFT JOIN _t_setting_bill_user a ON a.id_user=t.user_id
WHERE IFNULL(a.id_period, b.id_period) = #IDPERIODS
) billing ON iduser = user_id
GROUP BY user_id, idbilltype;
I've removed AND id_period=#IDPERIODS of the ON condition in both of LEFT JOIN in the subquery.
I've changed LEFT JOIN to JOIN in the outer query because you were doing LEFT JOIN with a WHERE condition on the data from the right reference (the subquery). Which, in practice is just a normal JOIN so LEFT JOIN is unnecessary. Therefore, I also removed WHERE period = #IDPERIODS from the outer query.
And that's it. Other than that, most of your original query structures are still intact.
Demo fiddle
Halo bro desugha,
I think you should make it on programatic way, you should re-arrange and create object to combine data from data_user INTO setting_bill_user
SELECT compacted_usr_bill.* FROM (
SELECT billusr.*,
usr.user_id AS usr_id_usr, usr.id_user_group AS usr_id_group
FROM (
SELECT bill.id_setting_bill,
CASE
WHEN bill.id_user > 0 AND bill.id_group_user = 0 THEN bill.id_user
WHEN bill.id_user = 0 AND bill.id_group_user > 0 THEN bill.id_group_user
ELSE bill.id_user
END AS grouped_id_usr,
bill.id_period, bill.id_bill_type, bill.amount_bill
FROM _t_setting_bill_user AS bill)
AS billusr
LEFT JOIN _t_data_user AS usr
ON billusr.grouped_id_usr IN(usr.user_id, usr.id_user_group)
) AS compacted_usr_bill
This query will combine them, you can filter again with Grouping or Programmatic way

Get n oldest rows, but no more than x that have the same value in a column

I have a simple table
CREATE TABLE `example` (
`id` int(12) NOT NULL,
`food` varchar(250) NOT NULL
);
With the following data
INSERT INTO `example` (`id`, `food`) VALUES
(1, 'apple'),
(2, 'apple'),
(3, 'apple'),
(4, 'apple'),
(5, 'apple'),
(6, 'apple'),
(7, 'apple'),
(8, 'banana'),
(9, 'banana'),
(10, 'potato'),
(11, 'potato'),
(12, 'potato'),
(13, 'banana'),
(14, 'banana'),
(15, 'banana');
I want to get the oldest 10 rows
SELECT *
FROM example
ORDER BY id ASC
LIMIT 10
But I don't want to get more than 5 rows where food has the same value.
My current query receives 7 apple (more than I want), 2 banana, and 1 potato. In the data provided, I'd want to receive 5 apple, 2 banana, and 3 potato.
How can I accomplish this?
Update:
SQL Group BY, Top N Items for each Group is not a duplicate because it involves a different database. In particular, GROUP BY works different in sql-server than it does in MySQL
You can add a count (in reverse) for each food . . . using variables or a correlated subquery. This will use the latter:
select t.*
from (select t.*,
(select count(*) from example t2 where t2.food = t.food and t2.id >= t.id) as seqnum
from example t
) t
where seqnum <= 5
order by id desc
limit 10;
I didn't create the table and test this, but it should give you what you want. Just a different approach than the one above.
Select *
From (Select ID, Food
, Count(Food) Over(Partition By Food Order by ID) as Appearances
From Your_Table) as a
Where a.Appearances <= 5
Order By ID Asc
You can obviously put the limit if you want.

Get time difference between all consecutive rows (latest one not printing)

I'm trying to retrieve all columns data along with the time difference between all consecutive rows from the following table, where (sender_id = 1 OR = 2) and (recipient_id = 2 OR = 1).
CREATE TABLE records (
id INT(11) AUTO_INCREMENT,
send_date DATETIME NOT NULL,
content TEXT NOT NULL,
sender_id INT(11) NOT NULL,
recipient_id INT(11) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO records (send_date, content, sender_id, recipient_id) VALUES
('2013-08-23 14:50:00', 'record 1/5', 1, 2),
('2013-08-23 14:51:00', 'record 2/5', 2, 1),
('2013-08-23 15:50:00', 'record 3/5', 2, 1),
('2013-08-23 15:50:13', 'record 4/5', 1, 2),
('2013-08-23 16:50:00', 'record 5/5', 1, 2);
Problem is my select query won't output the latest record because of the WHERE clause :
SELECT t1.content, DATE_FORMAT(t1.send_date, '%b, %D, %H:%i') AS 'pprint_date',
TIMESTAMPDIFF(MINUTE, t1.send_date, t2.send_date) AS 'duration'
FROM records t1, records t2
WHERE (t1.id = t2.id - 1) /*<= this subtraction excludes latest record*/
AND ((t1.sender_id = 1 AND t1.recipient_id = 2)
OR (t1.sender_id = 2 AND t1.recipient_id = 1))
ORDER BY t1.id ASC
How can I properly get the time difference between all consecutive records while still printing all of them ?
I would use a correlated subquery:
select r.*,
(select r2.send_date
from records r2
where (r2.sender_id in (1, 2) or r2.recipient_id in (1, 2)) and
r2.send_date > r.send_date
order by r2.send_date asc
limit 1
) as next_send_date
from records r
where r.sender_id in (1, 2) or r.recipient_id in (1, 2);
You can get the duration (instead of the next time) by using TIMESTAMPDIFF(MINUTE, r.send_date, r2.send_date) in the subquery. I think the first version is easier for you to test with to see what is happening.

Get maximum id if there are parent id otherwise get all results

I have a table and having the following data
CREATE TABLE IF NOT EXISTS `tbl_ticket` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL,
`ticket_title` varchar(250) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=16 ;
--
-- Dumping data for table `tbl_ticket`
--
INSERT INTO `tbl_ticket` (`id`, `parent_id`, `ticket_title`) VALUES
(1, 0, 'tyty'),
(2, 0, 'testing'),
(3, 0, 'test from ticket'),
(4, 0, 'test ticket'),
(5, 0, 'test ticket'),
(6, 0, 'test ticket'),
(7, 0, 'test ticket'),
(8, 5, 'test ticket'),
(9, 0, '1 Ticket'),
(10, 0, '2Ticket'),
(11, 2, 'ticket2'),
(12, 2, 'ticket1'),
(13, 0, 'title 1234'),
(14, 0, 'titles 1234'),
(15, 14, 'sample 1234');
I need to return all rows where id is not present in parent id from the table.
Also if id is present in the parent_id column, I want to get the row having the highest id which matches the parent_id.
i.e. I need to return rows with id 1, 3,4,6,7,8,9,10, 12,13, 15.
I tried this sql
SELECT `id` , `parent_id`
FROM `tbl_ticket`
WHERE id NOT
IN (
SELECT parent_id
FROM tbl_ticket
)
but it returns value 11 also, instead it should return 12 which is the row having highest id with parent_id =2
Assuming the 5 in your expected output is a typo, as 5 appears in the parent_id field for id=8, you can get your result by the union of two simple queries.
select t1.id
from tbl_ticket t1
where not exists (
select 1 from tbl_ticket
where parent_id = t1.id
)
and parent_id = 0
union all
select max(id)
from tbl_ticket
where parent_id <> 0
group by parent_id
order by id asc
Fiddle here
The query is in two parts. the first part gets all the tickets that are not present in another tickets parent_id field, and which themselves do not have a parent (parent_id = 0).
The second part of the query looks at those tickets that DO have a parent (parent_id <> 0), and for each group of tickets that share the same parent_id, selects the one with the max id.
The results are then combined with a union to give a single result set. Since the two result sets are mutually exclusive, we can use union all to skip over the duplicate check.
If I understand correctly, you can do this with not exists rather than combining two separate queries. The advantage is that no duplicate elimination is needed (as is needed when you use union):
select t.*
from tbl_ticket t
where not exists (select 1
from tbl_ticket t2
where t2.parent_id = t.id
) or
not exists (select 1
from tbl_ticket t2
where t2.parent_id = t.id and t2.id > t.id
);
The first gets all rows that have no parents. The second gets all rows with the maximum id for a parent.
For best performance, you want an index on tbl_ticket(parent_id, id).

Best way to update MySQL table with random values from another table

For this (pseudo code) example I have two tables in MySQL:
member { id, name }
names { name }
There are 100 members in member and 10 names. I want to use a random name from names to update the member table. So far I've got this, but, not sure if there is a better method to achieve it.
UPDATE member SET name = (SELECT name FROM names ORDER BY RAND() LIMIT 1);
The code will be executed from a script so I'm looking to avoid functions etc.
Thanks in advance.
You could avoid ordering by rand() by adding id column to your names table and using:
UPDATE member SET name = (SELECT name FROM names WHERE id=floor(1 + rand()*10 ) );
With only 10 names the result won't be much faster, but you would see the difference if you wanted to choose from a bigger set of names as sorting by rand() starts being inefficient quite fast and you do it for every row in members.
Update:
Seems like rand() inside where gives unpredictable results.
Use this one instead:
UPDATE member m1
JOIN ( select id, floor(1+rand()*10) as rnd from member ) m2 on m1.id=m2.id
JOIN names n on n.id = m2.rnd
SET m1.name=n.name
Number of rows affected may vary, if random name matches the one already in the table it doesnt count as update.
Tried to improve piotrm's solution. Seems it works;-)
CREATE TABLE member (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE names (
id INT(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO member VALUES
(1, NULL),
(2, NULL),
(3, NULL),
(4, NULL),
(5, NULL),
(6, NULL),
(7, NULL),
(8, NULL),
(9, NULL),
(10, NULL),
(11, NULL),
(12, NULL),
(13, NULL),
(14, NULL),
(15, NULL);
INSERT INTO names VALUES
(1, 'text1'),
(2, 'text2'),
(3, 'text3'),
(4, 'text4'),
(5, 'text5'),
(6, 'text6'),
(7, 'text7'),
(8, 'text8'),
(9, 'text9'),
(10, 'text10');
UPDATE
member m1
JOIN (SELECT id, #i:=FLOOR(1 + RAND() * 10), (SELECT name FROM names n WHERE n.id = #i) name FROM member) m2
ON m1.id = m2.id
SET
m1.name = m2.name;