The posts in my forum all have an individual permalink.
The posts are paginated and the users can sort them by date and rating.
When a permalink is accessed, the page the post resides on needs to be calculated due to the pagination.
The posts are stored with an auto incrementing id in a mysql innodb table.
At the moment I use the following for the calculation when the posts are sorted by their score (rating):
<?php
// These variables originate from the corresponding uri segments
// For example: http://domain.tld/topics/[ID]/[SLUG]/[POST_ID]
$post_id = $uri->segment(4);
$topic_id = $uri->segment(2);
$post_per_page = 10;
$query = $db->query('SELECT id FROM topic_posts WHERE topic_id = ' . $topic_id . ' ORDER BY score desc');
foreach ($query as $key => $post)
{
if ($post->id == $post_id)
{
$postOnPage = ceil(($key + 1) / $post_per_page);
break;
}
}
However, the amount of posts will keep increasing and fetching all posts seems awkward.
For the date sorting I use the following query, but it's not working for the rating sorting as the post id's are then not in incrementing order:
SELECT CEIL((COUNT(*) + 1) / $posts_per_page) FROM topic_posts WHERE topic_id = $topic_id AND id < $post_id;
So... How can I avoid the php foreach loop and achieve the same with the db query?
I just had an idea for a completely different approach, so I add it as a second answer instead of editing the first.
For this to work predictably, you always have to include at least one unique column into your sorting as a tie breaker. I assume that many of your posts will actually have the same rating, so maybe you should for example ORDER BY score DESC, id DESC to additionally order posts with the same rating as latest first (I think this might make sense for a forum anyway).
Then for the sort order mentioned above, you can get the number of posts that sort BEFORE the post in question with the following query:
SELECT COUNT(1) FROM topic_posts
WHERE topic_id = $topic_id
AND ((score > $post_score) OR (score = $post_score AND id > $post_id));
This is a query that you can optimize using indizes etc. as needed.
It is possible to generate an artificial "row number" within the query, which could then be used to calculate the page the post would appear on. However, depending on your schema and table size this query might become very costly, so be sure to check the performance.
I think it is best described with a demonstration:
First the table structure and some test data:
mysql> CREATE TABLE foo (a VARCHAR(10));
mysql> INSERT INTO foo VALUES ("foo"), ("bar"), ("baz");
A query that returns all results in some order with their row numbers attached:
mysql> SELECT f.*, #rownum := #rownum+1 AS rank FROM foo f, (SELECT #rownum := 0) r ORDER BY f.a;
+------+------+
| a | rank |
+------+------+
| bar | 1 |
| baz | 2 |
| foo | 3 |
+------+------+
3 rows in set (0.00 sec)
You can then use this to select only a particular row:
mysql> SELECT * FROM (SELECT f.*, #rownum := #rownum+1 AS rank FROM foo f, (SELECT #rownum := 0) r ORDER BY f.a) t WHERE a = "foo";
+------+------+
| a | rank |
+------+------+
| foo | 3 |
+------+------+
1 row in set (0.00 sec)
Essentially, you are wrapping the inner numbering query within an outer query that only selects the wanted result row. As you can see the rank is still the same as in the "all results" case, so can now use this row number to calculate your result page.
If you want to paginate over only a subset of all records in the table (for example all posts in a particular forum), that corresponding WHERE clause goes into the inner SELECT where the ORDER BY is:
mysql> SELECT * FROM (SELECT f.*, #rownum := #rownum+1 AS rank FROM foo f, (SELECT #rownum := 0) r WHERE a != "baz" ORDER BY f.a) t WHERE a = "foo";
+------+------+
| a | rank |
+------+------+
| foo | 2 |
+------+------+
1 row in set (0.00 sec)
The downside is that it actually has to iterate over all records (well, all records that match your inner WHERE clause), so it becomes very slow with large tables.
Related
I have the table:
id | date_submitted
1 | 01/01/2017
1 | 01/02/2017
2 | 01/03/2017
2 | 01/04/2017
I'm looking for the correct SQL to select each row, limited to one row per id that has the latest value in date_submitted.
So the SQL should return for the above table:
id | date_submitted
1 | 01/02/2017
2 | 01/04/2017
The query needs to select everything in the row, too.
Thanks for your help.
You can find max date for each id in subquery and join it with the original table to get all the rows with all the columns (assuming there are more columns apart from id and date_submitted) like this:
select t.*
from your_table t
inner join (
select id, max(date_submitted) date_submitted
from your_table
group by id
) t2 on t.id = t2.id
and t.date_submitted = t2.date_submitted;
Note that this query will return multiple rows for an id in case there are multiple rows with date_submitted equals to max date_submitted for that id. If you really want only one row per id, then the solution will be a bit different.
If you just need id and max date use:
select id, max(date_submitted) date_submitted
from your_table
group by id
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;
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'=>].
Lets say I have 3 columns and 3 rows, the first column is for ID, the second is names, third is votes. like:
+----+------+-------+
| id | name | votes |
+----+------+-------+
| 1 | bob | 7 |
| 2 | jill | 2 |
| 3 | jake | 9 |
+----+------+-------+
How can I have PHP compare the values in the votes field and sort it by whichever had the highest number, and attach a rank of #1,2,3, etc. depending on how many votes it had?
Each value will be displayed on a separate page. For example, if I went to 'bob's page' with the ID of 1, I would need it to display '#2 bob' since he would be ranked 2nd by votes.
You can make a separate column rank and update it by running the following code whenever your vote changes. This method will make you more efficient as in this you wont be sorting the table again and again when user visits his page:
$q = "select * from tableName order by votes DESC";
$a = mysql_query($q);
$count = 1;
while($arr = mysql_fetch_array($a){
$up = "update tableName set(rank) VALUES($count) WHERE name=$arr['name']";
$aq = mysql_query($up);
$count++;
}
Now on individual pages, you can just retrieve the rank value and show
$user = "Bob";
$q = "select rank from tableName where name=$user";
$a = mysql_query($q);
$arr = mysql_fetch_array($a);
echo $arr[0];
Also this(a slight modification in other answer) should work for you :-
SELECT #rownum:=#rownum+1 AS rank, name, vote FROM table, (SELECT #rownum:=0) as P ORDER BY vote DESC
You could read the values returned by your query into an array and then sort said array.
You want a SELECT query doing an ORDER BY votes, and create a special variable to handle the rank of a row:
SELECT #rownum:=#rownum+1 AS rank, name, vote FROM table, (SELECT #rownum:=0) ORDER BY vote DESC
This query should let you fetch an array with the rank, name, and number of votes of each person of your table.
Alternatively, you can just sort by vote, and add the rank value yourself with PHP.
You can use the MySQL 'ORDER BY' and display those ranks using PHP:
For this example:
<?php
//connection
//DB selection
$query = "SELECT * FROM table_votes ORDER BY votes DESC";
$result = mysql_query($query);
for(int $i=1; $row = mysql_fetch_array($result);i++)
{
echo "#".$i.$row['name']."<br/>";
}
?>
I need to update cells within a specific column based upon ids in another column. The column names are Prod_ID, Lang_ID and Descr:
Prod_ID | Lang_ID | Descr
--------+---------+------
A101 | 1 | TextA
A101 | 2 | TextB
A101 | 3 | TextC
For a group of rows with the same Prod_ID, I need to replace all subsequent descriptions (Descr column) with the description of the first row. The row with the correct description has always Lang_ID = 1. Also, the table may not be sorted by Lang_ID.
Example: TextA (Lang_ID = 1) should replace TextB and TextC because the Prod_IDs of the rows match.
You mentioned in a comment elsewhere that the "master" lang_id is always 1. That simplifies things greatly, and you can do this with a simple self-join (no subqueries :-)
This query selects all lang_1 rows, then joins them with all non-lang_1 rows of the same prod_id and updates those.
If Lang_ID=1 is always the "first"
UPDATE products
LEFT JOIN products as duplicates
ON products.Prod_ID=duplicates.Prod_ID
AND duplicates.Lang_ID != 1
SET duplicates.Descr = products.Descr
WHERE products.Lang_ID = 1
edit: If Lang_ID=1 may not be the "first"
you can join the table to itself via a an intermediate join which finds the lowest Lang_ID for that row. I have called the intermediate-join "lang_finder".
UPDATE products
LEFT JOIN (SELECT Prod_ID, MIN(Lang_ID) as Lang_ID FROM products GROUP BY Prod_ID) as lang_finder
ON products.prod_id=lang_finder.prod_id
LEFT JOIN products as cannonical_lang
ON products.Prod_ID = cannonical_lang.Prod_ID
AND lang_finder.Lang_ID = cannonical_lang.Lang_ID
SET products.Descr = cannonical_lang.Descr
Note that while it does use a subquery, it does not nest them. The subquery essentially just adds a column to the products table (virtually) with the value of the lowest Lang_ID, which then allows a self-join to match on that. So if there were a product with Lang_ID 3, 4, & 5, this would set the Descr on all of them to whatever was set for Lang_ID 3.
How about this?
UPDATE myTable dt1, myTable dt2
SET dt1.Descr = dt2.Descr
WHERE dt1.Prod_ID=dt2.Prod_ID;
Demo at sqlfiddle
Assuming that the correct description is always in the row of a group of rows with the same Prod_ID where Lang_ID has the smallest value, this MySQL query should work:
UPDATE your_table AS t1
JOIN (
SELECT Prod_ID, Descr
FROM (
SELECT *
FROM your_table
ORDER BY Lang_ID
) AS t3
GROUP BY Prod_ID
) AS t2
ON t1.Prod_ID = t2.Prod_ID
SET t1.Descr = t2.Descr;
The above can be used e.g. if Lang_ID is a primary or unique key. It also works if the corresponding Lang_ID has always the same minimum value (e.g. = 1) but in that case much less complex queries like this one are possible.