PHP MySQL find smallest missing number in column - php

I need to read a column with INT Order in MySQL and get from the column the lower number missing:
+--------+---------+
| ID | Order |
+--------+---------+
| 1 | 1 |
| 3 | 5 |
| 4 | 3 |
| 5 | 4 |
| 6 | 2 |
| 7 | 6 |
| 8 | 11 |
+--------+---------+
The result I need is the number 7, as 1 through 6 exist and other missing numbers are greater than 7.
$stmtpre = "SELECT Order FROM tabla ORDER BY Order DESC";
$data = $this -> DBMANAGER -> BDquery($stmtpre);
$count = 0;
while ($row = mysqli_fetch_assoc($data)){
$count++;
if($row['Order']!==$count){
$result= $count; #store first lower get
break;
}
}
return $result;

If the Order column is indexed, you could get the first missing number with SQL, without reading the complete table using an excluding LEFT JOIN:
SELECT t1.`Order` + 1 AS firstMissingOrder
FROM tabla t1
LEFT JOIN tabla t2 ON t2.`Order` = t1.`Order` + 1
WHERE t2.`Order` IS NULL
AND t1.`Order` <> (SELECT MAX(`Order`) FROM tabla)
ORDER BY t1.`Order`
LIMIT 1
or (maybe more intuitive)
SELECT t1.`Order` + 1 AS firstMissingOrder
FROM tabla t1
WHERE NOT EXISTS (
SELECT 1
FROM tabla t2
WHERE t2.`Order` = t1.`Order` + 1
)
AND t1.`Order` <> (SELECT MAX(`Order`) FROM tabla)
ORDER BY t1.`Order`
LIMIT 1
The second query will be converted by MySQL to the first one. So they are practicaly equal.
Update
Strawberry mentioned a good point: The first missing number might be 1, which is not covered in my query. But i wasn't able to find a solution, which is both - elegant and fast.
We could go the opposite way and search for the first number after a gap. But would need to join the table again to find the last existing number before that gap.
SELECT IFNULL(MAX(t3.`Order`) + 1, 1) AS firstMissingOrder
FROM tabla t1
LEFT JOIN tabla t2 ON t2.`Order` = t1.`Order` - 1
LEFT JOIN tabla t3 ON t3.`Order` < t1.`Order`
WHERE t1.`Order` <> 1
AND t2.`Order` IS NULL
GROUP BY t1.`Order`
ORDER BY t1.`Order`
LIMIT 1
MySQL (in my case MariaDB 10.0.19) is not able to optimize that query properly. It takes about one second on an indexed (PK) 1M row table, even though the first missing number is 9. I would expect the server to stop searching after t1.Order=10, but it seams not to do that.
Another way, which is fast but looks ugly (IMHO), is to use the original query in a subselect only if Order=1 exists. Otherwise return 1.
SELECT CASE
WHEN NOT EXISTS (SELECT 1 FROM tabla WHERE `Order` = 1) THEN 1
ELSE (
SELECT t1.`Order` + 1 AS firstMissingOrder
FROM tabla t1
LEFT JOIN tabla t2 ON t2.`Order` = t1.`Order` + 1
WHERE t2.`Order` IS NULL
AND t1.`Order` <> (SELECT MAX(`Order`) FROM tabla)
ORDER BY t1.`Order`
LIMIT 1
)
END AS firstMissingOrder
Or Using UNION
SELECT 1 AS firstMissingOrder FROM (SELECT 1) dummy WHERE NOT EXISTS (SELECT 1 FROM tabla WHERE `Order` = 1)
UNION ALL
SELECT firstMissingOrder FROM (
SELECT t1.`Order` + 1 AS firstMissingOrder
FROM tabla t1
LEFT JOIN tabla t2 ON t2.`Order` = t1.`Order` + 1
WHERE t2.`Order` IS NULL
AND t1.`Order` <> (SELECT MAX(`Order`) FROM tabla)
ORDER BY t1.`Order`
LIMIT 1
) sub
LIMIT 1

Might be the long way around, but here's one way:
while ($row = mysqli_fetch_assoc($data)) {
$orders[] = $row['Order'];
}
$result = min(array_diff(range(min($orders), max($orders)), $orders));
Create a range from the minimum order found to the maximum order found
Calculate the difference with the found orders to get missing orders
Find the lowest order number from the missing
This assumes that you want to use the lowest and highest numbers returned from the query as the range. If you want to always start at 1 use 1 instead of min($orders).
Also, as Strawberry points out, Order is a reserved word in MySQL so consider changing it or delimit it with back-ticks SELECT `Order` FROM tabla.

