MySQL limit result based on value in joined table - mysql

I have two tables, the first one contains a limit column. The number in this column must be used to limit the number of records received from the second table.
Is it possible to do this in just one query?
Below my tables and DEMO:
# Create table a
CREATE TABLE `a` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`limit` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
# Create table b
CREATE TABLE `b` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`master` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
# Fill table a
INSERT INTO `a` (`id`, `limit`)
VALUES
(1, 3);
# Fill table b
INSERT INTO `b` (`id`, `name`, `master`)
VALUES
(1, 'record 1', 'groupA'),
(2, 'record 2', 'groupB'),
(3, 'record 3', 'groupA'),
(4, 'record 4', 'groupB'),
(5, 'record 5', 'groupC'),
(6, 'record 6', 'groupC'),
(7, 'record 7', 'groupC'),
(8, 'record 8', 'groupA'),
(9, 'record 9', 'groupD'),
(10, 'record 10', 'groupD');
Query I tested:
SELECT b.*
FROM b
JOIN a ON a.id = 1
GROUP BY b.master
LIMIT 3
This selects only 3 records.
But now I want the limit to be read from table a. I tried to limit like this, but that fails:
SELECT b.*
FROM b
JOIN a ON a.id = 1
GROUP BY b.master
LIMIT a.limit
EDIT:
I've updated the question including the group by statement

