Fetch extra data for large number of IDs - php

I have a MySQL table containing information about objects. It looks like this:
+--------------+----+--------+-------+
| auto_incr_id | id | type | value |
+--------------+----+--------+-------+
| 1 | 1 | length | 105 |
| 2 | 1 | weight | 17 |
| 3 | 1 | price | 104 |
| 4 | 2 | length | 111 |
| 5 | 2 | weight | 18 |
| 6 | 2 | price | 87 |
+--------------+----+--------+-------+
I want to fetch the first x objects, sorted by length:
{
"id": 2,
"length": 111,
"weight": 18,
"price": 87
},
{
"id": 1,
"length": 105,
"weight": 17,
"price": 104
}
Here's what I do to achieve this:
First, I fetch the ids:
$type = "length";
$stmt = $mysqli->prepare("SELECT id FROM table WHERE type=? ORDER BY value DESC LIMIT ?");
$stmt->bind_param('si', $type, $limit);
$stmt->execute();
$result = $stmt->get_result();
$result = $result->fetch_all(MYSQLI_ASSOC);
Next, I get the other values for each object that was fetched:
$i = 0;
while ($i < count($result)) {
$stmt = $mysqli->prepare("SELECT type, value FROM table WHERE id=?");
$stmt->bind_param('i', $result[$i]['id']);
$stmt->execute();
$result_2 = $stmt->get_result();
$fetch = $result_2->fetch_all(MYSQLI_ASSOC);
$j = 0;
while ($j < count($fetch))
{
$result[$i][$fetch[$j]['type']] = $fetch[$j]['value'];
$j++;
}
}
This works great to fetch for example the top 5 ($limit = 5), but now I have a use case where I want to have this information for the top 10k or more. This take too much time, probably because it needs to go through the while loop and execute a statement for every found ID.
So I'm looking for a way to reduce the execution time getting this data. Any ideas?

Your issue appears to be a lack of understanding about table joins. There is a wrong and a right way to do this. The wrong way is very easy to understand, so I will show it so it makes sense...
select id, type, value from table where id in (select id from table where type='length');
That obviously gets the type/value combination from every id that has a value for type='length'. But, you aren't doing it ALL in the database. You still have to sort and group everything outside the database. Let's assume you only have type='length', 'weight' and 'price'. I can create three tables on the fly for that, creating attributes of length, weight, and price...
select l.id, length, weight, price from
(select id, value as length from table where type='length') l
left outer join
(select id, value as weight from table where type='weight') w
on l.id=w.id
left outer join
(select id, value as price from table where type='price') p
on l.id=p.id
order by length
Now, you will get each attribute in a row, one row per id. The id field is not guaranteed to be unique here. If you have the same ID with more than one length, it will show up more than once in the results. But, I hope you can see how I took the obvious subquery and turned it into a join to do all the work inside the database.
NOTE: I fixed the ambiguity error and added "left outer" to include rows where weight and price do not exist.

Well at least you can always use a subquery
SELECT id, type, value FROM table
WHERE id IN (SELECT id FROM table WHERE type=? ORDER BY value DESC LIMIT ?")
order by id

Related

Repeat query when conditions aren't met

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.

SQL Query get row rank or position on the main select query

I'm trying to get the Rank of a specific row from my SELECT query.
Here's my table :
| PID | Age |
|------ |----- |
| 5295 | 27 |
| 4217 | 26 |
| 2935 | 25 |
| 8706 | 24 |
My query and code: I'm using SQL Server
$x = 0;
$query = odbc_exec($connect, "SELECT * FROM mytable ORDER BY Age DESC"); // Ordering by "Age" Descending
while($row = odbc_fetch_array($query)) {
$x++;
if($row['PID'] == 2935) { break; // Stop }
}
echo $x; // output "3"
This method works, but the question is can this be done with simple standalone SQL Query instead of looping through all results then incrementing $x variable to get the specified row rank. because this table contains thousands of entries which will be a little laggy while looping till it find the desired row then stop looping.
The logic of this ranking since I order with Age descending, so for example it will start with 27 with rank 1 along to 24 with rank 4.
How can I achieve this?
You can use row_number() in a subquery to assign a rank to each record, then filter out in the outer query on the row you are interested in:
select *
from (
select t.*, row_number() over(order by age desc) rn
from mytable t
) t
where pid = 2935
In MySQL, window functions are supported in version 8.0 only. In SQL Server, they have been available for a long time.

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!

How to check if id exists in another table with current user not fetch

My A table stored all update of my web and V table stored which update viewed by user.
Now I want to create a SELECT statement that not fetch table A's which
id's date that id's of table V user_id=session id.
Case 1: To fetch all data from table A I used:
$id = 5, 10, 15; (for example)
$j = mysqli_query($dbh,"SELECT * FROM A WHERE `id`='".$id."' ORDER BY date DESC");
Case 2: Now select all ids where user_id = session id {If found any id reduce from case 1 fetch}
$myid = 50; ($session->id)
$p = mysqli_query($dbh,"SELECT post_id FROM V WHERE `post_id`='".$id."' AND `user_id` ='".$myid."'")
Table A:
id | from_id | to_id | date
---------------------------
5 |
10 |
15 |
20 |
Table V:
post_id | user_id
-----------------
5 | 45
15 | 50 (see: this is my id)
From above example I want fetch only ids 5 , 10 data where 15 reduce for match user_id = session id.
I don't fully understand what you want as the English isn't perfect. ;)
However, I'm certain what you want is a JOIN. See: https://dev.mysql.com/doc/refman/5.0/en/join.html

SQL Fetching single scalar using PDO

This question is more on this post. So I want to retrieve the number in a certain column. So let's say the table looks like this:
A | B | C | D | E | F
---------------------
0 | 50| 0 | 0 | 0 | 0
Let's say the user enters in B, I want to go fetch the current number in the table, which is 50. This is my SQL code:
$link = new PDO('**;dbname=**;charset=UTF-8','**','**');
$stmt = $link->prepare("SELECT MAX(:type) as max FROM table");
$stmt->bindParam(':type', $type);
$stmt->execute();
$used = $stmt->fetch(PDO::FETCH_ASSOC);
$used = $used["max"];
But this is just returning B rather than the number. How do I change my code to get the number? Thanks
SELECT MAX(:type) as max FROM table
Assuming :type gets set to "B", this will be executed as:
SELECT MAX("B") as max FROM table
The MAX of something with just one value will be that one value, i.e.:
SELECT "B" as max FROM table
Most likely you want to select the MAX from that column, not the string:
SELECT MAX(B) as max FROM table
Which you can do like this:
$stmt = $link->prepare("SELECT MAX($type) as max FROM table");
Obviously, this is vulnerable to SQL injection if $type comes from a user, but if you're getting a table name from a user, then you're doing something wrong anyway.
It's also possible that your table would make more sense like this:
type | value
------------
A | 0
B | 50
C | 0
And then you could get it with:
SELECT MAX(value) FROM tableName WHERE type = :type

Categories