Repeat query when conditions aren't met - php

I am making a kindof quiz. The quiz has 15 questions, for the quiz I need 5 questions of quiztype '1', 5 of quizType '2' and 5 of quizType '3'. Right now I'm counting quiztype '1'and quiztype '2' trough a loop and if conditions outside the loop aren't met, I get 15 new entry's and repeat the loop. I'm wondering, is there a better way to do this inside my query instead of using 2 objects?
This Is my code:
public function checkVariety($quizType, $data)
{
$i=0;
$i2=0;
foreach($quizType as $type) {
if ($type=='1') {
$i++;
}
if ($type=='2') {
$i2++;
}
}
if($i=='5' AND $i2=='5') {
$this->startQuiz($data);
return true;
} else {
$this->getRandom();
return false;
}
}
public function getRandom()
{
$stmt = $this->db->prepare("
SELECT id, quiz_type
FROM quiz
ORDER BY rand()
LIMIT 15
");
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$quizType[] = $row['quiz_type'];
$data[] = $row['id'];
}
$this->checkVariety($quizType, $data);
return true;
}

You could also combine this way.
The UNION was easily avoided by noting the difference in the SELECT statements was just to pick form values 1, 2, and 3. In SQL, this is easily done with form IN (1, 2, 3).
The problem with this is we can't easily use LIMIT 5, as you originally did, since all 15 rows are now in the same result.
This is where window functions comes into play. We can now process these rows using window specifications to isolate and operate on groups (by partition) of rows.
The example below is ROW_NUMBER() OVER (PARTITION BY form ORDER BY rand()) AS seq.
In short, this derives a new column (see: derived column), the contents of which is the position (row number) of this row within the group of rows with a matching form value (indicated in the PARTITION BY terms) and in the order specified by the ORDER BY terms of the OVER clause.
Your requirement is complicated slightly by the needed random order. It's not as easy to see how this window function use provides this nice row number ordering. You can test this by replacing the rand() term with something more recognizable ORDER BY exercise, which is the column I chose to represent some exercise identifier.
The WITH clause or Common Table Expression - CTE term is like a derived table or view, but provides more capability, like recursion. We can access it similar to any VIEW, Derived Table, base table, etc.
In the following CTE term, we select all the rows matching the 3 forms, and assign / generate a new seq column containing a row number (1 to n, within each partition), so that later we can just take seq <= 5 to limit the result to just the first 5 rows in each partition (form).
WITH cte AS (
SELECT *
, ROW_NUMBER() OVER (PARTITION BY form ORDER BY rand()) AS seq
FROM exercises
WHERE form IN (1, 2, 3)
)
SELECT * FROM cte
WHERE seq <= 5
ORDER BY form, seq
;
Result with test data:
+----------+------+-----+
| exercise | form | seq |
+----------+------+-----+
| 15 | 1 | 1 |
| 8 | 1 | 2 |
| 10 | 1 | 3 |
| 16 | 1 | 4 |
| 6 | 1 | 5 |
| 29 | 2 | 1 |
| 24 | 2 | 2 |
| 26 | 2 | 3 |
| 20 | 2 | 4 |
| 25 | 2 | 5 |
| 41 | 3 | 1 |
| 46 | 3 | 2 |
| 47 | 3 | 3 |
| 40 | 3 | 4 |
| 51 | 3 | 5 |
+----------+------+-----+

