A Delete-proof way to get the index of a table row? - php

I have this nice and neat way of loading posts for my blog website to fit specified page:
$end = $count - ($page * $ppp); //count = select max(id) from art;
$start = $count- ($page * $ppp) - ($ppp-1);
$nxtpage = $page +1; //this is set beforehand in case no posts exists
$prvpage = $page == 0 ? 0 : $page -1;
$sql = "SELECT
a.id AS id,
a.nazwa AS nazwa,
a.data AS data,
a.wstep AS wstep,
a.imgs AS imgs,
a.zdj AS zdj,
GROUP_CONCAT(t.nazwa) all_tags
FROM
art a INNER JOIN tagart ta ON a.id = ta.id INNER JOIN tags t ON t.idt = ta.idt
WHERE a.id BETWEEN $start AND $end
GROUP BY a.id
ORDER BY a.id desc";
This way I can load only a specified numer of posts depended by blogs page (pagination).
There is a pretty big problem with it tho.
Lets say my client make a mistake like writing BLACK PPL somewhere in one article half a year ago, and now he has to delete it.
Or even better, has to delete about 10 posts from it. When middle posts are deleted, the whole alrorithm gets messed up, because it scans posts based it their ID.
So my question here for you is what better way of picking the posts I could use, that would always get the correct order of the posts?

It looks like you're trying to implement your own way of doing LIMIT, which is a MySQL feature that handles pagination. Instead of manually defining your start and end ID's, you should be looking to order your posts and then only fetching the next X posts, no matter what their ID's are. Here's how you would do that
$start = ($page - 1) * $ppp;
$sql = "SELECT
a.id AS id,
a.nazwa AS nazwa,
a.data AS data,
a.wstep AS wstep,
a.imgs AS imgs,
a.zdj AS zdj,
GROUP_CONCAT(t.nazwa) all_tags
FROM
art a INNER JOIN tagart ta ON a.id = ta.id INNER JOIN tags t ON t.idt = ta.idt
GROUP BY a.id
ORDER BY a.id DESC
LIMIT $start,$ppp";
LIMIT is used as either
LIMIT 5 #Fetch first 5 items
or
LIMIT 5,10 #Starting from the 5th item, fetch the next 10 items

Instead of using max(id) to determine the number of posts, use count(id) or count(*) to actually count them. If a post gets deleted, the count can take that into account.
In the select query use limit to select the range of posts to show.

As you already figured out, you should not be relying on MAX(id) to count the number of records.
I would just use a separate count query to get the count. It's simple and relatively inexpensive:
SELECT COUNT (id) FROM art
And as others have already mentioned, use LIMIT to paginate instead of limiting by id.

Related

Efficient comment system pagination query