You cannot use user-defined MySQL variables or table fields in the LIMIT clause. What you can do is use a variable to enumerate records of table b. Then use this variable to apply the limit:
SELECT t.id, t.name
FROM (
SELECT id, name, #rn := #rn + 1 AS rn
FROM b
CROSS JOIN (SELECT #rn := 0) AS v
ORDER BY id) AS t
INNER JOIN a ON a.id = 1 AND t.rn <= a.`limit`;
Demo here
Edit:
Here's a version that handles groups. It limits the records of b to those groups having the biggest population:
SELECT b.id, b.name, b.master
FROM b
INNER JOIN (
SELECT master, #rn := #rn + 1 AS rn
FROM b
CROSS JOIN (SELECT #rn := 0) AS v
GROUP BY master
ORDER BY COUNT(*) DESC) AS t ON b.master = t.master
INNER JOIN a ON a.id = 1 AND t.rn <= a.`limit`;
Demo here

Related

select last inserted row based on date

Main Problem Is:- select last inserted row based on date
i want to be able to select distinct ref row with the last created_At date.
this is my table and data
DROP TABLE IF EXISTS `transactions_logs`;
CREATE TABLE IF NOT EXISTS `transactions_logs` (
`trans_log_Id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`etat_de_commande` varchar(100) NOT NULL,
`ref` varchar(10) NOT NULL,
`commentaire` text NOT NULL,
`staffId` bigint(20) UNSIGNED NOT NULL,
`Created_At` datetime NOT NULL,
PRIMARY KEY (`trans_log_Id`)
) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=latin1;
INSERT INTO `transactions_logs` (`trans_log_Id`, `etat_de_commande`, `ref`, `commentaire`, `staffId`, `Created_At`) VALUES
(1, 'waiting confirmation', '429735061', '', 1, '2020-11-09 12:11:43'),
(2, 'waiting confirmation', '472143970', '', 1, '2020-11-09 13:45:57'),
(3, 'confirmed', '429735061', '', 1, '2020-11-09 13:46:12'),
(4, 'ready', '429735061', '', 1, '2020-11-09 13:46:18'),
(5, 'picked', '429735061', '', 1, '2020-11-09 14:46:25');
COMMIT;
I want to be able to get this result
(2,'waiting confirmation','472143970',1,'2020-11-09 13:45:57'),
(5,'picked','429735061',1,'2020-11-09 14:46:25')
One option uses window functions, available in MySQL 8.0:
select *
from (
select t.*,
rank() over(partition by ref order by created_at desc) rn
from transactions_logs t
) t
where rn = 1
You can also use a correalted subquery for filtering - this works in all MySQL versions:
select t.*
from transactions_logs t
where t.created_at = (
select max(t1.created_at)
from transactions_logs t1
where t1.ref = t.ref
)
The latter would take advantage of an index on (ref, created_at).

Mysql 5.7 - get multiple values from sub select

In the query below I am "JOINING" another table where i.isPrimary > 0 and if all i.isPrimary are 0 I just get the first result.
The result set from the query is as expected, but I want to bring more values from each subselect.
I am getting the error: SQL Error (1241): Operand should contain 1 column(s).
How can this query be rewritten in order to get more results from each subselect?
Thanks
-- borrowed from https://stackoverflow.com/q/7745609/808921
CREATE TABLE IF NOT EXISTS `ResearchEntity` (
`id` int(6) unsigned NOT NULL,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `ResearchEntity` (`id`, `name`) VALUES
('1', 'one'),
('2', 'two'),
('3', 'three');
CREATE TABLE IF NOT EXISTS `ProfileImageEntity` (
`id` int(6) unsigned NOT NULL,
`isPrimary` int(1) unsigned NOT NULL,
`value` varchar(200) NOT NULL,
`researchId` int(2) unsigned NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `ProfileImageEntity` (`id`,`isPrimary`, `value`,`researchId`) VALUES
('1', 0, 'not primary',1),
('2', 0, 'not primary',1),
('3', 1, 'primary!!!',1),
('4', 0, 'primary!!!',2),
('5', 0, 'not primary',2),
('6', 0, 'not primary',2)
;
CREATE TABLE IF NOT EXISTS `UserNameEntity` (
`id` int(6) unsigned NOT NULL,
`isPrimary` int(1) unsigned NOT NULL,
`value` varchar(200) NOT NULL,
`researchId` int(2) unsigned NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `UserNameEntity` (`id`,`isPrimary`, `value`,`researchId`) VALUES
('1', 0, 'first one, should be returned',1),
('2', 0, 'not primary',1),
('3', 0, 'primary',1),
('4', 1, 'primary',3),
('5', 0, 'not primary',3),
('6', 0, 'not primary',3);
SQL FIDDLE
: http://sqlfiddle.com/#!9/028218/1
SELECT r.*,
(SELECT i.id FROM ProfileImageEntity i WHERE i.researchId = r.id ORDER BY i.isPrimary DESC, i.id ASC LIMIT 1 ) AS primaryImageId,
(SELECT i.id FROM UserNameEntity i WHERE i.researchId = r.id ORDER BY i.isPrimary DESC, i.id ASC LIMIT 1 ) AS primaryImageId
FROM ResearchEntity r
ORDER BY id DESC;
What I understood from your question and comment that you want more columns from sub Query which is not possible. So try this query:
It is easy in MySql 8 but you are using MySql 5.7 where it a little bit tricky So try this:
select
t1.*,
t2.id AS primaryImageId,
t2.value AS primaryImageValue,
t3.id AS primaryUserId,
t3.value AS primaryUserValue
from ResearchEntity t1
left join (
SELECT *,
IF(researchId=#last,#_seq:=#_seq+1,#_seq:=1) AS rn,
#last:=researchId
FROM ProfileImageEntity , (SELECT #_seq:=1, #last:=0) r
ORDER BY researchId,isPrimary DESC, id ASC
) t2 on t1.id=t2.researchId and t2.rn=1
left join (
SELECT *,
IF(researchId=#last,#_seq:=#_seq+1,#_seq:=1) AS rn,
#last:=researchId
FROM UserNameEntity , (SELECT #_seq:=1, #last:=0) r
ORDER BY researchId,isPrimary DESC, id ASC
) t3 on t1.id=t3.researchId and t3.rn=1
order by t1.id
DEMO
In MySql 8 using row_number()
with cte as (
SELECT *,
row_number() over (partition by researchId ORDER BY isPrimary DESC, id ASC) rn
FROM ProfileImageEntity
),
cte1 as (
sELECT *,
row_number() over (partition by researchId ORDER BY isPrimary DESC, id ASC) rn
FROM UserNameEntity
)
select
t1.*,
t2.id AS primaryImageId,
t2.value AS primaryImageValue,
t3.id AS primaryUserId,
t3.value AS primaryUserValue
from ResearchEntity t1 left join cte t2 on t1.id=t2.researchId and t2.rn=1
left join cte1 t3 on t1.id=t3.researchId and t3.rn=1
try left join
SELECT r.*,i.id FROM ResearchEntity r left join ProfileImageEntity i on r.id = i.researchId
ORDER BY i.isPrimary,i.id DESC;
you just need to left join 2 times
SELECT r.*,i.id,j.id FROM ResearchEntity r left join ProfileImageEntity i on r.id = i.researchId left join UserNameEntity j on r.id=j.researchId ORDER BY i.isPrimary,i.id DESC;

AVG limited to 3 last values for each group

Basically i have two tables:
Here's code to create two tables if this can help someone who will be willing to help me:
CREATE TABLE IF NOT EXISTS `coefficients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`datetime` datetime NOT NULL,
`campaign_id` int(11) NOT NULL,
`score` decimal(10,2) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
INSERT INTO `coefficients` (`id`, `datetime`, `campaign_id`, `score`) VALUES
(1, '2017-01-29 22:32:13', 1, 20.00),
(2, '2017-01-29 22:36:22', 1, 34.00),
(3, '2017-01-29 22:36:30', 1, 30.00),
(4, '2017-01-29 22:36:43', 1, 1000.00),
(5, '2017-01-29 22:37:13', 2, 10.00),
(6, '2017-01-29 22:37:26', 2, 15.00),
(7, '2017-01-29 22:37:43', 2, 20.00),
(8, '2017-01-29 22:30:51', 2, 1000.00);
CREATE TABLE IF NOT EXISTS `statistics` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`stats1` int(11) NOT NULL,
`stats2` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `statistics` (`id`, `campaign_id`, `stats1`, `stats2`) VALUES
(1, 1, 34, 38),
(2, 2, 23, 45);
I would like to get average coefficient for each campaign_id calculated based on latest 3 logged coefficients for each campaign_id.
Here's screenshot of two tables and result that i need to get:
data + result (visual representation)
The main problem is that i have no idea how to join these two tables if i need only average coefficient for each campaign_id based on 3 latest logged nu,bers for it :(
I will appreciate any help
Following query will give you the top 3 records per campaign_id from coefficients table:
SET #currcount = NULL, #currvalue = NULL;
SELECT id, campaign_id, score, c_index FROM (
SELECT
id, campaign_id, score,
#currcount := IF(#currvalue = campaign_id, #currcount + 1, 1) AS c_index,
#currvalue := campaign_id AS s
FROM coefficients
order by id
) AS a where c_index <= 3
Now, all you have to do is, add a GROUP BY to this query, calculate average score and join it with statistics table, e.g.:
SET #currcount = NULL, #currvalue = NULL;
SELECT a.id, a.campaign_id, avg(score), c_index, s.stats1, s.stats2 FROM (
SELECT
id, campaign_id, score,
#currcount := IF(#currvalue = campaign_id, #currcount + 1, 1) AS c_index,
#currvalue := campaign_id AS s
FROM coefficients
order by id
) AS a join statistics s on a.campaign_id = s.campaign_id
where c_index <= 3
group by campaign_id
Here's the SQL Fiddle.
In MySQL, the best way is usually to use variables. Getting the statistics is just a join, so that is not interesting. Let's get the average from the coefficients table:
select c.campaign_id, avg(c.score) as avg_score
from (select c.*,
(#rn := if(#c = c.campaign_id, #rn + 1,
if(#c := c.campaign_id, 1, 1)
)
) as rn
from coefficients c cross join
(select #rn := 0, #c := -1) params
order by c.campaign_id, c.datetime desc
) c
where rn <= 3
group by c.campaign_id;

Joining table with min(amount) does not work

I have 3 tables, but data is only fetch from 2 tables.
I'm trying to get the lowest bids for selected items and display user name with the lowest bid.
Currently query works until when we display user name, it shows wrong user name, which does not match the bid.
Below is working example of structure and query.
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE `bid` (
`id` int(11) NOT NULL,
`amount` float NOT NULL,
`user_id` int(11) NOT NULL,
`item_id` int(11) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;
INSERT INTO `bid` (`id`, `amount`, `user_id`, `item_id`) VALUES
(1, 9, 1, 1),
(2, 5, 2, 1),
(3, 4, 3, 1),
(4, 3, 4, 1),
(5, 4, 2, 2),
(6, 22, 5, 1);
-- --------------------------------------------------------
CREATE TABLE `item` (
`id` int(11) NOT NULL,
`name` varchar(100) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
INSERT INTO `item` (`id`, `name`) VALUES
(1, 'chair'),
(2, 'sofa'),
(3, 'table'),
(4, 'box');
-- --------------------------------------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(100) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
INSERT INTO `user` (`id`, `name`) VALUES
(1, 'James'),
(2, 'Don'),
(3, 'Hipes'),
(4, 'Sam'),
(5, 'Zakam');
ALTER TABLE `bid`
ADD PRIMARY KEY (`id`);
ALTER TABLE `item`
ADD PRIMARY KEY (`id`);
ALTER TABLE `user`
ADD PRIMARY KEY (`id`);
ALTER TABLE `bid`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=7;
ALTER TABLE `item`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=5;
ALTER TABLE `user`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=5;
Query 1:
SELECT b.id, b.item_id, MIN(b.amount) as amount, b.user_id, p.name
FROM bid b
LEFT JOIN user p ON p.id = b.user_id
WHERE b.item_id in (1, 2)
GROUP BY b.item_id
ORDER BY b.amount, b.item_id
Results:
| id | item_id | amount | user_id | name |
|----|---------|--------|---------|-------|
| 5 | 2 | 4 | 2 | Don |
| 1 | 1 | 3 | 1 | James |
Explanation of query:
Get the selected items (1, 2).
get the lowest bid for thous items - MIN(b.amount)
display user names, who has given the bid - LEFT JOIN user p on p.id = b.user_id (this is not working or I'm doing something wrong)
[Note] I can't use sub-query, I'm doing this in doctrine2 (php code) which limits mysql sub-query
No, you are not necessarily fetching the user_id who has given the bid. You group by item_id, so you get one result row per item. So you are aggregating and for every column you say what value you want to see for that item. E.g.:
MIN(b.amount) - the minimum amount of the item's records
MAX(b.amount) - the maximum amount of the item's records
AVG(b.amount) - the avarage amount of the item's records
b.amount - one of the amounts of the item's records arbitrarily chosen (as there are many amounts and you don't specify which you want to see, the DBMS simply choses one of them)
This said, b.user_id isn't necessarily the user who made the lowest bid, but just one random user of the users who made a bid.
Instead find the minimum bids and join again with your bid table to access the realted records:
select bid.id, bid.item_id, bid.amount, user.id as user_id, user.name
from bid
join
(
select item_id, min(amount) as amount
from bid
group by item_id
) as min_bid on min_bid.item_id = bid.item_id and min_bid.amount = bid.amount
join user on user.id = bid.user_id
order by bid.amount, bid.item_id;
You can solve this using a subquery. I am not 100% sure if this is the most efficient way, but at least it works.
SELECT b1.id, b1.item_id, b1.amount, b1.user_id, p.name
FROM bid b1
LEFT JOIN user p ON p.id = b1.user_id
WHERE b1.id = (
SELECT b2.id
FROM bid b2
WHERE b2.item_id IN (1, 2)
ORDER BY b2.amount LIMIT 1
)
This first selects for the lowest bid with for item 1 or 2 and then uses the id of that bid to find the information you need.
Edit
You are saying that Doctrine does not support subqueries. I have not used Doctrine a lot, but something like this should work:
$subQueryBuilder = $entityManager->createQueryBuilder();
$subQuery = $subQueryBuilder
->select('b2.id')
->from('bid', 'b2')
->where('b2.item_id IN (:items)')
->orderBy('b2.amount')
->setMaxResults(1)
->getDql();
$queryBuilder = $entityManager->createQueryBuilder();
$query = $queryBuilder
->select('b1.id', 'b1.item_id', 'b1.amount', 'b1.user_id', 'p.name')
->from('bid', 'b1')
->leftJoin('user', 'p', 'with', 'p.id = b1.user_id')
->where('b1.id = (' . $subQuery . ')')
->setParameter('items', [1, 2])
->getQuery()->getSingleResult();

sql join not in selection

I have news and news category. MANY MANY relation (3rd normal form)
I need to select all news which NOT in category with id = 10
if news have more categories (ex, 5,6,7,10,20) not select (because it has 10)
if for ex. (4,61,55) - select.
It needs to be done with JOIN and one query.
CREATE TABLE IF NOT EXISTS `news` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(333) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
INSERT INTO `news` (`id`, `name`) VALUES
(1, 'One '),
(2, 'Two');
CREATE TABLE IF NOT EXISTS `news_cat` (
`news_id` int(11) NOT NULL,
`cat_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `news_cat` (`news_id`, `cat_id`) VALUES
(1, 2),
(1, 5),
(2, 3),
(2, 4);
-
SELECT * from news left join news_cat on news_cat.news_id = news.id and cat_id !=5
returns both records, i need to modify this query to return only news with id = 2, because news with id = 2 have cat_id = 5
select news.id
from news
join news_cat on news.id = news_cat.news_id
group by news.id
having sum(case when news_cat.cat_id = 10 then 1 else 0 end) < 1
http://sqlfiddle.com/#!2/d61a0/1/0
Use left join to get ones that do not have cat_id = 10.
select a.*
from news a
left join news_cat b on b.cat_id = 10 and a.id = b.news_id
where isnull(b.news_id);
fiddle