I got it partially working thanks to the UNION method.
$stmt = $this->db->prepare("
SELECT *
FROM (SELECT * FROM exercises as e1 WHERE e1.form='1' ORDER BY rand() LIMIT 5) as f
UNION
SELECT *
FROM (SELECT * FROM exercises as e1 WHERE e1.form='2' ORDER BY rand() LIMIT 5) as f2
UNION
SELECT *
FROM (SELECT * FROM exercises as e1 WHERE e1.form='3' ORDER BY rand() LIMIT 5) as f3
ORDER BY rand()
");
$stmt->execute();
still having some problems though, but I will try to figure that out on my own first, and if I eventually need to, open another question.

Related

Get neighboring rows (2 above and 2 below) of a certain row in PHP while loop

I have a table like so (after doing a query on it to order it by score):
+---+-------+------+
|id | level |score |
+---+-------+------+
| 4 | 1 | 30 |
| 3 | 1 | 35 |
| 1 | 1 | 40 |
| 5 | 1 | 45 |
| 7 | 1 | 50 |
| 8 | 1 | 55 |
+---+-------+------+
I will output that to php in a while loop. So each row in the while loop will be the same as in the table above.
Essentially what I want to do is show 5 of these rows in a table (in html), with a certain row (e.g. where id=5) in the middle and have the two rows above and below it (in the correct order). This will be like a score board but only showing the user's score with the two above and two below.
E.g. say the user is id=5, I want to show
+---+-------+------+
|id | level |score |
+---+-------+------+
| 3 | 1 | 35 |
| 1 | 1 | 40 |
| 5 | 1 | 45 |
| 7 | 1 | 50 |
| 8 | 1 | 55 |
I am wondering does anyone know a way of doing this in php?
Basically
//select query output is in while loop
//get a certain row of the loop
//get the two rows above it and two rows below it
One method uses a lot of variables:
select t.*
from (select t.*,
lag(id, 1) over (order by score) as prev_id,
lag(id, 2) over (order by score) as prev_id2,
lead(id, 1) over (order by score) as next_id,
lead(id, 2) over (order by score) as next_id2
from t
) t
where 5 in (prev_id, prev_id2, next_id, next_id2, id)
order by score;
An alternative method is something like this:
(select t.*
from t
where t.score <= (select t2.score from t t2 where t2.id = 5)
order by score desc
limit 3
) union all
(select t.*
from t
where t.score > (select t2.score from t t2 where t2.id = 5)
order by score
limit 2
)
order by score;
This exactly syntax may not work in all databases, but the idea can easily be translated in whatever dialect of SQL. This also assumes that the scores are unique.

How do I get distinct rows by a column?

I have a huge number of rows that I'd like to get say, last 5 records inserted in that database from 10 different users. If the same user inserted the last 3 rows into database, we must get one row, skip the others two and move to get a row per user, until it count up to 5.
A database like that:
user_id | news_id | title
1 | 1 | foo-1
2 | 2 | foo-2
3 | 3 | foo-3
1 | 4 | baa
4 | 5 | baa0
5 | 6 | baa1
5 | 7 | baa2
6 | 8 | baa3
7 | 9 | baa4
Should return:
user_id | news_id | title
1 | 1 | foo-1
2 | 2 | foo-2
3 | 3 | foo-3
4 | 5 | baa0
5 | 6 | baa1
The current filter was done by PHP, like this:
$used = array();
while ($data = mysql_fetch_array($query)) {
$uid = $data['user_id'];
if(in_array($uid, $used))
continue;
array_push($used, $uid);
// do something with data
}
But I want to refactor it, and do the filter purely by mysql, if possible. I don't know much MySql and that's why I'm having problem to archive this...
Here's what I've tried
select DISTINCT(user_id), news_id, title from XXX
WHERE GROUP BY (news_id) DESC
LIMIT 0,5
How can I do that?
1 way you can do it is to generate a partitioned row number per user and then select 5 records where RowNumber = 1.
SELECT *
FROM
(
SELECT
d.user_id
,d.news_id
,d.title
,(#rn:= if(#uid = user_id, #rn + 1,
if(#uid:=user_id,1,1)
)
) as RowNumber
FROM
Data d
CROSS JOIN (SELECT #uid:=-1, #rn:=0) vars
ORDER BY
user_id
,news_id
) t
WHERE
t.RowNumber = 1
ORDER BY news_id
LIMIT 5;
http://rextester.com/JRIZI7402 - example to show it working
Note you can change the row order by simply changing the ORDER BY statement of the derived table so if you have a column that will signify the latest record e.g. an identity column or a datetime column you can use that, but user_id must be the first criteria to be partitioned correctly.
Do it from your query.
"SELECT * FROM table GROUP BY user_id ORDER BY news_id DESC LIMIT 5"
well, i think this will achieve what you are after.
select user_id, news_id, title from tableName
GROUP BY user_id
ORDER BY news_id DESC
LIMIT 0,5
Hope this helps!

DISTINCT * is not withdrawing the duplicity (MYSQL)

i already done everything to remove this duplicity on the database
On selecting a checkbox on the sectio "Bairros" i utilized as Array
for($m=0; $m<count($_POST["bairros"]); $m++){// LOOP 1
$pesquisar=($_POST["bairros"][$m]);
//Copy bairros(Array) and esporte (POST)
$query = "SELECT DISTINCT * FROM cadastro WHERE
(esporte1 = '".$_POST["esportes"]."' OR
esporte2 = '".$_POST["esportes"]."' OR
esporte3 = '".$_POST["esportes"]."' OR
esporte4 = '".$_POST["esportes"]."')
AND
(bairro1 = '".$pesquisar."' OR
bairro2 = '".$pesquisar."' OR
bairro3 = '".$pesquisar."' OR
bairro4 = '".$pesquisar."')
AND
ativarAparecer='sim' ORDER BY nomeCompleto ASC LIMIT 20";
$esporte= new consultar();
$esporte->executa($query);
//Loops
for($l=0; $l<$esporte->nrw; $l++){ //LOOP 2
echo $esporte->data["nomeCompleto"]."<br />";
$esporte->proximo();
} //close LOOP2
} //close LOOP1
Detail: this function object oriented, I believe that i'm doing something wrong at SQL or MYSQL, perhaps something is missing there.
SELECT DISTINCT *
Stop There. DISTINCT * can do what? Duplicate of what? it cant do that. Give it a field name to see unique values.
For example
SELECT DISTINCT nomeCompleto
Let's break this down. The DISTINCT clause will return unique sets based on the selected columns.
Let's say you have a table:
a | b | c
=========
1 | 2 | 3
1 | 1 | 3
1 | 2 | 4
Now if you SELECT DISTINCT a FROM table, you would get:
1
but if you SELECT DISTINCT a, b FROM table, you would get:
a | b
=====
1 | 2
1 | 1
That's because {1,2} is different from {1,1}, even though the a column is the same between those two sets.
Obviously, doing SELECT DISTINCT * FROM table would give you the original table because it uses all three columns as a "composition" of the unique set. If we amended the table to look like this:
a | b | c
=========
1 | 2 | 3
1 | 1 | 3
1 | 2 | 4
1 | 2 | 3
Then your result of SELECT DISTINCT * FROM table would give:
a | b | c
=========
1 | 2 | 3
1 | 1 | 3
1 | 2 | 4
because of the duplicate result set of {1, 2, 3}. However, since most tables have an auto-incrementing identifier as the primary key, there is almost always no difference between SELECT * and SELECT DISTINCT *.
Perhaps you're looking to GROUP BY a certain column?
How would I be using GROUP this in my script? Column that there are several equal records are this bairro, bairro2, bairro3, bairro4. Inside it is in numbers
bairro1 | bairro2 | bairro3 | bairro4
14 | 14 | 15 | 27
34 | 15 | 14 | 30
27 | 45 | 12 | 14

selecting the minimum amount records where the sum is greater than a certain number

I'm trying to put together a query that for a restaurant reservation system. The idea is that if there is no table big enough to sit the party size then to look through the other free tables and find two tables big enough to be put together to accommodate the party size.
Ideally I would like to be able to select the minimum of tables to as closely match the size of the party.
For example if there is a request for a table of twelve I would like to ideally find two of the tables for six and no more.
This is the query I've tried but it gives an empty result
select tbl_id, sum(max_seats) as sumseats from tbl_list
group by tbl_id having sumseats> 11
I have put a link to sql fiddle to show the table structure
http://sqlfiddle.com/#!2/5a6904/2/0
Try something like the following(You'll require something like PHP in addition to MySQL):
Check if a single table can seat the required number of people. If yes, print all the such tables in ascending order of capacity.
If no single table has capacity greater than or equal to the required capacity, reserve the table with highest capacity and deduct the capacity from the required capacity.
Goto step 1.
Code :
$bookedTables = array();
while ($requiredCapacity > 0) {
$query = "SELECT * FROM (SELECT tbl_id, max_seats FROM tbl_list WHERE max_seats > $requiredCapacity)table1 ORDER BY table1.max_seats ASC";
$result = mysql_query($query);
if (count($result)!=0) {
array_push($bookedTables, $result[0]['tbl_id'];
$requiredCapacity = $requiredCapacity - $result[0]['max_seats'];
}
else {
$query = "SELECT * FROM (SELECT tbl_id, max_seats FROM tbl_list)table1 ORDER BY table1.max_seats DESC";
if (count($result)!=0) {
array_push($bookedTables, $result[0]['tbl_id'];
$requiredCapacity = $requiredCapacity - $result[0]['max_seats'];
}
else {
echo "No more tables left";
break;
}
}
}
I'm not proposing this as a definitive answer, but it's something to think about...
SELECT * FROM tables;
+----+------+
| id | size |
+----+------+
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 4 | 2 |
| 5 | 4 |
| 6 | 4 |
| 7 | 4 |
| 8 | 6 |
| 9 | 6 |
| 10 | 8 |
+----+------+
SELECT *
, x.size + y.size + z.size pax
FROM tables x
LEFT
JOIN (SELECT * FROM tables UNION SELECT 0,0) y
ON y.size < x.size OR (y.size = x.size AND y.id < x.id)
LEFT
JOIN (SELECT * FROM tables UNION SELECT -1,0) z
ON z.size < y.size OR (z.size = y.size AND z.id < y.id)
HAVING pax >= 12
ORDER
BY pax
, x.size DESC
, y.size DESC
, z.size DESC
LIMIT 1;
+----+------+------+------+------+------+------+
| id | size | id | size | id | size | pax |
+----+------+------+------+------+------+------+
| 10 | 8 | 7 | 4 | -1 | 0 | 12 |
+----+------+------+------+------+------+------+

mysql speed of query - selecting max value of every 3 rows

I have a table that holds price information. I need to select the max value of every three rows. EXAMPLE:
Table `daily_high`
____ _______
| ID | HIGH |
| 1 | 24.65 |
| 2 | 24.93 |
| 3 | 26.02 |
| 4 | 25.33 |
| 5 | 25.16 |
| 6 | 25.91 |
| 7 | 26.05 |
| 8 | 28.13 |
| 9 | 27.07 |
|____|_______|
Desired output to new table (ID will be auto-increment so don't assume an association exists between this ID 1 and the daily_high ID 1:
____ ___________
| ID | 3MaxHIGH |
|____|___________|
| 1 | 26.02 |
| 2 | 25.91 |
| 3 | 28.13 |
|____|___________|
I want to compare IDs 1,2, and 3 to determine the high value among them. Then once I have compared 1-3, I want to move on to 4 through 6, then 7 through 9, etc until I've done this for all values contained in the table (currently about 400,000 values). I have written code that uses
SELECT max(HIGH) FROM daily_high as dh1 JOIN (SELECT max(HIGH) FROM daily_high WHERE id >= dh1 AND id < (dh1.id + 3))
This works but is horribly slow. I've tried using the SELECT statement where I identify the column values to be pull for display, meaning between the SELECT and FROM parts of the query.
I've tried to use JOIN to join all 3 rows onto the same table for comparison but it too is horribly slow. By slow I mean just under 10 seconds to gather information for 20 rows. This means that the query has analyzed 60 rows (20 groups of 3) in 9.65879893303 seconds (I didn't make this up, I used microtime() to calculate it.
Anyone have any suggestions for faster code than what I've got?
Keep in mind that my actual table is not the same as what I've posted above, but it the concept is the same.
Thanks for any help.
If you ID it continous you can make this
SELECT floor(id/3) as range, max(HIGH) FROM daily_high GROUP BY range;
Why not to use DIV operator for grouping your aggregation:
SELECT (id-1) DIV 3 + 1 AS ID, MAX(high) AS 3MaxHIGH
FROM daily_high
GROUP BY (id-1) DIV 3
This query gives the same result.
ID 3MaxHIGH
1 26.02
2 25.91
3 28.13
I was unable to run your query, and I believe that this one is faster.
UPD: To ensure that you have valid groups for your ranges, use this query:
select id, high, (id-1) div 3 + 1 from daily_high
result:
id high (id-1) div 3 + 1
1 24.65 1
2 24.93 1
3 26.02 1
4 25.33 2
5 25.16 2
6 25.91 2
7 26.05 3
8 28.13 3
9 27.07 3
Fuller answer with an example. The following code will do what I think you want.
SELECT FLOOR((row - 1) / 3), MAX(Sub1.high)
FROM (SELECT #row := #row + 1 as row, daily_high.*
FROM daily_high, (SELECT #row := 0) r) Sub1
GROUP BY FLOOR((row - 1) / 3)
ORDER BY Sub1.ID
The below query worked for me on a test table. perhaps not the best, but the other solutions failed on my test table.
This does require the ID's to be sequential. Also be sure to put an index on High aswell for speed.
SELECT FLOOR(T1.Id/3)+1 AS Id, ROUND(GREATEST(T1.High, T2.High, T3.High),2) AS High FROM `daily_high` T1, `daily_high` T2, `daily_high` T3
WHERE T2.Id=T1.Id+1
AND T3.Id=T2.Id+1
AND MOD(T1.Id, 3)=1
logic: if(id is divisible by 3, id/3-1, id/3)
select if(mod(id,3) = 0,floor(id/3)-1,floor(id/3)) as group_by_col , max(HIGH)
FROM daily_high GROUP BY group_by_col;

Categories