So I've been looking around the web about any information about pagination.
From what I've seen there are 3 kinds, (LIMIT, OFFSET) a, (WHERE id > :num ORDER BY id LIMIT 10) b and (cursor pagination) c like those used on facebook and twitter.
I decided that for my project I'll go with the "b" option as it looks pretty straightforward and efficient.
I'm trying to create some kind of "facebook" like post and comment system, but not as complex.
I have a ranking system for the posts and comments and top 2 comments for each post that are fetched with the post.
The rest of the comments for each specific post are being fetched when people click on to see more comments.
This is a query for post comments:
SELECT
c.commentID,
c.externalPostID,
c.numOfLikes,
c.createdAt,
c.customerID,
c.numOfComments,
(CASE WHEN cl.customerID IS NULL THEN false ELSE true END) isLiked,
cc.text,
cu.reputation,
cu.firstName,
cu.lastName,
c.ranking
FROM
(SELECT *
FROM Comments
WHERE Comments.externalPostID = :externalPostID) c
LEFT JOIN CommentLikes cl ON cl.commentID = c.commentID AND cl.customerID = :customerID
INNER JOIN CommentContent cc ON cc.commentTextID = c.commentID
INNER JOIN Customers cu ON cu.customerID = c.customerID
ORDER BY c.weight DESC, c.createdAt ASC LIMIT 10 OFFSET 2
offset 2 is because there were 2 comments being fetched earlier as top 2 comments.
I'm looking for a way similar to this of seeking next 10 comments each time through the DB without going through all the rows like with LIMIT,OFFSET
The problem is that I have two columns that are sorting the results and I won't allow me to use this method:
SELECT * FROM Comments WHERE id > :lastId LIMIT :limit;
HUGE thanks for the helpers !
Solution So Far:
In order to to have an efficient pagination we need to have a single column with as much as possible unique values that make a sequence to help us sort the data and paginate through.
My example uses two columns to sort the data so it makes a problem.
What I did is combine time(asc sorting order) and weight of the comment(desc sorting order), weight is a total of how much that comment is being engaged by users.
I achieved it by getting the pure int number out of the DateTime format and dividing the number by the weight let's call the result,"ranking" .
this way a comment with a weight will always have a lower ranking ,than a comment without a weight.
DateTime after stripping is a 14 digit int ,so it shouldn't make a problem dividing it by another number.
So now we have one column that sorts the comments in a way that comments with engagement will be at the top and after that will come the older comments ,so on until the newly posted comments at the end.
Now we can use this high performance pagination method that scales well:
SELECT * FROM Comments WHERE ranking > :lastRanking ORDER BY ASC LIMIT :limit;
Ok i want to say about other way, in my opinion this very useful.
$rowCount = 10; //this is count of row that is fetched every time
$page = 1; //this is for calculating offset . you must increase only this value every time
$offset = ($page - 1) * $rowCount; //offset
SELECT
c.commentID,
c.externalPostID,
c.numOfLikes,
c.createdAt,
c.customerID,
c.numOfComments,
(CASE WHEN cl.customerID IS NULL THEN false ELSE true END) isLiked,
cc.text,
cu.reputation,
cu.firstName,
cu.lastName,
c.ranking
FROM
(SELECT *
FROM Comments
WHERE Comments.externalPostID = :externalPostID) c
LEFT JOIN CommentLikes cl ON cl.commentID = c.commentID AND cl.customerID = :customerID
INNER JOIN CommentContent cc ON cc.commentTextID = c.commentID
INNER JOIN Customers cu ON cu.customerID = c.customerID
ORDER BY c.ranking DESC, c.createdAt ASC LIMIT $rowCount OFFSET $offset
There can be an error because i didn't check it , please don't make it matter

Select random rows from one MYSQL table with join

Afternoon folks,
I have had a good dig around and can't find the answer, so a good time to ask!
I'd like to select random rows from one table and then join into this random rows from another table where the ID that I have is the same. It would also be great to only select where I have an entry in the second table. I have tried all manner of sub-queries but am getting a bit lost. An inner join as read will do it but again with the randomness of it all!! Grrr...
SELECT
tracks.track_id,
cuttings.square_cutting,
cuttings.cutting_2,
cuttings.cutting_3,
cuttings.blog_text
FROM tbl_tracks tracks,
(SELECT
square_cutting,
cutting_2,
cutting_3,
blog_text
FROM
tbl_cuttings
WHERE track_id = tracks.track_id <-- wont find it, obviously!!
ORDER BY RAND()
LIMIT 1) cuttings
WHERE tracks.active = '1' ORDER BY RAND()
Thanks in advance for any help.
So:
I'd like random tracks showing
track id -> with random cuttings, of which there can be many but I just want 1.
It would then be ideal to only show a result if there is a cutting associated with that track.
Hope that helps.
I'm now trying to go a step further with this and order this by a RAND() seed as I'm now having to add in pagination. Only problem is that its not giving me back the same random list due to a given seed. Any Ideas?
SELECT
tracks.track_id,
cuttings.square_cutting,
cuttings.cutting_2,
cuttings.cutting_3,
cuttings.blog_text
FROM tbl_tracks tracks
INNER JOIN
(SELECT track_id,
square_cutting,
cutting_2,
cutting_3,
blog_text
FROM
tbl_cuttings
ORDER BY RAND()) cuttings ON tracks.track_id = cuttings.track_id
WHERE tracks.active = '1'
ORDER BY RAND(1)
LIMIT 0,4;
you could use an inner join
SELECT
tracks.track_id,
cuttings.square_cutting,
cuttings.cutting_2,
cuttings.cutting_3,
cuttings.blog_text
FROM tbl_tracks tracks
INNER JOIN
(SELECT track_id,
square_cutting,
cutting_2,
cutting_3,
blog_text
FROM
tbl_cuttings
ORDER BY RAND()
LIMIT 1) cuttings on cuttings.track_id = tracks.track_id
WHERE tracks.active = '1'
ORDER BY RAND()