From PHP side:
i work more around the solution:
Fisrt call Function:
$stmtpre = "SELECT Order FROM tabla ORDER BY Order ASC";
$data = $this -> DBMANAGER -> BDqueryFirstMissingINT($stmtpre, DATABASE);
echo $data;
On second
function BDqueryFirstMissingINT($stmtpre,$dbUsing){
$data = $this -> BDquery($stmtpre, $dbUsing); #run the query
$count = 0;
while ($row = mysqli_fetch_array($data)){
$count++;
$value = (int)$row[0];
if($value!==$count){
$result = $count;
break;
}
}
return $result;
}
Thank for you help

Here's one idea...
SELECT x.my_order + 1 missing
FROM
( SELECT my_order FROM my_table
UNION
SELECT 0
) x
LEFT
JOIN my_table y
ON y.my_order = x.my_order + 1
WHERE y.my_order IS NULL
ORDER
BY missing
LIMIT 1;

Related

Combine the results of 2 queries, where there is no join or union

I would like to append the results of 2 queries into one result set.
SELECT n.member_no, n.surname, n.first_name
FROM `names` AS n
WHERE member_no = '1003';
SELECT s.registration
FROM `system` AS s
WHERE s.RECNUM = 1;
This must return one record with data from the names table plus data from the system (one record) table
Member_no | surname | first_name | registration
--------------------------------------------------
1003 | Brown | Peter | My registration
You can use CrossJoin:
SELECT n.member_no, n.surname, n.first_name, s.registration
FROM names AS n
CROSS JOIN system s
WHERE n.member_no = '1003' and s.RECNUM = 1;
we can correlate your registration and name tables based on row_number(). You may want to try below query.
SELECT rn.member_no, rn.surname, n.first_name, s.registration
FROM
(SELECT member_no, surname, first_name, row_number() over (order by member_no) rn
FROM `names`) n
LEFT JOIN
(SELECT row_number() over (order by RECNUM) rn, registration
FROM `system`) s on s.rn = n.rn
WHERE n.member_no = '1003'
Try this one.
SELECT DISTINCT n.member_no, n.surname, n.first_name,s.registration
FROM `names` AS n, `system` AS s
WHERE s.RECNUM = 1 AND member_no = '1003';

Select unique pairs of users from a table at random

