Codeigniter 3.0 query bug - php

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'=>].

Related

Get position of an ID based on MySQL COUNT result

I am not even sure if this has been answered because I don't even know how to coin the problem. But here is what am trying to do.
I am using COUNT() to create a tabular representation of a data from top to bottom for a 30 day period.
SELECT id FROM table WHERE col = '123' AND date >= DATE_SUB(CURRENT_DATE, INTERVAL DAYOFMONTH(CURRENT_DATE)-1 DAY) AND date <= LAST_DAY(CURRENT_DATE) GROUP BY id ORDER BY COUNT(id) DESC
And I get the result with the most at the top
id | col
==========
id3 | 123
id5 | 123
id2 | 123
id4 | 123
id8 | 123
id5 | 123
id1 | 123
id9 | 123
id7 | 123
This works fine for a tabular view and I can use ol to create a numbering system from 1 - 10. My issue is, I want to be able to tell the position of any given id. Eg. if I want to get the position of id9 in this count result i.e. 8, how do I do that?
If you are using MySQL v8.0 or higher you can use the RANK function:
SELECT COUNT(*), RANK() OVER (ORDER BY COUNT(id) DESC) AS r FROM table GROUP BY id ORDER BY COUNT(id) DESC;
For previous version of mysql, you need to create the variable your self:
SELECT COUNT(*), #rank := #rank + 1 AS r FROM table, (SELECT #rank := 0) temp ORDER BY COUNT(id) DESC;
Note SELECT #rank := 0 initiate the variable.
Updated:
To select a specific id and it's rank, you can use:
SELECT * FROM (
SELECT id, COUNT(*), RANK() OVER (ORDER BY COUNT(id) DESC) AS r FROM table GROUP BY id ORDER BY COUNT(id) DESC
) ranked WHERE id = ?;

Row ranking query works in MySQL but not in PHP [duplicate]

This question already has an answer here:
Mysqli doesn't allow multiple queries?
(1 answer)
Closed 4 years ago.
I've looked at about a dozen posts about queries not working in PHP, but they all haven't been able to solve my problem - hopefully this is an easy one!
What I am attempting to do here is "rank" the rows based on their total yearly sales, and that's worked pretty well so far. When I run my query in MySQL, it works properly - I get back hundreds of results.
SET #row_number := 0;
SELECT #row_number := #row_number + 1 AS row_number,
TotalRevenue,
CompanyID
FROM (
SELECT CompanyID,
SUM(Sales_Amt) AS TotalRevenue
FROM Sales,
(SELECT #row_number := 0) r
GROUP BY CompanyID
) t
WHERE TotalRevenue > 0
ORDER BY TotalRevenue DESC
Produces:
row_number | TotalRevenue | CompanyID
-----------+--------------+----------
1 | 81130.00 | 333
2 | 72234.00 | 876
3 | 62653.00 | 123
4 | 54408.40 | 999
5 | 44548.00 | 111
However, when I run it via PHP, I get back the error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELECT #row_number := #row_number + 1 AS row_number,
TotalRevenue' at line 2
Based on other posts here, I've tried:
adding `` around my column names
adding '' around my column names
adding := instead of = when I created #row_number
confirming that all spaces are added in after my query in PHP so that key words are not squished together
setting my mysqli->charset to utf-8
Here's my PHP code, in case you need that as well:
$query = "SET #row_number := 0;
SELECT #row_number := #row_number + 1 AS `row_number`,
TotalRevenue,
CompanyID
FROM (
SELECT CompanyID,
SUM(`Sales_Amt`) AS TotalRevenue
FROM Sales
GROUP BY CompanyID
) t
WHERE TotalRevenue > 0
ORDER BY TotalRevenue DESC";
if (!$result = $mysqli->query($query))
{
print_r($mysqli->error);
}
Hoping this will be something really simple that I am just NOT seeing.
Thanks!
To add another option aside from S. Dev's answer, you can use mysqli multi query;
$query = "SET #row_number := 0;
SELECT #row_number := #row_number + 1 AS `row_number`,
TotalRevenue,
CompanyID
FROM (
SELECT CompanyID,
SUM(`Sales_Amt`) AS TotalRevenue
FROM Sales
GROUP BY CompanyID
) t
WHERE TotalRevenue > 0
ORDER BY TotalRevenue DESC";
if (!$result = $mysqli->multi_query($query))
{
print_r($mysqli->error);
}
This will allow you to preform more than 1 query at one time, keeping it all within the same remit of the query
It also uses slightly less overhead as you are only doing (technically) 1 query as there is only 1 connection rather than 2 separate calls
It could be worth looking into stored procedures to make it so you don't have to set variables, rather, pass them into the query itself

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;

Updating a table based on a query of the same table

I have a table which stores information on standings in multiple leagues, think of this as a fantasy site. The structure is as follows in terms of columns.
league_id | user_id | total_points | prediction_difference | current_position | last_position
In order to calculate the current standings I am issuing the following query:
SELECT
*
FROM f_u_standings
WHERE league_id = 1
ORDER BY total_points DESC,
prediction_difference DESC
My question is, now I have this result set, how can I then perform an UPDATE based on the SELECT query which updates the current_position column? My programming language of choice on this project is PHP.
you can update with a select.. this assumes you have an ID for each row
UPDATE TABLE f_u_standings fs,
(
SELECT
*
----- do what you want to change current_position -----
FROM f_u_standings
WHERE league_id = 1
ORDER BY total_points DESC,
prediction_difference DESC
) temp
SET fs.current_position = temp.current_position WHERE fs.id = temp.id
This may be closer to what you need:
UPDATE f_u_standings fs,
(SELECT #rownum:=#rownum+1 rownum, id
FROM f_u_standings, (SELECT #rownum := 0) init
WHERE league_id = 1
ORDER BY total_points DESC,
prediction_difference DESC) temp
SET fs.current_position = temp.rownum
WHERE fs.id = temp.id

Determine page from post permalink in paginated forum

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.

Categories