Sorting data from MySQL by SUM with Grouping By - not working properly

I have a problem with mysql - i'm kind new to it, byt looking to improve my skills :)
Have a code like this:
if($where > 0) $query = mysql_query("SELECT img.*, user.user as owner_name, cat.name as cat_name FROM tentego_img AS img LEFT JOIN tablicacms_users AS user ON user.id = img.owner LEFT JOIN tentego_img_cat AS cat ON cat.id = img.cat WHERE img.`is_waiting` LIKE ".$where.$cat." INNER JOIN tentego_img_vote ON tentego_img.id = tentego_img_vote.object_id GROUP BY tentego_img_vote.object_id ORDER BY SUM ( (CASE WHEN tentego_img_vote.vote = '0' THEN '-1' ELSE '1' END) ) DESC LIMIT ".$page.",".$objPerPage);
I need to make sorting by number of votes, sorted descending.
Still it makes results sorted by it own way.
In table I have rows:
ID - vote id for table purpose
object_id- id of object joined with another table to show results.
User ID - user id
Vote - where values are 0 for dislike and 1 for like (so -1 for 0, and +1 for 1)
So, as I understand i need to sum up all records for each of unique object_id, then sort by sum of vote values of each.
This code worked before my script provider decide to upgrade it, so right now i dont know how to fix it :(

How do I echo out a name -only if- specific rows in one table match specific rows in another based on an id?

Here's my problem: I'm making a crafting system for a game, and I already have my database filled with information for resources required to craft items.
Here are what my relevant tables look like:
table #edible_resources
(edible_resource_id, edible_resource_name, hunger_points, degeneration_id)
table #edible_ground
(id, resource, amount, location)
table #req_crafting_edible
(req_crafting_edible_id, edible_resource_id, req_resource_amount, created_item_id)
table #items
(item_id, item_name, degeneration_id, is_grounded, is_stackable, can_equip, can_edit)
What I want to do is -only- echo out the craftable item's name if, and only if -all- required resources (and their required amounts) are on the ground in the location of the character.
I have a query that comes close:
SELECT items.item_name, items.item_id FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
ORDER BY items.item_name
But this shows me craftable items regardless if I have ALL the required items in the area. It shows me items as long as I have -one- of their required resources.
Is there a way to only show the name of a craftable item only if I have -all- the required resources (and their amounts) in edible_ground where location = $current_location?
For more information on what I've tried:
$get_char = mysql_query("SELECT current_char FROM accounts WHERE account_id ='".$_SESSION['user_id']."'");
$current_char = mysql_result($get_char, 0, 'current_char');
$get_loc = mysql_query("SELECT current_location FROM characters WHERE character_id = $current_char");
$current_location = mysql_result($get_loc, 0, 'current_location');
//---------------------------------------------------------------COOKED FOOD
$get_food = mysql_query("SELECT items.item_name, items.item_id FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
ORDER BY items.item_name");
while ($food = mysql_fetch_array($get_food)){
echo $food['item_name'].'<br>';
}
This returns:
Baked Fish
Charred Fish
Fish Soup
Glazed Berry
Cake
Grilled Fish
Sashimi
Seafood Soup
Sushi
Udon
On the ground:
1 fish
1 honey
Even though fish soup, berry cake, udon etc needs much more than just the one fish that's in the area.
Can anyone help me figure this out? I'd be forever grateful; I've spent a few days already trying to myself. Please?
And before anyone says anything, I know I need to start using mysqli; unfortunately I didn't even realize it existed when I started to make the game (and learn PHP at the same time months ago), so I'll have to painfully go back and change it all in an update.
You want a HAVING clause to check the count of the records you are grouping through the INNER JOINs.
HAVING count(*) = (
SELECT count(*)
FROM req_crafting_edible
WHERE req_crafting_edible.created_item_id = items.item_id
)
Edit:
So basically you need to know two pieces of information:
How many different resources are required
Do each of those resources have the required amounts
The first is solved by the sub query above.
Your query as-is satisfies the second point but only for 1 resource.
HAVING basically does some special magic on your group clause. HAVING count(*) means there are X records being grouped together. Because of how the join works, you will have 1 item.name for each resource. The sub select gives you the count of how many different resources, and therefore grouped records, are needed for that item. Comparing that sub query with the count(*) of the grouping ensures you have all the needed resources.
And here is the final query, modifying your code above:
SELECT items.item_name, items.item_id
FROM items
INNER JOIN req_crafting_edible
ON req_crafting_edible.created_item_id = items.item_id
INNER JOIN edible_ground
ON edible_ground.resource = req_crafting_edible.edible_resource_id
AND edible_ground.amount >= req_crafting_edible.req_resource_amount
WHERE edible_ground.location = $current_location
GROUP BY items.item_name
HAVING count(*) = (
SELECT count(*)
FROM req_crafting_edible
WHERE req_crafting_edible.created_item_id = items.item_id
)
ORDER BY items.item_name
You only actually want the data from the items table, right? If so I would move to using an exists model:
SELECT I.item_name, I.item_id FROM items I
WHERE NOT EXISTS
(SELECT created_item_id
FROM req_crafting_edible R
WHERE R.created_item_id = I.item_id
AND NOT EXISTS
(SELECT G.resource
FROM edible_ground G
WHERE G.resource = R.edible_resource_id
AND edible_ground.location = $current_location
AND G.amount >= R.req_resource_amount))
ORDER BY I.item_name
I don't have your database to check this, but the logic goes like this:
Find the items that don't have any unsatisfied requirements.
Find the unsatisfied requirements for the current item. (IE. Find
requirements that don't have resources on the ground)
Find the edible resources that match the current requirement, are at
this location, and have enough.
I don't work in mysql as much at the moment, but let me know if this doesn't work.

Codeigniter: Selecting the SUM of rows give the SUM times GROUP_CONCAT results?

I'm querying four tables (which are: resources, tag_list, resource_tags and votes) and trying to retrieve a list of resources with each list item having grouped tags and the sum of votes for that resource.
This is my current model:
$this->db->select('*');
$this->db->select('GROUP_CONCAT(DISTINCT tag SEPARATOR " | ") AS tags, SUM(vote) AS sumvotes');
$this->db->from('resources');
$this->db->join('resource_tags', 'resources.r_id = resource_tags.resource_id', 'left');
$this->db->join('tag_list', 'tag_list.t_id = resource_tags.tag_id', 'left');
$this->db->join('votes', 'votes.resource_id = resources.r_id', 'left');
$this->db->where('resources.published', '1');
$this->db->group_by('resources.r_id');
$this->db->order_by('votes.vote', 'desc');
$query = $this->db->get();
Edit: Here is the raw generated SQL
SELECT *, GROUP_CONCAT(DISTINCT tag SEPARATOR " | ") AS tags, SUM(vote) AS sumvotes
FROM (`olu_resources`)
LEFT JOIN `olu_resource_tags` ON `olu_resources`.`r_id` = `olu_resource_tags`.`resource_id`
LEFT JOIN `olu_tag_list` ON `olu_tag_list`.`t_id` = `olu_resource_tags`.`tag_id`
LEFT JOIN `olu_votes` ON `olu_votes`.`resource_id` = `olu_resources`.`r_id`
WHERE `olu_resources`.`published` = '1'
GROUP BY `olu_resources`.`r_id`
ORDER BY `olu_votes`.`vote` desc
It seems to do everything except for calculating the correct number of votes, it returns the number of votes there are multiplied by the number of tags that item has.
Does anyone know why this is happening? Or how to go about fixing this query?
Why don't you use a sub-query to get the votes?
Active Record in CI is great to use for simple queries but difficult (and slow) when you have more complex queries like yours.
It is probably all possible with Active Record though I would check the profiler first and look at the difference in speed. Last time I did something like that it made a 7 second difference.
I would try something like this:
$SQL = "SELECT *, GROUP_CONCAT(DISTINCT tag SEPARATOR ' | '),
(SELECT SUM(vote) FROM olu_votes WHERE olu_votes.resource_id = olu_resources.r_id) AS sumvotes
FROM (olu_resources)
LEFT JOIN olu_resource_tags ON olu_resources.r_id = olu_resource_tags.resource_id
LEFT JOIN olu_tag_list ON olu_tag_list.t_id = olu_resource_tags.tag_id
LEFT JOIN olu_votes ON olu_votes.resource_id = olu_resources.r_id
WHERE olu_resources.published = '1'
GROUP BY olu_resources.r_id
ORDER BY olu_votes.vote desc"
$this->db->query($SQL);
This should hopefully also resolve your problem. Let me know if this works!
if needed I'll create a test query on my own system to get you the result you want.

Categories