I want to create random user pairs between our database users.
I have the following user table:
Table: tbl_users
user_id | name
--------+--------------
1 | Jay
2 | Ram
3 | John
4 | Kevin
5 | Jenny
6 | Tony
I want to generate a random result like this:
from_id | to_id
--------+---------
1 | 6
5 | 3
2 | 4
Can this be done in MySQL only?
This is indeed a duplicate of a previous question, so the answer is there.
However, even if it is indeed possible in MySQL doing this there is not really recommended. PHP is a much better tool for handling this, as what you're doing is actually manipulating data as per some business rule. It'll be a lot easier to maintain by doing it in PHP, and I suspect that it'll be less resource-intensive as well.
A possible way to do this, which I'd prefer. Is to do a random sort in SQL, and then pair up two and two rows against each other. Something like this:
$grouping = {};
// Fetching both rows to ensure that we actually have an even number paired up.
while ($row = $res->fetch_array () && $row2 = $res->fetch_array ()) {
$grouping[] = {$row['name'], $row2['name']};
}
If you want to allow for an unmatched user to be listed, simply move the second fetch to the inside of the loop. Then deal with the potentially missing result there.
You can use the following code to generate your list:
select max(from_id) as from_id,
max(to_id) as to_id
from (
select
case when rownum mod 2 = 1 then user_id else null end as from_id,
case when rownum mod 2 = 0 then user_id else null end as to_id,
(rownum - 1) div 2 as pairnum
from (
select user_id, #rownum := #rownum + 1 as rownum
from
(select #rownum := 0) as init,
(select user_id from tbl_user order by rand()) as randlist
) as randlistrownum
) as randlistpairs
group by pairnum;
Step by step, this will:
order the userlist in random order
assign a rownumber to it (otherwise the order will have no meaning)
assign two consecutive rows the same pairnum (rownum = 1 and rownum = 2 get the value pairnum = 0, the next two rows will get pairnum = 1 and so on)
the first row of these paired rows will get the values from_id = user_id and to_id = null, the other row will be to_id = user_id and from_id = null
group by these pairs together to make them into one row
if you have an odd number of users, one user will end up with to_id = null, because it has no partner
A little more compact if you prefer shorter code:
select max(case when rownum mod 2 = 1 then user_id else null end) as from_id,
max(case when rownum mod 2 = 0 then user_id else null end) as to_id
from (
select user_id, #rownum := #rownum + 1 as rownum, (#rownum - 1) div 2 as pairnum
from
(select #rownum := 0) as init,
(select user_id from tbl_user order by rand()) as randlist
) as randlistpairs
group by pairnum;

Codeigniter 3.0 query bug

Duplicate this table: User_Posts
ID | Upvotes | Downvotes | CAT |
___________________________________
42134 | 5 | 3 | Blogs|
------------------------------------
12342 | 7 | 1 | Blogs|
-------------------------------------
19344 | 6 | 2 | Blogs|
------------------------------------
I need to get the rank of an item within it's category. Therefore ID: 19344 will have Rank position 2, with 4 upvotes, behind 12342 with 6 upvotes. Rank is determined by (upvotes-downvotes) count within it's category.
So I wrote this MySQL query.
SELECT rank FROM (SELECT *, #rownum:=#rownum + 1 AS rank
FROM User_Posts where CAT= 'Blogs' order by
(Upvotes-Downvotes) DESC) d,
(SELECT #rownum:=0) t2 WHERE POST_ID = '19344'
Returns to me (Rank = 2) when run directly in mysql. This is the correct result
However when I try to build it out through code-igniter's query builder I get the
$table = 'User_Posts';
$CAT= 'Blogs';
$POST_ID = '19344';
$sql = "SELECT rank FROM (SELECT *, #rownum:=#rownum + 1 AS
rank FROM $table where CAT= ?
order by (Upvotes-Downvotes) DESC) d,
(SELECT #rownum:=0) t2 WHERE POST_ID= ?";
$query= $this->db->query($sql, array($CAT,$POST_ID))->row_array();
returns to me an empty result: array(rank=>);
so then my question is... but why?
I will also accept an answer will an alternative way to run this query from code-igniters query builder, but ideally I would like to know why this thing is broken.
I've had a similar issue in the past, turns out I had to initialize the variable with a separate query first, I am not sure if this is still the case, but give it a try anyway.
//initialize the variable, before running the ranking query.
$this->db->query('SELECT 0 INTO #rownum');
$query= $this->db->query($sql, array($CAT,$POST_ID))->row_array();
Exactly I don't know why your code is not working. I wrote another solution it will work. Try below code.
$select="FIND_IN_SET( (upvote-downvote), (SELECT GROUP_CONCAT( (upvote-downvote) ORDER BY (upvote-downvote) DESC ) as total FROM (User_Posts))) as rank";
$this->db->select($select,FALSE);
$this->db->from('(User_Posts)',FALSE);
$this->db->where('ID',19344);
$this->db->where('CAT','Blogs');
$query = $this->db->get();
Write a Stored Function to do the query. Then have Codeigniter merely do
query("SELECT PostRank(?,?)", $CAT, $POST_ID);
Restriction: Since you cannot do PREPARE inside a Stored Function, this function will necessarily be specific to one table, User_Posts.
I'm not entirely sure if this is the problem, but I'd be initialising #rownum in the subquery:
SELECT rank
FROM (
SELECT *,
#rownum:=#rownum + 1 AS rank
FROM $table
JOIN (SELECT #rownum := 0) init
WHERE CAT= ?
ORDER BY (Upvotes-Downvotes) DESC
) d
WHERE post_id = ?
Otherwise I'd be worried that #rownum is undefined (NULL) and stays that way while rank is calculated (NULL + 1 = NULL), only being assigned the value of 0 afterwards. Thus rank is returned as NULL and you get ['rank'=>].
Running this again in a constant connection (directly in MySQL) would then give you the correct result as #rownum would start from the value 0 from the previous query and rank would be calculated correctly.
I'm guessing codeigniter starts a new connection/transaction each time the query is run and #rownum starts at NULL each time, giving ['rank'=>].

MySQL: Select LAST N rows while SUM less Then number

I've got a huge table:
+------+---------+-----+-----+-------+------+---------+
|ticker|data_date|price|count|oper_id|ext_nr|oper_summ|
+------+---------+-----+-----+-------+------+---------+
|SBER |2015-08..|70.00|15 |0 |251528|1050.00 |
|AFLT |2015-08..|30.00|5 |0 |251525|150.00 |
|SBER |2015-08..|69.00|10 |1 |251521|690.00 |
|SBER |2015-08..|71.00|15 |1 |251513|1065.00 |
|SBER |2015-08..|72.00|15 |0 |251512|1080.00 |
data_date format: 2015-01-05 09:59:59
UNIQUE KEY `idx_ticker_ext_nr` (`ticker`,`ext_nr`)
I need to SELECT LAST N rows WHERE SUM(oper_summ) will be LESS THEN 10000
I've found similar topic: limiting the rows to where the sum a column equals a certain value in MySQL
SELECT
O.ext_nr,
O.price, O.count,
O.oper_summ,
(SELECT
sum(oper_summ) FROM Table1
WHERE ext_nr <= O.ext_nr) 'RunningTotal'
FROM Table1 O
HAVING RunningTotal <= 10000
but unable to make it work in my coditions...
Found a solution:
SET #msum := 0;
select t1.* from
(select m.*,
(#msum := #msum + m.oper_summ) as cumul_oper_summ from jos_fintools_data m order by m.data_date DESC )
t1 where t1.cumul_oper_summ <= 10000;
credits goes to toomanyredirects: limiting the rows to where the sum a column equals a certain value in MySQL
Use variables:
SELECT o.*
FROM (SELECT O.ext_nr, O.price, O.count, O.oper_summ,
(#os := #os + oper_summ) as RunningTotal
FROM Table1 O CROSS JOIN
(SELECT #os := 0) params
ORDER BY data_date desc
) o
HAVING RunningTotal <= 10000;
Note: you need to order by something in the subquery. I'm not sure what the right column is. My best guess is the date column.
SET #msum := 0;
select t1.* from
(select m.*,
(#msum := #msum + m.oper_summ) as cumul_oper_summ from jos_fintools_data m order by m.data_date DESC )
t1 where t1.cumul_oper_summ <= 10000;

Mysql replace two value in one query?

BEFORE
id | cat_id | order
33 | 1 | 1
34 | 1 | 2
AFTER
id | cat_id | order
33 | 1 | 2
34 | 1 | 1
Now using 4 query
$db is wrap $mysqli for using placeholder and injection defense
get first record by id
$curr = $db->q('SELECT id,order,cat_id FROM `tbl` WHERE id`=? FOR UPDATE',
33)->fetch_assoc();
if exist first record find next record by order field
if($curr){
$next = $db->q('SELECT id,order FROM `tbl` WHERE `cat_id`=? AND
`order`>? ORDER BY `order` LIMIT 1 FOR UPDATE',
$curr['cat_id'],$curr['order']));
if exist first and second recorn change order value
if($prev['id']){
$db->q("UPDATE `tbl` SET `order`=? WHERE `id`=?",$next['order'],$curr['id']);
$db->q("UPDATE `tbl` SET `order`=? WHERE `id`=?",$curr['order'],$next['id']);
}
}
Important! Checking exist two record, lock rows for update
MySQL doesn't support update with the same table in the FROM statement. So because of this there are (select * from TBL) as t2 in inner subqueries.
Also EXISTS condition in the first CASE WHEN is to prevent update if the second record doesn't exists ("if exist first and second records change order value")
Here is a SQLfiddle example
UPDATE tbl as t1
SET `order`=
CASE WHEN id = 33
and
EXISTS (SELECT ID from (select * from TBL) t2 where
cat_id=t1.Cat_Id
and `order`>t1.`order`
ORDER BY `order`
LIMIT 1)
THEN
(SELECT `order` from (select * from TBL) t2 where
cat_id=t1.Cat_Id
and `order`>t1.`order`
ORDER BY `order`
LIMIT 1)
WHEN id <>33 THEN
(SELECT `order` from (select * from TBL) t2 where
cat_id=t1.Cat_Id
and `order`<t1.`order`
ORDER BY `order` DESC
LIMIT 1 )
ELSE `order`
END
where id =33
or
(SELECT ID from (select * from TBL) t2 where
cat_id=t1.Cat_Id
and `order`<t1.`order`
ORDER BY `order` DESC
LIMIT 1) =33
With one query it's:
UPDATE
`tbl`
SET
`order`=CASE
WHEN `order`=2 THEN 1
WHEN `order`=1 THEN 2
END;
WHERE
`order` IN (1,2)
or, for id's condition:
UPDATE
`tbl`
SET
`order`=CASE
WHEN `order`=2 THEN 1
WHEN `order`=1 THEN 2
END;
WHERE
id = $id
To swap 2 fields by row id try:
UPDATE `tbl` AS tbl1
JOIN `tbl` AS tbl2 ON ( tbl1.id = 33 AND tbl2.id = 34 )
SET
tbl1.order = tbl2.order, tbl2.order = tbl1.order
Also you can set your desired value instead of swap between 2 fileds.
If needed, you can add a where clause like below to swap where cat_id are 1 in two rows:
WHERE
tbl1.cat_id = 1 AND tbl2.cat_id = 1
Update:
If your order numbers are unique for any cat_id you can try this way:
UPDATE `tbl` AS tbl1
JOIN `tbl` AS tbl2 ON ( tbl1.order = 1 AND tbl2.order = 2 )
SET
tbl1.order = tbl2.order, tbl2.order = tbl1.order
WHERE
tbl1.cat_id = 1 AND tbl2.cat_id = 1
It works if your order field is int, Otherwise you should quote order values in query.
See the result on SQLFiddle

Categories